/**
 * v2.6 Phase 3 (3.3) — chat/session close-or-archive cleanup hook (coder side).
 *
 * Chat/session close + archive + delete all live in apps/server (Docker), which
 * cannot see the host worktree dirs (/tmp/booworktrees), run git on them, or reach
 * the warm agent processes the dispatcher pooled in THIS (host systemd) process. So
 * — exactly like the `worktree-risk` guard — the server signals the coder when a
 * chat/session closes, and the coder does the real teardown:
 *   1. dispose the chat's warm-ACP backends (`agentPool.closeChat`) — kills the
 *      goose/qwen child processes for that chat,
 *   2. close the chat's opencode session on the shared server (`closeSession`),
 *   3. mark every `agent_sessions` row for the chat 'closed' + (when the session's
 *      last open chat closes) remove the shared session worktree, preflighting
 *      work-at-risk so uncommitted/unmerged work is never silently dropped
 *      (`closeChatBackendState`).
 *
 * Idempotent: closing an already-closed chat is a no-op (0 rows, no backend).
 *
 * SERVER WIRING (not done here — apps/server, out of this batch's scope): the
 * server's `POST /api/chats/:id/archive`, `DELETE /api/chats/:id`, and the
 * session archive/delete routes should fire-and-forget
 *   fetch(`${BOOCODER_URL}/api/chats/${id}/close`, { method: 'POST' })
 * after publishing their WS frame (best-effort; the orphan-worktree reaper +
 * idle-pool eviction are the backstop if the call is missed).
 */
import type { FastifyInstance } from 'fastify';
import type { Sql } from '../db.js';
import { agentPool, OPENCODE_POOL_KEY } from '../services/agent-pool.js';
import { closeChatBackendState } from '../services/worktrees.js';
import type { AgentSessionHandle } from '../services/agent-backend.js';

export function registerLifecycleRoutes(app: FastifyInstance, sql: Sql): void {
  // POST /api/chats/:chatId/close — tear down all warm state for a chat tab.
  app.post<{ Params: { chatId: string }; Querystring: { force?: string } }>(
    '/api/chats/:chatId/close',
    async (req) => {
      const chatId = req.params.chatId;
      const force = req.query.force === 'true' || req.query.force === '1';

      // 1. Close the chat's opencode session on the SHARED server (the server is
      //    not chat-keyed, so agentPool.closeChat won't touch it). Resolve the
      //    stored opencode session id and ask the backend to drop it.
      const ocRows = await sql<{ agent: string; agent_session_id: string | null; worktree_id: string | null; session_id: string | null }[]>`
        SELECT agent, agent_session_id, worktree_id, session_id
        FROM agent_sessions
        WHERE chat_id = ${chatId} AND backend = 'opencode_server'
      `;
      const ocBackend = agentPool.peek(OPENCODE_POOL_KEY, 'opencode');
      if (ocBackend) {
        for (const row of ocRows) {
          if (!row.agent_session_id) continue;
          const handle: AgentSessionHandle = {
            sessionId: row.session_id ?? '',
            agent: row.agent,
            backend: 'opencode_server',
            chatId,
            worktreeId: row.worktree_id ?? '',
            agentSessionId: row.agent_session_id,
            serverPort: null,
          };
          await ocBackend.closeSession(handle).catch((err) => {
            app.log.warn({ err: err instanceof Error ? err.message : String(err), chatId }, 'lifecycle: opencode closeSession threw');
          });
        }
      }

      // 2. Dispose any warm-ACP backends pooled under this chat (kills the
      //    goose/qwen child + marks its agent row closed via the backend).
      const disposed = await agentPool.closeChat(chatId);

      // 3. DB + worktree truth: mark agent rows closed; remove the shared session
      //    worktree iff this was the session's last open chat (preflight at-risk).
      const result = await closeChatBackendState(sql, chatId, { force });

      app.log.info({ chatId, disposed, ...result }, 'lifecycle: chat closed');
      return { ok: true, disposed, ...result };
    },
  );

  // POST /api/sessions/:sessionId/close — close every open chat in a session
  // (session archive/delete). Loops the chat-close path so the same preflight +
  // teardown applies per chat; the worktree is removed on the last one.
  app.post<{ Params: { sessionId: string }; Querystring: { force?: string } }>(
    '/api/sessions/:sessionId/close',
    async (req) => {
      const sessionId = req.params.sessionId;
      const force = req.query.force === 'true' || req.query.force === '1';

      const chats = await sql<{ id: string }[]>`
        SELECT id FROM chats WHERE session_id = ${sessionId}
      `;
      const results: { chatId: string; disposed: string[]; worktreeRemoved: boolean; worktreeAtRisk: boolean }[] = [];
      for (const c of chats) {
        const ocBackend = agentPool.peek(OPENCODE_POOL_KEY, 'opencode');
        if (ocBackend) {
          const ocRows = await sql<{ agent: string; agent_session_id: string | null; worktree_id: string | null; session_id: string | null }[]>`
            SELECT agent, agent_session_id, worktree_id, session_id
            FROM agent_sessions WHERE chat_id = ${c.id} AND backend = 'opencode_server'
          `;
          for (const row of ocRows) {
            if (!row.agent_session_id) continue;
            await ocBackend.closeSession({
              sessionId: row.session_id ?? '',
              agent: row.agent,
              backend: 'opencode_server',
              chatId: c.id,
              worktreeId: row.worktree_id ?? '',
              agentSessionId: row.agent_session_id,
              serverPort: null,
            }).catch(() => {});
          }
        }
        const disposed = await agentPool.closeChat(c.id);
        const r = await closeChatBackendState(sql, c.id, { force });
        results.push({ chatId: c.id, disposed, worktreeRemoved: r.worktreeRemoved, worktreeAtRisk: r.worktreeAtRisk });
      }

      app.log.info({ sessionId, chats: results.length }, 'lifecycle: session closed');
      return { ok: true, results };
    },
  );
}
