/**
 * public/launcher.ts — the home "session chooser" shown when no tab is open.
 *
 * Opening the app no longer auto-creates or auto-restores tabs. Instead this
 * start screen lists the host's running sessions as live preview thumbnails
 * (read-only xterm, same as the manage page) so the user picks which to open —
 * or starts a new one. Sessions persist server-side, so opening one replays its
 * full scrollback. The thumbnail card + preview plumbing is shared with the
 * manage page via public/preview-grid.ts (DRY).
 */

import type { LiveSessionInfo, OrphanSessionInfo } from '../src/types.js'
import {
  el,
  relTime,
  makePreviewCard,
  updatePreviewCard,
  loadPreviewInto,
  disposeCard,
  fetchLiveSessions,
  fetchOrphanSessions,
  fetchOrphanPreview,
  countIdleOrphans,
  renderPreview,
  orphanAsLive,
  killOrphanReq,
  cleanupOrphansReq,
  type PreviewCard,
} from './preview-grid.js'

export interface LauncherHooks {
  onOpen: (id: string) => void
  onNew: () => void
}

const THUMB_W = 320
const THUMB_MAX_H = 200
const REFRESH_MS = 5000

export interface Launcher {
  setVisible(v: boolean): void
  refresh(): void
}

export function mountLauncher(host: HTMLElement, hooks: LauncherHooks): Launcher {
  const root = el('div', 'launcher')
  root.style.display = 'none'
  host.appendChild(root)

  const head = el('div', 'launcher-head')
  head.append(el('div', 'launcher-title', 'Your sessions'))
  const sub = el('div', 'launcher-sub', '')
  head.append(sub)
  root.append(head)

  const grid = el('div', 'mg-grid')
  root.append(grid)

  // A: tmux sessions that outlived the server process. Its own section, below the
  // live grid, because these are recoveries rather than things you are working on
  // — and it stays hidden entirely when there are none (the common case, and the
  // only case when tmux is off).
  const orphanSection = el('div', 'launcher-orphans')
  orphanSection.style.display = 'none'
  const orphanHead = el('div', 'launcher-orphans-head')
  const orphanTitle = el('div', 'launcher-orphans-title', 'Recoverable')
  const orphanSub = el('div', 'launcher-orphans-sub', '')
  const cleanupBtn = el('button', 'launcher-cleanup', 'Clean up idle 7d+')
  cleanupBtn.title = 'Kill unattached recoverable sessions with no tmux activity for 7 days'
  orphanHead.append(orphanTitle, orphanSub, cleanupBtn)
  const orphanGrid = el('div', 'mg-grid')
  const orphanMore = el('div', 'launcher-orphans-more', '')
  orphanMore.style.display = 'none'
  orphanSection.append(orphanHead, orphanGrid, orphanMore)
  root.append(orphanSection)

  // "New session" is a tile that leads the grid (matches the session cards),
  // instead of a heavy header button. Re-prepended on every refresh.
  const newTile = el('button', 'mg-new-card')
  newTile.title = 'Start a new session'
  newTile.setAttribute('aria-label', 'New session')
  newTile.append(el('div', 'mg-new-plus', '＋'), el('div', 'mg-new-label', 'New session'))
  newTile.addEventListener('click', () => hooks.onNew())

  const cards = new Map<string, PreviewCard>()
  const orphanCards = new Map<string, PreviewCard>()
  /** Ids whose preview has actually RENDERED. Latching on "card exists" instead
   *  left a card that lost its one request (429, or a 404 racing the list) blank
   *  forever, because nothing ever asked again. */
  const orphanPreviewed = new Set<string>()
  let timer: ReturnType<typeof setInterval> | null = null
  /** Bumped by every teardown and every new refresh. An in-flight refresh compares
   *  it after each await and bails if it is stale — otherwise a slow poll could
   *  resurrect cards for sessions already killed, or re-mount them into a root that
   *  setVisible(false) has just cleared. */
  let generation = 0

  const CLEANUP_IDLE_DAYS = 7
  /** Thumbnails are xterm instances; a host with dozens of stale tmux sessions
   *  would otherwise mount dozens of terminals on the home screen. */
  const MAX_ORPHAN_CARDS = 24

  /** Kill a session (DELETE /live-sessions/:id) then refresh the grid. */
  async function killOne(id: string): Promise<void> {
    await fetch(`/live-sessions/${encodeURIComponent(id)}`, { method: 'DELETE' }).catch(() => {})
    void refresh()
  }

  function makeCard(s: LiveSessionInfo): PreviewCard {
    const kill = el('button', 'mg-kill', 'Kill ✕')
    kill.title = 'Kill this session'
    kill.addEventListener('click', () => void killOne(s.id))
    return makePreviewCard(s, {
      onOpen: (id) => hooks.onOpen(id),
      extraActions: () => [kill],
    })
  }

  async function refresh(): Promise<void> {
    const sessions = await fetchLiveSessions()

    sub.textContent = sessions.length
      ? `${sessions.length} running on this host — pick one to open`
      : 'No sessions running yet'

    // Keep the New-session tile as the first grid cell.
    if (grid.firstChild !== newTile) grid.prepend(newTile)

    const seen = new Set<string>()
    for (const s of sessions) {
      seen.add(s.id)
      let card = cards.get(s.id)
      if (!card) {
        card = makeCard(s)
        cards.set(s.id, card)
        grid.append(card.el)
      }
      updatePreviewCard(card, s)
      card.meta.textContent = `${s.cwd ?? 'unknown dir'} · ${s.cols}×${s.rows} · ${relTime(s.createdAt)} old`
      void loadPreviewInto(s.id, card, THUMB_W, THUMB_MAX_H)
    }
    for (const [id, card] of cards) {
      if (!seen.has(id)) {
        disposeCard(card)
        cards.delete(id)
      }
    }

    await refreshOrphans()
  }

  /** Kill one orphan, then refresh. A refusal (403 from the Origin guard behind a
   *  proxy, 404 from a lost race) must be visible — silently doing nothing reads as
   *  a broken button. */
  async function killOrphan(id: string): Promise<void> {
    const ok = await killOrphanReq(id)
    if (!ok) orphanSub.textContent = `Could not end session ${id.slice(0, 8)} — it may already be gone.`
    void refresh()
  }

  /** Opening an orphan revives it: attaching with its id re-adopts the tmux
   *  session in place, after which it appears in the live grid above. */
  function makeOrphanCard(o: OrphanSessionInfo): PreviewCard {
    const kill = el('button', 'mg-kill', 'Kill ✕')
    kill.title = 'End this session’s shell'
    kill.addEventListener('click', () => void killOrphan(o.id))
    return makePreviewCard(orphanAsLive(o), {
      onOpen: (id) => hooks.onOpen(id),
      extraActions: () => [kill],
    })
  }

  async function refreshOrphans(): Promise<void> {
    const myGen = generation
    const orphans = await fetchOrphanSessions()
    if (myGen !== generation) return // torn down or superseded while fetching

    // null = the request failed. Leave the section exactly as it is: reporting
    // "none" would tear down every mounted xterm and rebuild it on recovery.
    if (orphans === null) return

    if (orphans.length === 0) {
      orphanSection.style.display = 'none'
      for (const card of orphanCards.values()) disposeCard(card)
      orphanCards.clear()
      orphanPreviewed.clear()
      return
    }

    orphanSection.style.display = ''
    const idle = orphans.filter((o) => !o.attached).length
    orphanSub.textContent =
      `${orphans.length} tmux session${orphans.length === 1 ? '' : 's'} not open here — ` +
      `open one to take it over${idle > 0 ? `, ${idle} unattended` : ''}`

    // Server-sorted most-recently-active first, so the cap keeps the ones you are
    // plausibly looking for. The tail is what the cleanup button is for.
    const shown = orphans.slice(0, MAX_ORPHAN_CARDS)
    const hidden = orphans.length - shown.length

    const seen = new Set<string>()
    for (const o of shown) {
      seen.add(o.id)
      let card = orphanCards.get(o.id)
      if (card === undefined) {
        card = makeOrphanCard(o)
        orphanCards.set(o.id, card)
        orphanGrid.append(card.el)
      }
      updatePreviewCard(card, orphanAsLive(o))
      card.meta.textContent =
        `${o.cols}×${o.rows} · active ${relTime(o.lastActivityAt)} ago · ` +
        `${relTime(o.createdAt)} old`
      // ONCE per card, not on every 5 s tick: each preview spawns a tmux
      // capture-pane, and re-capturing every card every tick would mean dozens of
      // processes a minute for screens that are, by definition, not being driven
      // from here. Fire-and-forget so a slow capture never stalls the next card.
      if (!orphanPreviewed.has(o.id)) {
        const target = card
        const targetId = o.id
        void fetchOrphanPreview(targetId).then((p) => {
          if (p === null || myGen !== generation) return
          renderPreview(target, p, THUMB_W, THUMB_MAX_H)
          orphanPreviewed.add(targetId) // only now stop asking
        })
      }
    }
    for (const [id, card] of orphanCards) {
      if (!seen.has(id)) {
        disposeCard(card)
        orphanCards.delete(id)
        orphanPreviewed.delete(id)
      }
    }

    // Never truncate silently.
    orphanMore.textContent =
      hidden > 0
        ? `${hidden} older session${hidden === 1 ? '' : 's'} not shown — clean up, or attach by id from a terminal with: tmux attach -t web_<id>`
        : ''
    orphanMore.style.display = hidden > 0 ? '' : 'none'
  }

  cleanupBtn.addEventListener('click', () => {
    // Ask the SERVER how many it would end. orphanCards is capped at
    // MAX_ORPHAN_CARDS while the cleanup matches across every session the server
    // enumerates, so counting cards would understate the blast radius — on a host
    // with 69 orphans the dialog would have said 24.
    void countIdleOrphans(CLEANUP_IDLE_DAYS).then((n) => {
      if (n === null) {
        orphanSub.textContent = 'Could not check how many sessions are idle — not cleaning up.'
        return
      }
      if (n === 0) {
        orphanSub.textContent = `Nothing to clean up: no unattended session has been idle ${CLEANUP_IDLE_DAYS} days.`
        return
      }
      if (
        !window.confirm(
          `End ${n} session${n === 1 ? '' : 's'}?\n\n` +
            `These are recoverable sessions with no tmux activity for ${CLEANUP_IDLE_DAYS} days. ` +
            `Their shells will be terminated. Sessions someone is attached to are never touched.`,
        )
      ) {
        return
      }
      void cleanupOrphansReq(CLEANUP_IDLE_DAYS).then((ids) => {
        if (ids.length === 0) orphanSub.textContent = 'Cleanup ended no sessions.'
        void refresh()
      })
    })
  })

  return {
    setVisible(v: boolean): void {
      root.style.display = v ? 'block' : 'none'
      if (v) {
        void refresh()
        if (timer === null) {
          timer = setInterval(() => {
            if (root.style.display !== 'none') void refresh()
          }, REFRESH_MS)
        }
      } else {
        if (timer !== null) {
          clearInterval(timer)
          timer = null
        }
        for (const card of cards.values()) disposeCard(card)
        cards.clear()
        grid.replaceChildren()
        generation += 1 // invalidate any refresh still in flight
        for (const card of orphanCards.values()) disposeCard(card)
        orphanCards.clear()
        orphanPreviewed.clear()
        orphanGrid.replaceChildren()
        orphanSection.style.display = 'none'
      }
    },
    refresh(): void {
      void refresh()
    },
  }
}
