defmodule MicrowavepropWeb.StatusLive do
  @moduledoc false
  use MicrowavepropWeb, :live_view

  import Ecto.Query

  alias Microwaveprop.Cache
  alias Microwaveprop.Radio.Contact
  alias Microwaveprop.Repo
  alias Microwaveprop.Weather.NarrClient

  @impl true
  def mount(_params, _session, socket) do
    if connected?(socket) do
      Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "db:contact_status")
      Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "db:oban_jobs")
      # Rust prop-grid-rs emits NOTIFY propagation_ready on completion —
      # PropagationNotifyListener fans it out as `propagation:updated` so
      # the grid_tasks panel transitions from "running" → "done" without
      # a manual refresh.
      Phoenix.PubSub.subscribe(Microwaveprop.PubSub, "propagation:updated")
    end

    stats = fetch_stats()
    unprocessed = count_unprocessed()
    db_stats = fetch_db_stats()
    grid_tasks = fetch_grid_tasks_stats()
    hrrr_point_tasks = fetch_hrrr_point_tasks_stats()

    {:ok,
     assign(socket,
       page_title: "Status",
       stats: stats,
       unprocessed: unprocessed,
       db_stats: db_stats,
       grid_tasks: grid_tasks,
       hrrr_point_tasks: hrrr_point_tasks,
       refresh_timer: nil
     )}
  end

  @impl true
  def handle_info({:contact_status_changed, _data}, socket) do
    {:noreply, schedule_refresh(socket)}
  end

  def handle_info({:oban_job_changed, _data}, socket) do
    {:noreply, schedule_refresh(socket)}
  end

  def handle_info({:propagation_updated, _valid_times}, socket) do
    {:noreply, schedule_refresh(socket)}
  end

  def handle_info(:refresh_stats, socket) do
    {:noreply,
     assign(socket,
       stats: fetch_stats(),
       unprocessed: count_unprocessed(),
       db_stats: fetch_db_stats(),
       grid_tasks: fetch_grid_tasks_stats(),
       hrrr_point_tasks: fetch_hrrr_point_tasks_stats(),
       refresh_timer: nil
     )}
  end

  defp schedule_refresh(%{assigns: %{refresh_timer: ref}} = socket) when is_reference(ref) do
    # Already scheduled, skip
    socket
  end

  defp schedule_refresh(socket) do
    # 2s debounce — bulk enrichment runs produce hundreds of status_changed
    # events per second, and each stats refresh fires 8+ count queries.
    ref = Process.send_after(self(), :refresh_stats, 2_000)
    assign(socket, refresh_timer: ref)
  end

  defp count_unprocessed do
    Cache.fetch_or_store({__MODULE__, :count_unprocessed}, 2_000, fn ->
      count_unprocessed_raw()
    end)
  end

  defp count_unprocessed_raw do
    # Previously issued 7 separate `SELECT COUNT(*) FROM contacts WHERE
    # <field> IN (...)` queries. Postgres ran 7 index scans over the
    # same 58k+ row table on every 2s status refresh; collapse into a
    # single scan that returns every needed count via FILTER clauses.
    %{rows: [[total, terrain, hrrr, weather, iemre, radar, all_done]]} =
      Repo.query!("""
      SELECT
        COUNT(*) FILTER (WHERE pos1 IS NOT NULL),
        COUNT(*) FILTER (WHERE pos1 IS NOT NULL
          AND terrain_status::text IN ('pending','queued','processing','failed')),
        COUNT(*) FILTER (WHERE pos1 IS NOT NULL
          AND hrrr_status::text IN ('pending','queued','processing','failed')),
        COUNT(*) FILTER (WHERE pos1 IS NOT NULL
          AND weather_status::text IN ('pending','queued','processing','failed')),
        COUNT(*) FILTER (WHERE pos1 IS NOT NULL
          AND iemre_status::text IN ('pending','queued','processing','failed')),
        COUNT(*) FILTER (WHERE pos1 IS NOT NULL
          AND radar_status::text IN ('pending','queued','processing','failed')),
        COUNT(*) FILTER (WHERE pos1 IS NOT NULL
          AND hrrr_status::text IN ('complete','unavailable')
          AND weather_status::text IN ('complete','unavailable')
          AND terrain_status::text IN ('complete','unavailable')
          AND iemre_status::text IN ('complete','unavailable'))
      FROM contacts
      """)

    narr_candidates = count_narr_candidates()
    narr_done = count_narr_done()

    %{
      terrain: terrain,
      hrrr: hrrr,
      weather: weather,
      iemre: iemre,
      radar: radar,
      narr_candidates: narr_candidates,
      narr_done: narr_done,
      remaining: max(terrain, max(hrrr, max(weather, iemre))),
      complete: all_done,
      total: total
    }
  end

  # NARR is the fallback for pre-2014 contacts (HRRR archive starts 2014).
  # Candidate eligibility is defined by qso_timestamp, not hrrr_status —
  # pre-2014 rows bounce between :queued and :unavailable every cron cycle
  # and the UI must count them either way.
  defp count_narr_candidates do
    coverage_end = NarrClient.coverage_end()

    Repo.one(
      from(c in Contact,
        where: c.qso_timestamp < ^coverage_end and not is_nil(c.pos1),
        select: count()
      )
    )
  end

  # "Done" means a narr_profiles row within 0.15° of the contact and
  # stamped at the floor-snapped 3-hour analysis mark.
  #
  # NARR analyses live at 00/03/06/09/12/15/18/21 UTC. The fetcher calls
  # `NarrClient.snap_to_analysis_hour/1`, which floors qso_timestamp to the
  # previous 3-hour mark — so a contact at 18:50Z yields a profile valid at
  # 18:00Z. The SQL snap below must stay in sync with that Elixir function.
  defp count_narr_done do
    coverage_end = NarrClient.coverage_end()

    %{rows: [[done]]} =
      Repo.query!(
        """
        SELECT count(*)
          FROM contacts c
         WHERE c.qso_timestamp < $1
           AND c.pos1 IS NOT NULL
           AND EXISTS (
             SELECT 1
               FROM narr_profiles p
              WHERE p.lat BETWEEN ((c.pos1->>'lat')::float - 0.15) AND ((c.pos1->>'lat')::float + 0.15)
                AND p.lon BETWEEN ((c.pos1->>'lon')::float - 0.15) AND ((c.pos1->>'lon')::float + 0.15)
                AND p.valid_time = date_trunc('hour', c.qso_timestamp)
                                   - make_interval(hours => CAST(EXTRACT(HOUR FROM c.qso_timestamp) AS integer) % 3)
           )
        """,
        [coverage_end]
      )

    done
  end

  defp fetch_grid_tasks_stats do
    Cache.fetch_or_store({__MODULE__, :grid_tasks_stats}, 2_000, fn -> fetch_grid_tasks_stats_raw() end)
  end

  # Snapshot of the Rust prop-grid-rs work queue. Mirrors the Oban panel —
  # "what's running, what's pending, what's retrying" — but pulls from the
  # grid_tasks hand-off table instead of oban_jobs.
  defp fetch_grid_tasks_stats_raw do
    running =
      Repo.all(
        from(t in "grid_tasks",
          where: t.status == "running",
          order_by: [asc: t.run_time, asc: t.forecast_hour],
          select: %{
            run_time: t.run_time,
            forecast_hour: t.forecast_hour,
            attempt: t.attempt,
            claimed_at: t.claimed_at
          }
        )
      )

    counts_by_status =
      from(t in "grid_tasks",
        where: t.status in ["queued", "failed"],
        group_by: t.status,
        select: {t.status, count(t.id)}
      )
      |> Repo.all()
      |> Map.new()

    done_1h =
      Repo.one(
        from(t in "grid_tasks",
          where: t.status == "done" and t.completed_at > ago(1, "hour"),
          select: count(t.id)
        )
      )

    %{
      running: running,
      queued: Map.get(counts_by_status, "queued", 0),
      failed: Map.get(counts_by_status, "failed", 0),
      done_1h: done_1h
    }
  end

  defp fetch_hrrr_point_tasks_stats do
    Cache.fetch_or_store({__MODULE__, :hrrr_point_tasks_stats}, 2_000, fn ->
      fetch_hrrr_point_tasks_stats_raw()
    end)
  end

  # Snapshot of the Rust hrrr-point-rs work queue (per-QSO HRRR batches,
  # one row per valid_time). Same shape as grid_tasks_stats so the UI
  # can render both in a single table.
  defp fetch_hrrr_point_tasks_stats_raw do
    running_count =
      Repo.one(
        from(t in "hrrr_fetch_tasks",
          where: t.status == "running",
          select: count(t.id)
        )
      )

    counts_by_status =
      from(t in "hrrr_fetch_tasks",
        where: t.status in ["queued", "failed"],
        group_by: t.status,
        select: {t.status, count(t.id)}
      )
      |> Repo.all()
      |> Map.new()

    done_1h =
      Repo.one(
        from(t in "hrrr_fetch_tasks",
          where: t.status == "done" and t.completed_at > ago(1, "hour"),
          select: count(t.id)
        )
      )

    %{
      running: running_count,
      queued: Map.get(counts_by_status, "queued", 0),
      failed: Map.get(counts_by_status, "failed", 0),
      done_1h: done_1h
    }
  end

  defp fetch_stats do
    Cache.fetch_or_store({__MODULE__, :stats}, 2_000, fn -> fetch_stats_raw() end)
  end

  defp fetch_stats_raw do
    jobs =
      Repo.all(
        from(j in "oban_jobs",
          where: j.state in ["available", "executing", "scheduled", "retryable"],
          group_by: [j.worker, j.state],
          select: {j.worker, j.state, count(j.id)}
        )
      )

    by_worker =
      jobs
      |> Enum.group_by(&elem(&1, 0), fn {_, state, count} -> {state, count} end)
      |> Enum.map(&summarize_worker/1)
      |> Enum.sort_by(& &1.total, :desc)

    completed_1h =
      Repo.one(
        from(j in "oban_jobs",
          where: j.state == "completed" and j.completed_at > ago(1, "hour"),
          select: count(j.id)
        )
      )

    %{by_worker: by_worker, completed_1h: completed_1h}
  end

  defp summarize_worker({worker, states}) do
    short_name = worker |> String.split(".") |> List.last()

    %{
      worker: short_name,
      total: Enum.reduce(states, 0, fn {_, c}, acc -> acc + c end),
      executing: state_count(states, "executing"),
      available: state_count(states, "available"),
      retryable: state_count(states, "retryable")
    }
  end

  defp state_count(states, name) do
    Enum.find_value(states, 0, fn {s, c} -> if s == name, do: c end)
  end

  defp fetch_db_stats do
    # pg_class + pg_stat_user_tables join is slow — 5s cache is fine since
    # table row counts barely change in a 5-second window.
    Cache.fetch_or_store({__MODULE__, :db_stats}, 5_000, fn -> fetch_db_stats_raw() end)
  end

  defp fetch_db_stats_raw do
    %{rows: rows} =
      Repo.query!("""
      SELECT
        c.relname as table,
        s.n_live_tup as rows,
        s.n_dead_tup as dead,
        pg_total_relation_size(c.oid) as bytes
      FROM pg_class c
      JOIN pg_namespace n ON n.oid = c.relnamespace
      JOIN pg_stat_user_tables s ON s.relid = c.oid
      WHERE n.nspname = 'public'
        AND c.relkind = 'r'
        AND c.relname NOT LIKE 'oban_%'
        AND c.relname NOT LIKE 'schema_%'
        AND NOT EXISTS (SELECT 1 FROM pg_inherits WHERE inhrelid = c.oid)
      UNION ALL
      SELECT
        parent.relname,
        COALESCE(SUM(cs.n_live_tup), 0)::bigint,
        COALESCE(SUM(cs.n_dead_tup), 0)::bigint,
        COALESCE(SUM(pg_total_relation_size(child.oid)), 0)::bigint
      FROM pg_class parent
      JOIN pg_namespace n ON n.oid = parent.relnamespace
      JOIN pg_inherits inh ON inh.inhparent = parent.oid
      JOIN pg_class child ON child.oid = inh.inhrelid
      LEFT JOIN pg_stat_user_tables cs ON cs.relid = child.oid
      WHERE n.nspname = 'public' AND parent.relkind = 'p'
      GROUP BY parent.relname
      ORDER BY bytes DESC
      LIMIT 20
      """)

    tables =
      Enum.map(rows, fn [table, row_count, dead, bytes] ->
        %{table: table, rows: row_count, dead: dead, size: format_bytes(bytes)}
      end)

    %{rows: total_rows} =
      Repo.query!("SELECT pg_database_size(current_database())")

    [[db_bytes]] = total_rows

    # Enrichment status breakdown
    %{rows: status_rows} =
      Repo.query!("""
      SELECT
        'hrrr' as type, hrrr_status as status, count(*) as cnt FROM contacts GROUP BY hrrr_status
      UNION ALL
      SELECT
        'weather', weather_status, count(*) FROM contacts GROUP BY weather_status
      UNION ALL
      SELECT
        'terrain', terrain_status, count(*) FROM contacts GROUP BY terrain_status
      UNION ALL
      SELECT
        'iemre', iemre_status, count(*) FROM contacts GROUP BY iemre_status
      UNION ALL
      SELECT
        'radar', radar_status, count(*) FROM contacts GROUP BY radar_status
      ORDER BY 1, 2
      """)

    statuses =
      Enum.group_by(status_rows, fn [type, _, _] -> type end, fn [_, status, count] ->
        {status, count}
      end)

    # Per-table row counts from pg_stat estimates (already fetched above, avoids slow count(*))
    table_counts = Map.new(tables, fn %{table: t, rows: r} -> {t, r} end)

    # NARR doesn't have its own status column — synthesize from HRRR unavailable
    # + narr_profiles row count.
    narr_count = Map.get(table_counts, "narr_profiles", 0)

    hrrr_unavailable =
      statuses |> Map.get("hrrr", []) |> Enum.find_value(0, fn {s, c} -> if s == "unavailable", do: c end)

    statuses =
      Map.put(statuses, "narr", [
        {"candidates (HRRR unavail)", hrrr_unavailable},
        {"profiles fetched", narr_count}
      ])

    hrrr_count = Map.get(table_counts, "hrrr_profiles", 0)
    terrain_count = Map.get(table_counts, "terrain_profiles", 0)
    obs_count = Map.get(table_counts, "surface_observations", 0)
    sounding_count = Map.get(table_counts, "soundings", 0)
    iemre_count = Map.get(table_counts, "iemre_observations", 0)
    contact_count = max(Map.get(table_counts, "contacts", 0), 1)

    %{
      db_size: format_bytes(db_bytes),
      tables: tables,
      statuses: statuses,
      totals: %{
        hrrr_profiles: hrrr_count,
        narr_profiles: Map.get(table_counts, "narr_profiles", 0),
        terrain_profiles: terrain_count,
        surface_observations: obs_count,
        soundings: sounding_count,
        iemre_observations: iemre_count,
        contacts: contact_count
      },
      averages: %{
        hrrr_per_contact: Float.round(hrrr_count / contact_count, 1),
        obs_per_contact: Float.round(obs_count / contact_count, 1),
        soundings_per_contact: Float.round(sounding_count / contact_count, 1)
      }
    }
  end

  defp format_number(n) when is_integer(n) do
    n
    |> Integer.to_string()
    |> String.graphemes()
    |> Enum.reverse()
    |> Enum.chunk_every(3)
    |> Enum.map_join(",", &Enum.join/1)
    |> String.reverse()
    |> String.replace(~r/^,/, "")
  end

  defp format_number(n) when is_float(n), do: format_number(round(n))
  defp format_number(n), do: to_string(n)

  defp status_class("complete"), do: "text-success"
  defp status_class("pending"), do: "text-warning"
  defp status_class("queued"), do: "text-info"
  defp status_class("processing"), do: "text-info"
  defp status_class("failed"), do: "text-error"
  defp status_class("unavailable"), do: "opacity-40"
  defp status_class(_), do: ""

  defp format_bytes(bytes) when bytes >= 1_073_741_824, do: "#{Float.round(bytes / 1_073_741_824, 1)} GB"

  defp format_bytes(bytes) when bytes >= 1_048_576, do: "#{Float.round(bytes / 1_048_576, 1)} MB"

  defp format_bytes(bytes) when bytes >= 1024, do: "#{Float.round(bytes / 1024, 1)} KB"

  defp format_bytes(bytes), do: "#{bytes} B"

  @impl true
  def render(assigns) do
    ~H"""
    <Layouts.app flash={@flash} current_scope={@current_scope}>
      <.header>
        Status
        <:subtitle>Enrichment progress, job queues, and database health</:subtitle>
      </.header>

      <div class="grid grid-cols-1 md:grid-cols-4 gap-4 my-6">
        <div class="stat bg-base-200 rounded-box overflow-hidden md:col-span-2">
          <div class="stat-title">Enrichment Progress</div>
          <div class="stat-value text-2xl">
            {format_number(@unprocessed.complete)} / {format_number(@unprocessed.total)}
          </div>
          <div class="stat-desc">fully enriched</div>
          <div class="flex flex-col gap-1.5 mt-3">
            <%= for {key, label, complete, denom} <- [
              {"hrrr", "HRRR", @unprocessed.total - @unprocessed.hrrr, @unprocessed.total},
              {"weather", "Weather", @unprocessed.total - @unprocessed.weather, @unprocessed.total},
              {"terrain", "Terrain", @unprocessed.total - @unprocessed.terrain, @unprocessed.total},
              {"iemre", "IEMRE", @unprocessed.total - @unprocessed.iemre, @unprocessed.total},
              {"radar", "Radar", @unprocessed.total - @unprocessed.radar, @unprocessed.total},
              {"narr", "NARR", @unprocessed.narr_done, @unprocessed.narr_candidates}
            ] do %>
              <% pct = if denom > 0, do: round(complete * 100 / denom), else: 0 %>
              <div class="flex items-center gap-2 text-xs" data-progress-key={key}>
                <span class="w-14 shrink-0">{label}</span>
                <progress
                  class="progress progress-primary flex-1 min-w-0"
                  value={pct}
                  max={100}
                />
                <span class="shrink-0 font-mono opacity-60 whitespace-nowrap">
                  {format_number(complete)} / {format_number(denom)}
                </span>
              </div>
            <% end %>
          </div>
        </div>
        <div class="stats stats-vertical bg-base-200 rounded-box md:col-span-2">
          <div class="stat">
            <div class="stat-title">Active/Queued Jobs</div>
            <div class="stat-value text-2xl">
              {Enum.reduce(@stats.by_worker, 0, fn w, acc -> acc + w.total end)}
            </div>
          </div>
          <div class="stat">
            <div class="stat-title">Completed (last hour)</div>
            <div class="stat-value text-2xl">{format_number(@stats.completed_1h)}</div>
          </div>
        </div>
      </div>

      <h2 class="text-base font-semibold mb-2">Job Queue Status</h2>
      <%= if @stats.by_worker == [] do %>
        <p class="text-sm text-base-content/50 italic">No active jobs.</p>
      <% else %>
        <div class="overflow-x-auto">
          <table class="table table-sm table-zebra">
            <thead>
              <tr>
                <th>Worker</th>
                <th>Executing</th>
                <th>Pending</th>
                <th>Retryable</th>
                <th>Total</th>
              </tr>
            </thead>
            <tbody>
              <%= for w <- @stats.by_worker do %>
                <tr>
                  <td class="font-semibold">{w.worker}</td>
                  <td>
                    <%= if w.executing > 0 do %>
                      <span class="flex items-center gap-1">
                        <span class="loading loading-spinner loading-xs"></span> {w.executing}
                      </span>
                    <% else %>
                      0
                    <% end %>
                  </td>
                  <td>{w.available}</td>
                  <td>
                    <%= if w.retryable > 0 do %>
                      <span class="text-warning">{w.retryable}</span>
                    <% else %>
                      0
                    <% end %>
                  </td>
                  <td>{w.total}</td>
                </tr>
              <% end %>
            </tbody>
          </table>
        </div>
      <% end %>

      <div class="divider" />

      <h2 class="text-base font-semibold mb-2">Rust Workers</h2>
      <div id="grid-tasks-panel" class="overflow-x-auto mb-4">
        <table class="table table-sm">
          <thead>
            <tr>
              <th>Worker</th>
              <th>Running</th>
              <th>Queued</th>
              <th>Retrying</th>
              <th>Done (1h)</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td class="font-semibold">prop-grid-rs</td>
              <td>
                <%= if @grid_tasks.running == [] do %>
                  0
                <% else %>
                  <span class="flex items-center gap-2">
                    <span class="loading loading-spinner loading-xs"></span>
                    <span class="font-mono text-xs">
                      <%= for r <- @grid_tasks.running do %>
                        <span class="badge badge-sm badge-info mr-1">
                          f{String.pad_leading(Integer.to_string(r.forecast_hour), 2, "0")}
                        </span>
                      <% end %>
                    </span>
                    <span class="opacity-60 text-xs">running</span>
                  </span>
                <% end %>
              </td>
              <td>{@grid_tasks.queued}</td>
              <td>
                <%= if @grid_tasks.failed > 0 do %>
                  <span class="text-warning">{@grid_tasks.failed}</span>
                <% else %>
                  0
                <% end %>
              </td>
              <td>{format_number(@grid_tasks.done_1h)}</td>
            </tr>
            <tr>
              <td class="font-semibold">hrrr-point-rs</td>
              <td>
                <%= if @hrrr_point_tasks.running == 0 do %>
                  0
                <% else %>
                  <span class="flex items-center gap-2">
                    <span class="loading loading-spinner loading-xs"></span>
                    <span class="font-mono text-xs">{@hrrr_point_tasks.running}</span>
                    <span class="opacity-60 text-xs">batches</span>
                  </span>
                <% end %>
              </td>
              <td>{format_number(@hrrr_point_tasks.queued)}</td>
              <td>
                <%= if @hrrr_point_tasks.failed > 0 do %>
                  <span class="text-warning">{@hrrr_point_tasks.failed}</span>
                <% else %>
                  0
                <% end %>
              </td>
              <td>{format_number(@hrrr_point_tasks.done_1h)}</td>
            </tr>
          </tbody>
        </table>
      </div>

      <h2 class="text-base font-semibold mb-2">Enrichment Status by Type</h2>
      <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
        <%= for {type, label} <- [{"hrrr", "HRRR"}, {"weather", "Weather"}, {"terrain", "Terrain"}, {"iemre", "IEMRE"}, {"radar", "Radar"}, {"narr", "NARR"}] do %>
          <div class="bg-base-200 rounded-box p-3 text-xs">
            <div class="font-semibold mb-1">{label}</div>
            <%= for {status, count} <- Map.get(@db_stats.statuses, type, []) do %>
              <div class="flex justify-between">
                <span class={status_class(status)}>{status}</span>
                <span class="font-mono">{format_number(count)}</span>
              </div>
            <% end %>
          </div>
        <% end %>
      </div>

      <h2 class="text-base font-semibold mb-2">Database</h2>
      <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
        <div class="stat bg-base-200 rounded-box overflow-hidden">
          <div class="stat-title">Total DB Size</div>
          <div class="stat-value text-xl font-mono">{@db_stats.db_size}</div>
        </div>
        <div class="stat bg-base-200 rounded-box overflow-hidden">
          <div class="stat-title">HRRR Profiles</div>
          <div class="stat-value text-xl font-mono">
            {format_number(@db_stats.totals.hrrr_profiles)}
          </div>
          <div class="stat-desc">{@db_stats.averages.hrrr_per_contact} per contact</div>
        </div>
        <div class="stat bg-base-200 rounded-box overflow-hidden">
          <div class="stat-title">Surface Observations</div>
          <div class="stat-value text-xl font-mono">
            {format_number(@db_stats.totals.surface_observations)}
          </div>
          <div class="stat-desc">{@db_stats.averages.obs_per_contact} per contact</div>
        </div>
        <div class="stat bg-base-200 rounded-box overflow-hidden">
          <div class="stat-title">Soundings</div>
          <div class="stat-value text-xl font-mono">
            {format_number(@db_stats.totals.soundings)}
          </div>
          <div class="stat-desc">{@db_stats.averages.soundings_per_contact} per contact</div>
        </div>
      </div>

      <h2 class="text-base font-semibold mb-2">Table Sizes</h2>
      <div class="overflow-x-auto mb-4">
        <table class="table table-xs table-zebra">
          <thead>
            <tr>
              <th>Table</th>
              <th class="text-right">Rows</th>
              <th class="text-right">Dead</th>
              <th class="text-right">Size</th>
            </tr>
          </thead>
          <tbody>
            <%= for t <- @db_stats.tables do %>
              <tr>
                <td class="font-mono text-xs">{t.table}</td>
                <td class="text-right font-mono text-xs">{format_number(t.rows)}</td>
                <td class={[
                  "text-right font-mono text-xs",
                  t.dead > 0 && "text-warning"
                ]}>
                  {format_number(t.dead)}
                </td>
                <td class="text-right font-mono text-xs">{t.size}</td>
              </tr>
            <% end %>
          </tbody>
        </table>
      </div>

      <p class="text-xs text-base-content/50 mt-4">Live updates via database notifications.</p>
    </Layouts.app>
    """
  end
end
