# frozen_string_literal: true

module Mongoid
  module Locker
    # Set of methods to interact with the database.
    module Wrapper
      # Finds provided document with provided options, locks (sets +locking_name_field+ and +locked_at_field+ fields), and returns the fields.
      #
      # @example
      #   Mongoid::Locker::Wrapper.find_and_lock(document, opts)
      #   #=> {"locked_at"=>2019-03-19 07:51:24 UTC, "locking_name"=>nil}
      #
      # @example
      #   Mongoid::Locker::Wrapper.find_and_lock(document, opts)
      #   # => nil
      #
      # @param doc [Mongoid::Document]
      # @param opts [Hash] (see #with_lock)
      # @return [Hash] with +locking_name_field+ and +locked_at_field+ fields
      # @return [nil] if the document was not found, was already locked
      def self.find_and_lock(doc, opts)
        model = doc.class
        filter = {
          _id: doc.id,
          '$or': [
            {
              '$or': [
                { model.locking_name_field => { '$exists': false } },
                { model.locked_at_field => { '$exists': false } }
              ]
            },
            {
              '$or': [
                { model.locking_name_field => { '$eq': nil } },
                { model.locked_at_field => { '$eq': nil } }
              ]
            },
            {
              '$expr': { '$gte': ['$$NOW', { '$add': ["$#{model.locked_at_field}", model.lock_timeout * 1000] }] } # The expr means "Time.now.utc >= model.locked_at_field + model.lock_timeout * 1000"
            }
          ]
        }
        update = {
          '$set': { model.locking_name_field => opts[:locking_name] },
          '$currentDate': { model.locked_at_field => true }
        }
        options = {
          return_document: :after,
          projection: { _id: false, model.locking_name_field => true, model.locked_at_field => true },
          write_concern: model.locker_write_concern
        }

        model.collection.find_one_and_update(filter, update, options)
      end

      # Finds provided document with provided options, unlocks (sets +locking_name_field+ and +locked_at_field+ fields to +nil+).
      #
      # @example
      #   Mongoid::Locker::Wrapper.find_and_unlock(doc, opts)
      #   #=> true
      #   Mongoid::Locker::Wrapper.find_and_unlock(doc, opts)
      #   #=> false
      #
      # @param doc [Mongoid::Document]
      # @param opts [Hash] (see #with_lock)
      # @return [Boolean]
      # @return [true] if the document was unlocked
      # @return [false] if the document was not found, was not unlocked
      def self.find_and_unlock(doc, opts)
        model = doc.class
        filter = {
          _id: doc.id,
          model.locking_name_field => opts[:locking_name]
        }
        update = {
          '$set': {
            model.locking_name_field => nil,
            model.locked_at_field => nil
          }
        }
        options = { write_concern: model.locker_write_concern }

        result = model.collection.update_one(filter, update, options)
        result.ok? && result.written_count == 1
      end

      # Returns value of +locked_at_field+ field for provided document.
      #
      # @example
      #   Mongoid::Locker::Wrapper.locked_at(document)
      #   #=> 2019-06-03 13:50:46 UTC
      #
      # @param doc [Mongoid::Document]
      # @return [Time] +locked_at_field+ field time
      # @return [nil] if response was failed
      def self.locked_at(doc)
        result = doc.class.collection.find(
          { _id: doc.id },
          projection: { _id: false, doc.locked_at_field => true },
          limit: 1
        ).first

        result[doc.locked_at_field.to_s] if result
      end

      # Returns the local database server time in UTC.
      #
      # @example
      #   Mongoid::Locker::Wrapper.current_mongodb_time(User)
      #   #=> 2019-03-19 07:24:36 UTC
      #
      # @param model [Class] the model class
      # @return [Time] current time
      # @return [nil] if response was failed
      def self.current_mongodb_time(model)
        info = model.collection.database.command(isMaster: 1)
        info.ok? ? info.documents.first['localTime'] : nil
      end
    end
  end
end
