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

require 'contrast/components/logger'
require 'fileutils'
require 'json'

module Contrast
  module Agent
    module Reporting
      # This class will facilitate the Audit functionality and it will be
      # controlled from the configuration classes
      class Audit
        include Contrast::Components::Logger::InstanceMethods

        attr_reader :path_for_requests, :path_for_responses

        def initialize
          generate_paths if enabled?
        end

        # This method will be handling the auditing of the requests and responses we send to TeamServer. If the audit
        # feature is enabled, we'll log to file the request and/or response JSON objects.
        #
        # @param event [Contrast::Agent::Reporting::ReportingEvent] One of the DTMs valid for the
        #   event field of Contrast::Agent::Reporting::ReportingEvent
        # @param response_data [Net::HTTPResponse]
        def audit_event event, response_data = nil
          return unless ::Contrast::API.request_audit_requests || ::Contrast::API.request_audit_responses

          file_name = event.cs__respond_to?(:file_name) ? event.file_name : event.cs__class.cs__name.to_s.downcase
          data = event.event_json
          log_data(:request, file_name, data) if data
          return unless ::Contrast::API.request_audit_responses

          data = response_data&.body ||
              { response_code: response_data.code, response_body: 'There is no available response body' }.to_json
          log_data(:response, file_name, data)
        rescue StandardError => e
          logger.error('Audit failed with error: ', event: event, error: e)
        end

        private

        # This method will proceed with passing the data with to the writing method
        # @param type [Symbol] This is the type of the file /:request, :response/
        # @param file_name[String] file_name to log
        # @param data[String] String representation if the logged data
        def log_data type, file_name, data = nil
          write_to_file(type, file_name, data) if enabled?
        end

        # This method will be actually writing to the file
        # @param type [Symbol] This is the type of the file /:request, :response/
        # @param event_name [String] the type portion of the file to which to write
        # @param data [any] The data to be written to the file
        def write_to_file type, event_name, data = nil
          time = Contrast::Utils::Timer.now_ms
          destination = type == :request ? path_for_requests : path_for_responses
          # If the feature is disabled or we have yet to create the directory structure, then we could have a nil
          # destination. In that case, take no action
          return unless destination

          # Get process id and thread id to make sure we don't overwrite files
          process_id = Process.pid
          thread_id = Thread.current.object_id
          event_title = event_name.gsub('::', '-')
          filename = "#{ time }_#{ thread_id }_#{ process_id }_#{ event_title }_teamserver_#{ type }.json"
          filepath = File.join(destination, filename)
          logger.debug('Writing to file', eventname: event_name, filename: filename, filepath: filepath)
          # Here is use append mode, because of a slightly possibility of overwriting an existing file
          File.open(filepath, 'a') do |f|
            f.write(Contrast::Utils::StringUtils.force_utf8(data))
            f.close
          end
        rescue StandardError => e
          logger.warn('Saving audit failed', e: e)
        end

        # Here we will generate the directories for the requests and responses
        def generate_paths
          message_directories = File.expand_path(path_to_audits)
          make_directory(message_directories) unless Dir.exist?(message_directories)

          requests_destination = File.expand_path(File.join(message_directories, '/requests'))
          responses_destination = File.expand_path(File.join(message_directories, '/responses'))

          make_directory(requests_destination) if enabled_for_requests? && !Dir.exist?(requests_destination)
          make_directory(responses_destination) if enabled_for_responses? && !Dir.exist?(responses_destination)

          @path_for_requests ||= requests_destination if enabled_for_requests?
          @path_for_responses ||= responses_destination if enabled_for_responses?
        rescue StandardError => e
          logger.warn('Generating the paths failed with: ', e: e)
        end

        # Make the directory provided, including any required intermediary directories.
        # We do this here in order to make allow us to easily override directory
        # creation while testing.
        def make_directory directory
          FileUtils.mkdir_p(directory)
        end

        # Retrieves the configuration value if the request audit is enabled
        # @return [Boolean]
        def enabled?
          ::Contrast::API.request_audit_enable
        end

        # The boolean values for the requests and the responses should be taken under
        # consideration only if it's in combination with enabled
        # So in order for us to actually audit the requests, we need:
        # - enabled? -> ture and enabled_for_requests? -> true
        # The same is for the responses
        #
        #
        # Retrieve the configuration value if the audit for requests is enabled
        # @return [Boolean]
        def enabled_for_requests?
          ::Contrast::API.request_audit_requests
        end

        # Retrieve the configuration value if the audit for responses is enabled
        # @return [Boolean]
        def enabled_for_responses?
          ::Contrast::API.request_audit_requests
        end

        # Retrieve the configuration value for the path of the audits
        # The value will be read from the configuration yml
        # but if it isn't defined any - here will be returned the default path from
        # Contrast::Config::RequestAuditConfiguration::DEFAULT_PATH
        # @return [String]
        def path_to_audits
          ::Contrast::API.request_audit_path
        end
      end
    end
  end
end