import type {
  Agent,
  Message,
  MessageMetadata,
  Project,
  Session,
} from '../../types/api.js';
import { buildMessagesPayload } from './payload.js';
import { DOOM_LOOP_THRESHOLD } from './sentinels.js';
import { streamCompletion, samplerOptsFromAgent } from './stream-phase.js';
import { createContentFlusher } from './content-flusher.js';
import { finalizeStreamedRow } from './error-handler.js';
import type {
  InferenceContext,
  StreamResult,
  TurnArgs,
} from './types.js';

// Synthetic system note appended to the cap-hit summary call. Verbatim from
// the v1.8.2 spec — do not paraphrase: the model is more reliable when the
// instruction is short, declarative, and identical across calls.
const CAP_HIT_SUMMARY_NOTE = (limit: number) =>
  `You've reached the tool budget (${limit} calls). Produce the best answer you can with what you have. Do not call more tools.`;

const DOOM_LOOP_NOTE = (name: string) =>
  `You called ${name} with the same arguments ${DOOM_LOOP_THRESHOLD} times in a row. Stop calling it. Produce the best answer you can with what you have.`;

// v1.14.0: step-cap wrap-up note. Names the step limit rather than the tool
// budget. The sentinel reuses metadata.kind = 'cap_hit' so the frontend
// CapHitSentinel component renders it without changes.
const STEP_CAP_NOTE = (steps: number, cap: number) =>
  `You've reached the step limit (${steps}/${cap} steps). Produce the best answer you can with what you have. Do not call more tools.`;

// P5: the ONE generic wrap-up flow shared by the three sentinel summaries
// (cap-hit, doom-loop, step-cap). Each reuses the in-flight assistant slot to
// stream a short tools-disabled summary, finalizes via the same 3-outcome
// branch (complete / cancelled / failed), bumps the session, then drops a
// sentinel and the chat_status. The three differ only in:
//   - `note`: the synthetic system instruction appended to the summary call.
//   - `errorText`: the fallback used in the failed-status metadata + error frame.
//   - sentinel timing: cap-hit inserts BEFORE the stream (`beforeStream`);
//     doom-loop + step-cap insert AFTER the session bump (`afterSession`).
//   - `logMsg` / `logFields`: per-kind log line + extra fields.
// All three use error_reason / chat_status reason = 'summary_after_cap_failed'
// (doom-loop reuses it deliberately — the user-visible failure mode is the
// same "model gave up mid-summary"; the ErrorReason union is shared and the UI
// surfaces a generic "summary failed" line for every sentinel path).
interface WrapUpOpts {
  note: string;
  errorText: string;
  logMsg: string;
  logFields: Record<string, unknown>;
  beforeStream?: () => Promise<void>;
  afterSession?: () => Promise<void>;
}

async function runWrapUpSummary(
  ctx: InferenceContext,
  args: TurnArgs,
  session: Session,
  project: Project,
  history: Message[],
  agent: Agent | null,
  opts: WrapUpOpts,
): Promise<void> {
  const { sessionId, chatId, assistantMessageId, signal } = args;

  if (opts.beforeStream) await opts.beforeStream();

  const messages = await buildMessagesPayload(session, project, history, agent, ctx.log);
  messages.push({ role: 'system', content: opts.note });

  const startedRow = await ctx.sql<{ started_at: string }[]>`
    UPDATE messages
    SET started_at = clock_timestamp()
    WHERE id = ${assistantMessageId}
    RETURNING started_at
  `;
  const startedAt = startedRow[0]?.started_at ?? null;

  ctx.publish(sessionId, {
    type: 'message_started',
    message_id: assistantMessageId,
    chat_id: chatId,
    role: 'assistant',
  });

  let accumulated = '';
  const flusher = createContentFlusher(ctx.sql, assistantMessageId, () => accumulated);

  let summaryOk = false;
  let summarySoftCancelled = false;
  let summaryError: string | null = null;
  let result: StreamResult | null = null;
  try {
    result = await streamCompletion(
      ctx,
      session.model,
      messages,
      { tools: null, ...samplerOptsFromAgent(agent) },
      (delta) => {
        accumulated += delta;
        ctx.publish(sessionId, {
          type: 'delta',
          message_id: assistantMessageId,
          chat_id: chatId,
          content: delta,
        });
        flusher.scheduleFlush();
      },
      undefined,
      signal,
    );
    summaryOk = true;
  } catch (err) {
    if (err instanceof Error && err.name === 'AbortError') {
      summarySoftCancelled = true;
    } else {
      summaryError = err instanceof Error ? err.message : String(err);
    }
  } finally {
    await flusher.drain();
  }

  // Finalize the summary message based on the three outcomes. The sentinel is
  // inserted regardless (before or after, per opts) so the user always has the
  // appropriate affordance — even on a partial / failed summary the chat
  // history shows where the loop stopped.
  if (summaryOk && result) {
    await finalizeStreamedRow(ctx, {
      sessionId,
      chatId,
      messageId: assistantMessageId,
      model: session.model,
      content: result.content,
      completionTokens: result.completionTokens,
      promptTokens: result.promptTokens,
      startedAt,
    });
  } else if (summarySoftCancelled) {
    await ctx.sql`
      UPDATE messages
      SET content = ${accumulated},
          status = 'cancelled',
          finished_at = clock_timestamp()
      WHERE id = ${assistantMessageId}
    `;
    ctx.publish(sessionId, {
      type: 'message_complete',
      message_id: assistantMessageId,
      chat_id: chatId,
    });
  } else {
    const errMeta: MessageMetadata = {
      kind: 'error',
      error_reason: 'summary_after_cap_failed',
      error_text: summaryError ?? opts.errorText,
    };
    await ctx.sql`
      UPDATE messages
      SET content = ${accumulated},
          status = 'failed',
          finished_at = clock_timestamp(),
          metadata = ${ctx.sql.json(errMeta as never)}
      WHERE id = ${assistantMessageId}
    `;
    ctx.publish(sessionId, {
      type: 'error',
      message_id: assistantMessageId,
      chat_id: chatId,
      error: summaryError ?? opts.errorText,
      reason: 'summary_after_cap_failed',
    });
  }

  // Bump session/chat updated_at exactly once for this turn.
  const [sessRow] = await ctx.sql<{ project_id: string; name: string; updated_at: string }[]>`
    UPDATE sessions SET updated_at = clock_timestamp()
    WHERE id = ${sessionId}
    RETURNING project_id, name, updated_at
  `;
  ctx.publishUser({
    type: 'session_updated',
    session_id: sessionId,
    project_id: sessRow!.project_id,
    name: sessRow!.name,
    updated_at: sessRow!.updated_at,
  });

  if (opts.afterSession) await opts.afterSession();

  // Status frame fires last so the dot color reflects the terminal state.
  // Success → idle, abort → idle (user-driven stop), error → error+reason.
  if (summaryOk || summarySoftCancelled) {
    ctx.publishUser({ type: 'chat_status', chat_id: chatId, status: 'idle', at: new Date().toISOString() });
  } else {
    ctx.publishUser({
      type: 'chat_status',
      chat_id: chatId,
      status: 'error',
      at: new Date().toISOString(),
      reason: 'summary_after_cap_failed',
    });
  }

  ctx.log.info(
    { sessionId, chatId, assistantMessageId, ...opts.logFields, summaryOk, summaryCancelled: summarySoftCancelled },
    opts.logMsg,
  );
}

// v1.8.2: cap-hit summary flow. Called instead of erroring when the loop hits
// its budget. The cap-hit sentinel is inserted FIRST (before the summary
// stream) so the UI shows the Continue affordance regardless of summary
// outcome.
export async function runCapHitSummary(
  ctx: InferenceContext,
  args: TurnArgs,
  session: Session,
  project: Project,
  history: Message[],
  agent: Agent | null,
  budget: number,
): Promise<void> {
  await runWrapUpSummary(ctx, args, session, project, history, agent, {
    note: CAP_HIT_SUMMARY_NOTE(budget),
    errorText: 'summary failed',
    logMsg: 'inference cap-hit summary finished',
    logFields: { budget },
    beforeStream: () => insertCapHitSentinel(ctx, args.sessionId, args.chatId, agent, budget),
  });
}

// v1.11.6: doom-loop wrap-up. The doom-loop sentinel is inserted AFTER the
// session bump (no Continue affordance — continuing would re-trigger the loop
// with the same tools available; the user needs to restate or switch agents).
export async function runDoomLoopSummary(
  ctx: InferenceContext,
  args: TurnArgs,
  session: Session,
  project: Project,
  history: Message[],
  agent: Agent | null,
  loop: { name: string; args: Record<string, unknown> },
): Promise<void> {
  await runWrapUpSummary(ctx, args, session, project, history, agent, {
    note: DOOM_LOOP_NOTE(loop.name),
    errorText: 'doom-loop summary failed',
    logMsg: 'inference doom-loop summary finished',
    logFields: { loopedTool: loop.name },
    afterSession: () => insertDoomLoopSentinel(ctx, args.sessionId, args.chatId, loop),
  });
}

// v1.14.0: step-cap wrap-up. Reuses the cap_hit sentinel (inserted AFTER the
// session bump) so the frontend CapHitSentinel component renders it without
// changes; the content text distinguishes step cap from budget.
export async function runStepCapSummary(
  ctx: InferenceContext,
  args: TurnArgs,
  session: Session,
  project: Project,
  history: Message[],
  agent: Agent | null,
  steps: number,
  cap: number,
): Promise<void> {
  await runWrapUpSummary(ctx, args, session, project, history, agent, {
    note: STEP_CAP_NOTE(steps, cap),
    errorText: 'step-cap summary failed',
    logMsg: 'inference step-cap summary finished',
    logFields: { steps, cap },
    afterSession: () => insertCapHitSentinel(ctx, args.sessionId, args.chatId, agent, cap),
  });
}

// P5: the ONE INSERT + message_started → delta → message_complete frame
// sequence shared by every sentinel inserter. The sentinel row is a
// role='system', status='complete' message; the static content rides the same
// streaming-frame path useSessionStream's reducer uses for assistant messages
// (the delta carries the full text in one chunk).
async function insertSentinel(
  ctx: InferenceContext,
  sessionId: string,
  chatId: string,
  metadata: MessageMetadata,
  content: string,
): Promise<void> {
  const [row] = await ctx.sql<{ id: string }[]>`
    INSERT INTO messages (session_id, chat_id, role, content, status, created_at, metadata)
    VALUES (${sessionId}, ${chatId}, 'system', ${content}, 'complete', clock_timestamp(), ${ctx.sql.json(metadata as never)})
    RETURNING id
  `;
  ctx.publish(sessionId, {
    type: 'message_started',
    message_id: row!.id,
    chat_id: chatId,
    role: 'system',
  });
  ctx.publish(sessionId, {
    type: 'delta',
    message_id: row!.id,
    chat_id: chatId,
    content,
  });
  ctx.publish(sessionId, {
    type: 'message_complete',
    message_id: row!.id,
    chat_id: chatId,
    metadata,
  });
}

async function insertCapHitSentinel(
  ctx: InferenceContext,
  sessionId: string,
  chatId: string,
  agent: Agent | null,
  budget: number,
): Promise<void> {
  // Hard ceiling: count prior cap_hit sentinels in this chat. After two
  // continues (sentinel count of 2), the next sentinel reports can_continue
  // false and the UI disables the Continue button.
  const priorRows = await ctx.sql<{ count: number }[]>`
    SELECT COUNT(*)::int AS count
    FROM messages
    WHERE chat_id = ${chatId}
      AND role = 'system'
      AND metadata->>'kind' = 'cap_hit'
  `;
  const priorCount = priorRows[0]?.count ?? 0;
  const canContinue = priorCount < 2;
  const metadata: MessageMetadata = {
    kind: 'cap_hit',
    used: budget,
    limit: budget,
    agent_name: agent?.name ?? null,
    can_continue: canContinue,
  };
  const content = `Reached tool budget (${budget}/${budget}). Continue to extend.`;
  await insertSentinel(ctx, sessionId, chatId, metadata, content);
}

async function insertDoomLoopSentinel(
  ctx: InferenceContext,
  sessionId: string,
  chatId: string,
  loop: { name: string; args: Record<string, unknown> },
): Promise<void> {
  // No hard-ceiling / can-continue logic here — doom-loop is a different
  // failure mode from cap-hit. Continuing would re-trigger the loop with
  // the same tools available; the user needs to restate their question
  // or switch agents instead.
  const metadata: MessageMetadata = {
    kind: 'doom_loop',
    tool_name: loop.name,
    args: loop.args,
    threshold: DOOM_LOOP_THRESHOLD,
  };
  const content = `Detected ${DOOM_LOOP_THRESHOLD} identical calls to ${loop.name}. Stopping the tool-call loop. Produce the best answer you can with what you have.`;
  await insertSentinel(ctx, sessionId, chatId, metadata, content);
}

// #12 MistakeTracker: heterogeneous-failure recovery sentinel. A role='system',
// status='complete' row firing the standard sentinel frame sequence. Two
// variants distinguished by `escalated`:
//   - escalated:false → a nudge fired; recovery guidance was injected into the
//     model's next step and the loop continued. can_continue is true (the turn
//     is still live).
//   - escalated:true  → the nudge didn't break the failure run; the turn was
//     stopped (cap-hit-style). can_continue is true so the UI can still offer a
//     Continue affordance — a fresh user turn resets the tracker.
export async function insertMistakeRecoverySentinel(
  ctx: InferenceContext,
  sessionId: string,
  chatId: string,
  opts: { failureKinds: string[]; count: number; escalated: boolean; canContinue: boolean },
): Promise<void> {
  const metadata: MessageMetadata = {
    kind: 'mistake_recovery',
    failure_kinds: opts.failureKinds,
    count: opts.count,
    escalated: opts.escalated,
    can_continue: opts.canContinue,
  };
  const content = opts.escalated
    ? `Repeated different errors persisted after a recovery nudge (${opts.count} in a row). Stopping the tool-call loop.`
    : `Hit ${opts.count} different errors in a row. Injected recovery guidance and continuing.`;
  await insertSentinel(ctx, sessionId, chatId, metadata, content);
}
