/**
 * System message handling for WsBridge.
 *
 * Processes `system` type messages from the CLI, including:
 * - `init`: session initialization (model, cwd, tools, permissions, MCP servers)
 * - `status`: state transitions (e.g., compacting)
 * - Forwarded system events: compact_boundary, task_notification, files_persisted,
 *   hook_started, hook_progress, hook_response
 *
 * Extracted from the WsBridge class to reduce its size. Functions accept a
 * {@link SystemMessageDeps} object for dependency injection, keeping this module
 * decoupled from the WsBridge instance.
 */

import { createLogger } from "./logger.js";
import type { CLISystemMessage, BrowserIncomingMessage, BackgroundTask, Teammate } from "./session-types.js";
import type { Session } from "./ws-bridge-types.js";

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

export interface SystemMessageDeps {
  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>;
  getOnCLISessionId: () => ((sessionId: string, cliSessionId: string) => void) | null;
  sendToCLI: (session: Session, ndjson: string) => void;
}

/**
 * Handle system messages from the CLI.
 * Extracted from WsBridge.handleSystemMessage to reduce class size.
 */
export function handleSystemMessage(
  session: Session,
  msg: CLISystemMessage,
  deps: SystemMessageDeps,
): void {
  if (msg.subtype === "init") {
    handleSystemInit(session, msg, deps);
    return;
  }

  if (msg.subtype === "status") {
    handleSystemStatus(session, msg, deps);
    return;
  }

  if (msg.subtype === "compact_boundary") {
    forwardSystemEvent(session, {
      subtype: "compact_boundary",
      compact_metadata: msg.compact_metadata,
      uuid: msg.uuid,
      session_id: msg.session_id,
    }, deps);
    return;
  }

  if (msg.subtype === "task_notification") {
    // Upsert background task on session state
    upsertBackgroundTask(session, msg);

    // Broadcast updated background tasks list to browsers
    deps.broadcastToBrowsers(session, {
      type: "background_tasks_update",
      tasks: session.state.background_tasks ?? [],
    });
    deps.persistSession(session);

    // Also forward as system_event for chat log
    forwardSystemEvent(session, {
      subtype: "task_notification",
      task_id: msg.task_id,
      status: msg.status,
      output_file: msg.output_file,
      summary: msg.summary,
      uuid: msg.uuid,
      session_id: msg.session_id,
    }, deps);
    return;
  }

  if (msg.subtype === "files_persisted") {
    forwardSystemEvent(session, {
      subtype: "files_persisted",
      files: msg.files,
      failed: msg.failed,
      processed_at: msg.processed_at,
      uuid: msg.uuid,
      session_id: msg.session_id,
    }, deps);
    return;
  }

  if (msg.subtype === "hook_started") {
    // Check if this is a teammate-related hook event
    if (isTeammateHookEvent(msg.hook_event)) {
      handleTeammateHookStarted(session, msg, deps);
    }

    forwardSystemEvent(session, {
      subtype: "hook_started",
      hook_id: msg.hook_id,
      hook_name: msg.hook_name,
      hook_event: msg.hook_event,
      uuid: msg.uuid,
      session_id: msg.session_id,
    }, deps);
    return;
  }

  if (msg.subtype === "hook_progress") {
    forwardSystemEvent(session, {
      subtype: "hook_progress",
      hook_id: msg.hook_id,
      hook_name: msg.hook_name,
      hook_event: msg.hook_event,
      stdout: msg.stdout,
      stderr: msg.stderr,
      output: msg.output,
      uuid: msg.uuid,
      session_id: msg.session_id,
    }, deps, { persistInHistory: false });
    return;
  }

  if (msg.subtype === "hook_response") {
    // Check if this is a teammate-related hook response
    if (isTeammateHookEvent(msg.hook_event)) {
      handleTeammateHookResponse(session, msg, deps);
    }

    forwardSystemEvent(session, {
      subtype: "hook_response",
      hook_id: msg.hook_id,
      hook_name: msg.hook_name,
      hook_event: msg.hook_event,
      output: msg.output,
      stdout: msg.stdout,
      stderr: msg.stderr,
      exit_code: msg.exit_code,
      outcome: msg.outcome,
      uuid: msg.uuid,
      session_id: msg.session_id,
    }, deps);
    return;
  }

  if (msg.subtype === "rate_limit") {
    handleRateLimit(session, msg, deps);
    return;
  }

  // Unknown system subtypes are intentionally ignored until we map them.
}

function handleSystemInit(
  session: Session,
  msg: CLISystemMessage & { subtype: "init" },
  deps: SystemMessageDeps,
): void {
  // Store the CLI's internal session_id so we can --resume on relaunch
  const cliSessionIdCb = deps.getOnCLISessionId();
  if (msg.session_id && cliSessionIdCb) {
    cliSessionIdCb(session.id, msg.session_id);
  }

  session.state.model = msg.model;
  // For containerized sessions, the CLI reports /workspace as its cwd.
  // Keep the host path (set by markContainerized()) for correct project grouping.
  if (!session.state.is_containerized) {
    session.state.cwd = msg.cwd;
  }
  session.state.tools = msg.tools;
  session.state.permissionMode = msg.permissionMode;
  session.state.claude_code_version = msg.claude_code_version;
  session.state.mcp_servers = msg.mcp_servers;
  session.state.agents = msg.agents ?? [];
  session.state.slash_commands = msg.slash_commands ?? [];
  session.state.skills = msg.skills ?? [];

  // Fire-and-forget async git info resolution.
  void deps.refreshGitInfo(session, { broadcastUpdate: true, notifyPoller: true });

  deps.broadcastToBrowsers(session, {
    type: "session_init",
    session: session.state,
  });
  deps.persistSession(session);

  // Flush any messages queued before CLI was initialized
  if (session.pendingMessages.length > 0) {
    log.info({ sessionId: session.id, count: session.pendingMessages.length }, "Flushing queued messages after init");
    const queued = session.pendingMessages.splice(0);
    for (const ndjson of queued) {
      deps.sendToCLI(session, ndjson);
    }
  }
}

function handleSystemStatus(
  session: Session,
  msg: CLISystemMessage & { subtype: "status" },
  deps: SystemMessageDeps,
): void {
  session.state.is_compacting = msg.status === "compacting";

  if (msg.permissionMode) {
    session.state.permissionMode = msg.permissionMode;
  }

  deps.broadcastToBrowsers(session, {
    type: "status_change",
    status: msg.status ?? null,
  });
}

/**
 * Forward a system event to browsers and optionally persist in history.
 */
export function forwardSystemEvent(
  session: Session,
  event: Extract<BrowserIncomingMessage, { type: "system_event" }>["event"],
  deps: Pick<SystemMessageDeps, "broadcastToBrowsers" | "appendToHistory" | "persistSession">,
  options: { persistInHistory?: boolean } = {},
): void {
  const browserMsg: BrowserIncomingMessage = {
    type: "system_event",
    event,
    timestamp: Date.now(),
  };

  if (options.persistInHistory !== false) {
    deps.appendToHistory(session, browserMsg);
  }

  deps.broadcastToBrowsers(session, browserMsg);

  // Persist after broadcasting for events that modify history.
  // Transient events (persistInHistory: false) skip persistence.
  if (options.persistInHistory !== false) {
    deps.persistSession(session);
  }
}

/**
 * Handle rate_limit system messages from the CLI.
 * Updates session state with SDK rate limit data and broadcasts to browsers.
 */
function handleRateLimit(
  session: Session,
  msg: CLISystemMessage & { subtype: "rate_limit" },
  deps: SystemMessageDeps,
): void {
  session.state.sdk_rate_limits = {
    utilization_percent: msg.utilization_percent,
    reset_at: msg.reset_at,
    window_duration_mins: msg.window_duration_mins,
  };

  deps.broadcastToBrowsers(session, {
    type: "session_update",
    session: {
      sdk_rate_limits: session.state.sdk_rate_limits,
    },
  });

  deps.persistSession(session);
  log.info(
    { sessionId: session.id, utilization: msg.utilization_percent, resetAt: msg.reset_at },
    "SDK rate limit updated",
  );
}

/**
 * Upsert a background task on the session state from a task_notification event.
 * Creates a new entry on first notification, updates existing on subsequent ones.
 */
function upsertBackgroundTask(
  session: Session,
  msg: CLISystemMessage & { subtype: "task_notification" },
): void {
  if (!session.state.background_tasks) {
    session.state.background_tasks = [];
  }

  const existing = session.state.background_tasks.find((t) => t.task_id === msg.task_id);
  const isTerminal = msg.status === "completed" || msg.status === "failed" || msg.status === "stopped";

  if (existing) {
    existing.status = msg.status;
    existing.summary = msg.summary || existing.summary;
    existing.output_file = msg.output_file || existing.output_file;
    if (isTerminal && !existing.completed_at) {
      existing.completed_at = Date.now();
    }
  } else {
    const task: BackgroundTask = {
      task_id: msg.task_id,
      status: msg.status,
      summary: msg.summary || undefined,
      output_file: msg.output_file || undefined,
      started_at: Date.now(),
    };
    if (isTerminal) {
      task.completed_at = Date.now();
    }
    session.state.background_tasks.push(task);
  }

  log.info(
    { sessionId: session.id, taskId: msg.task_id, status: msg.status },
    "Background task upserted",
  );
}

// ─── Teammate (Agent Teams) Handling ────────────────────────────────────────

/** Hook events that indicate teammate lifecycle changes. */
const TEAMMATE_HOOK_EVENTS = new Set([
  "SubagentStart",
  "SubagentStop",
  "TeammateIdle",
  "TaskCompleted",
]);

function isTeammateHookEvent(hookEvent: string): boolean {
  return TEAMMATE_HOOK_EVENTS.has(hookEvent);
}

/**
 * Handle hook_started events for teammate lifecycle.
 * SubagentStart creates or updates a teammate as "running".
 */
function handleTeammateHookStarted(
  session: Session,
  msg: CLISystemMessage & { subtype: "hook_started" },
  deps: Pick<SystemMessageDeps, "broadcastToBrowsers" | "persistSession">,
): void {
  if (msg.hook_event === "SubagentStart") {
    // The hook_started for SubagentStart means a subagent is being launched.
    // We don't have full payload yet (agent_id, agent_type come in callback input),
    // so we use hook_id as a stable identifier and hook_name as display name.
    upsertTeammate(session, {
      id: msg.hook_id,
      name: msg.hook_name || msg.hook_id,
      status: "running",
      startedAt: Date.now(),
    });
    broadcastTeammatesUpdate(session, deps);
  }
}

/**
 * Handle hook_response events for teammate lifecycle.
 * SubagentStop → completed/failed, TeammateIdle → idle, TaskCompleted → task info.
 */
function handleTeammateHookResponse(
  session: Session,
  msg: CLISystemMessage & { subtype: "hook_response" },
  deps: Pick<SystemMessageDeps, "broadcastToBrowsers" | "persistSession">,
): void {
  if (msg.hook_event === "SubagentStop") {
    const status = msg.outcome === "success" ? "completed" : "failed";
    upsertTeammate(session, {
      id: msg.hook_id,
      name: msg.hook_name || msg.hook_id,
      status,
      startedAt: Date.now(),
      completedAt: Date.now(),
    });
    broadcastTeammatesUpdate(session, deps);
  } else if (msg.hook_event === "TeammateIdle") {
    upsertTeammate(session, {
      id: msg.hook_id,
      name: msg.hook_name || msg.hook_id,
      status: "idle",
      startedAt: Date.now(),
    });
    broadcastTeammatesUpdate(session, deps);
  } else if (msg.hook_event === "TaskCompleted") {
    // Try to associate with an existing teammate or create one
    upsertTeammate(session, {
      id: msg.hook_id,
      name: msg.hook_name || msg.hook_id,
      status: "completed",
      startedAt: Date.now(),
      completedAt: Date.now(),
    });
    broadcastTeammatesUpdate(session, deps);
  }
}

/**
 * Parse teammate info from hook callback input payloads.
 * Called from the hook_callback control_request handler when teammate fields are present.
 */
export function handleTeammateHookCallbackInput(
  session: Session,
  hookEventName: string,
  input: Record<string, unknown>,
  deps: Pick<SystemMessageDeps, "broadcastToBrowsers" | "persistSession">,
): void {
  if (hookEventName === "SubagentStart") {
    const agentId = (input.agent_id as string) || (input.hook_id as string) || "";
    const agentType = (input.agent_type as string) || "";
    upsertTeammate(session, {
      id: agentId,
      name: agentType || agentId,
      status: "running",
      agentType,
      startedAt: Date.now(),
    });
    broadcastTeammatesUpdate(session, deps);
  } else if (hookEventName === "SubagentStop") {
    const agentId = (input.agent_id as string) || (input.hook_id as string) || "";
    const agentType = (input.agent_type as string) || "";
    upsertTeammate(session, {
      id: agentId,
      name: agentType || agentId,
      status: "completed",
      agentType,
      startedAt: Date.now(),
      completedAt: Date.now(),
    });
    broadcastTeammatesUpdate(session, deps);
  } else if (hookEventName === "TeammateIdle") {
    const teammateName = (input.teammate_name as string) || "";
    const teamName = (input.team_name as string) || "";
    upsertTeammate(session, {
      id: teammateName || teamName,
      name: teammateName,
      status: "idle",
      teamName,
      startedAt: Date.now(),
    });
    broadcastTeammatesUpdate(session, deps);
  } else if (hookEventName === "TaskCompleted") {
    const taskId = (input.task_id as string) || "";
    const taskSubject = (input.task_subject as string) || "";
    const taskDescription = (input.task_description as string) || undefined;
    const teammateName = (input.teammate_name as string) || undefined;
    const teamName = (input.team_name as string) || undefined;
    const id = teammateName || taskId;
    upsertTeammate(session, {
      id,
      name: teammateName || taskSubject || taskId,
      status: "completed",
      task: taskSubject,
      taskDescription,
      teamName,
      startedAt: Date.now(),
      completedAt: Date.now(),
    });
    broadcastTeammatesUpdate(session, deps);
  }
}

/**
 * Upsert a teammate on the session state.
 * Creates a new entry if the ID is not found, updates existing otherwise.
 */
function upsertTeammate(session: Session, update: Teammate): void {
  if (!session.state.teammates) {
    session.state.teammates = [];
  }

  const existing = session.state.teammates.find((t) => t.id === update.id);
  if (existing) {
    existing.status = update.status;
    existing.name = update.name || existing.name;
    existing.task = update.task ?? existing.task;
    existing.taskDescription = update.taskDescription ?? existing.taskDescription;
    existing.teamName = update.teamName ?? existing.teamName;
    existing.agentType = update.agentType ?? existing.agentType;
    if (update.completedAt) {
      existing.completedAt = update.completedAt;
    }
  } else {
    session.state.teammates.push({ ...update });
  }

  log.info(
    { sessionId: session.id, teammateId: update.id, status: update.status },
    "Teammate upserted",
  );
}

/** Broadcast the current teammates list to all connected browsers. */
function broadcastTeammatesUpdate(
  session: Session,
  deps: Pick<SystemMessageDeps, "broadcastToBrowsers" | "persistSession">,
): void {
  deps.broadcastToBrowsers(session, {
    type: "teammates_update",
    teammates: session.state.teammates ?? [],
  });
  deps.persistSession(session);
}
