# 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
    end

    include LfsTokenHelper

    DEFAULT_EXPIRE_TIME = 1800

    attr_accessor :actor

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

    def token(expire_time: DEFAULT_EXPIRE_TIME)
      HMACToken.new(actor).token(expire_time)
    end

    def token_valid?(token_to_check)
      HMACToken.new(actor).token_valid?(token_to_check) ||
        LegacyRedisDeviseToken.new(actor).token_valid?(token_to_check)
    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

    private # rubocop:disable Lint/UselessAccessModifier

    class HMACToken
      include LfsTokenHelper

      def initialize(actor)
        @actor = actor
      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.encoded
      end

      def token_valid?(token_to_check)
        decoded_token = JSONWebToken::HMACToken.decode(token_to_check, secret).first
        decoded_token.dig('data', 'actor') == actor_name
      rescue JWT::DecodeError
        false
      end

      private

      attr_reader :actor

      def secret
        salt + key
      end

      def salt
        case actor
        when DeployKey, Key
          actor.fingerprint.delete(':').first(16)
        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)
        end
      end

      def key
        # Take 16 characters of attr_encrypted_db_key_base, as that's what the
        # cipher needs exactly
        Settings.attr_encrypted_db_key_base.first(16)
      end
    end

    # TODO: LegacyRedisDeviseToken and references need to be removed after
    # next released milestone
    #
    class LegacyRedisDeviseToken
      TOKEN_LENGTH = 50
      DEFAULT_EXPIRY_TIME = 1800 * 1000 # 30 mins

      def initialize(actor)
        @actor = actor
      end

      def token_valid?(token_to_check)
        Devise.secure_compare(stored_token, token_to_check)
      end

      def stored_token
        Gitlab::Redis::SharedState.with { |redis| redis.get(state_key) }
      end

      # This method exists purely to facilitate legacy testing to ensure the
      # same redis key is used.
      #
      def store_new_token(expiry_time_in_ms = DEFAULT_EXPIRY_TIME)
        Gitlab::Redis::SharedState.with do |redis|
          new_token = Devise.friendly_token(TOKEN_LENGTH)
          redis.set(state_key, new_token, px: expiry_time_in_ms)
          new_token
        end
      end

      private

      attr_reader :actor

      def state_key
        "gitlab:lfs_token:#{actor.class.name.underscore}_#{actor.id}"
      end
    end
  end
end