defmodule EventStore.Config.Parser do
  @moduledoc false

  @config_defaults [
    column_data_type: "bytea",
    enable_hard_deletes: false,
    schema: "public",
    timeout: 15_000
  ]

  def parse(config) do
    config
    |> Enum.reduce(@config_defaults, fn
      {:port, value}, config ->
        Keyword.put(config, :port, get_config_integer(value))

      {:session_mode_url, value}, config ->
        parsed_value = get_config_value(value) |> parse_url()

        Keyword.put(config, :session_mode_pool, parsed_value)

      {:timeout, value}, config ->
        Keyword.put(config, :timeout, get_config_timeout(value))

      {:url, value}, config ->
        parsed_value = get_config_value(value) |> parse_url()

        Keyword.merge(config, parsed_value)

      {key, value}, config ->
        Keyword.put(config, key, get_config_value(value))
    end)
    |> Keyword.put(:pool, EventStore.Config.get_pool())
  end

  # Converts a database url into a Keyword list
  defp parse_url(url)

  defp parse_url(""), do: []

  defp parse_url(url) do
    info = URI.parse(url)

    if is_nil(info.host) do
      raise ArgumentError, message: "host is not present"
    end

    if is_nil(info.path) or not (info.path =~ ~r"^/([^/])+$") do
      raise ArgumentError, message: "path should be a database name"
    end

    destructure [username, password], info.userinfo && String.split(info.userinfo, ":")
    "/" <> database = info.path

    opts =
      Enum.reject(
        [
          username: username,
          password: password,
          database: database,
          hostname: info.host,
          port: info.port
        ],
        fn {_k, v} -> is_nil(v) end
      )

    query_opts = parse_uri_query(info)

    for {k, v} <- opts ++ query_opts,
        not is_nil(v),
        do: {k, if(is_binary(v), do: URI.decode(v), else: v)}
  end

  defp parse_uri_query(%URI{query: nil}), do: []

  defp parse_uri_query(%URI{query: query}) do
    query
    |> URI.query_decoder()
    |> Enum.reduce([], fn
      {"ssl", "true"}, acc ->
        [{:ssl, true} | acc]

      {"ssl", "false"}, acc ->
        [{:ssl, false} | acc]

      {key, value}, acc when key in ["pool_size", "queue_target", "queue_interval", "timeout"] ->
        [{String.to_atom(key), parse_integer!(key, value)} | acc]

      {key, _value}, _acc ->
        raise ArgumentError, message: "unsupported query parameter `#{key}`"
    end)
  end

  defp parse_integer!(key, value) do
    case Integer.parse(value) do
      {int, ""} ->
        int

      _ ->
        raise ArgumentError,
          message: "can not parse value `#{value}` for parameter `#{key}` as an integer"
    end
  end

  defp get_config_value(value, default \\ nil)

  defp get_config_value({:system, env_var}, default) do
    case System.get_env(env_var) do
      nil -> default
      val -> val
    end
  end

  defp get_config_value({:system, env_var, default}, _default) do
    case System.get_env(env_var) do
      nil -> default
      val -> val
    end
  end

  defp get_config_value(nil, default), do: default
  defp get_config_value(value, _default), do: value

  defp get_config_integer(value, default \\ nil) do
    case get_config_value(value) do
      nil ->
        default

      n when is_integer(n) ->
        n

      n ->
        case Integer.parse(n) do
          {i, _} -> i
          :error -> default
        end
    end
  end

  defp get_config_timeout(value, default \\ nil) do
    case get_config_value(value) do
      nil ->
        default

      :infinity ->
        :infinity

      n when is_integer(n) ->
        n

      n ->
        case Integer.parse(n) do
          {i, _} -> i
          :error -> default
        end
    end
  end
end
