defmodule Poolex.Private.Options.Parser do
  @moduledoc false

  alias Poolex.Private.Options.Parsed

  # Interval between retry attempts for workers that failed to start (1 second by default)
  @default_failed_workers_retry_interval to_timeout(second: 1)

  @spec parse(list(Poolex.poolex_option())) :: Parsed.t()
  def parse(options) do
    %Parsed{
      busy_workers_impl: parse_optional_module(options, :busy_workers_impl, Poolex.Workers.Impl.List),
      failed_workers_retry_interval:
        parse_optional_timeout(options, :failed_workers_retry_interval, @default_failed_workers_retry_interval),
      idle_overflowed_workers_impl:
        parse_optional_module(options, :idle_overflowed_workers_impl, Poolex.Workers.Impl.List),
      idle_workers_impl: parse_optional_module(options, :idle_workers_impl, Poolex.Workers.Impl.List),
      max_overflow: parse_optional_non_neg_integer(options, :max_overflow, 0),
      pool_id: parse_pool_id(options),
      pool_size_metrics: parse_optional_boolean(options, :pool_size_metrics, false),
      waiting_callers_impl: parse_optional_module(options, :waiting_callers_impl, Poolex.Callers.Impl.ErlangQueue),
      worker_args: parse_optional_list(options, :worker_args, []),
      worker_module: parse_required_module(options, :worker_module),
      worker_shutdown_delay: parse_optional_timeout(options, :worker_shutdown_delay, 0),
      worker_start_fun: parse_optional_atom(options, :worker_start_fun, :start_link),
      workers_count: parse_required_non_neg_integer(options, :workers_count)
    }
  end

  @doc false
  def parse_pool_id(options) do
    case Keyword.get(options, :pool_id) do
      nil -> Keyword.fetch!(options, :worker_module)
      pool_id -> pool_id
    end
  end

  defp parse_optional_atom(options, key, default) do
    options
    |> parse_optional_option(key, default)
    |> validate_atom()
  end

  defp parse_optional_list(options, key, default) do
    options
    |> parse_optional_option(key, default)
    |> validate_list()
  end

  defp parse_optional_boolean(options, key, default) do
    options
    |> parse_optional_option(key, default)
    |> validate_boolean()
  end

  defp parse_optional_module(options, key, default) do
    options
    |> parse_optional_option(key, default)
    |> validate_module()
  end

  defp parse_required_module(options, key) do
    options
    |> parse_required_option(key)
    |> validate_module()
  end

  defp parse_optional_timeout(options, key, default) do
    options
    |> parse_optional_option(key, default)
    |> validate_timeout()
  end

  defp parse_required_non_neg_integer(options, key) do
    options
    |> parse_required_option(key)
    |> validate_non_neg_integer()
  end

  defp parse_optional_non_neg_integer(options, key, default) do
    options
    |> parse_optional_option(key, default)
    |> validate_non_neg_integer()
  end

  # Universal parse functions
  defp parse_required_option(options, key) do
    case Keyword.fetch(options, key) do
      {:ok, value} -> value
      :error -> raise ArgumentError, "Missing required option: #{inspect(key)}"
    end
  end

  defp parse_optional_option(options, key, default) do
    case Keyword.fetch(options, key) do
      {:ok, value} -> value
      :error -> default
    end
  end

  # Validators
  defp validate_module(value) when is_atom(value) do
    if Code.ensure_loaded?(value) do
      value
    else
      raise ArgumentError, "Module #{inspect(value)} is not loaded or does not exist"
    end
  end

  defp validate_module(value) do
    raise ArgumentError, "Expected a module atom, got: #{inspect(value)}"
  end

  defp validate_non_neg_integer(value) when is_integer(value) and value >= 0 do
    value
  end

  defp validate_non_neg_integer(value) do
    raise ArgumentError, "Expected a non-negative integer, got: #{inspect(value)}"
  end

  defp validate_timeout(value) when is_integer(value) and value >= 0 do
    value
  end

  defp validate_timeout(:infinity) do
    :infinity
  end

  defp validate_timeout(value) do
    raise ArgumentError, "Expected a non-negative integer or :infinity, got: #{inspect(value)}"
  end

  defp validate_boolean(value) when is_boolean(value) do
    value
  end

  defp validate_boolean(value) do
    raise ArgumentError, "Expected a boolean value, got: #{inspect(value)}"
  end

  defp validate_list(value) when is_list(value) do
    value
  end

  defp validate_list(value) do
    raise ArgumentError, "Expected a list, got: #{inspect(value)}"
  end

  defp validate_atom(value) when is_atom(value) do
    value
  end

  defp validate_atom(value) do
    raise ArgumentError, "Expected an atom, got: #{inspect(value)}"
  end
end
