# frozen_string_literal: true

module Gitlab
  class LfsToken
    module LfsTokenHelper
      def user?
        actor.is_a?(User)
      end

      def actor_name
        user? ? actor.username : "lfs+deploy-key-#{actor.id}"
      end

      def container_gid
        container ? container.to_gid.to_s : nil
      end
    end

    include LfsTokenHelper

    DEFAULT_EXPIRE_TIME = 7200 # Default value 2 hours

    attr_accessor :actor

    def initialize(actor, container)
      @actor =
        case actor
        when DeployKey, User
          actor
        when Key
          actor.user
        else
          raise 'Bad Actor'
        end

      @container = container
    end

    def token
      HMACToken.new(actor, container).token(DEFAULT_EXPIRE_TIME)
    end

    # When the token is an lfs one and the actor
    # is blocked or the password has been changed,
    # the token is no longer valid
    def token_valid?(token_to_check)
      HMACToken.new(actor, container).token_valid?(token_to_check) && valid_user?
    end

    def deploy_key_pushable?(project)
      actor.is_a?(DeployKey) && actor.can_push_to?(project)
    end

    def type
      user? ? :lfs_token : :lfs_deploy_token
    end

    def valid_user?
      return true unless user?

      !actor.blocked? && !actor.password_expired_if_applicable?
    end

    def authentication_payload(repository_http_path)
      {
        username: actor_name,
        lfs_token: token,
        repository_http_path: repository_http_path,
        expires_in: DEFAULT_EXPIRE_TIME
      }
    end

    def basic_encoding
      ActionController::HttpAuthentication::Basic.encode_credentials(actor_name, token)
    end

    private

    attr_reader :container

    class HMACToken
      include LfsTokenHelper

      def initialize(actor, container)
        @actor = actor
        @container = container
      end

      def token(expire_time)
        hmac_token = JSONWebToken::HMACToken.new(secret)
        hmac_token.expire_time = Time.now + expire_time
        hmac_token[:data] = { actor: actor_name }
        hmac_token[:data][:container_gid] = container_gid if container

        hmac_token.encoded
      end

      def token_valid?(token_to_check)
        decoded_token = JSONWebToken::HMACToken.decode(token_to_check, secret).first
        return false if decoded_token.dig('data', 'actor') != actor_name

        token_container = decoded_token.dig('data', 'container_gid')
        return true if token_container.blank? || container.blank?

        token_container == container_gid
      rescue JWT::DecodeError
        false
      end

      private

      attr_reader :actor, :container

      def secret
        case actor
        when DeployKey, Key
          actor.fingerprint_sha256.first(16) +
            # Since fingerprint is based on the public key, let's take more bytes from db_key_base
            ::Gitlab::Encryption::KeyProvider[:db_key_base_32].encryption_key.secret
        when User
          # Take the last 16 characters as they're more unique than the first 16
          actor.id.to_s + actor.encrypted_password.last(16) +
            ::Gitlab::Encryption::KeyProvider[:db_key_base].encryption_key.secret.first(16)
        end
      end
    end
  end
end
