# frozen_string_literal: true

# Copyright (C) 2015-2020 MongoDB Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require 'mongo/error'

module Mongo
  module Operation
    # Shared executable behavior of operations.
    #
    # @since 2.5.2
    # @api private
    module Executable
      include ResponseHandling

      # @return [ Operation::Context | nil ] the operation context used to
      #   execute this operation.
      attr_accessor :context

      def do_execute(connection, context, options = {})
        # Save the context on the instance, to avoid having to pass it as a
        # parameter to every single method. There are many legacy methods that
        # still accept it as a parameter, which are left as-is for now to
        # minimize the impact of this change. Moving forward, it may be
        # reasonable to refactor things so this saved reference is used instead.
        @context = context

        session&.materialize_if_needed
        unpin_maybe(session, connection) do
          add_error_labels(connection, context) do
            check_for_network_error do
              add_server_diagnostics(connection) do
                get_result(connection, context, options) do |result|
                  if session
                    if session.in_transaction? &&
                       connection.description.load_balancer?
                      if session.pinned_connection_global_id
                        unless session.pinned_connection_global_id == connection.global_id
                          raise(
                            Error::InternalDriverError,
                            "Expected operation to use connection #{session.pinned_connection_global_id} but it used #{connection.global_id}"
                          )
                        end
                      elsif !session.committing_transaction? && !session.aborting_transaction?
                        session.pin_to_connection(connection.global_id, connection: connection)
                        connection.pin(:transaction)
                      end
                    end

                    if session.snapshot?
                      session.snapshot_timestamp = result.snapshot_timestamp
                    end
                  end

                  process_result(result, connection)
                end
              end
            end
          end
        end
      end

      def execute(connection, context:, options: {})
        if Lint.enabled? && !connection.is_a?(Mongo::Server::Connection)
          raise Error::LintError, "Connection argument is of wrong type: #{connection}"
        end

        do_execute(connection, context, options).tap do |result|
          validate_result(result, connection, context)
        end
      end

      private

      def result_class
        Result
      end

      def get_result(connection, context, options = {})
        message = build_message(connection, context)
        connection.tracer.trace_command(message, context, connection) do
          result = result_class.new(*dispatch_message(message, connection, context, options), context: context,
                                                                                              connection: connection)
          if block_given?
            yield result
          else
            result
          end
        end
      end

      # Returns a Protocol::Message or nil as reply.
      def dispatch_message(message, connection, context, options = {})
        message = message.maybe_encrypt(connection, context)
        reply = connection.dispatch([ message ], context, options)
        [ reply, connection.description, connection.global_id ]
      end

      # @param [ Mongo::Server::Connection ] connection The connection on which
      #   the operation is performed.
      # @param [ Mongo::Operation::Context ] context The operation context.
      def build_message(connection, context)
        msg = message(connection)
        if (server_api = context.server_api)
          msg = msg.maybe_add_server_api(server_api)
        end
        msg
      end

      def process_result(result, connection)
        connection.server.update_cluster_time(result)

        process_result_for_sdam(result, connection)

        session.process(result) if session

        result
      end

      def process_result_for_sdam(result, connection)
        if (result.not_master? || result.node_recovering?) &&
           connection.generation >= connection.server.pool.generation(service_id: connection.service_id)
          keep_pool = if result.node_shutting_down?
                        false
                      else
                        # Max wire version needs to be examined while the server is known
                        connection.description.server_version_gte?('4.2')
                      end

          connection.server.unknown!(
            keep_connection_pool: keep_pool,
            generation: connection.generation,
            service_id: connection.service_id,
            topology_version: result.topology_version
          )

          connection.server.scan_semaphore.signal
        end
      end

      NETWORK_ERRORS = [
        Error::SocketError,
        Error::SocketTimeoutError
      ].freeze

      def check_for_network_error
        yield
      rescue *NETWORK_ERRORS
        session&.dirty!
        raise
      end
    end
  end
end
