import type { Sql } from '../db.js';
import type { WsFrame } from '@boocode/contracts/ws-frames';

export type TerminalMessageStatus = 'cancelled' | 'failed';

/**
 * F1 (D-7) — decide the terminal status a Stop'd / errored external turn lands in.
 *
 * A user Stop (the per-task AbortController fired) or a thrown `AbortError` is a
 * deliberate, non-error outcome → `'cancelled'`. A genuine thrown error → `'failed'`.
 * Keeping the two distinct keeps the human-inbox / failure surfaces honest.
 *
 * Pure (no DB / IO) so the mapping is unit-testable in isolation.
 */
export function classifyTerminalStatus(opts: { aborted: boolean; error?: unknown }): TerminalMessageStatus {
  if (opts.aborted) return 'cancelled';
  if (opts.error instanceof Error && opts.error.name === 'AbortError') return 'cancelled';
  return 'failed';
}

/**
 * F1 (OCE-001 / OCE-002) — finalize a streaming assistant message into a terminal
 * state and publish the matching `message_complete` frame.
 *
 * Idempotent via `WHERE status = 'streaming'`: a second call (a double-Stop, or an
 * abort short-circuit followed by the catch block) updates zero rows and does NOT
 * re-publish, so the frontend reducer settles the message exactly once. It also
 * never clobbers a row that already finished cleanly (`complete`) — the abort that
 * raced a clean finish is a no-op.
 *
 * Returns `true` iff this call performed the finalization (the row was still
 * streaming); `false` if it was already terminal or the id is absent (the throw
 * preceded the row's creation).
 */
export async function finalizeStreamingMessage(
  sql: Sql,
  publishFrame: (sessionId: string, frame: WsFrame) => void,
  opts: {
    sessionId: string;
    chatId: string;
    assistantId: string;
    status: TerminalMessageStatus;
    model: string | null;
    /** Partial accumulated text to persist; omit to leave the row's content untouched. */
    content?: string;
  },
): Promise<boolean> {
  const { sessionId, chatId, assistantId, status, model, content } = opts;
  if (!assistantId) return false;

  const rows =
    content !== undefined
      ? await sql<{ id: string }[]>`
          UPDATE messages
          SET content = ${content}, status = ${status}, finished_at = clock_timestamp()
          WHERE id = ${assistantId} AND status = 'streaming'
          RETURNING id
        `
      : await sql<{ id: string }[]>`
          UPDATE messages
          SET status = ${status}, finished_at = clock_timestamp()
          WHERE id = ${assistantId} AND status = 'streaming'
          RETURNING id
        `;

  if (rows.length === 0) return false;

  publishFrame(sessionId, {
    type: 'message_complete',
    message_id: assistantId,
    chat_id: chatId,
    model,
    status,
  } as WsFrame);
  return true;
}
