defmodule Forum.WsHandler do
  @moduledoc """
  One instance of this module runs per connected WebSocket client.
  Each instance is its own BEAM process — isolated state, isolated crashes.

  Client messages arrive in `handle_in/2`. Reply with `{:push, frame, state}`,
  do nothing with `{:ok, state}`, or close with `{:stop, reason, state}`.
  """
  @behaviour WebSock

  require Logger

  @impl true
  def init(opts) do
    Registry.register(Forum.FormsSubscriberRegistry, :forms, nil)
    Logger.info("ws connected: user_id=#{opts[:user_id]}")

    state = %{
      message_count: 0,
      user_id: opts[:user_id],
      email: opts[:email]
    }

    welcome = Jason.encode!(%{
      type: "welcome to this session: ",
      user_id: opts[:user_id]
    })

    schedule_heartbeat()

    {:push, {:text, welcome}, state}
  end

  # Text frame from client.
  # Supported commands (sent as JSON over the wire):
  #   {"type": "ping"}                -> {"type": "pong"}
  #   {"type": "send", "text": "hi"}  -> inserts row, returns the row
  #   {"type": "list"}                -> returns latest 50 rows
  #   {"type": "get_doc"}             -> returns the backend body HTML
  #   {"type": "reload_forms"}        -> re-reads forms.html and reconciles runtimes
  #   {"type": "list_processes"}      -> returns a snapshot of all live processes
  #   {"type": "list_logs"}           -> returns recent request logs
  #   {"type": "list_vm_memory"}      -> returns BEAM VM memory categories
  # Anything else echoes back.
  @impl true
  def handle_in({raw, [opcode: :text]}, state) do
    state = %{state | message_count: state.message_count + 1}

    response =
      case Jason.decode(raw) do
        {:ok, %{"type" => "ping"}} ->
          %{type: "pong"}

        {:ok, %{"type" => "send", "text" => text}} when is_binary(text) and text != "" ->
          row = Forum.DB.insert_message(text)
          %{type: "inserted", row: row}

        {:ok, %{"type" => "list"}} ->
          %{type: "list", rows: Forum.DB.list_messages(limit: 50)}

        {:ok, %{"type" => "get_doc"}} ->
          %{type: "doc", html: Forum.Forms.html()}

        {:ok, %{"type" => "reload_forms"}} ->
          :ok = Forum.Forms.reload()
          %{type: "doc", html: Forum.Forms.html()}

        {:ok, %{"type" => "list_processes"}} ->
          %{type: "processes", rows: Forum.Processes.list()}

        {:ok, %{"type" => "list_modules"}} ->
          %{type: "modules", rows: Forum.Modules.list()}

        {:ok, %{"type" => "list_logs"}} ->
          %{type: "logs", rows: Forum.LogStore.list()}

        {:ok, %{"type" => "list_vm_memory"}} ->
          %{type: "vm_memory", rows: Forum.VmMemory.list()}

        _ ->
          %{type: "echo", raw: raw, count: state.message_count}
      end

    {:push, {:text, Jason.encode!(response)}, state}
  end

  # Binary frame from client — ignore in this scaffold.
  def handle_in({_data, [opcode: :binary]}, state), do: {:ok, state}

  # Messages sent to this process's mailbox from other processes.
  @impl true
  def handle_info({:forms_reloaded, html}, state) do
    {:push, {:text, Jason.encode!(%{type: "doc", html: html})}, state}
  end

  def handle_info(_msg, state), do: {:ok, state}

  def handle_info(:heartbeat, state) do
    schedule_heartbeat()
    {:push, {:ping, ""}, state}
  end

  defp schedule_heartbeat, do: Process.send_after(self(), :heartbeat, 30_000)

  @impl true
  def terminate(reason, _state) do
    Logger.info("ws disconnected: #{inspect(reason)}")
    :ok
  end
end
