# frozen_string_literal: true

require "doorkeeper/config/abstract_builder"
require "doorkeeper/config/option"
require "doorkeeper/config/validations"

module Doorkeeper
  # Doorkeeper option DSL could be reused in extensions to build their own
  # configurations. To use the Option DSL gems need to define `builder_class` method
  # that returns configuration Builder class. This exception raises when they don't
  # define it.
  #
  class Config
    # Default Doorkeeper configuration builder
    class Builder < AbstractBuilder
      # Provide support for an owner to be assigned to each registered
      # application (disabled by default)
      # Optional parameter confirmation: true (default false) if you want
      # to enforce ownership of a registered application
      #
      # @param opts [Hash] the options to confirm if an application owner
      #   is present
      # @option opts[Boolean] :confirmation (false)
      #   Set confirm_application_owner variable
      def enable_application_owner(opts = {})
        @config.instance_variable_set(:@enable_application_owner, true)
        confirm_application_owner if opts[:confirmation].present? && opts[:confirmation]
      end

      def confirm_application_owner
        @config.instance_variable_set(:@confirm_application_owner, true)
      end

      # Provide support for dynamic scopes (e.g. user:*) (disabled by default)
      # Optional parameter delimiter (default ":") if you want to customize
      # the delimiter separating the scope name and matching value.
      #
      # @param opts [Hash] the options to configure dynamic scopes
      def enable_dynamic_scopes(opts = {})
        @config.instance_variable_set(:@enable_dynamic_scopes, true)
        @config.instance_variable_set(:@dynamic_scopes_delimiter, opts[:delimiter] || ':')
      end

      # Define default access token scopes for your provider
      #
      # @param scopes [Array] Default set of access (OAuth::Scopes.new)
      # token scopes
      def default_scopes(*scopes)
        @config.instance_variable_set(:@default_scopes, OAuth::Scopes.from_array(scopes))
      end

      # Define default access token scopes for your provider
      #
      # @param scopes [Array] Optional set of access (OAuth::Scopes.new)
      # token scopes
      def optional_scopes(*scopes)
        @config.instance_variable_set(:@optional_scopes, OAuth::Scopes.from_array(scopes))
      end

      # Define scopes_by_grant_type to limit certain scope to certain grant_type
      # @param { Hash } with grant_types as keys.
      # Default set to {} i.e. no limitation on scopes usage
      def scopes_by_grant_type(hash = {})
        @config.instance_variable_set(:@scopes_by_grant_type, hash)
      end

      # Change the way client credentials are retrieved from the request object.
      # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
      # falls back to the `:client_id` and `:client_secret` params from the
      # `params` object.
      #
      # @param methods [Array] Define client credentials
      def client_credentials(*methods)
        @config.instance_variable_set(:@client_credentials_methods, methods)
      end

      # Change the way access token is authenticated from the request object.
      # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
      # falls back to the `:access_token` or `:bearer_token` params from the
      # `params` object.
      #
      # @param methods [Array] Define access token methods
      def access_token_methods(*methods)
        @config.instance_variable_set(:@access_token_methods, methods)
      end

      # Issue access tokens with refresh token (disabled if not set)
      def use_refresh_token(enabled = true, &block)
        @config.instance_variable_set(
          :@refresh_token_enabled,
          block || enabled,
        )
      end

      # Reuse access token for the same resource owner within an application
      # (disabled by default)
      # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383
      def reuse_access_token
        @config.instance_variable_set(:@reuse_access_token, true)
      end

      # Enable support for multiple database configurations with read replicas.
      # When enabled, wraps database write operations to ensure they use the primary
      # (writable) database when automatic role switching is enabled.
      #
      # For ActiveRecord (Rails 6.1+), this uses `ActiveRecord::Base.connected_to(role: :writing)`.
      # Other ORM extensions can implement their own primary database targeting logic.
      #
      # This prevents `ActiveRecord::ReadOnlyError` when using read replicas with Rails
      # automatic role switching. Enable this if your application uses multiple databases
      # with automatic role switching for read replicas.
      #
      # See: https://guides.rubyonrails.org/active_record_multiple_databases.html#activating-automatic-role-switching
      def enable_multiple_database_roles
        @config.instance_variable_set(:@enable_multiple_database_roles, true)
      end

      # Choose to use the url path for native autorization codes 
      # Enabling this flag sets the authorization code response route for
      # native redirect uris to oauth/authorize/<code>. The default is
      # oauth/authorize/native?code=<code>.
      # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/1143
      def use_url_path_for_native_authorization
        @config.instance_variable_set(:@use_url_path_for_native_authorization, true)
      end

      # TODO: maybe make it more generic for other flows too?
      # Only allow one valid access token obtained via client credentials
      # per client. If a new access token is obtained before the old one
      # expired, the old one gets revoked (disabled by default)
      def revoke_previous_client_credentials_token
        @config.instance_variable_set(:@revoke_previous_client_credentials_token, true)
      end

      # Only allow one valid access token obtained via authorization code
      # per client. If a new access token is obtained before the old one
      # expired, the old one gets revoked (disabled by default)
      def revoke_previous_authorization_code_token
        @config.instance_variable_set(:@revoke_previous_authorization_code_token, true)
      end

      # Require non-confidential apps to use PKCE (send a code_verifier) when requesting
      # an access_token using an authorization code (disabled by default)
      def force_pkce
        @config.instance_variable_set(:@force_pkce, true)
      end

      # Use an API mode for applications generated with --api argument
      # It will skip applications controller, disable forgery protection
      def api_only
        @config.instance_variable_set(:@api_only, true)
      end

      # Enables polymorphic Resource Owner association for Access Grant and
      # Access Token models. Requires additional database columns to be setup.
      def use_polymorphic_resource_owner
        @config.instance_variable_set(:@polymorphic_resource_owner, true)
      end

      # Forbids creating/updating applications with arbitrary scopes that are
      # not in configuration, i.e. `default_scopes` or `optional_scopes`.
      # (disabled by default)
      def enforce_configured_scopes
        @config.instance_variable_set(:@enforce_configured_scopes, true)
      end

      # Enforce request content type as the spec requires:
      # disabled by default for backward compatibility.
      def enforce_content_type
        @config.instance_variable_set(:@enforce_content_type, true)
      end

      # Allow optional hashing of input tokens before persisting them.
      # Will be used for hashing of input token and grants.
      #
      # @param using
      #   Provide a different secret storage implementation class for tokens
      # @param fallback
      #   Provide a fallback secret storage implementation class for tokens
      #   or use :plain to fallback to plain tokens
      def hash_token_secrets(using: nil, fallback: nil)
        default = "::Doorkeeper::SecretStoring::Sha256Hash"
        configure_secrets_for :token,
                              using: using || default,
                              fallback: fallback
      end

      # Allow optional hashing of application secrets before persisting them.
      # Will be used for hashing of input token and grants.
      #
      # @param using
      #   Provide a different secret storage implementation for applications
      # @param fallback
      #   Provide a fallback secret storage implementation for applications
      #   or use :plain to fallback to plain application secrets
      def hash_application_secrets(using: nil, fallback: nil)
        default = "::Doorkeeper::SecretStoring::Sha256Hash"
        configure_secrets_for :application,
                              using: using || default,
                              fallback: fallback
      end

      private

      # Configure the secret storing functionality
      def configure_secrets_for(type, using:, fallback:)
        raise ArgumentError, "Invalid type #{type}" if %i[application token].exclude?(type)

        @config.instance_variable_set(:"@#{type}_secret_strategy", using.constantize)

        if fallback.nil?
          return
        elsif fallback.to_sym == :plain
          fallback = "::Doorkeeper::SecretStoring::Plain"
        end

        @config.instance_variable_set(:"@#{type}_secret_fallback_strategy", fallback.constantize)
      end
    end

    # Replace with `default: Builder` when we drop support of Rails < 5.2
    mattr_reader(:builder_class) { Builder }

    extend Option
    include Validations

    option :resource_owner_authenticator,
           as: :authenticate_resource_owner,
           default: (lambda do |_routes|
             ::Rails.logger.warn(
               I18n.t("doorkeeper.errors.messages.resource_owner_authenticator_not_configured"),
             )

             nil
           end)

    option :admin_authenticator,
           as: :authenticate_admin,
           default: (lambda do |_routes|
             ::Rails.logger.warn(
               I18n.t("doorkeeper.errors.messages.admin_authenticator_not_configured"),
             )

             head :forbidden
           end)

    option :resource_owner_from_credentials,
           default: (lambda do |_routes|
             ::Rails.logger.warn(
               I18n.t("doorkeeper.errors.messages.credential_flow_not_configured"),
             )

             nil
           end)

    # Hooks for authorization
    option :before_successful_authorization,      default: ->(_controller, _context = nil) {}
    option :after_successful_authorization,       default: ->(_controller, _context = nil) {}
    # Hooks for strategies responses
    option :before_successful_strategy_response,  default: ->(_request) {}
    option :after_successful_strategy_response,   default: ->(_request, _response) {}
    # Allows to customize Token Introspection response
    option :custom_introspection_response,        default: ->(_token, _context) { {} }

    option :skip_authorization,             default: ->(_routes) {}
    option :access_token_expires_in,        default: 7200
    option :custom_access_token_expires_in, default: ->(_context) { nil }
    option :authorization_code_expires_in,  default: 600
    option :orm,                            default: :active_record
    option :native_redirect_uri,            default: "urn:ietf:wg:oauth:2.0:oob", deprecated: true
    option :grant_flows,                    default: %w[authorization_code client_credentials]
    option :pkce_code_challenge_methods,    default: %w[plain S256]
    option :handle_auth_errors,             default: :render
    option :token_lookup_batch_size,        default: 10_000
    # Sets the token_reuse_limit
    # It will be used only when reuse_access_token option in enabled
    # By default it will be 100
    # It will be used for token reusablity to some threshold percentage
    # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/1189
    option :token_reuse_limit,              default: 100

    # Don't require client authentication for password grants. If client credentials
    # are present they will still be validated, and the grant rejected if the credentials
    # are invalid.
    #
    # This is discouraged. Spec says that password grants always require a client.
    #
    # See https://github.com/doorkeeper-gem/doorkeeper/issues/1412#issuecomment-632750422
    # and https://github.com/doorkeeper-gem/doorkeeper/pull/1420
    #
    # Since many applications use this unsafe behavior in the wild, this is kept as a
    # not-recommended option. You should be aware that you are not following the OAuth
    # spec, and understand the security implications of doing so.
    option :skip_client_authentication_for_password_grant,
           default: false

    # Hook to allow arbitrary user-client authorization
    option :authorize_resource_owner_for_client,
           default: ->(_client, _resource_owner) { true }

    # Allows to customize OAuth grant flows that +each+ application support.
    # You can configure a custom block (or use a class respond to `#call`) that must
    # return `true` in case Application instance supports requested OAuth grant flow
    # during the authorization request to the server. This configuration +doesn't+
    # set flows per application, it only allows to check if application supports
    # specific grant flow.
    #
    # For example you can add an additional database column to `oauth_applications` table,
    # say `t.array :grant_flows, default: []`, and store allowed grant flows that can
    # be used with this application there. Then when authorization requested Doorkeeper
    # will call this block to check if specific Application (passed with client_id and/or
    # client_secret) is allowed to perform the request for the specific grant type
    # (authorization, password, client_credentials, etc).
    #
    # Example of the block:
    #
    #   ->(flow, client) { client.grant_flows.include?(flow) }
    #
    # In case this option invocation result is `false`, Doorkeeper server returns
    # :unauthorized_client error and stops the request.
    #
    # @param allow_grant_flow_for_client [Proc] Block or any object respond to #call
    # @return [Boolean] `true` if allow or `false` if forbid the request
    #
    option :allow_grant_flow_for_client,    default: ->(_grant_flow, _client) { true }

    # Allows to forbid specific Application redirect URI's by custom rules.
    # Doesn't forbid any URI by default.
    #
    # @param forbid_redirect_uri [Proc] Block or any object respond to #call
    #
    option :forbid_redirect_uri,            default: ->(_uri) { false }

    # WWW-Authenticate Realm (default "Doorkeeper").
    #
    # @param realm [String] ("Doorkeeper") Authentication realm
    #
    option :realm,                          default: "Doorkeeper"

    # Forces the usage of the HTTPS protocol in non-native redirect uris
    # (enabled by default in non-development environments). OAuth2
    # delegates security in communication to the HTTPS protocol so it is
    # wise to keep this enabled.
    #
    # @param [Boolean] boolean_or_block value for the parameter, true by default in
    # non-development environment
    #
    # @yield [uri] Conditional usage of SSL redirect uris.
    # @yieldparam [URI] Redirect URI
    # @yieldreturn [Boolean] Indicates necessity of usage of the HTTPS protocol
    #   in non-native redirect uris
    #
    option :force_ssl_in_redirect_uri,      default: !Rails.env.development?

    # Use a custom class for generating the access token.
    # https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-access-token-generator
    #
    # @param access_token_generator [String]
    #   the name of the access token generator class
    #
    option :access_token_generator,
           default: "Doorkeeper::OAuth::Helpers::UniqueToken"

    # Allows additional data to be received when granting access to an Application, and for this
    # additional data to be sent with subsequently generated access tokens. The access grant and
    # access token models will both need to respond to the specified attribute names.
    #
    # @param attributes [Array] The array of custom attribute names to be saved
    #
    option :custom_access_token_attributes,
           default: []

    # Use a custom class for generating the application secret.
    # https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-application-secret-generator
    #
    # @param application_secret_generator [String]
    #   the name of the application secret generator class
    #
    option :application_secret_generator,
           default: "Doorkeeper::OAuth::Helpers::UniqueToken"

    # Default access token generator is a SecureRandom class from Ruby stdlib.
    # This option defines which method will be used to generate a unique token value.
    #
    # @param default_generator_method [Symbol]
    #   the method name of the default access token generator
    #
    option :default_generator_method, default: :urlsafe_base64

    # The controller Doorkeeper::ApplicationController inherits from.
    # Defaults to ActionController::Base.
    # https://doorkeeper.gitbook.io/guides/configuration/other-configurations#custom-controllers
    #
    # @param base_controller [String] the name of the base controller
    option :base_controller,
           default: (lambda do
             api_only ? "ActionController::API" : "ActionController::Base"
           end)

    # The controller Doorkeeper::ApplicationMetalController inherits from.
    # Defaults to ActionController::API.
    #
    # @param base_metal_controller [String] the name of the base controller
    option :base_metal_controller,
           default: "ActionController::API"

    option :access_token_class,
           default: "Doorkeeper::AccessToken"

    option :access_grant_class,
           default: "Doorkeeper::AccessGrant"

    option :application_class,
           default: "Doorkeeper::Application"

    # Allows to set blank redirect URIs for Applications in case
    # server configured to use URI-less grant flows.
    #
    option :allow_blank_redirect_uri,
           default: (lambda do |grant_flows, _application|
             grant_flows.exclude?("authorization_code") &&
               grant_flows.exclude?("implicit")
           end)

    # Configure protection of token introspection request.
    # By default this configuration allows to introspect a token by
    # another token of the same application, or to introspect the token
    # that belongs to authorized client, or access token has been introspected
    # is a public one (doesn't belong to any client)
    #
    # You can define any custom rule you need or just disable token
    # introspection at all.
    #
    # @param token [Doorkeeper::AccessToken]
    #   token to be introspected
    #
    # @param authorized_client [Doorkeeper::Application]
    #   authorized client (if request is authorized using Basic auth with
    #   Client Credentials for example)
    #
    # @param authorized_token [Doorkeeper::AccessToken]
    #   Bearer token used to authorize the request
    #
    option :allow_token_introspection,
           default: (lambda do |token, authorized_client, authorized_token|
             if authorized_token
               authorized_token.application == token&.application
             elsif token&.application
               authorized_client == token.application
             else
               true
             end
           end)

    attr_reader :reuse_access_token,
                :enable_multiple_database_roles,
                :token_secret_fallback_strategy,
                :application_secret_fallback_strategy

    def clear_cache!
      %i[
        application_model
        access_token_model
        access_grant_model
      ].each do |var|
        remove_instance_variable("@#{var}") if instance_variable_defined?("@#{var}")
      end
    end

    # Doorkeeper Access Token model class.
    #
    # @return [ActiveRecord::Base, Mongoid::Document, Sequel::Model]
    #
    def access_token_model
      @access_token_model ||= access_token_class.constantize
    end

    # Doorkeeper Access Grant model class.
    #
    # @return [ActiveRecord::Base, Mongoid::Document, Sequel::Model]
    #
    def access_grant_model
      @access_grant_model ||= access_grant_class.constantize
    end

    # Doorkeeper Application model class.
    #
    # @return [ActiveRecord::Base, Mongoid::Document, Sequel::Model]
    #
    def application_model
      @application_model ||= application_class.constantize
    end

    def api_only
      @api_only ||= false
    end

    def enforce_content_type
      @enforce_content_type ||= false
    end

    def refresh_token_enabled?
      if defined?(@refresh_token_enabled)
        @refresh_token_enabled
      else
        false
      end
    end

    def resolve_controller(name)
      config_option = public_send(:"#{name}_controller")
      controller_name = if config_option.respond_to?(:call)
                          instance_exec(&config_option)
                        else
                          config_option
                        end

      controller_name.constantize
    end

    def revoke_previous_client_credentials_token?
      option_set? :revoke_previous_client_credentials_token
    end

    def revoke_previous_authorization_code_token?
      option_set? :revoke_previous_authorization_code_token
    end

    def force_pkce?
      option_set? :force_pkce
    end

    def enforce_configured_scopes?
      option_set? :enforce_configured_scopes
    end

    def enable_application_owner?
      option_set? :enable_application_owner
    end

    def enable_dynamic_scopes?
      option_set? :enable_dynamic_scopes
    end

    def dynamic_scopes_delimiter
      @dynamic_scopes_delimiter
    end

    def polymorphic_resource_owner?
      option_set? :polymorphic_resource_owner
    end

    def confirm_application_owner?
      option_set? :confirm_application_owner
    end

    def raise_on_errors?
      handle_auth_errors == :raise
    end

    def redirect_on_errors?
      handle_auth_errors == :redirect
    end

    def application_secret_hashed?
      instance_variable_defined?(:"@application_secret_strategy")
    end

    def token_secret_strategy
      @token_secret_strategy ||= ::Doorkeeper::SecretStoring::Plain
    end

    def application_secret_strategy
      @application_secret_strategy ||= ::Doorkeeper::SecretStoring::Plain
    end

    def default_scopes
      @default_scopes ||= OAuth::Scopes.new
    end

    def optional_scopes
      @optional_scopes ||= OAuth::Scopes.new
    end

    def scopes
      @scopes ||= default_scopes + optional_scopes
    end

    def scopes_by_grant_type
      @scopes_by_grant_type ||= {}
    end

    def pkce_code_challenge_methods_supported
      return [] unless access_grant_model.pkce_supported?
      
      pkce_code_challenge_methods
    end

    def client_credentials_methods
      @client_credentials_methods ||= %i[from_basic from_params]
    end

    def access_token_methods
      @access_token_methods ||= %i[
        from_bearer_authorization
        from_access_token_param
        from_bearer_param
      ]
    end

    def enabled_grant_flows
      @enabled_grant_flows ||= calculate_grant_flows.map { |name| Doorkeeper::GrantFlow.get(name) }.compact
    end

    def authorization_response_flows
      @authorization_response_flows ||= enabled_grant_flows.select(&:handles_response_type?) +
                                        deprecated_authorization_flows
    end

    def token_grant_flows
      @token_grant_flows ||= calculate_token_grant_flows
    end

    def authorization_response_types
      authorization_response_flows.map(&:response_type_matches)
    end

    def token_grant_types
      token_grant_flows.map(&:grant_type_matches)
    end

    # [NOTE]: deprecated and will be removed soon
    def deprecated_token_grant_types_resolver
      @deprecated_token_grant_types ||= calculate_token_grant_types
    end
    
    def native_authorization_code_route
      @use_url_path_for_native_authorization = false unless defined?(@use_url_path_for_native_authorization)
      @use_url_path_for_native_authorization ? '/:code' : '/native'
    end

    # [NOTE]: deprecated and will be removed soon
    def deprecated_authorization_flows
      response_types = calculate_authorization_response_types

      if response_types.any?
        ::Kernel.warn <<~WARNING
          Please, don't patch Doorkeeper::Config#calculate_authorization_response_types method.
          Register your custom grant flows using the public API:
          `Doorkeeper::GrantFlow.register(grant_flow_name, **options)`.
        WARNING
      end

      response_types.map do |response_type|
        Doorkeeper::GrantFlow::FallbackFlow.new(response_type, response_type_matches: response_type)
      end
    end

    # [NOTE]: deprecated and will be removed soon
    def calculate_authorization_response_types
      []
    end

    # [NOTE]: deprecated and will be removed soon
    def calculate_token_grant_types
      types = grant_flows - ["implicit"]
      types << "refresh_token" if refresh_token_enabled?
      types
    end

    # Calculates grant flows configured by the user in Doorkeeper
    # configuration considering registered aliases that is exposed
    # to single or multiple other flows.
    #
    def calculate_grant_flows
      configured_flows = grant_flows.map(&:to_s)
      aliases = Doorkeeper::GrantFlow.aliases.keys.map(&:to_s)

      flows = configured_flows - aliases
      aliases.each do |flow_alias|
        next unless configured_flows.include?(flow_alias)

        flows.concat(Doorkeeper::GrantFlow.expand_alias(flow_alias))
      end

      flows.flatten.uniq
    end

    def allow_blank_redirect_uri?(application = nil)
      if allow_blank_redirect_uri.respond_to?(:call)
        allow_blank_redirect_uri.call(grant_flows, application)
      else
        allow_blank_redirect_uri
      end
    end

    def allow_grant_flow_for_client?(grant_flow, client)
      return true unless option_defined?(:allow_grant_flow_for_client)

      allow_grant_flow_for_client.call(grant_flow, client)
    end

    def option_defined?(name)
      instance_variable_defined?("@#{name}")
    end

    private

    # Helper to read boolearized configuration option
    def option_set?(instance_key)
      var = instance_variable_get("@#{instance_key}")
      !!(defined?(var) && var)
    end

    def calculate_token_grant_flows
      flows = enabled_grant_flows.select(&:handles_grant_type?)
      flows << Doorkeeper::GrantFlow.get("refresh_token") if refresh_token_enabled?
      flows
    end
  end
end
