# frozen_string_literal: true

class ApiKey < ActiveRecord::Base
  class KeyAccessError < StandardError
  end

  has_many :api_key_scopes
  belongs_to :user
  belongs_to :created_by, class_name: "User"

  scope :active, -> { where("revoked_at IS NULL") }
  scope :revoked, -> { where.not(revoked_at: nil) }
  scope :with_key, ->(key) { where(key_hash: ApiKey.hash_key(key)) }

  validates :description, length: { maximum: 255 }
  validate :valid_scope_mode_scopes

  enum :scope_mode, %i[global read_only granular].freeze

  after_initialize :generate_key

  def generate_key
    if !key_hash
      @key ||= SecureRandom.hex(32) # Not saved to DB
      self.truncated_key = key[0..3]
      self.key_hash = ApiKey.hash_key(key)
    end
  end

  def key
    unless key_available?
      raise KeyAccessError.new "API key is only accessible immediately after creation"
    end
    @key
  end

  def key_available?
    @key.present?
  end

  def self.last_used_epoch
    SiteSetting.api_key_last_used_epoch.presence
  end

  def self.revoke_unused_keys!
    return if SiteSetting.revoke_api_keys_unused_days == 0 # Never expire keys
    to_revoke =
      active.where(
        "GREATEST(last_used_at, created_at, updated_at, :epoch) < :threshold",
        epoch: last_used_epoch,
        threshold: SiteSetting.revoke_api_keys_unused_days.days.ago,
      )

    to_revoke.find_each do |api_key|
      ApiKey.transaction do
        api_key.update!(revoked_at: Time.zone.now)

        StaffActionLogger.new(Discourse.system_user).log_api_key(
          api_key,
          UserHistory.actions[:api_key_update],
          changes: api_key.saved_changes,
          context:
            I18n.t(
              "staff_action_logs.api_key.automatic_revoked",
              count: SiteSetting.revoke_api_keys_unused_days,
            ),
        )
      end
    end
  end

  def self.revoke_max_life_keys!
    return if SiteSetting.revoke_api_keys_maxlife_days == 0

    revoke_days_ago = SiteSetting.revoke_api_keys_maxlife_days.days.ago
    to_revoke = ApiKey.active.where("created_at < ?", revoke_days_ago)

    to_revoke.find_each do |api_key|
      ApiKey.transaction do
        api_key.update!(revoked_at: Time.zone.now)

        StaffActionLogger.new(Discourse.system_user).log_api_key(
          api_key,
          UserHistory.actions[:api_key_update],
          changes: api_key.saved_changes,
          context:
            I18n.t(
              "staff_action_logs.api_key.automatic_revoked_max_life",
              count: SiteSetting.revoke_api_keys_maxlife_days,
            ),
        )
      end
    end
  end

  def self.hash_key(key)
    Digest::SHA256.hexdigest key
  end

  def request_allowed?(env)
    if allowed_ips.present? && allowed_ips.none? { |ip| ip.include?(Rack::Request.new(env).ip) }
      return false
    end
    if RouteMatcher.new(methods: :get, actions: %w[session#scopes about#index]).match?(env: env)
      return true
    end

    api_key_scopes.blank? || api_key_scopes.any? { |s| s.permits?(env) }
  end

  def update_last_used!(now = Time.zone.now)
    return if last_used_at && (last_used_at > 1.minute.ago)

    # using update_column to avoid the AR transaction
    update_column(:last_used_at, now)
  end

  private

  def valid_scope_mode_scopes
    if granular? && api_key_scopes.empty?
      errors.add(
        :api_key_scopes,
        I18n.t("activerecord.errors.models.api_key.base.at_least_one_granular_scope"),
      )
    elsif read_only? && !valid_read_only_scope?
      errors.add(:api_key_scopes, I18n.t("activerecord.errors.models.api_key.base.read_only_scope"))
    end
  end

  def valid_read_only_scope?
    api_key_scopes.size == 1 && api_key_scopes.first.resource == "global" &&
      api_key_scopes.first.action == "read"
  end
end

# == Schema Information
#
# Table name: api_keys
#
#  id            :integer          not null, primary key
#  allowed_ips   :inet             is an Array
#  description   :text
#  hidden        :boolean          default(FALSE), not null
#  key_hash      :string           not null
#  last_used_at  :datetime
#  revoked_at    :datetime
#  scope_mode    :integer
#  truncated_key :string           not null
#  created_at    :datetime         not null
#  updated_at    :datetime         not null
#  created_by_id :integer
#  user_id       :integer
#
# Indexes
#
#  index_api_keys_on_key_hash  (key_hash)
#  index_api_keys_on_user_id   (user_id)
#
