# AUTOGENERATED: This file was generated using the mix task `lexgen`.
defmodule Atproto do
  @default_pds_hostname Application.compile_env(
                          :atproto,
                          :default_pds_hostname,
                          "https://bsky.social"
                        )

  @typedoc """
  A type representing the names of the options that can be passed to `query/3` and `procedure/3`.
  """
  @type xrpc_opt :: :pds_hostname | :authorization

  @typedoc """
  A keyword list of options that can be passed to `query/3` and `procedure/3`.
  """
  @type xrpc_opts :: [{xrpc_opt(), any()}]

  @doc """
  Converts a JSON string, or decoded JSON map, into a struct based on the given module.

  This function uses `String.to_existing_atom/1` to convert the keys of the map to atoms, meaning this will throw an error if the input JSON contains keys which are not already defined as atoms in the existing structs or codebase.
  """
  @spec decode_to_struct(module(), binary() | map()) :: map()
  def decode_to_struct(module, json) when is_binary(json) do
    decode_to_struct(module, Jason.decode!(json, keys: :atoms!))
  end

  def decode_to_struct(module, map) when is_map(map) do
    Map.merge(module.new(), map)
  end

  @doc """
  Raises an error if any required parameters are missing from the given map.
  """
  @spec ensure_required(map(), [String.t()]) :: map()
  def ensure_required(params, required) do
    if Enum.all?(required, fn key -> Map.has_key?(params, key) end) do
      params
    else
      raise ArgumentError, "Missing one or more required parameters: #{Enum.join(required, ", ")}"
    end
  end

  @doc """
  Executes a "GET" HTTP request and returns the response body as a map.

  If the `:pds_hostname` option is not provided, the default PDS hostname as provided in the compile-time configuration will be used.
  """
  @spec query(map(), String.t(), xrpc_opts()) :: Req.Request.t()
  def query(params, target, opts \\ []) do
    target
    |> endpoint(opts)
    |> URI.new!()
    |> URI.append_query(URI.encode_query(params))
    |> Req.get(build_req_auth(opts))
    |> handle_response(opts)
  end

  @doc """
  Executes a "POST" HTTP request and returns the response body as a map.

  If the `:pds_hostname` option is not provided, the default PDS hostname as provided in the compile-time configuration will be used.
  """
  @spec procedure(map(), String.t(), xrpc_opts()) :: {:ok | :refresh | :error, map()}
  def procedure(params, target, opts \\ []) do
    req_opts =
      opts
      |> build_req_auth()
      |> build_req_headers(opts, target)
      |> build_req_body(params, target)

    target
    |> endpoint(opts)
    |> URI.new!()
    |> Req.post(req_opts)
    |> handle_response(opts)
  end

  defp build_req_auth(opts) do
    case Keyword.get(opts, :access_token) do
      nil ->
        case Keyword.get(opts, :admin_token) do
          nil ->
            []

          token ->
            [auth: {:basic, "admin:#{token}"}]
        end

      token ->
        [auth: {:bearer, token}]
    end
  end

  defp build_req_headers(req_opts, opts, "com.atproto.repo.uploadBlob") do
    [
      {:headers,
       [
         {"Content-Type", Keyword.fetch!(opts, :content_type)},
         {"Content-Length", Keyword.fetch!(opts, :content_length)}
       ]}
      | req_opts
    ]
  end

  defp build_req_headers(req_opts, _opts, _target), do: req_opts

  defp build_req_body(opts, blob, "com.atproto.repo.uploadBlob") do
    [{:body, blob} | opts]
  end

  defp build_req_body(opts, %{} = params, _target) when map_size(params) > 0 do
    [{:json, params} | opts]
  end

  defp build_req_body(opts, _params, _target), do: opts

  defp endpoint(target, opts) do
    (Keyword.get(opts, :pds_hostname) || @default_pds_hostname) <> "/xrpc/" <> target
  end

  defp handle_response({:ok, %Req.Response{} = response}, opts) do
    case response.status do
      x when x in 200..299 ->
        {:ok, response.body}

      _ ->
        if response.body["error"] == "ExpiredToken" do
          {:ok, user} =
            Com.Atproto.Server.RefreshSession.main(%{},
              access_token: Keyword.get(opts, :refresh_token)
            )

          {:refresh, user}
        else
          {:error, response.body}
        end
    end
  end

  defp handle_response(error, _opts), do: error

  @doc """
  Converts a "map-like" entity into a standard map. This will also omit any entries that have a `nil` value.

  This is useful for converting structs or schemas into regular maps before sending them over XRPC requests.

  You may optionally pass in an keyword list of options:

  - `:stringify` - `boolean` - If `true`, converts the keys to strings. Otherwise, converts keys to atoms. Default is `false`.
    - *Note*: When `false`, this feature uses the `to_existing_atom/1` function to avoid reckless conversion of string keys.
  """
  @spec to_map(map() | struct()) :: map()
  def to_map(%{__struct__: _} = m, opts \\ []) do
    string_keys = Keyword.get(opts, :stringify, false)

    m
    |> Map.drop([:__struct__, :__meta__])
    |> Enum.map(fn
      {_, nil} ->
        nil

      {k, v} when is_atom(k) ->
        if string_keys, do: {to_string(k), v}, else: {k, v}

      {k, v} when is_binary(k) ->
        if string_keys, do: {k, v}, else: {String.to_existing_atom(k), v}
    end)
    |> Enum.reject(&is_nil/1)
    |> Enum.into(%{})
  end
end
