lib/huggingface_client/hub/enterprise/gated_repos.ex

defmodule HuggingfaceClient.Hub.GatedRepos do
  @moduledoc """
  Manage access requests for gated repositories on the HuggingFace Hub.

  Gated repos require users to request access before downloading. As a repo admin
  you can view pending requests and accept, reject, or cancel them.

  See: https://huggingface.co/docs/hub/models-gated

  ## Example

      # List pending requests for a gated model
      {:ok, requests} = HuggingfaceClient.list_pending_access_requests("my-org/gated-model",
        access_token: "hf_admin_token"
      )

      # Accept a specific user's request
      :ok = HuggingfaceClient.accept_access_request("my-org/gated-model",
        user: "alice",
        access_token: "hf_admin_token"
      )
  """

  alias HuggingfaceClient.Error.InputError
  alias HuggingfaceClient.Hub.Client

  # ── List Requests ─────────────────────────────────────────────────────────────

  @doc """
  Lists pending access requests for a gated repository.
  """
  @spec list_pending(String.t(), keyword()) :: {:ok, [map()]} | {:error, Exception.t()}
  def list_pending(repo, opts \\ []) do
    list_requests(repo, "pending", opts)
  end

  @doc """
  Lists accepted access requests for a gated repository.
  """
  @spec list_accepted(String.t(), keyword()) :: {:ok, [map()]} | {:error, Exception.t()}
  def list_accepted(repo, opts \\ []) do
    list_requests(repo, "accepted", opts)
  end

  @doc """
  Lists rejected access requests for a gated repository.
  """
  @spec list_rejected(String.t(), keyword()) :: {:ok, [map()]} | {:error, Exception.t()}
  def list_rejected(repo, opts \\ []) do
    list_requests(repo, "rejected", opts)
  end

  # ── Manage Requests ───────────────────────────────────────────────────────────

  @doc """
  Accepts a user's access request for a gated repository.

  ## Options

  - `:user` — username of the user to accept (required)
  - `:type` — repo type (default: `:model`)
  - `:access_token`
  """
  @spec accept(String.t(), keyword()) :: :ok | {:error, Exception.t()}
  def accept(repo, opts \\ []) do
    user = opts[:user] || raise InputError, ":user is required"
    manage_request(repo, user, "accept", opts)
  end

  @doc """
  Rejects a user's access request for a gated repository.
  """
  @spec reject(String.t(), keyword()) :: :ok | {:error, Exception.t()}
  def reject(repo, opts \\ []) do
    user = opts[:user] || raise InputError, ":user is required"
    manage_request(repo, user, "reject", opts)
  end

  @doc """
  Cancels a user's accepted access (moves them back to pending).
  """
  @spec cancel(String.t(), keyword()) :: :ok | {:error, Exception.t()}
  def cancel(repo, opts \\ []) do
    user = opts[:user] || raise InputError, ":user is required"
    manage_request(repo, user, "cancel", opts)
  end

  @doc """
  Grants access to a user directly (bypassing the request flow).

  ## Options

  - `:user` — username (required)
  - `:type` — repo type (default: `:model`)
  """
  @spec grant_access(String.t(), keyword()) :: :ok | {:error, Exception.t()}
  def grant_access(repo, opts \\ []) do
    user = opts[:user] || raise InputError, ":user is required"
    token = opts[:access_token]
    type = Keyword.get(opts, :type, :model)

    url = "#{Client.hub_url()}/api/#{type_prefix(type)}#{repo}/user-access/#{user}/grant"

    case Client.post(url, %{}, token, opts) do
      {:ok, _} -> :ok
      {:error, _} = err -> err
    end
  end

  @doc """
  Revokes a user's access to a gated repository.
  """
  @spec revoke_access(String.t(), keyword()) :: :ok | {:error, Exception.t()}
  def revoke_access(repo, opts \\ []) do
    user = opts[:user] || raise InputError, ":user is required"
    token = opts[:access_token]
    type = Keyword.get(opts, :type, :model)

    url = "#{Client.hub_url()}/api/#{type_prefix(type)}#{repo}/user-access/#{user}/revoke"

    case Client.post(url, %{}, token, opts) do
      {:ok, _} -> :ok
      {:error, _} = err -> err
    end
  end

  # ── Private ───────────────────────────────────────────────────────────────────

  defp list_requests(repo, status, opts) do
    token = opts[:access_token]
    type = Keyword.get(opts, :type, :model)
    url = "#{Client.hub_url()}/api/#{type_prefix(type)}#{repo}/user-access?status=#{status}"

    case Client.get(url, token, opts) do
      {:ok, body} when is_list(body) -> {:ok, body}
      {:ok, %{"requests" => list}} -> {:ok, list}
      {:ok, other} -> {:ok, List.wrap(other)}
      err -> err
    end
  end

  defp manage_request(repo, user, action, opts) do
    token = opts[:access_token]
    type = Keyword.get(opts, :type, :model)
    url = "#{Client.hub_url()}/api/#{type_prefix(type)}#{repo}/user-access/#{user}/#{action}"

    case Client.post(url, %{}, token, opts) do
      {:ok, _} -> :ok
      {:error, _} = err -> err
    end
  end

  defp type_prefix(:model), do: ""
  defp type_prefix(:dataset), do: "datasets/"
  defp type_prefix(:space), do: "spaces/"
  defp type_prefix(_), do: ""
end