defmodule Forum.PublicSite do
  @moduledoc """
  A per-network process for serving public static websites on separate ports.
  """
  use GenServer

  def child_spec(opts) do
    network = opts |> Keyword.fetch!(:network) |> normalize_network()

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

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

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

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

  @impl true
  def init(network) do
    {:ok, %{network: normalize_network(network), 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(%{network: network}, request) do
    roots = public_roots(network)

    case roots do
      [] ->
        %{
          status: 404,
          content_type: "text/plain",
          body: "No public website configured for #{network}"
        }

      [primary | fallbacks] ->
        static_response(primary, request.path, fallbacks)
    end
  end

  defp static_response(root, path, fallback_roots \\ []) 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)

      versioned = find_versioned_file([root | fallback_roots], path) ->
        send_file(versioned)

      fallback = find_fallback_file(fallback_roots, path) ->
        send_file(fallback)

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

  defp find_versioned_file(roots, path) do
    case path |> String.trim_leading("/") |> Path.split() do
      [_version | rest] when rest != [] ->
        stripped_path = "/" <> Path.join(rest)
        find_fallback_file(roots, stripped_path)

      _ ->
        nil
    end
  end

  defp find_fallback_file(roots, path) do
    Enum.find_value(roots, fn root ->
      file_path = resolve_path(root, path)

      cond do
        file_path == :invalid ->
          nil

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

        File.regular?(file_path) ->
          file_path

        true ->
          nil
      end
    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 public_roots("cs") do
    ["networks/cs/website"]
    |> Enum.map(&Forum.Assets.path/1)
    |> Enum.filter(&File.dir?/1)
  end

  defp public_roots("comalyr") do
    [
      "networks/comalyr/comalyr.com/ui/desktop",
      "networks/comalyr/comalyr.com/ui"
    ]
    |> Enum.map(&Forum.Assets.path/1)
    |> Enum.filter(&File.dir?/1)
  end

  defp public_roots(network) do
    [
      Path.join(["networks", network, "website"]),
      Path.join(["networks", network, "public"]),
      Path.join(["networks", network, "www"])
    ]
    |> Enum.map(&Forum.Assets.path/1)
    |> Enum.filter(&File.dir?/1)
  end

  defp via(network),
    do: {:via, Registry, {Forum.ProcessRegistry, {:public_site, normalize_network(network)}}}
end
