# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
# frozen_string_literal: true

module Contrast
  module Agent
    module Reporting
      # Module to house the Resend logic, when there is no response from TS.
      module Resend
        # How manny times the client will try to rescue from above error before raising an
        # error to reflect the events.
        #
        RESCUE_ATTEMPTS = 3
        TIMEOUT = 5
        RETRY_ERRORS = [
          Net::OpenTimeout, Net::ReadTimeout, EOFError, OpenSSL::SSL::SSLError, Resolv::ResolvError,
          Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::ESHUTDOWN, Errno::EHOSTDOWN, NameError,
          Errno::EHOSTUNREACH, Errno::EISCONN, Errno::ECONNABORTED, Errno::ENETRESET, Errno::ENETUNREACH, SocketError,
          NameError, NoMethodError, Timeout::Error, RuntimeError
        ].cs__freeze
        RESENDING_MESSAGE = '[Reporter] Resending message...'
        END_RESENDING_MESSAGE = '[Reporter] Reporter tried to resend event and received error:'

        # This method is mainly used in inside error handling and startup mesasges sending.
        #
        # @param event [Contrast::Agent::Reporting::ReportingEvent] event to resend
        # @param connection [Net::HTTP] open connection
        # @param error [StandardError] error that was raised
        # @return [Net::HTTPResponse, nil] response from TS
        def try_resend event, connection, error
          # The timeout for agent to sleep is set here, we don't need to raise an error,
          # but also 15 min is a long time to wait for example if event is startup message.
          # Override timeout
          if Contrast::Agent::Reporting::ReporterClientUtils::STARTUP_EVENTS.include?(event.cs__class)
            response_handler.suspend_reporting(RESENDING_MESSAGE,
                                               TIMEOUT,
                                               error,
                                               event: event,
                                               log_error: false,
                                               startup: true)
          end
          mode.enter_resend_mode
          handle_resend(event, connection)
        end

        # This method will handle the error and will decide if we need to resend the event
        #
        # @param event [Contrast::Agent::Reporting::ReportingEvent]
        # @param connection [Net::HTTP] open connection
        # @return [Net::HTTPResponse, nil] response from TS
        def handle_resend event, connection
          if sleep?
            logger.debug("[Reporter] sleeping for #{ TIMEOUT } seconds before resending...")
            Thread.current.send(:sleep, timeout)
            wake_up
          end
          response = mode.status == mode.resending ? send_event(event, connection) : nil
          response_success! if response
          response
        end

        # Handles errors that occurs before the ResponseHandler do not have a response, or response
        # code to hande. This Errors requires different handling, because most of errors here
        # occurs when the response cannot be read, there is open ssl error, or a certain Timeout
        # is reached.
        #
        # @param event [Contrast::Agent::Reporting::ReportingEvent]
        #   One of the DTMs valid for the event field of Contrast::Api::Dtm::Message
        # @param error [StandardError]
        # @param connection [Net::HTTP] open connection
        # @return nil [NilClass] to be passed as response
        def handle_response_error event, connection, error
          return end_of_rescue(event, error) if mode.resend.rescue_attempts >= RESCUE_ATTEMPTS

          if RETRY_ERRORS.include?(error.cs__class)
            mode.resend.increase_rescue_attempts
            try_resend(event, connection, error)
          else
            end_of_rescue(event, error)
          end
        end

        # End of rescue attempts.
        #
        # @param event [Contrast::Agent::Reporting::ReportingEvent]
        # @param error [StandardError]
        # @return [NilClass]
        def end_of_rescue event, error
          if Contrast::Agent::Reporting::ReporterClientUtils::STARTUP_EVENTS.include?(event.cs__class)
            # Agent didn't send it's startup events, There will be reporting errors, disable reporting.
            disable_reporting(event, error)
          else
            # There is an error in one of the reported messages, log that error and continue.
            logger.error(END_RESENDING_MESSAGE,
                         resend_attempts: Contrast::Agent::Reporting::Resend::RESCUE_ATTEMPTS,
                         connection_error: error,
                         client: Contrast::Agent::Reporting::ReporterClient::SERVICE_NAME,
                         event_id: event&.__id__,
                         event_type: event&.cs__class&.cs__name)
          end
        end

        # Disable reporting when there is an error that cannot be handled.
        #
        # @param event [Contrast::Agent::Reporting::ReportingEvent]
        # @param error [StandardError]
        # @return [NilClass]
        def disable_reporting event, error
          logger.error('[Agent] Disabling Reporting...', error: error.message, event_type: event.cs__class&.cs__name)
          Contrast::AGENT.disable!
        end

        # Methods that will initialize the resending state.
        class Status
          # RESEND_STARTUP_EVENTS = 4

          def initialize
            @_rescue_attempts = 0
          end

          # return how many times the client has tried to rescue from error.
          #
          # @return [Integer]
          def rescue_attempts
            @_rescue_attempts ||= 0
          end

          # Reset Error rescues attempts.
          #
          # @return [Integer]
          def reset_rescue_attempts
            @_rescue_attempts = 0
          end

          # Increase the Error rescues attempts.
          #
          # @return [Integer]
          def increase_rescue_attempts
            @_rescue_attempts += 1
          end
        end
      end
    end
  end
end