/**
 * Result message handling and token accounting for WsBridge.
 *
 * Processes `result` type messages from the CLI after each turn completes.
 * Responsibilities:
 * - Updates session cost and turn counters
 * - Aggregates token usage from modelUsage data (input, output, cache read/creation)
 * - Computes context window utilization percentage
 * - Triggers auto-naming after the first successful result
 * - Initiates auto-dequeue of the next queued user message
 * - Fires git-info refresh to detect branch changes after each turn
 *
 * Extracted from the WsBridge class to reduce its size. Functions accept a
 * {@link ResultMessageDeps} object for dependency injection.
 */

import type { CLIResultMessage, BrowserIncomingMessage } from "./session-types.js";
import type { Session } from "./ws-bridge-types.js";
import { handleInterrupt } from "./ws-bridge-controls.js";
import { createLogger } from "./logger.js";

const log = createLogger("ws-bridge-result");

export interface ResultMessageDeps {
  broadcastToBrowsers: (session: Session, msg: BrowserIncomingMessage) => void;
  appendToHistory: (session: Session, msg: BrowserIncomingMessage) => void;
  persistSession: (session: Session) => void;
  refreshGitInfo: (session: Session, opts: { broadcastUpdate: boolean; notifyPoller: boolean }) => Promise<void>;
  getAutoNamingAttempted: () => Set<string>;
  getOnFirstTurnCompleted: () => ((sessionId: string, firstUserMessage: string) => void) | null;
  processQueue: (session: Session, isError: boolean) => void;
  sendToCLI: (session: Session, ndjson: string) => void;
}

/**
 * Handle result messages from the CLI.
 * Extracted from WsBridge.handleResultMessage to reduce class size.
 */
export function handleResultMessage(
  session: Session,
  msg: CLIResultMessage,
  deps: ResultMessageDeps,
): void {
  // Update session cost/turns
  session.state.total_cost_usd = msg.total_cost_usd;
  session.state.num_turns = msg.num_turns;

  // Update lines changed (CLI may send these in result)
  if (typeof msg.total_lines_added === "number") {
    session.state.total_lines_added = msg.total_lines_added;
  }
  if (typeof msg.total_lines_removed === "number") {
    session.state.total_lines_removed = msg.total_lines_removed;
  }

  // Extract token usage from modelUsage
  updateTokenAccounting(session, msg);

  // Fire-and-forget async git refresh after each turn in case branch moved.
  void deps.refreshGitInfo(session, { broadcastUpdate: true, notifyPoller: true });

  const browserMsg: BrowserIncomingMessage = {
    type: "result",
    data: msg,
  };
  deps.appendToHistory(session, browserMsg);
  deps.broadcastToBrowsers(session, browserMsg);
  deps.persistSession(session);

  // Check budget after cost update
  checkBudget(session, deps);

  // Trigger auto-naming after the first successful result for this session.
  triggerAutoNaming(session, msg, deps);

  // Auto-dequeue: process next queued message after result
  deps.processQueue(session, msg.is_error);
}

/**
 * Check session cost against budget and send warnings or interrupt.
 */
function checkBudget(
  session: Session,
  deps: Pick<ResultMessageDeps, "broadcastToBrowsers" | "appendToHistory" | "persistSession" | "sendToCLI">,
): void {
  const { budget_usd, total_cost_usd } = session.state;
  if (budget_usd == null || budget_usd <= 0) return;

  const costStr = `$${total_cost_usd.toFixed(2)}`;
  const budgetStr = `$${budget_usd.toFixed(2)}`;

  if (total_cost_usd >= budget_usd) {
    // Budget exceeded — interrupt the session
    log.warn(
      { sessionId: session.id, cost: total_cost_usd, budget: budget_usd },
      "Session budget exceeded, interrupting",
    );
    const msg: BrowserIncomingMessage = {
      type: "error",
      message: `⚠️ Session budget exceeded: ${costStr} / ${budgetStr} — session interrupted`,
    };
    deps.appendToHistory(session, msg);
    deps.broadcastToBrowsers(session, msg);
    handleInterrupt(session, deps.sendToCLI);
    deps.persistSession(session);
  } else if (
    total_cost_usd >= budget_usd * 0.8 &&
    !session.state.budget_warning_sent
  ) {
    // Approaching budget limit (80%) — send a warning
    log.info(
      { sessionId: session.id, cost: total_cost_usd, budget: budget_usd },
      "Session approaching budget limit",
    );
    session.state.budget_warning_sent = true;
    const msg: BrowserIncomingMessage = {
      type: "error",
      message: `⚠️ Session approaching budget limit: ${costStr} / ${budgetStr}`,
    };
    deps.appendToHistory(session, msg);
    deps.broadcastToBrowsers(session, msg);
    deps.persistSession(session);
  }
}

/**
 * Update session token accounting from a result message's modelUsage data.
 */
function updateTokenAccounting(session: Session, msg: CLIResultMessage): void {
  if (!msg.modelUsage) return;

  let totalInput = 0;
  let totalOutput = 0;
  let totalCacheRead = 0;
  let totalCacheCreation = 0;
  let contextWindow = 0;

  for (const usage of Object.values(msg.modelUsage)) {
    totalInput += usage.inputTokens || 0;
    totalOutput += usage.outputTokens || 0;
    totalCacheRead += usage.cacheReadInputTokens || 0;
    totalCacheCreation += usage.cacheCreationInputTokens || 0;
    if (usage.contextWindow > 0) {
      contextWindow = usage.contextWindow;
    }
  }

  session.state.input_tokens = totalInput;
  session.state.output_tokens = totalOutput;
  session.state.cache_read_tokens = totalCacheRead;
  session.state.cache_creation_tokens = totalCacheCreation;

  if (contextWindow > 0) {
    session.state.context_window = contextWindow;
    const pct = Math.round(
      ((totalInput + totalOutput) / contextWindow) * 100
    );
    session.state.context_used_percent = Math.max(0, Math.min(pct, 100));
  }
}

/**
 * Trigger auto-naming after the first successful result for a session.
 * Note: num_turns counts all internal tool-use turns, so it's typically > 1
 * even on the first user interaction. We track per-session instead.
 */
function triggerAutoNaming(
  session: Session,
  msg: CLIResultMessage,
  deps: Pick<ResultMessageDeps, "getAutoNamingAttempted" | "getOnFirstTurnCompleted">,
): void {
  const autoNaming = deps.getAutoNamingAttempted();
  const firstTurnCb = deps.getOnFirstTurnCompleted();

  if (
    !msg.is_error &&
    firstTurnCb &&
    !autoNaming.has(session.id)
  ) {
    autoNaming.add(session.id);
    const firstUserMsg = session.messageHistory.find(
      (m) => m.type === "user_message",
    );
    if (firstUserMsg && firstUserMsg.type === "user_message") {
      firstTurnCb(session.id, firstUserMsg.content);
    }
  }
}
