defmodule Forum.Router do
  use Plug.Router
  use Joken.Config
  require Logger

  plug Plug.Logger
  plug :log_request
  plug :fetch_cookies
  plug :verify_session_host
  plug :match
  plug :dispatch

  defp verify_session_host(conn, _opts) do
    if conn.host == "session.frm.so" do
      case authenticate(conn) do
        {:ok, claims} ->
          conn
          |> Plug.Conn.assign(:claims, claims)

        :error ->
          conn |> send_resp(401, "unauthorized") |> halt()
      end
    else
      conn
    end
  end

  defp authenticate(conn) do
    with token when is_binary(token) <- conn.cookies["auth_token"], {:ok, claims} <- verify_token(token) do
      {:ok, claims}
    else
      _ -> :error
    end
  end

  defp verify_token(token) do
    secret = Application.fetch_env!(:forum, :auth)[:jwt_secret]
    signer = Joken.Signer.create("HS256", secret)

    case Joken.verify_and_validate(%{}, token, signer) do
      {:ok, claims} -> {:ok, claims}
      {:error, _reason} = err -> err
    end
  end

  get "/admin", host: "session.frm.so", do: handle_admin(conn)
  get "/admin/graphyellow.svg", host: "session.frm.so", do: admin_asset(conn, "ui/graphyellow.svg", "image/svg+xml")
  get "/admin/*path", host: "session.frm.so", do: handle_admin(conn)

  get "/", host: "session.frm.so" do
      user_id = conn.assigns.claims["id"]
      email = conn.assigns.claims["email"]
      conn
      |> WebSockAdapter.upgrade(Forum.WsHandler, %{user_id: user_id, email: email}, timeout: :infinity)
      |> halt()
  end

  match _ do
    send_resp(conn, 404, "not found")
  end

  defp handle_admin(conn) do
    if websocket_upgrade?(conn) do
      conn
      |> WebSockAdapter.upgrade(Forum.WsHandler, %{}, timeout: :infinity)
      |> halt()
    else
      admin_page(conn)
    end
  end

  defp websocket_upgrade?(conn) do
    connection = get_req_header(conn, "connection") |> Enum.join(",") |> String.downcase()
    upgrade = get_req_header(conn, "upgrade") |> List.first() |> to_string() |> String.downcase()

    String.contains?(connection, "upgrade") and upgrade == "websocket"
  end

  defp admin_page(conn) do
    html = File.read!(Forum.Assets.path("ui/admin.html"))

    conn
    |> put_resp_content_type("text/html")
    |> send_resp(200, html)
  end

  defp admin_asset(conn, path, content_type) do
    conn
    |> put_resp_content_type(content_type)
    |> send_file(200, Forum.Assets.path(path))
  end

  defp log_request(conn, _opts) do
    started_at = System.monotonic_time(:microsecond)

    Plug.Conn.register_before_send(conn, fn conn ->
      duration_us = System.monotonic_time(:microsecond) - started_at

      Forum.LogStore.add(%{
        "source_ip" => remote_ip(conn),
        "host" => conn.host || "",
        "method" => conn.method,
        "path" => conn.request_path,
        "query_string" => conn.query_string,
        "status" => conn.status || 0,
        "duration_ms" => Float.round(duration_us / 1_000, 2)
      })

      conn
    end)
  end

  defp remote_ip(conn) do
    case Plug.Conn.get_req_header(conn, "x-real-ip") do
      [ip | _] -> ip
      [] -> fallback_ip(conn)
    end
  end

  defp fallback_ip(%{remote_ip: remote_ip}) when is_tuple(remote_ip) do
    remote_ip |> :inet.ntoa() |> to_string()
  end

  defp fallback_ip(_), do: ""
end
