# 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'

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

        # 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
        #
        # @param connection [Net::HTTP] open connection
        def send_agent_startup connection
          logger.debug('Preparing to send startup messages')

          STARTUP_EVENTS.each do |event|
            startup_event = event.new
            send_event(startup_event, connection)
          rescue StandardError => e
            handle_error(startup_event, e)
          end
          logger.debug('Startup messages sent')
        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 standard error case, logs and set status for failure
        #
        # @param event [Contrast::Agent::Reporting::ReportingEvent]
        #   One of the DTMs valid for the event field of Contrast::Api::Dtm::Message
        # @param error_msg [StandardError]
        # @return nil [NilClass] to be passed as response
        def handle_error event, error_msg
          status.failure!
          logger.error('Unable to send message.',
                       error_msg,
                       client: Contrast::Agent::Reporting::ReporterClient::SERVICE_NAME,
                       event_id: event&.__id__,
                       event_type: event&.cs__class&.cs__name)
          nil
        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)
          status.success! if res
          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?('*') }
          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('Unable to handle 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)
            # compress:
            gzip = Zlib::GzipWriter.new(StringIO.new)
            gzip << event.event_json
            request.body = gzip.close.string
            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