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

require 'zlib'
require 'stringio'
require 'contrast/components/logger'
require 'contrast/components/scope'
require 'contrast/agent/reporting/reporting_events/application_startup'
require 'contrast/agent/reporting/reporting_utilities/reporter_client'
require 'contrast/agent/reporting/reporting_utilities/endpoints'
require 'contrast/agent/reporting/reporting_utilities/resend'

module Contrast
  module Agent
    module Reporting
      # This module holds utilities required by the reporting service client
      module ReporterClientUtils
        include Contrast::Agent::Reporting::Resend
        include Contrast::Components::Logger::InstanceMethods
        include Contrast::Components::Scope::InstanceMethods
        include Contrast::Agent::Reporting::Endpoints

        STRING_ENCODING = 'BINARY'

        # List the events that need to be sent when agent starts up.
        # The order here matters -- DO NOT CHANGE IT!!! >_< - HM
        #
        # If you add more, update the test in reporter_client_spec.rb
        STARTUP_EVENTS = [
          Contrast::Agent::Reporting::AgentStartup,
          Contrast::Agent::Reporting::ApplicationStartup
        ].cs__freeze

        def audit
          @_audit ||= Contrast::Agent::Reporting::Audit.new
        end

        private

        # Send Agent Startup event. If error occurs, it will try to resend the message.
        #
        # @param connection [Net::HTTP] open connection
        def send_agent_startup connection
          logger.debug('[Reporter] Preparing to send startup messages')
          STARTUP_EVENTS.each { |event| send_event(event.new, connection) }
          logger.debug('[Reporter] Startup messages sent.') if status.startup_messages_sent?
        end

        # Disable reporting and log the error
        #
        # @param event [Contrast::Agent::Reporting::ReportingEvent]
        # @param error [StandardError]
        def disable_reporting event, error
          status.failure!
          mode.resend.reset_rescue_attempts
          mode.status = mode.disabled
          message = '[Reporter] Unable to send message.'
          response_handler.stop_reporting(message,
                                          application: Contrast::APP_CONTEXT.name, # rubocop:disable Security/Module/Name
                                          connection_error: error,
                                          client: Contrast::Agent::Reporting::ReporterClient::SERVICE_NAME,
                                          event_id: event&.__id__,
                                          event_type: event&.cs__class&.cs__name)
          nil
        end

        def response_success!
          status.success!
          mode.enter_run_mode
          mode.resend.reset_rescue_attempts
        end

        # This method will build headers of the request required for TS communication
        #
        # @param request [Net::HTTPRequest]
        # @return [Net::HTTPRequest]
        def build_headers request
          build_application_headers(request)
          build_encode_and_compress_headers(request)
          request['Authorization'] = @headers.authorization
          request['Server-Name'] = @headers.server_name
          request['Server-Path'] = @headers.server_path
          request['Server-Type'] = @headers.server_type
          request['X-Contrast-Agent'] = @headers.agent_version
          request['X-Contrast-Header-Encoding'] = @headers.encoding
          request['Session-ID'] = @headers.session_id
          request
        end

        # Handles response processing and sets status
        #
        # @param event [Contrast::Agent::Reporting::ReportingEvent] The event sent to TeamServer.
        # @param response [Net::HTTP::Response]
        def process_settings_response response, event
          res = response_handler.process(response, event)
          if res
            status.success!
            mode.resend.reset_rescue_attempts
          else
            status.failure!
          end
          res
        end

        # Given a response from preflght, when the finding hash is desired, then send the finding to which it pertains.
        # The method accepts any Contrast::Agent::Reporting::ReportingEvent, but will short circuit if it is not a
        # Contrast::Agent::Reporting::Preflight.
        #
        # @param event [Contrast::Agent::Reporting::ReportingEvent] The event to send to TeamServer. Really a
        #   child of the ReportingEvent rather than a literal one.
        # @param response [Net::HTTPResponse,nil] The response we handle and read from
        # @param connection [Net::HTTP] open connection
        def process_preflight_response event, response, connection
          return unless event.cs__is_a?(Contrast::Agent::Reporting::Preflight)
          return unless response&.body && connection

          findings_to_return = response.body.split(',').delete_if { |el| el.include?('*') }
          mode.resend.reset_rescue_attempts
          findings_to_return.each do |index|
            preflight_message = event.messages[index.to_i]
            corresponding_finding = Contrast::Agent::Reporting::ReportingStorage.delete(preflight_message&.data)
            next unless corresponding_finding

            send_event(corresponding_finding, connection)
          end
        rescue StandardError => e
          logger.error('[Reporter] Unable to handle preflight response', e)
        end

        # Convert the given event into an appropriate Net::HTTPRequest object, setting the request headers and
        # assigning endpoint the endpoint appropriate for the event and casting its hash to a JSON body.
        #
        # @param event event [Contrast::Agent::Reporting::ReportingEvent] The event to send to TeamServer. Really a
        #   child of the ReportingEvent rather than a literal one.
        # @return [Net::HTTP::Post,Net::HTTP::Put]
        def build_request event
          with_contrast_scope do
            request = case event.event_method
                      when :PUT
                        Net::HTTP::Put.new(event.event_endpoint)
                      when :GET
                        Net::HTTP::Get.new(event.event_endpoint)
                      else # :POST
                        Net::HTTP::Post.new(event.event_endpoint)
                      end
            build_headers(request)
            event.attach_headers(request)
            request.body = compress_event(event)
            request
          end
        end

        # Adds the compression and encoding Headers required for sending
        # compress and encoded body payload.
        #
        # @param request [Net::HTTPRequest]
        def build_encode_and_compress_headers request
          request['Content-Type'] = @headers.content_type
          request['X-Contrast-Header-Encoding'] = @headers.encoding
          request['X-Contrast-Encoding'] = @headers.compression
          request['Content-Encoding'] = @headers.compression
        end

        # Adds corresponding application headers to request.
        #
        # @param request [Net::HTTPRequest]
        def build_application_headers request
          app_version = @headers.app_version
          request['API-Key'] = @headers.api_key
          request['Application-Language'] = @headers.app_language
          request['Application-Name'] = @headers.app_name
          request['Application-Path'] = @headers.app_path
          request['Application-Version'] = app_version if app_version
        end
      end
    end
  end
end