/**
 * Internal, agent-facing routes.
 *
 * These are the endpoints the orchestrator agent calls (via `curl` from its
 * `pi` subprocess) to record its work: posting card comments, moving cards,
 * adding references, and upserting documentation pages. They are the only way
 * the kanban/docs SQLite store is mutated by an agent, keeping all writes in
 * this process.
 *
 * Every mutation requires an `x-agent-token` header matching a run's token.
 * Tokens are single-use-per-run secrets issued at run creation and baked into
 * the agent's prompt, so they never need to be shared with the UI.
 */

import { Hono } from 'hono';
import { randomUUID } from 'node:crypto';
import { db } from '../db.js';
import type { Column } from '../types.js';
import { isColumn } from '../types.js';
import { renderMarkdown } from '../markdown.js';
import { runManager } from '../orchestrator/runs.js';
import { appendRunEvent } from '../orchestrator/events.js';

export const internal = new Hono<{
  Variables: { runId: string; cardId: string };
}>();

/** Middleware: require a valid agent token and attach the owning run's card id. */
internal.use('*', async (c, next) => {
  const token = c.req.header('x-agent-token');
  if (!token) return c.json({ error: 'x-agent-token header required' }, 401);
  const run = runManager.getByToken(token);
  if (!run) return c.json({ error: 'invalid or expired agent token' }, 401);
  c.set('runId', run.id);
  c.set('cardId', run.cardId);
  await next();
});

/** Post a comment on the run's card as the agent. */
internal.post('/cards/:id/comments', async (c) => {
  const cardId = c.req.param('id');
  if (!(cardExists(cardId))) return c.json({ error: 'card not found' }, 404);

  const body = await c.req.json().catch(() => ({}) as { text?: unknown });
  const text = typeof body.text === 'string' ? body.text.trim() : '';
  if (!text) return c.json({ error: "'text' is required" }, 400);

  const runId = c.get('runId');
  const comment = {
    id: randomUUID(),
    cardId,
    text,
    author: 'Agent',
    createdAt: Date.now(),
  };
  db.prepare(
    'INSERT INTO comments (id, card_id, text, author, created_at) VALUES (?, ?, ?, ?, ?)',
  ).run(comment.id, comment.cardId, comment.text, comment.author, comment.createdAt);

  if (runId) {
    logEvent(runId, 'log', { level: 'info', text: `Comment on ${cardId}: ${text.slice(0, 160)}` });
  }
  return c.json(comment, 201);
});

/** Move the run's card to a new column. */
internal.patch('/cards/:id', async (c) => {
  const cardId = c.req.param('id');
  if (!cardExists(cardId)) return c.json({ error: 'card not found' }, 404);

  const body = await c.req.json().catch(() => ({}) as { status?: unknown });
  if (!isColumn(body.status)) return c.json({ error: "body must include a valid 'status'" }, 400);
  // Only a human may mark a card done — an agent must never complete cards.
  if (body.status === 'done') {
    return c.json(
      {
        error:
          "agents cannot set a card to 'done'. Leave it 'in-progress' with a summary comment; a human reviews and completes the card.",
      },
      403,
    );
  }

  const now = new Date().toISOString();
  db.prepare('UPDATE cards SET status = ?, updated_at = ? WHERE id = ?').run(
    body.status as Column,
    now,
    cardId,
  );

  const runId = c.get('runId');
  if (runId) logEvent(runId, 'log', { level: 'info', text: `Moved ${cardId} → ${body.status}` });
  return c.json({ cardId, status: body.status });
});

/** Add a user reference (doc/custom/external) to a card. */
internal.post('/cards/:id/references', async (c) => {
  const cardId = c.req.param('id');
  if (!cardExists(cardId)) return c.json({ error: 'card not found' }, 404);

  const body = await c.req.json().catch(() => ({}) as Record<string, unknown>);
  const label = typeof body.label === 'string' ? body.label.trim() : '';
  const href = typeof body.href === 'string' ? body.href.trim() : '';
  const type = typeof body.type === 'string' ? body.type : 'doc';
  if (!label || !href) return c.json({ error: "'label' and 'href' are required" }, 400);

  db.prepare(
    'INSERT OR REPLACE INTO card_references (card_id, label, type, href, canonical) VALUES (?, ?, ?, ?, 0)',
  ).run(cardId, label, type, href);

  const runId = c.get('runId');
  if (runId) logEvent(runId, 'log', { level: 'info', text: `Linked ${cardId} → ${href}` });
  return c.json({ cardId, label, type, href, removable: true }, 201);
});

interface UpsertPageBody {
  title?: unknown;
  icon?: unknown;
  blurb?: unknown;
  content?: unknown;
  slug?: unknown;
}

/**
 * Create or update a custom documentation page (upsert by slug).
 *
 * This is how the agent persists design decisions and completed-work summaries
 * into the documentation — the central store of truth. Content containing HTML
 * is stored verbatim as beautified HTML; otherwise it is rendered from markdown.
 */
internal.post('/pages', async (c) => {
  const body = await c.req.json().catch(() => ({}) as UpsertPageBody);
  const title = typeof body.title === 'string' && body.title.trim() ? body.title.trim() : 'Untitled';
  const icon = typeof body.icon === 'string' && body.icon.trim() ? body.icon.trim().slice(0, 4) : '◈';
  const blurb = typeof body.blurb === 'string' ? body.blurb.trim() : '';
  const content = typeof body.content === 'string' ? body.content : '';
  if (!content.trim()) return c.json({ error: "'content' is required" }, 400);

  const slug = (typeof body.slug === 'string' && body.slug.trim() ? body.slug.trim() : slugify(title)).slice(0, 64);
  const looksHtml = /<[a-z][\s\S]*>/i.test(content);
  const html = looksHtml ? content : renderMarkdown(content);
  const now = new Date().toISOString();

  const existing = db.prepare('SELECT id FROM custom_pages WHERE slug = ?').get(slug) as { id: string } | undefined;
  let id: string;
  if (existing) {
    id = existing.id;
    db.prepare(
      `UPDATE custom_pages SET title = ?, icon = ?, blurb = ?, content = ?, html = ?, beautified = ?, updated_at = ? WHERE id = ?`,
    ).run(title, icon, blurb, content, html, looksHtml ? 1 : 0, now, id);
  } else {
    id = randomUUID();
    db.prepare(
      `INSERT INTO custom_pages (id, slug, title, icon, blurb, content, html, beautified, created_at, updated_at)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
    ).run(id, slug, title, icon, blurb, content, html, looksHtml ? 1 : 0, now, now);
  }

  const runId = c.get('runId');
  if (runId) logEvent(runId, 'log', { level: 'info', text: `Documented "${title}" → /docs/custom/${slug}` });

  const row = db.prepare('SELECT * FROM custom_pages WHERE id = ?').get(id);
  return c.json(hydratePage(row), existing ? 200 : 201);
});

// --- helpers ---------------------------------------------------------------

function cardExists(id: string): boolean {
  return Boolean(db.prepare('SELECT 1 FROM cards WHERE id = ?').get(id));
}

function slugify(input: string): string {
  return (
    input
      .toLowerCase()
      .trim()
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/^-+|-+$/g, '')
      .slice(0, 64) || `page-${randomUUID().slice(0, 8)}`
  );
}

function hydratePage(row: unknown): Record<string, unknown> {
  const r = row as {
    id: string; slug: string; title: string; icon: string; blurb: string;
    content: string; html: string; beautified: number; created_at: string; updated_at: string;
  };
  return {
    id: r.id, slug: r.slug, title: r.title, icon: r.icon, blurb: r.blurb,
    content: r.content, html: r.html, beautified: r.beautified === 1,
    createdAt: r.created_at, updatedAt: r.updated_at,
  };
}

/** Append a slim event to a run's persisted history. */
function logEvent(runId: string, type: string, data: Record<string, unknown>): void {
  appendRunEvent(runId, type, data);
}
