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

require 'contrast/agent/assess/policy/trigger_method'
require 'contrast/components/logger'
require 'contrast/components/scope'

module Contrast
  module Utils
    # This utility allows us to report invalid configurations detected in
    # customer applications, as determined by Configuration Rules at runtime.
    module InvalidConfigurationUtil
      include Contrast::Components::Logger::InstanceMethods
      include Contrast::Components::Scope::InstanceMethods

      CS__PATH = 'path'
      CS__SESSION_ID = 'sessionId'
      CS__SNIPPET = 'snippet'

      # Build and report a finding for the given rule
      #
      # @param rule_id [String] the rule that was violated by the configuration
      # @param user_provided_options [Hash] the configuration value(s) which
      #   violated the rule
      # @param call_location [Thread::Backtrace::Location] the location where
      #   the bad configuration was set
      def cs__report_finding rule_id, user_provided_options, call_location
        with_contrast_scope do
          finding = Contrast::Api::Dtm::Finding.new
          finding.version = Contrast::Agent::Assess::Policy::TriggerMethod::CURRENT_FINDING_VERSION
          finding.rule_id = rule_id
          set_properties(finding, user_provided_options, call_location)
          hash = Contrast::Utils::HashDigest.generate_config_hash(finding)
          finding.hash_code = Contrast::Utils::StringUtils.force_utf8(hash)
          finding.preflight = Contrast::Utils::PreflightUtil.create_preflight(finding)
          if Contrast::Agent::Reporter.enabled? # TODO: RUBY-1438 -- remove
            cs__report_new_finding(hash, rule_id, user_provided_options, call_location)
          else
            Contrast::Agent::Assess::Policy::TriggerMethod.report_finding(finding)
          end
        end
      rescue StandardError => e
        logger.error('Unable to build a finding', e, rule: rule_id)
      end

      def cs__report_new_finding hash_code, rule_id, user_provided_options, call_location
        new_preflight = Contrast::Agent::Reporting::Preflight.new
        new_preflight_message = Contrast::Agent::Reporting::PreflightMessage.new
        new_preflight_message.hash_code = hash_code
        new_preflight_message.data = "#{ rule_id },#{ hash_code }"
        new_preflight.messages << new_preflight_message

        ruby_finding = Contrast::Agent::Reporting::Finding.new rule_id
        ruby_finding.hash_code = hash_code
        set_new_finding_properties(ruby_finding, user_provided_options, call_location)
        Contrast::Agent.reporter&.send_event(new_preflight)
        Contrast::Agent::Reporting::ReportingStorage[hash_code] = ruby_finding
      end

      private

      # Set the properties needed to report and subsequently render this finding on the finding given.
      #
      # @param finding [Contrast::Api::Dtm::Finding] the configuration finding to populate
      # @param user_provided_options [Hash] the configuration value(s) which
      #   violated the rule
      # @param call_location [Thread::Backtrace::Location] the location where
      #   the bad configuration was set
      def set_properties finding, user_provided_options, call_location
        path = call_location.path
        # just get the file name, not the full path
        path = path.split(Contrast::Utils::ObjectShare::SLASH).last
        session_id = user_provided_options[:key].to_s if user_provided_options
        finding.properties[CS__SESSION_ID] = Contrast::Utils::StringUtils.force_utf8(session_id)
        finding.properties[CS__PATH] = Contrast::Utils::StringUtils.force_utf8(path)
        file_path = call_location.absolute_path
        snippet = file_snippet(file_path, call_location)
        finding.properties[CS__SNIPPET] = Contrast::Utils::StringUtils.force_utf8(snippet)
      end

      def file_snippet file_path, call_location
        idx = call_location&.lineno
        if file_path && idx && File.exist?(file_path)
          idx = idx > 5 ? idx - 5 : 0
          snippet = +''
          File.foreach(file_path).with_index do |line, line_num|
            next unless line_num >= idx
            break if line_num > idx + 10

            snippet << line
            snippet << Contrast::Utils::ObjectShare::NEW_LINE
          end
          return snippet
        end
        call_location&.label&.dup
      end

      def set_new_finding_properties finding, user_provided_options, call_location
        path = call_location.path
        # just get the file name, not the full path
        path = path.split(Contrast::Utils::ObjectShare::SLASH).last
        session_id = user_provided_options[:key].to_s if user_provided_options
        finding.properties[CS__SESSION_ID] = session_id
        finding.properties[CS__PATH] = path
        file_path = call_location.absolute_path
        snippet = file_snippet(file_path, call_location)
        finding.properties[CS__SNIPPET] = snippet
      end
    end
  end
end