/**
 * T5 (v0.9) — dashboard host list with `●online/offline/draining/revoked` status.
 *
 * Renders `api.listHosts()` as cards (subdomain, status dot, last_seen, an "open" action routing to
 * the terminal view for that host_id) and polls for live `●online` flips. The UI reflects
 * server-authorized hosts only — there is NO input path to name a foreign host_id (INV1/INV3
 * defense-in-depth; the real gate is server-side). A `revoked` host is non-interactive (INV12 UI).
 *
 * All dynamic text is set via `textContent` (never `innerHTML`) — XSS discipline.
 */
import type { ApiClient } from './api-client'
import type { HostRecord, HostStatus } from './api-schemas'
import { ApiError } from './errors'

const DEFAULT_POLL_MS = 5000

export interface Dashboard {
  refresh(): Promise<void>
  dispose(): void
}

export interface DashboardOpts {
  readonly pollMs?: number
  /** Routes to the terminal view for a host; injected so the module owns no navigation policy. */
  readonly onOpen?: (hostId: string) => void
}

/** Statuses that must NOT expose an "open" action in the UI (INV12 defense-in-depth). */
const NON_INTERACTIVE: ReadonlySet<HostStatus> = new Set<HostStatus>(['revoked'])

function buildCard(host: HostRecord, opts: DashboardOpts): HTMLElement {
  const card = document.createElement('div')
  card.className = 'host-card'
  card.dataset['hostId'] = host.hostId
  card.dataset['status'] = host.status

  const dot = document.createElement('span')
  dot.className = `status-dot status-${host.status}`
  dot.setAttribute('aria-label', host.status)
  dot.textContent = '●'

  const name = document.createElement('span')
  name.className = 'host-subdomain'
  name.textContent = host.subdomain

  const seen = document.createElement('span')
  seen.className = 'host-last-seen'
  seen.textContent = host.lastSeen

  card.append(dot, name, seen)

  if (NON_INTERACTIVE.has(host.status)) {
    const blocked = document.createElement('span')
    blocked.className = 'host-blocked'
    blocked.textContent = 'revoked'
    card.append(blocked)
  } else {
    const open = document.createElement('button')
    open.className = 'host-open'
    open.type = 'button'
    open.textContent = 'open'
    open.addEventListener('click', () => opts.onOpen?.(host.hostId))
    card.append(open)
  }
  return card
}

export function mountDashboard(root: HTMLElement, api: ApiClient, opts: DashboardOpts = {}): Dashboard {
  const pollMs = opts.pollMs ?? DEFAULT_POLL_MS

  const list = document.createElement('div')
  list.className = 'host-list'
  const banner = document.createElement('p')
  banner.className = 'dashboard-error'
  banner.setAttribute('role', 'alert')
  root.append(banner, list)

  let timer: ReturnType<typeof setInterval> | null = null
  let disposed = false

  async function refresh(): Promise<void> {
    try {
      const hosts = await api.listHosts()
      if (disposed) return
      banner.textContent = ''
      list.replaceChildren(...hosts.map((h) => buildCard(h, opts)))
    } catch (err) {
      if (disposed) return
      // Zod drift or a rejection surfaces as a banner; polling continues (never renders undefined).
      banner.textContent =
        err instanceof ApiError ? `Could not load hosts (${err.kind}).` : 'Could not load hosts.'
    }
  }

  void refresh()
  timer = setInterval(() => void refresh(), pollMs)

  return {
    refresh,
    dispose(): void {
      disposed = true
      if (timer) clearInterval(timer)
    },
  }
}
