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

require 'logger'
require 'singleton'

require 'contrast/extension/module'
require 'contrast/logger/application'
require 'contrast/logger/format'
require 'contrast/logger/request'
require 'contrast/logger/time'
require 'contrast/logger/log'
require 'contrast/components/config'
require 'contrast/utils/log_utils'

module Contrast
  # Used as a wrapper around our logging. The module option specifically adds in a new method for error that raises the
  # logged exception, used in testing so that we can see if anything unexpected happens without it being swallowed
  # while still providing safe options for customers.
  module Logger
    # For development set following env var to raise logged exceptions instead of just logging.
    if ENV['CONTRAST__AGENT__RUBY_MORE_COWBELL']
      ::Logger.class_eval do
        alias_method :cs__error, :error
        alias_method :cs__warn, :warn

        def error *args, **kwargs
          if kwargs.empty?
            cs__error(*args)
          else
            cs__error(*args, **kwargs)
          end
          args.each { |arg| raise arg if arg && arg.cs__class < Exception }
        end
      end
    end

    # This is the CEF Logger implementation. It uses the default ::Logger.
    class CEFLog
      include Singleton
      include ::Contrast::Utils::LogUtils
      include ::Contrast::Utils::CEFLogUtils

      attr_reader :previous_path, :previous_level

      def initialize
        build_logger
      end

      # Given new settings from TeamServer, update our logging to use the new file and level, assuming they weren't
      # set by local configuration.
      #
      # @param log_level [String] the level at which to log, as provided by TeamServer settings
      def build_logger log_level = nil
        current_level_const = find_valid_level(log_level)
        level_change = current_level_const != previous_level

        # don't needlessly recreate logger
        return if @cef_logger && !level_change

        @previous_level = current_level_const

        @_cef_logger = build(path: DEFAULT_CEF_NAME, level_const: current_level_const)
        # If we're logging to a new path, then let's start it w/ our helpful
        # data gathering messages
        # log_update if path_change
      rescue StandardError => e
        # rubocop:disable Rails/Output
        if @_cef_logger
          @_cef_logger.error('Unable to process update to LoggerManager.', e)
        else
          puts 'Unable to process update to LoggerManager.'
          raise e if ENV['CONTRAST__AGENT__RUBY_MORE_COWBELL']

          puts e.message
          puts e.backtrace.join("\n")
        end
        # rubocop:enable Rails/Output
      end

      def cef_logger
        @_cef_logger
      end

      def log msg, level = @_cef_logger.level
        case level
        when ::Logger::Severity::INFO
          @_cef_logger.info(msg)
        when ::Logger::Severity::ERROR
          @_cef_logger.error(msg)
        when ::Logger::Severity::WARN
          @_cef_logger.warn(msg)
        when ::Logger::Severity::FATAL
          @_cef_logger.fatal(msg)
        else
          @_cef_logger.debug(msg)
        end
      end

      def virtual_patch_message patch, outcome
        message = "Virtual Patch #{ patch.fetch(:name, '') } - #{ patch[:uuid] } was triggered by this request."
        log [message, patch, outcome], ::Logger::Severity::DEBUG
      end

      def bot_blocking_message matching_bot, outcome
        message = "User agent #{ matching_bot[:user_agent] } matched the disallowed value #{ matching_bot[:bot] }"
        log [message, matching_bot, outcome], ::Logger::Severity::DEBUG
      end

      def ip_denylisted_message remote_ip, block_entry, outcome
        message = "IP Address #{ remote_ip } matched the disallowed value" \
                  "#{ block_entry[:ip] } in the IP Blacklist #{ block_entry[:uuid] }"
        log [message, block_entry, outcome], ::Logger::Severity::DEBUG
      end

      def successful_attack rule_id, outcome, input_type = nil, input_value = nil
        if input_type.present? && input_value.present?
          successful_attack_with_input = "#{ input_type } had a value that successfully exploited" \
                                         "#{ rule_id } - #{ input_value }"
          log [successful_attack_with_input, rule_id, outcome], ::Logger::Severity::WARN
        else
          successful_attack_wo_input = "An effective attack was detected against #{ rule_id }"
          log [successful_attack_wo_input, rule_id, outcome], ::Logger::Severity::WARN
        end
      end

      def ineffective_attack rule_id, outcome, input_type = nil, input_value = nil
        if input_type.present? && input_value.present?
          ineffective_attack_with_input = "#{ input_type } had a value that matched a signature for, " \
                                          "but did not successfully exploit #{ rule_id } - #{ input_value }"
          log [ineffective_attack_with_input, rule_id, outcome], ::Logger::Severity::WARN
        else
          ineffective_attack_wo_input = "An unsuccessful attack was detected against #{ rule_id }"
          log [ineffective_attack_wo_input, rule_id, outcome], ::Logger::Severity::WARN
        end
      end

      # newer - currently not in the agent, currently is a probe for us
      def suspicious_attack rule_id, outcome, input_type = nil, input_value = nil
        if input_type.present? && input_value.present?
          suspicious_attack_with = "#{ input_type } included a potential attack value that was detected" \
                                   "as suspicious using #{ rule_id } - #{ input_value }"
          log [suspicious_attack_with, rule_id, outcome], ::Logger::WARN
        elsif input_value.present?
          suspicious_attack_without = "Suspicious activity indicates a potential attack using #{ rule_id }"
          log [suspicious_attack_without, rule_id, outcome], ::Logger::WARN
        end
      end
    end
  end
end