# frozen_string_literal: true

require "dry/configurable"

module Hanami
  class Config
    # Hanami actions config
    #
    # This exposes all the settings from the standalone `Hanami::Action` class, pre-configured with
    # sensible defaults for actions within a full Hanami app. It also provides additional settings
    # for further integration of actions with other full stack app components.
    #
    # @since 2.0.0
    # @api public
    class Actions
      include Dry::Configurable

      # @!attribute [rw] cookies
      #   Sets or returns a hash of cookie options for actions.
      #
      #   The hash is wrapped by {Hanami::Config::Actions::Cookies}, which also provides an
      #   `enabled?` method, returning true in the case of any options provided.
      #
      #   @example
      #     config.actions.cookies = {max_age: 300}
      #
      #   @return [Hanami::Config::Actions::Cookies]
      #
      #   @api public
      #   @since 2.0.0
      setting :cookies, default: {}, constructor: -> options { Cookies.new(options) }

      # @!attribute [rw] sessions
      #   Sets or returns the session store (and its options) for actions.
      #
      #   The given values are taken as an argument list to be passed to {Config::Sessions#initialize}.
      #
      #   The configured session store is used when setting up the app or slice
      #   {Slice::ClassMethods#router router}.
      #
      #   @example
      #     config.actions.sessions = :cookie, {secret: "xyz"}
      #
      #   @return [Config::Sessions]
      #
      #   @see Config::Sessions
      #   @see Slice::ClassMethods#router
      #
      #   @api public
      #   @since 2.0.0
      setting :sessions, constructor: proc { |storage, *options| Sessions.new(storage, *options) }

      # @!attribute [rw] csrf_protection
      #   Sets or returns whether CSRF protection should be enabled for action classes.
      #
      #   Defaults to true if {#sessions} is enabled. You can override this by explicitly setting a
      #   true or false value.
      #
      #   When true, this will include `Hanami::Action::CSRFProtection` in all action classes.
      #
      #   @return [Boolean]
      #
      #   @api public
      #   @since 2.0.0
      setting :csrf_protection

      # Returns the Content Security Policy config for actions.
      #
      # The resulting policy is set as a default `"Content-Security-Policy"` response header.
      #
      # @return [Hanami::Config::Actions::ContentSecurityPolicy]
      #
      # @api public
      # @since 2.0.0
      attr_accessor :content_security_policy

      # Returns the proc to generate Content Security Policy nonce values.
      #
      # The current Rack request object is provided as an optional argument
      # to the proc, enabling the generation of nonces based on session IDs.
      #
      # @example Independent random nonce (default)
      #   -> { SecureRandom.urlsafe_base64(16) }
      #
      # @example Session dependent nonce
      #   ->(request) { Digest::SHA256.base64digest(request.session[:uuid])[0, 16] }
      #
      # @return [Proc]
      #
      # @api public
      # @since x.x.x
      setting :content_security_policy_nonce_generator, default: -> { SecureRandom.urlsafe_base64(16) }

      # @!attribute [rw] method_override
      #   Sets or returns whether HTTP method override should be enabled for action classes.
      #
      #   Defaults to true. You can override this by explicitly setting a
      #   true or false value.
      #
      #   When true, this will mount `Rack::MethodOverride` in the Rack middleware stack of the App.
      #
      #   @return [Boolean]
      #
      #   @api public
      #   @since 2.1.0
      setting :method_override, default: true

      # @!attribute [rw] name_inference_base
      #   @api private
      #   @since 2.1.0
      setting :name_inference_base, default: "actions"

      # @!attribute [rw] view_name_inferrer
      #   @api private
      #   @since 2.1.0
      setting :view_name_inferrer, default: Slice::ViewNameInferrer

      # @!attribute [rw] view_name_inference_base
      #   @api private
      #   @since 2.1.0
      setting :view_name_inference_base, default: "views"

      # @api private
      attr_reader :base_config
      protected :base_config

      # @api private
      def initialize(*, **options)
        super()

        @base_config = Hanami::Action.config.dup
        @content_security_policy = ContentSecurityPolicy.new

        configure_defaults
      end

      # @api private
      def initialize_copy(source)
        super
        @base_config = source.base_config.dup
        @content_security_policy = source.content_security_policy.dup
      end
      private :initialize_copy

      # @api private
      def finalize!(app_config)
        @base_config.root_directory = app_config.root

        # A nil value for `csrf_protection` means it has not been explicitly configured
        # (neither true nor false), so we can default it to whether sessions are enabled
        self.csrf_protection = sessions.enabled? if csrf_protection.nil?

        if content_security_policy
          default_headers["Content-Security-Policy"] = content_security_policy.to_s
        end
      end

      # @api public
      # @since x.x.x
      def content_security_policy? = !!@content_security_policy

      private

      # Apply defaults for base config
      def configure_defaults
        self.default_headers = {
          "X-Frame-Options" => "DENY",
          "X-Content-Type-Options" => "nosniff",
          "X-XSS-Protection" => "1; mode=block"
        }
      end

      def method_missing(name, *args, &block)
        if config.respond_to?(name)
          config.public_send(name, *args, &block)
        elsif base_config.respond_to?(name)
          base_config.public_send(name, *args, &block)
        else
          super
        end
      end

      def respond_to_missing?(name, _include_all = false)
        config.respond_to?(name) || base_config.respond_to?(name) || super
      end
    end
  end
end
