require "pact_broker/version"
require "pact_broker/error"
require "semantic_logger"
require "forwardable"
require "pact_broker/config/runtime_configuration"
require "anyway/auto_cast"
require "request_store"

module PactBroker
  def self.configuration
    RequestStore.store[:pact_broker_configuration] ||= Configuration.default_configuration
  end

  def self.set_configuration(configuration)
    RequestStore.store[:pact_broker_configuration] = configuration
  end

  # @private, for testing only
  def self.reset_configuration
    RequestStore.store[:pact_broker_configuration] = Configuration.default_configuration
  end

  class Configuration
    extend Forwardable

    delegate PactBroker::Config::RuntimeConfiguration.getter_and_setter_method_names => :runtime_configuration

    attr_accessor :database_connection
    attr_accessor :example_data_seeder
    attr_accessor :html_pact_renderer, :version_parser, :sha_generator
    attr_accessor :content_security_policy, :hal_browser_content_security_policy_overrides
    attr_accessor :api_error_reporters
    attr_reader :custom_logger
    attr_accessor :policy_builder, :policy_scope_builder
    alias_method :policy_finder=, :policy_builder=
    alias_method :policy_scope_finder=, :policy_scope_builder=

    attr_accessor :runtime_configuration

    def initialize
      @runtime_configuration = PactBroker::Config::RuntimeConfiguration.new
      @before_resource_hook = ->(resource){}
      @after_resource_hook = ->(resource){}
      @authenticate_with_basic_auth = nil
      @authorize = nil
      @api_error_reporters = []
    end

    def self.default_configuration
      require "pact_broker/versions/parse_semantic_version"
      require "pact_broker/pacts/generate_sha"

      config = Configuration.new
      config.html_pact_renderer = default_html_pact_render
      config.version_parser = PactBroker::Versions::ParseSemanticVersion
      config.sha_generator = PactBroker::Pacts::GenerateSha
      config.example_data_seeder = lambda do
        # Do the require in the lambda, not at the top of the file, because we need
        # the database connection to be made before loading any Sequel::Model classes.
        require "pact_broker/db/seed_example_data"
        PactBroker::DB::SeedExampleData.call
      end

      # TODO get rid of unsafe-inline
      config.content_security_policy = {
        script_src: "'self' 'unsafe-inline'",
        style_src: "'self' 'unsafe-inline'",
        img_src: "'self' data: #{URI(config.shields_io_base_url).host}",
        font_src: "'self' data:",
        base_uri: "'self'",
        frame_src: "'self'",
        frame_ancestors: "'self'"
      }
      config.hal_browser_content_security_policy_overrides = {
        script_src: "'self' 'unsafe-inline' 'unsafe-eval'",
        frame_ancestors: "'self'"
      }
      config.policy_builder = -> (object) { DefaultPolicy.new(nil, object) }
      config.policy_scope_builder = -> (scope) { scope }
      config
    end

    def override_runtime_configuration!(overrides)
      new_runtime_configuration = runtime_configuration.dup
      valid_overrides, invalid_overrides = identify_valid_and_invalid_configuration_overrides(new_runtime_configuration, overrides)

      if logger.debug?
        logger.debug("Overriding runtime configuration", overrides: valid_overrides, ignoring: invalid_overrides)
      end

      valid_overrides.each do | key, value |
        override_or_merge_value(new_runtime_configuration, key, value)
      end

      self.runtime_configuration = new_runtime_configuration
    end

    def logger_from_runtime_configuration
      @logger_from_runtime_configuration ||= begin
        SemanticLogger.default_level = runtime_configuration.log_level
        if runtime_configuration.log_stream == :file
          path = runtime_configuration.log_dir + "/pact_broker.log"
          FileUtils.mkdir_p(runtime_configuration.log_dir)
          @default_appender = SemanticLogger.add_appender(file_name: path, formatter: runtime_configuration.log_format)
        else
          @default_appender = SemanticLogger.add_appender(io: $stdout, formatter: runtime_configuration.log_format)
        end
        @logger_from_runtime_configuration = SemanticLogger["pact-broker"]
      end
    end

    def logger
      custom_logger || logger_from_runtime_configuration
    end

    def logger= logger
      if @default_appender && SemanticLogger.appenders.include?(@default_appender)
        SemanticLogger.remove_appender(@default_appender)
        @default_appender = nil
      end
      @custom_logger = logger
    end

    def log_configuration
      logger.info "------------------------------------------------------------------------"
      logger.info "PACT BROKER CONFIGURATION:"
      runtime_configuration.log_configuration(logger)
      logger.info "------------------------------------------------------------------------"
    end

    def self.default_html_pact_render
      lambda { |pact, options|
        require "pact_broker/api/renderers/html_pact_renderer"
        PactBroker::Api::Renderers::HtmlPactRenderer.call pact, options
      }
    end

    def show_backtrace_in_error_response?
      !!(ENV["RACK_ENV"] && ENV["RACK_ENV"].downcase != "production")
    end

    def authentication_configured?
      !!authenticate || !!authenticate_with_basic_auth
    end

    def authenticate &block
      if block_given?
        @authenticate = block
      else
        @authenticate
      end
    end

    def authenticate_with_basic_auth &block
      if block_given?
        @authenticate_with_basic_auth = block
      else
        @authenticate_with_basic_auth
      end
    end

    def authorization_configured?
      !!authorize
    end

    def authorize &block
      if block_given?
        @authorize = block
      else
        @authorize
      end
    end

    def before_resource &block
      if block_given?
        @before_resource_hook = block
      else
        @before_resource_hook
      end
    end

    def after_resource &block
      if block_given?
        @after_resource_hook = block
      else
        @after_resource_hook
      end
    end

    def add_api_error_reporter &block
      if block_given?
        unless block.arity == 2
          raise ConfigurationError.new("api_error_notfifier block must accept two arguments, 'error' and 'options'")
        end
        @api_error_reporters << block
        nil
      end
    end

    def show_webhook_response?
      webhook_host_whitelist&.any?
    end

    def enable_badge_resources= enable_badge_resources
      puts "Pact Broker configuration property `enable_badge_resources` is deprecated. Please use `enable_public_badge_access`"
      self.enable_public_badge_access = enable_badge_resources
    end

    def load_from_database!
      # Can't require a Sequel::Model class before the connection has been set
      require "pact_broker/config/load"
      PactBroker::Config::Load.call(runtime_configuration)
    end

    private

    def identify_valid_and_invalid_configuration_overrides(new_runtime_configuration, overrides)
      valid_overrides = {}
      invalid_overrides = {}

      overrides.each do | key, value |
        if new_runtime_configuration.respond_to?("#{key}=")
          valid_overrides[key] = Anyway::AutoCast.call(value)
        else
          invalid_overrides[key] = Anyway::AutoCast.call(value)
        end
      end

      return valid_overrides, invalid_overrides
    end

    def override_or_merge_value(new_runtime_configuration, key, value)
      # Do a merge if original value and new value are both hashes
      if value.is_a?(Hash) && new_runtime_configuration.public_send(key).is_a?(Hash)
        new_runtime_configuration.public_send("#{key}=", new_runtime_configuration.public_send(key).merge(value))
      else
        new_runtime_configuration.public_send("#{key}=", value)
      end
    end
  end
end
