# frozen_string_literal: true

module Unified
  module CrudOperations
    def crud_find(op)
      get_find_view(op).to_a
    end

    def find_one(op)
      get_find_view(op).first
    end

    def get_find_view(op)
      collection = entities.get(:collection, op.use!('object'))
      use_arguments(op) do |args|
        filter = args.use!('filter')
        session = args.use('session')

        opts = extract_options(args, 'let', 'comment',
                               'allowDiskUse', 'returnKey', 'projection',
                               'skip', 'hint', 'maxTimeMS', 'timeoutMS',
                               'collation', 'noCursorTimeout', 'oplogReplay', 'allowPartialResults',
                               'timeoutMode', 'maxAwaitTimeMS', 'cursorType', 'timeoutMode',
                               { 'showRecordId' => :show_disk_loc, 'max' => :max_value, 'min' => :min_value },
                               allow_extra: true)
        symbolize_options!(opts, :timeout_mode, :cursor_type)

        opts[:session] = entities.get(:session, session) if session

        req = collection.find(filter, **opts)
        if (batch_size = args.use('batchSize'))
          req = req.batch_size(batch_size)
        end
        if (sort = args.use('sort'))
          req = req.sort(sort)
        end
        if (limit = args.use('limit'))
          req = req.limit(limit)
        end
        if (projection = args.use('projection'))
          req = req.projection(projection)
        end
        req
      end
    end

    def count(op)
      collection = entities.get(:collection, op.use!('object'))
      use_arguments(op) do |args|
        opts = extract_options(args, 'comment', 'timeoutMS', 'maxTimeMS', allow_extra: true)
        if (session = args.use('session'))
          opts[:session] = entities.get(:session, session)
        end
        collection.count(args.use!('filter'), **opts)
      end
    end

    def count_documents(op)
      collection = entities.get(:collection, op.use!('object'))
      use_arguments(op) do |args|
        opts = extract_options(args, 'comment', 'timeoutMS', 'maxTimeMS', allow_extra: true)
        if (session = args.use('session'))
          opts[:session] = entities.get(:session, session)
        end
        collection.find(args.use!('filter')).count_documents(**opts)
      end
    end

    def estimated_document_count(op)
      collection = entities.get(:collection, op.use!('object'))
      use_arguments(op) do |args|
        opts = extract_options(args, 'comment', 'timeoutMS', 'maxTimeMS', allow_extra: true)
        if (session = args.use('session'))
          opts[:session] = entities.get(:session, session)
        end
        collection.estimated_document_count(**opts)
      end
    end

    def distinct(op)
      collection = entities.get(:collection, op.use!('object'))
      use_arguments(op) do |args|
        opts = extract_options(args, 'comment', 'timeoutMS', 'maxTimeMS', allow_extra: true)
        if (session = args.use('session'))
          opts[:session] = entities.get(:session, session)
        end
        req = collection.find(args.use!('filter'), **opts).distinct(args.use!('fieldName'), **opts)
        req.to_a
      end
    end

    def find_one_and_update(op)
      collection = entities.get(:collection, op.use!('object'))
      use_arguments(op) do |args|
        filter = args.use!('filter')
        update = args.use!('update')
        opts = {
          let: args.use('let'),
          comment: args.use('comment'),
          hint: args.use('hint'),
          upsert: args.use('upsert'),
          timeout_ms: args.use('timeoutMS'),
          max_time_ms: args.use('maxTimeMS')
        }
        if (return_document = args.use('returnDocument'))
          opts[:return_document] = return_document.downcase.to_sym
        end
        if (session = args.use('session'))
          opts[:session] = entities.get(:session, session)
        end
        collection.find_one_and_update(filter, update, **opts)
      end
    end

    def find_one_and_replace(op)
      collection = entities.get(:collection, op.use!('object'))
      use_arguments(op) do |args|
        filter = args.use!('filter')
        update = args.use!('replacement')
        opts = {
          let: args.use('let'),
          comment: args.use('comment'),
          hint: args.use('hint'),
          sort: args.use('sort'),
          timeout_ms: args.use('timeoutMS'),
          max_time_ms: args.use('maxTimeMS')
        }
        if (return_document = args.use('returnDocument'))
          opts[:return_document] = return_document.downcase.to_sym
        end
        if (session = args.use('session'))
          opts[:session] = entities.get(:session, session)
        end
        collection.find_one_and_replace(filter, update, **opts)
      end
    end

    def find_one_and_delete(op)
      collection = entities.get(:collection, op.use!('object'))
      use_arguments(op) do |args|
        filter = args.use!('filter')
        opts = {
          let: args.use('let'),
          comment: args.use('comment'),
          hint: args.use('hint'),
          sort: args.use('sort'),
          timeout_ms: args.use('timeoutMS'),
          max_time_ms: args.use('maxTimeMS')
        }
        if (session = args.use('session'))
          opts[:session] = entities.get(:session, session)
        end
        collection.find_one_and_delete(filter, **opts)
      end
    end

    def insert_one(op)
      collection = entities.get(:collection, op.use!('object'))
      use_arguments(op) do |args|
        opts = {
          comment: args.use('comment'),
          timeout_ms: args.use('timeoutMS'),
          max_time_ms: args.use('maxTimeMS')
        }
        if (session = args.use('session'))
          opts[:session] = entities.get(:session, session)
        end
        collection.insert_one(args.use!('document'), **opts)
      end
    end

    def insert_many(op)
      collection = entities.get(:collection, op.use!('object'))
      use_arguments(op) do |args|
        opts = {
          comment: args.use('comment'),
          timeout_ms: args.use('timeoutMS'),
          max_time_ms: args.use('maxTimeMS')
        }
        unless (ordered = args.use('ordered')).nil?
          opts[:ordered] = ordered
        end
        if (session = args.use('session'))
          opts[:session] = entities.get(:session, session)
        end
        collection.insert_many(args.use!('documents'), **opts)
      end
    end

    def update_one(op)
      collection = entities.get(:collection, op.use!('object'))
      use_arguments(op) do |args|
        opts = {
          let: args.use('let'),
          comment: args.use('comment'),
          hint: args.use('hint'),
          upsert: args.use('upsert'),
          timeout_ms: args.use('timeoutMS'),
          max_time_ms: args.use('maxTimeMS'),
          sort: args.use('sort')
        }
        if (session = args.use('session'))
          opts[:session] = entities.get(:session, session)
        end
        collection.update_one(args.use!('filter'), args.use!('update'), **opts)
      end
    end

    def update_many(op)
      collection = entities.get(:collection, op.use!('object'))
      use_arguments(op) do |args|
        opts = {
          let: args.use('let'),
          comment: args.use('comment'),
          hint: args.use('hint'),
          timeout_ms: args.use('timeoutMS'),
          max_time_ms: args.use('maxTimeMS')
        }
        if (session = args.use('session'))
          opts[:session] = entities.get(:session, session)
        end
        collection.update_many(args.use!('filter'), args.use!('update'), **opts)
      end
    end

    def replace_one(op)
      collection = entities.get(:collection, op.use!('object'))
      use_arguments(op) do |args|
        opts = {
          comment: args.use('comment'),
          upsert: args.use('upsert'),
          let: args.use('let'),
          hint: args.use('hint'),
          timeout_ms: args.use('timeoutMS'),
          max_time_ms: args.use('maxTimeMS'),
          sort: args.use('sort')
        }
        if (session = args.use('session'))
          opts[:session] = entities.get(:session, session)
        end
        collection.replace_one(
          args.use!('filter'),
          args.use!('replacement'),
          **opts
        )
      end
    end

    def delete_one(op)
      collection = entities.get(:collection, op.use!('object'))
      use_arguments(op) do |args|
        opts = {
          let: args.use('let'),
          comment: args.use('comment'),
          hint: args.use('hint'),
          timeout_ms: args.use('timeoutMS'),
          max_time_ms: args.use('maxTimeMS')
        }
        if (session = args.use('session'))
          opts[:session] = entities.get(:session, session)
        end
        collection.delete_one(args.use!('filter'), **opts)
      end
    end

    def delete_many(op)
      collection = entities.get(:collection, op.use!('object'))
      use_arguments(op) do |args|
        opts = {
          let: args.use('let'),
          comment: args.use('comment'),
          hint: args.use('hint'),
          timeout_ms: args.use('timeoutMS'),
          max_time_ms: args.use('maxTimeMS')
        }
        if (session = args.use('session'))
          opts[:session] = entities.get(:session, session)
        end
        collection.delete_many(args.use!('filter'), **opts)
      end
    end

    def bulk_write(op)
      collection = entities.get(:collection, op.use!('object'))
      use_arguments(op) do |args|
        requests = args.use!('requests').map do |req|
          convert_bulk_write_spec(req)
        end
        opts = {}
        opts[:ordered] = args.use!('ordered') if args.key?('ordered')
        if (session = args.use('session'))
          opts[:session] = entities.get(:session, session)
        end
        if (comment = args.use('comment'))
          opts[:comment] = comment
        end
        if (let = args.use('let'))
          opts[:let] = let
        end
        if (timeout_ms = args.use('timeoutMS'))
          opts[:timeout_ms] = timeout_ms
        end
        if (max_time_ms = args.use('maxTimeMS'))
          opts[:max_time_ms] = max_time_ms
        end
        collection.bulk_write(requests, **opts)
      end
    end

    def aggregate(op)
      obj = entities.get_any(op.use!('object'))
      args = op.use!('arguments')
      pipeline = args.use!('pipeline')

      opts = extract_options(args, 'let', 'comment', 'batchSize', 'maxTimeMS',
                             'allowDiskUse', 'timeoutMode', 'timeoutMS', 'maxTimeMS', 'maxAwaitTimeMS', allow_extra: true)
      symbolize_options!(opts, :timeout_mode)

      if (session = args.use('session'))
        opts[:session] = entities.get(:session, session)
      end

      raise NotImplementedError, "Unhandled spec keys: #{args} in #{test_spec}" unless args.empty?

      obj.aggregate(pipeline, **opts).to_a
    end

    def create_find_cursor(op)
      obj = entities.get_any(op.use!('object'))
      args = op.use!('arguments')

      filter = args.use('filter')
      if (session = args.use('session'))
        session = entities.get(:session, session)
      end
      opts = extract_options(args, 'batchSize', 'timeoutMS', 'cursorType', 'maxAwaitTimeMS')
      symbolize_options!(opts, :cursor_type)
      opts[:session] = session if session

      view = obj.find(filter, opts)
      view.each # to initialize the cursor

      view.cursor
    end

    private

    def convert_bulk_write_spec(spec)
      raise NotImplementedError, 'Must have exactly one item' unless spec.keys.length == 1

      op, spec = spec.first
      spec = UsingHash[spec]
      out = case op
            when 'insertOne'
              spec.use!('document')
            when 'updateOne', 'updateMany'
              {
                filter: spec.use('filter'),
                update: spec.use('update'),
                upsert: spec.use('upsert'),
                array_filters: spec.use('arrayFilters'),
                hint: spec.use('hint'),
              }
            when 'replaceOne'
              {
                filter: spec.use('filter'),
                replacement: spec.use('replacement'),
                upsert: spec.use('upsert'),
                hint: spec.use('hint'),
              }
            when 'deleteOne', 'deleteMany'
              {
                filter: spec.use('filter'),
                hint: spec.use('hint'),
              }
            else
              raise NotImplementedError, "Unknown operation #{op}"
            end
      out[:sort] = spec.use('sort') if %w[ updateOne replaceOne ].include?(op) && spec.key?('sort')
      raise NotImplementedError, "Unhandled keys: #{spec}" unless spec.empty?

      { Utils.underscore(op) => out }
    end
  end
end
