# frozen_string_literal: true

module DoorkeeperMongodb
  module Mixins
    module Mongoid
      module AccessGrantMixin
        extend ActiveSupport::Concern

        include Doorkeeper::OAuth::Helpers
        include Doorkeeper::Models::Expirable
        include Doorkeeper::Models::Revocable
        include Doorkeeper::Models::Accessible
        include Doorkeeper::Models::Scopes
        include Doorkeeper::Models::SecretStorable
        include Doorkeeper::Orm::Concerns::Mongoid::ResourceOwnerable
        include BaseMixin
        include JsonSerializable

        included do
          belongs_to_opts = {
            class_name: "Doorkeeper::Application",
            inverse_of: :access_grants,
          }

          # Doorkeeper 5.3 has custom classes for defining OAuth roles
          if DoorkeeperMongodb.doorkeeper_version?(5, 3)
            belongs_to_opts[:class_name] = Doorkeeper.config.application_class
          end

          # optional associations added in Mongoid 6
          belongs_to_opts[:optional] = true if ::Mongoid::VERSION[0].to_i >= 6

          belongs_to :application, belongs_to_opts

          if Doorkeeper.configuration.try(:polymorphic_resource_owner?)
            belongs_to :resource_owner, polymorphic: true
          end

          validates :resource_owner_id, :application_id, :token, :expires_in, :redirect_uri, presence: true
          validates :token, uniqueness: true

          before_validation :generate_token, on: :create
        end

        # never uses pkce, if pkce migrations were not generated
        def uses_pkce?
          pkce_supported? && code_challenge.present?
        end

        def pkce_supported?
          respond_to? :code_challenge
        end

        def plaintext_token
          if secret_strategy.allows_restoring_secrets?
            secret_strategy.restore_secret(self, :token)
          else
            @raw_token
          end
        end

        module ClassMethods
          # Searches for Doorkeeper::AccessGrant record with the
          # specific token value.
          #
          # @param token [#to_s] token value (any object that responds to `#to_s`)
          #
          # @return [Doorkeeper::AccessGrant, nil] AccessGrant object or nil
          #   if there is no record with such token
          #
          def by_token(token)
            find_by_plaintext_token(:token, token)
          end

          # Revokes AccessGrant records that have not been revoked and associated
          # with the specific Application and Resource Owner.
          #
          # @param application_id [Integer]
          #   ID of the Application
          # @param resource_owner [Mongoid::Document, Integer]
          #   instance of the Resource Owner model
          #
          def revoke_all_for(application_id, resource_owner, clock = Time)
            by_resource_owner(resource_owner)
              .where(application_id: application_id, revoked_at: nil)
              .update_all(revoked_at: clock.now.utc)
          end

          # Implements PKCE code_challenge encoding without base64 padding as described in the spec.
          # https://tools.ietf.org/html/rfc7636#appendix-A
          #   Appendix A.  Notes on Implementing Base64url Encoding without Padding
          #
          #   This appendix describes how to implement a base64url-encoding
          #   function without padding, based upon the standard base64-encoding
          #   function that uses padding.
          #
          #       To be concrete, example C# code implementing these functions is shown
          #   below.  Similar code could be used in other languages.
          #
          #   static string base64urlencode(byte [] arg)
          #   {
          #       string s = Convert.ToBase64String(arg); // Regular base64 encoder
          #       s = s.Split('=')[0]; // Remove any trailing '='s
          #       s = s.Replace('+', '-'); // 62nd char of encoding
          #       s = s.Replace('/', '_'); // 63rd char of encoding
          #       return s;
          #   }
          #
          #   An example correspondence between unencoded and encoded values
          #   follows.  The octet sequence below encodes into the string below,
          #   which when decoded, reproduces the octet sequence.
          #
          #   3 236 255 224 193
          #
          #   A-z_4ME
          #
          # https://ruby-doc.org/stdlib-2.1.3/libdoc/base64/rdoc/Base64.html#method-i-urlsafe_encode64
          #
          # urlsafe_encode64(bin)
          # Returns the Base64-encoded version of bin. This method complies with
          # "Base 64 Encoding with URL and Filename Safe Alphabet" in RFC 4648.
          # The alphabet uses '-' instead of '+' and '_' instead of '/'.

          # @param code_verifier [#to_s] a one time use value (any object that responds to `#to_s`)
          #
          # @return [#to_s] An encoded code challenge based on the provided verifier suitable for PKCE validation
          def generate_code_challenge(code_verifier)
            padded_result = Base64.urlsafe_encode64(Digest::SHA256.digest(code_verifier))
            padded_result.split("=")[0] # Remove any trailing '='
          end

          def pkce_supported?
            new.pkce_supported?
          end

          ##
          # Determines the secret storing transformer
          # Unless configured otherwise, uses the plain secret strategy
          def secret_strategy
            ::Doorkeeper.config.token_secret_strategy
          end

          ##
          # Determine the fallback storing strategy
          # Unless configured, there will be no fallback
          def fallback_secret_strategy
            ::Doorkeeper.config.token_secret_fallback_strategy
          end
        end

        private

        # Generates token value with UniqueToken class.
        #
        # @return [String] token value
        #
        def generate_token
          return if self[:token].present?

          @raw_token = UniqueToken.generate
          secret_strategy.store_secret(self, :token, @raw_token)
        end
      end
    end
  end
end
