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

require 'json'
require 'net/http'
require 'contrast/components/logger'
require 'contrast/utils/net_http_base'
require 'contrast/config/diagnostics/monitor'
require 'contrast/agent/reporting/connection_status'
require 'contrast/agent/reporting/reporting_utilities/headers'
require 'contrast/agent/reporting/reporting_utilities/endpoints'
require 'contrast/agent/reporting/reporting_events/server_settings'
require 'contrast/agent/reporting/reporting_utilities/response_handler'
require 'contrast/agent/reporting/reporting_events/application_settings'
require 'contrast/agent/reporting/reporting_events/agent_effective_config'
require 'contrast/agent/reporting/reporting_utilities/reporter_client_utils'

module Contrast
  module Agent
    module Reporting
      # This class creates a Net::HTTP client and initiates a connection to the provided result
      # @attr_reader headers [Contrast::Agent::Reporting::Headers]
      class ReporterClient < Contrast::Utils::NetHttpBase
        attr_reader :headers

        include Contrast::Agent::Reporting::Endpoints
        include Contrast::Agent::Reporting::ReporterClientUtils
        include Contrast::Agent::Reporting::ResponseHandlerUtils
        include Contrast::Components::Logger::InstanceMethods

        # @return [Array<Class>] events that may result in configuration changes
        RECORDABLE_EVENTS = [
          Contrast::Agent::Reporting::ServerSettings,
          Contrast::Agent::Reporting::ApplicationSettings,
          Contrast::Agent::Reporting::AgentStartup,
          Contrast::Agent::Reporting::ApplicationStartup
        ].cs__freeze
        SERVICE_NAME = 'Reporter'
        REPORT_CONFIG_WHEN = %w[200 304].cs__freeze

        def initialize
          @headers = Contrast::Agent::Reporting::Headers.new
          super()
        end

        # This method initializes the Net::HTTP client we'll need. it will validate
        # the connection and make the first request. If connection is valid and response
        # is available then the open connection is returned.
        #
        # @return [Net::HTTP, nil] Return open connection or nil
        def initialize_connection
          # for this client we would use proxy and custom certificate file if available
          super(SERVICE_NAME, Contrast::API.api_url, use_proxy: true, use_custom_cert: true)
        end

        # Start the client for first time and sent startup event
        #
        # @param connection [Net::HTTP] open connection
        def startup! connection
          return if status.startup_messages_sent?
          return unless connection

          send_agent_startup(connection)
        end

        # Check event type and send it to appropriate TS endpoint
        #
        # @param event [Contrast::Agent::Reporting::ReportingEvent] The event to send to TeamServer. Really a
        #   child of the ReportingEvent rather than a literal one.
        # @param connection [Net::HTTP] open connection
        # @return response [Net::HTTP::Response, nil] response from TS if no response
        def send_event event, connection
          return unless connection

          logger.debug('[Reporter] Preparing to send reporting event', event_class: event.cs__class)

          request = build_request(event)
          response = connection.request(request)
          audit&.audit_event(event, response) if ::Contrast::API.request_audit_enable
          process_settings_response(response, event)
          report_configuration(response, event)
          process_preflight_response(event, response, connection)
          response
        rescue StandardError => e
          handle_error(event, e)
        end

        # Write effective config to file:
        # If we are here the create and server messages are sent and the code received is
        # 200 or 304. In case of 304 there will be no new settings and we can write current ones.
        # This is done on every settings request.
        #
        # @param response [Contrast::Agent::Reporting::Response, nil]
        # @param event [Contrast::Agent::Reporting::ReportingEvent]
        def report_configuration response, event
          return unless response

          diagnostics.config.determine_config_status(response_handler.last_response_code || response.code)
          return unless REPORT_CONFIG_WHEN.include?(response_handler.last_response_code || response.code)
          return unless RECORDABLE_EVENTS.include?(event&.cs__class)

          logger.info('[Reporter Diagnostics] last response code:', response_code: response_handler.last_response_code)
          diagnostics.write_to_file
          config_event = Contrast::Agent::Reporting::AgentEffectiveConfig.new(diagnostics)
          Contrast::Agent.reporter.send_event(config_event)
        end

        def status
          @_status ||= Contrast::Agent::Reporting::ConnectionStatus.new
        end

        def response_handler
          @_response_handler ||= Contrast::Agent::Reporting::ResponseHandler.new
        end

        def diagnostics
          @_diagnostics ||= Contrast::Config::Diagnostics::Monitor.new(Contrast::LOGGER.path)
        end

        def sleep?
          response_handler.sleep?
        end

        def timeout
          response_handler.timeout
        end

        def mode
          response_handler.mode
        end

        def reset_mode
          response_handler.reset_mode
        end

        def wake_up
          response_handler.wake_up
        end
      end
    end
  end
end