defmodule Gel.Client do
  @moduledoc """
  Сlient is a structure that stores a custom configuration to execute EdgeQL queries
    and has a reference to a connection or pool of connections.

  After starting the pool via `Gel.start_link/1` or siblings,
    the client instance for the pool will be implicitly registered.

  In case you want to change the behavior of your queries, you will use the `Gel.Client`,
    which is acceptable by all `Gel` API and will be provided to you in a callback
    in the `Gel.transaction/3` function.
  """

  alias Gel.Client.State

  defstruct [
    :conn,
    readonly: false,
    transaction_options: [],
    retry_options: [],
    state: %State{}
  ]

  @typedoc """
  Client is structure with stored configuration for executing EdgeQL queries
    and reference to pool or connection.

  Fields:

    * `:conn` - reference to connection or pool of connections.
    * `:readonly` - flag specifying that the client is read-only.
    * `:transaction_options` - options for Gel transactions.
    * `:retry_options` - options for a retry rule for transactions retries.
    * `:state` - execution context that affects the execution of EdgeQL commands.
  """
  @type t() :: %__MODULE__{
          conn: DBConnection.conn(),
          readonly: boolean(),
          transaction_options: list(transaction_option()),
          retry_options: list(retry_option()),
          state: State.t()
        }

  @typedoc """
  Options for Gel transactions.

  These options are responsible for building the appropriate EdgeQL statement to start transactions and
    they correspond to [the EdgeQL transaction statement](https://docs.geldata.com/database/reference/edgeql/tx_start#parameters).

  Supported options:

    * `:isolation` - If `:serializable` is used, the built statement will use the `isolation serializable` mode.
      Currently only `:serializable` is supported by this client and Gel.
    * `:readonly` - if set to `true` then the built statement will use `read only` mode,
      otherwise `read write` will be used. The default is `false`.
    * `:deferrable` - if set to `true` then the built statement will use `deferrable` mode,
      otherwise `not deferrable` will be used. The default is `false`.
  """
  @type transaction_option() ::
          {:isolation, :serializable}
          | {:readonly, boolean()}
          | {:deferrable, boolean()}

  @typedoc """
  Options for a retry rule for transactions retries.

  See `Gel.transaction/3`.

  Supported options:

    * `:attempts` - the number of attempts to retry the transaction in case of an error.
    * `:backoff` - function to determine the backoff before the next attempt to run a transaction.
  """
  @type retry_rule() ::
          {:attempts, pos_integer()}
          | {:backoff, (pos_integer() -> timeout())}

  @typedoc """
  Options for transactions and read-only queries retries.

  See `Gel.transaction/3`.

  Supported options:

    * `:transaction_conflict` - the rule that will be used in case of any transaction conflict.
    * `:network_error` - rule which will be used when any network error occurs on the client.
  """
  @type retry_option() ::
          {:transaction_conflict, retry_rule()}
          | {:network_error, retry_rule()}

  @doc false
  @spec as_readonly(t()) :: t()
  def as_readonly(client) do
    client = to_client(client)
    %__MODULE__{client | readonly: true}
  end

  @doc false
  @spec with_transaction_options(t(), list(transaction_option())) :: t()
  def with_transaction_options(client, options) do
    client = to_client(client)
    %__MODULE__{client | transaction_options: options}
  end

  @doc false
  @spec with_retry_options(t(), list(retry_option())) :: t()
  def with_retry_options(client, options) do
    client = to_client(client)
    %__MODULE__{client | retry_options: Keyword.merge(client.retry_options, options)}
  end

  @doc false
  @spec with_state(t(), State.t()) :: t()
  def with_state(client, state) do
    client = to_client(client)
    %__MODULE__{client | state: state}
  end

  @doc false
  @spec with_default_module(t(), String.t() | nil) :: t()
  def with_default_module(client, module \\ nil) do
    client = to_client(client)
    %__MODULE__{client | state: State.with_default_module(client.state, module)}
  end

  @doc false
  @spec with_module_aliases(t(), %{String.t() => String.t()}) :: t()
  def with_module_aliases(client, aliases \\ %{}) do
    client = to_client(client)
    %__MODULE__{client | state: State.with_module_aliases(client.state, aliases)}
  end

  @doc false
  @spec without_module_aliases(t(), list(String.t())) :: t()
  def without_module_aliases(client, aliases \\ []) do
    client = to_client(client)
    %__MODULE__{client | state: State.without_module_aliases(client.state, aliases)}
  end

  @doc false
  @spec with_config(t(), State.config()) :: t()
  def with_config(client, config \\ %{}) do
    client = to_client(client)
    %__MODULE__{client | state: State.with_config(client.state, config)}
  end

  @doc false
  @spec without_config(t(), list(State.config_key())) :: t()
  def without_config(client, config_keys \\ []) do
    client = to_client(client)
    %__MODULE__{client | state: State.without_config(client.state, config_keys)}
  end

  @doc false
  @spec with_globals(t(), %{String.t() => String.t()}) :: t()
  def with_globals(client, globals \\ %{}) do
    client = to_client(client)
    %__MODULE__{client | state: State.with_globals(client.state, globals)}
  end

  @doc false
  @spec without_globals(t(), list(String.t())) :: t()
  def without_globals(client, global_names \\ []) do
    client = to_client(client)
    %__MODULE__{client | state: State.without_globals(client.state, global_names)}
  end

  @doc false
  @spec to_conn(t() | DBConnection.conn()) :: t()

  def to_conn(%__MODULE__{conn: conn}) do
    conn
  end

  def to_conn(conn) do
    conn
  end

  @doc false
  @spec to_options(t()) :: Keyword.t()

  def to_options(%__MODULE__{} = client) do
    capabilities =
      if client.readonly do
        [:readonly]
      else
        []
      end

    [
      capabilities: capabilities,
      transaction_options: client.transaction_options,
      retry_options: client.retry_options,
      edgeql_state: client.state
    ]
  end

  defp to_client(%__MODULE__{} = client) do
    client
  end

  defp to_client(conn) do
    %__MODULE__{conn: conn}
  end
end
