defmodule Forum.Forms.Watcher do
  @moduledoc """
  Watches the canonical forms.html file and reloads `Forum.Forms` when it changes.

  This intentionally uses a small polling loop instead of an external file watcher
  dependency so it works the same way in dev, test, and releases.
  """
  use GenServer
  require Logger

  @poll_ms 500
  @debounce_ms 200

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  @impl true
  def init(_opts) do
    paths = paths()
    state = %{paths: paths, signature: signature(paths), debounce_ref: nil}

    schedule_poll()
    {:ok, state}
  end

  @impl true
  def handle_info(:poll, state) do
    paths = paths()
    next_signature = signature(paths)
    state = %{state | paths: paths}

    state =
      if next_signature != state.signature do
        schedule_reload(%{state | signature: next_signature})
      else
        state
      end

    schedule_poll()
    {:noreply, state}
  end

  def handle_info({:reload, signature}, %{signature: signature} = state) do
    case reload_forms() do
      :ok ->
        Logger.info("Forum.Forms.Watcher: reloaded #{Enum.join(state.paths, ", ")}")
        broadcast_reload()

      {:error, reason} ->
        Logger.error("Forum.Forms.Watcher: reload failed: #{inspect(reason)}")
    end

    {:noreply, %{state | debounce_ref: nil}}
  end

  def handle_info({:reload, _stale_signature}, state) do
    {:noreply, state}
  end

  defp schedule_reload(%{debounce_ref: ref} = state) when is_reference(ref) do
    Process.cancel_timer(ref)
    schedule_reload(%{state | debounce_ref: nil})
  end

  defp schedule_reload(state) do
    ref = Process.send_after(self(), {:reload, state.signature}, @debounce_ms)
    %{state | debounce_ref: ref}
  end

  defp schedule_poll do
    Process.send_after(self(), :poll, @poll_ms)
  end

  defp paths do
    [Forum.Forms.path() | Enum.map(Forum.Forms.network_paths(), fn {_slug, path} -> path end)]
  end

  defp signature(paths) do
    Enum.map(paths, fn path ->
      case File.read(path) do
        {:ok, raw} -> {path, :ok, :erlang.phash2(raw), byte_size(raw)}
        {:error, reason} -> {path, :error, reason}
      end
    end)
  end

  defp reload_forms do
    try do
      Forum.Forms.reload()
    rescue
      error -> {:error, error}
    catch
      kind, reason -> {:error, {kind, reason}}
    else
      :ok -> :ok
      other -> {:error, other}
    end
  end

  defp broadcast_reload do
    html = Forum.Forms.html()

    for {pid, _value} <- Registry.lookup(Forum.FormsSubscriberRegistry, :forms) do
      send(pid, {:forms_reloaded, html})
    end
  end
end
