defmodule Forum.Network do
  @moduledoc """
  A per-network process for serving the app's forms view.

  Requests to `/` use the root forms. Requests to `/<network>` use the
  matching network forms from `priv/networks/<network>/index.html`.
  """
  use GenServer

  @default_hosts MapSet.new(["localhost", "127.0.0.1", "0.0.0.0", ""])

  def child_spec(opts) do
    slug = opts |> Keyword.fetch!(:slug) |> normalize_slug()

    %{
      id: {:network, slug},
      start: {__MODULE__, :start_link, [[slug: slug]]},
      restart: :permanent,
      shutdown: 5_000,
      type: :worker
    }
  end

  def start_link(opts) do
    slug = Keyword.fetch!(opts, :slug)
    GenServer.start_link(__MODULE__, slug, name: via(slug))
  end

  def normalize_slug(slug) when is_binary(slug) do
    slug
    |> String.downcase()
    |> String.split(":", parts: 2)
    |> hd()
  end

  def serve(pid, request) do
    GenServer.call(pid, {:serve, request})
  end

  @impl true
  def init(slug) do
    {:ok, %{slug: normalize_slug(slug), root: site_root(slug), started_at: DateTime.utc_now()}}
  end

  @impl true
  def handle_call({:serve, request}, _from, state) do
    response = do_serve(state, request)
    {:reply, response, state}
  end

  defp do_serve(%{slug: slug} = state, request) do
    path = Map.fetch!(request, :path)

    cond do
      default_site?(slug) and path in ["", "/"] ->
        desktop_response()

      network_site?(slug) and path in ["", "/"] ->
        network_forms_response(slug)

      File.dir?(state.root) ->
        static_response(state.root, path)

      true ->
        %{
          status: 404,
          content_type: "text/plain",
          body:
            "No network configured for #{slug}. Create files under #{Path.relative_to_cwd(state.root)}."
        }
    end
  end

  defp desktop_response do
    body =
      Forum.Assets.path("ui/desktop.html")
      |> File.read!()
      |> String.replace("<!-- FORMS_HTML -->", Forum.Forms.html())

    %{status: 200, content_type: "text/html", body: body}
  end

  defp network_forms_response(slug) do
    body =
      Forum.Assets.path("ui/desktop.html")
      |> File.read!()
      |> String.replace("<!-- FORMS_HTML -->", Forum.Forms.html(slug))

    %{status: 200, content_type: "text/html", body: body}
  end

  defp static_response(root, path) do
    file_path = resolve_path(root, path)

    cond do
      file_path == :invalid ->
        %{status: 403, content_type: "text/plain", body: "Forbidden"}

      File.dir?(file_path) and File.regular?(Path.join(file_path, "index.html")) ->
        send_file(Path.join(file_path, "index.html"))

      File.regular?(file_path) ->
        send_file(file_path)

      true ->
        %{status: 404, content_type: "text/plain", body: "Not found"}
    end
  end

  defp resolve_path(root, path) do
    relative =
      path
      |> String.trim_leading("/")
      |> URI.decode()

    candidate = Path.expand(relative, root)
    rel_to_root = Path.relative_to(candidate, Path.expand(root))

    if rel_to_root == ".." or String.starts_with?(rel_to_root, "../") do
      :invalid
    else
      candidate
    end
  end

  defp send_file(path) do
    %{status: 200, content_type: content_type(path), body: File.read!(path)}
  end

  defp content_type(path) do
    case Path.extname(path) do
      ".html" -> "text/html"
      ".css" -> "text/css"
      ".js" -> "application/javascript"
      ".json" -> "application/json"
      ".svg" -> "image/svg+xml"
      ".png" -> "image/png"
      ".jpg" -> "image/jpeg"
      ".jpeg" -> "image/jpeg"
      ".webp" -> "image/webp"
      ".gif" -> "image/gif"
      ".ico" -> "image/x-icon"
      ".txt" -> "text/plain"
      _ -> "application/octet-stream"
    end
  end

  defp site_root(slug) do
    source_path = Path.expand(Path.join(["priv", "networks", normalize_slug(slug)]), File.cwd!())

    if File.exists?(source_path) do
      source_path
    else
      Application.app_dir(:forum, Path.join(["priv", "networks", normalize_slug(slug)]))
    end
  end

  defp default_site?(slug), do: MapSet.member?(@default_hosts, slug)
  defp network_site?(slug), do: slug in Forum.Networks.network_slugs()
  defp via(slug), do: {:via, Registry, {Forum.ProcessRegistry, {:network, normalize_slug(slug)}}}
end
