defmodule Bolt.Sips.Routing.RoutingTable do
  @moduledoc ~S"""
  representing the routing table elements

  There are a couple of ways to get the routing table from the server, for recent Neo4j servers, and with the
  latest version of Bolt.Sips, you could use this query:

      Bolt.Sips.query!(Bolt.Sips.conn, "call dbms.cluster.routing.getRoutingTable($props)", %{props: %{}})

      [
        %{
          "servers" => [
            %{"addresses" => ["localhost:7687"], "role" => "WRITE"},
            %{"addresses" => ["localhost:7689", "localhost:7688"], "role" => "READ"},
            %{
              "addresses" => ["localhost:7688", "localhost:7689", "localhost:7687"],
              "role" => "ROUTE"
            }
          ],
          "ttl" => 300
        }
      ]

  """

  @type t :: %__MODULE__{
          roles: %{(:read | :write | :route | :direct) => %{String.t() => non_neg_integer}},
          updated_at: non_neg_integer,
          ttl: non_neg_integer
        }
  defstruct roles: %{}, ttl: 300, updated_at: 0

  alias Bolt.Sips.Utils

  @write "WRITE"
  @read "READ"
  @route "ROUTE"

  @spec parse(map) :: __MODULE__.t() | {:error, String.t()}
  def parse(%{"servers" => servers, "ttl" => ttl}) do
    with {:ok, roles} <- parse_servers(servers),
         {:ok, ttl} <- parse_ttl(ttl) do
      %__MODULE__{roles: roles, ttl: ttl, updated_at: Utils.now()}
    end
  end

  def parse(map),
    do: {:error, "not a valid routing table: " <> inspect(map)}

  @spec parse_servers(list()) :: {:ok, map()}
  defp parse_servers(servers) do
    parsed_servers =
      servers
      |> Enum.reduce(%{}, fn %{"addresses" => addresses, "role" => role}, acc ->
        with {:ok, atomized_role} <- to_atomic_role(role) do
          roles =
            addresses
            |> Enum.reduce(acc, fn address, acc ->
              Map.update(acc, atomized_role, %{address => 0}, &Map.put(&1, address, 0))
            end)

          roles
        else
          _ -> acc
        end
      end)

    {:ok, parsed_servers}
  end

  defp to_atomic_role(role) when role in [@read, @write, @route] do
    atomic_role =
      case role do
        @read -> :read
        @write -> :write
        @route -> :route
        _ -> :direct
      end

    {:ok, atomic_role}
  end

  defp to_atomic_role(_), do: {:error, :alien_role}

  def parse_ttl(ttl), do: {:ok, ensure_integer(ttl)}

  @doc false
  def ttl_expired?(updated_at, ttl) do
    updated_at + ttl <= Utils.now()
  end

  defp ensure_integer(ttl) when is_nil(ttl), do: 0
  defp ensure_integer(ttl) when is_binary(ttl), do: String.to_integer(ttl)
  defp ensure_integer(ttl) when is_integer(ttl), do: ttl
  defp ensure_integer(ttl), do: raise(ArgumentError, "invalid ttl: " <> inspect(ttl))
end
