import type { Hono } from 'hono';
import {
  createAgentPanel,
  deleteAgentPanel,
  getAgentPanelByPanelId,
  listAgentPanels,
  toAgentPanelView,
  updateAgentPanel,
} from 'officerdb';
import { introduceAgentPanel } from '../agent-handoff/deliver';
import { logger } from './logger';
import { OFFICER_API_URL } from '../../officer-url.mjs';

/**
 * The browser's half of the address book: name a panel, look up what a panel is, rename, remove.
 *
 * Mounted on the chat router rather than given its own prefix, because it is the same capability —
 * these routes create and name Claude sessions, which is what `chat` already grants. A new top-level
 * mount would have meant a new capability entry claiming the same authority under a second name.
 *
 * The agent-facing door is separate and deliberately so: `servers/api/agent-handoff/router.ts`.
 */

/** A name has to survive being typed into a prompt and into a shell, so keep it boring. */
const NAME_RE = /^[a-z0-9][a-z0-9-]{0,30}[a-z0-9]$|^[a-z0-9]$/;

const API_ORIGIN = OFFICER_API_URL;

export function registerAgentPanelRoutes(router: Hono<any>): void {
  // GET /chat/agent-panels?dashboardId=… — the address book for one dashboard.
  router.get('/agent-panels', async (ctx) => {
    const user = ctx.get('user');
    const dashboardId = ctx.req.query('dashboardId')?.trim();
    if (!dashboardId) return ctx.text('dashboardId is required', 400);
    const panels = await listAgentPanels(user.id, dashboardId);
    return ctx.json({ agents: panels.map(toAgentPanelView) });
  });

  // POST /chat/agent-panels — name a panel, minting its permanent sessionKey.
  //
  // Idempotent on (dashboardId, panelId): a double-submit or a remount returns the existing agent
  // rather than a second one. It has to be — the sessionKey is the panel's whole continuity, and
  // minting a second one would abandon the first session with no way back to it.
  router.post('/agent-panels', async (ctx) => {
    const user = ctx.get('user');
    const body = (ctx.get('body') ?? {}) as Record<string, unknown>;

    const dashboardId = typeof body.dashboardId === 'string' ? body.dashboardId.trim() : '';
    const panelId = typeof body.panelId === 'string' ? body.panelId.trim() : '';
    const name = typeof body.name === 'string' ? body.name.trim().toLowerCase() : '';
    const cwd = typeof body.cwd === 'string' && body.cwd.trim() ? body.cwd.trim() : null;
    const rolePrompt = typeof body.rolePrompt === 'string' && body.rolePrompt.trim() ? body.rolePrompt.trim() : null;

    if (!dashboardId || !panelId) return ctx.text('dashboardId and panelId are required', 400);
    if (!NAME_RE.test(name)) {
      return ctx.text('Name must be lowercase letters, digits and dashes — e.g. "frontend", "code-reviewer"', 400);
    }

    const existing = await getAgentPanelByPanelId(user.id, dashboardId, panelId);
    if (existing) return ctx.json({ agent: toAgentPanelView(existing), created: false });

    let created;
    try {
      created = await createAgentPanel({ userId: user.id, dashboardId, panelId, name, cwd, rolePrompt });
    } catch (err) {
      // uq_agent_panels_address — the address must resolve to one agent or a handoff is ambiguous.
      if (String(err).includes('uq_agent_panels_address')) {
        return ctx.text(`There is already an agent named "${name}" on this dashboard`, 409);
      }
      throw err;
    }

    // The introduction is the session's first turn: it is how the agent learns its own name, its peers
    // and the command it uses to reach them. Fire-and-forget on purpose — the panel should render
    // immediately, and if this fails the agent is still addressable, just uninformed. Logged, not
    // swallowed, so "it never introduced itself" is diagnosable.
    void listAgentPanels(user.id, dashboardId)
      .then((peers) => introduceAgentPanel(created, peers, API_ORIGIN))
      .catch((err) => logger.error('Failed to introduce agent panel', { name, dashboardId, error: String(err) }));

    return ctx.json({ agent: toAgentPanelView(created), created: true });
  });

  // PATCH /chat/agent-panels/:id — rename, re-scope, or record that the panel moved.
  router.patch('/agent-panels/:id', async (ctx) => {
    const user = ctx.get('user');
    const id = Number(ctx.req.param('id'));
    if (!Number.isFinite(id)) return ctx.text('Bad id', 400);
    const body = (ctx.get('body') ?? {}) as Record<string, unknown>;

    const patch: Record<string, unknown> = {};
    if (typeof body.name === 'string') {
      const name = body.name.trim().toLowerCase();
      if (!NAME_RE.test(name)) return ctx.text('Name must be lowercase letters, digits and dashes', 400);
      patch.name = name;
    }
    if ('cwd' in body) patch.cwd = typeof body.cwd === 'string' && body.cwd.trim() ? body.cwd.trim() : null;
    if ('rolePrompt' in body) patch.rolePrompt = typeof body.rolePrompt === 'string' ? body.rolePrompt : null;
    if (typeof body.panelId === 'string' && body.panelId.trim()) patch.panelId = body.panelId.trim();

    const updated = await updateAgentPanel(user.id, id, patch);
    if (!updated) return ctx.text('No such agent', 404);
    return ctx.json({ agent: toAgentPanelView(updated) });
  });

  // DELETE /chat/agent-panels/:id — forget the agent. The Claude transcript is NOT deleted: it lives
  // in ~/.claude/projects and is readable from /chat like any other conversation. Only the address
  // and the panel's claim on that session go away.
  router.delete('/agent-panels/:id', async (ctx) => {
    const user = ctx.get('user');
    const id = Number(ctx.req.param('id'));
    if (!Number.isFinite(id)) return ctx.text('Bad id', 400);
    await deleteAgentPanel(user.id, id);
    return ctx.json({ ok: true });
  });
}
