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

cs__scoped_require 'logger'

cs__scoped_require 'contrast/core_extensions/module'
cs__scoped_require 'contrast/components/interface'

module Contrast
  module Agent
    # This class functions to serve as a wrapper around our logging, as we need
    # to be able to dynamically update level based on updates to TeamServer.
    class LoggerManager
      include Contrast::Components::Interface
      access_component :app_context, :logging, :config

      DEFAULT_NAME     = 'contrast.log'
      DEFAULT_LEVEL    = ::Logger::INFO
      VALID_LEVELS     = %w[INFO WARN DEBUG FATAL UNKNOWN ERROR].cs__freeze
      STDOUT_STR       = 'STDOUT'
      STDERR_STR       = 'STDERR'

      attr_reader :current,
                  :previous_path,
                  :previous_level

      def initialize
        update
      rescue StandardError => e
        Contrast::Agent::FeatureState::Logging::FALLBACK.puts 'Unable to initialize regular logger in LoggerManager.'
        Contrast::Agent::FeatureState::Logging::FALLBACK.puts e.backtrace.join(Contrast::Utils::ObjectShare::NEW_LINE)
      end

      def update log_file = nil, log_level = nil
        config = CONFIG.root.agent.logger

        config_path  = config.path&.length.to_i.positive? ? config.path : nil
        config_level = config.level&.length&.positive? ? config.level : nil

        # config > settings > default
        path        = valid_path(config_path   || log_file)
        level_const = valid_level(config_level || log_level)

        # don't needlessly recreate logger
        return if @current && (path == previous_path) && (level_const == previous_level)

        @previous_path  = path
        @previous_level = level_const

        @current = build(path: path, level_const: level_const)

        @current.debug { 'Initialized new contrast agent logger' }

        self
      rescue StandardError => e
        Contrast::Agent::FeatureState::Logging::FALLBACK.puts 'Unable to process update to LoggerManager.'
        Contrast::Agent::FeatureState::Logging::FALLBACK.puts e.backtrace.join(Contrast::Utils::ObjectShare::NEW_LINE)
      end

      private

      def default_progname
        "Contrast #{ APP_CONTEXT.name }"
      end

      def build path: STDOUT_STR, progname: default_progname, level_const: DEFAULT_LEVEL
        current = case path
                  when STDOUT_STR, STDERR_STR
                    ::Logger.new(Object.cs__const_get(path))
                  else
                    ::Logger.new(path)
                  end

        current.progname = progname
        current.level = level_const

        current.formatter = proc do |severity_f, datetime_f, progname_f, msg_f|
          "#{ datetime_f.strftime('%Y-%m-%d %H:%M:%S,%L') } [#{ progname_f }] #{ severity_f } - #{ msg_f }\n"
        end

        current
      end

      def valid_path path
        path = path.nil? ? Contrast::Utils::ObjectShare::EMPTY_STRING : path
        return path if path == STDOUT_STR
        return path if path == STDERR_STR

        path = DEFAULT_NAME if path.empty?
        if Contrast::Utils::IOUtil.write_permission?(path)
          path
        else
          if File.directory?(path)
            Contrast::Agent::FeatureState::Logging::FALLBACK.puts "Unable to create log in directory: #{ path }. \n Writing to standard out instead"
          else
            Contrast::Agent::FeatureState::Logging::FALLBACK.puts "Unable to write to log file: #{ path } \n Writing to standard out instead"
          end

          STDOUT_STR
        end
      end

      def valid_level level
        level = level.nil? ? DEFAULT_LEVEL : level
        level = 'DEBUG' if level == 'TRACE'
        if VALID_LEVELS.include?(level)
          Object.cs__const_get("::Logger::#{ level }")
        else
          DEFAULT_LEVEL
        end
      rescue StandardError
        DEFAULT_LEVEL
      end
    end
  end
end