import { randomUUID } from "node:crypto";
import { createLogger } from "./logger.js";
import type {
  CLIControlResponseMessage,
  BrowserIncomingMessage,
  McpServerDetail,
  InitializeResponseData,
  HookCallbackInput,
  HookCallbackResponseData,
} from "./session-types.js";
import type { Session } from "./ws-bridge-types.js";
import { handleTeammateHookCallbackInput } from "./ws-bridge-system.js";

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

export interface RewindFilesResult {
  canRewind: boolean;
  error?: string;
  filesChanged?: string[];
  insertions?: number;
  deletions?: number;
}

export function handleInterrupt(
  session: Session,
  sendToCLI: (session: Session, ndjson: string) => void,
): void {
  const ndjson = JSON.stringify({
    type: "control_request",
    request_id: randomUUID(),
    request: { subtype: "interrupt" },
  });
  sendToCLI(session, ndjson);
}

export function handleSetModel(
  session: Session,
  model: string,
  sendToCLI: (session: Session, ndjson: string) => void,
): void {
  const ndjson = JSON.stringify({
    type: "control_request",
    request_id: randomUUID(),
    request: { subtype: "set_model", model },
  });
  sendToCLI(session, ndjson);
}

export function handleSetPermissionMode(
  session: Session,
  mode: string,
  sendToCLI: (session: Session, ndjson: string) => void,
): void {
  const ndjson = JSON.stringify({
    type: "control_request",
    request_id: randomUUID(),
    request: { subtype: "set_permission_mode", mode },
  });
  sendToCLI(session, ndjson);
}

export function handleSetMaxThinkingTokens(
  session: Session,
  maxThinkingTokens: number | null,
  sendToCLI: (session: Session, ndjson: string) => void,
): void {
  const ndjson = JSON.stringify({
    type: "control_request",
    request_id: randomUUID(),
    request: { subtype: "set_max_thinking_tokens", max_thinking_tokens: maxThinkingTokens },
  });
  sendToCLI(session, ndjson);
}

export function handleSetOutputStyle(
  session: Session,
  outputStyle: string,
  sendToCLI: (session: Session, ndjson: string) => void,
): void {
  const ndjson = JSON.stringify({
    type: "control_request",
    request_id: randomUUID(),
    request: { subtype: "set_output_style", output_style: outputStyle },
  });
  sendToCLI(session, ndjson);
  session.state.output_style = outputStyle;
}

export function handleSetFastMode(
  session: Session,
  fastMode: boolean,
  sendToCLI: (session: Session, ndjson: string) => void,
): void {
  const ndjson = JSON.stringify({
    type: "control_request",
    request_id: randomUUID(),
    request: { subtype: "set_fast_mode", fast_mode: fastMode },
  });
  sendToCLI(session, ndjson);
  session.state.fast_mode = fastMode;
}

export function handleControlResponse(
  session: Session,
  msg: CLIControlResponseMessage,
  loggerWarn: (message: string) => void,
): void {
  const reqId = msg.response.request_id;
  const pending = session.pendingControlRequests.get(reqId);
  if (!pending) return;
  session.pendingControlRequests.delete(reqId);
  if (msg.response.subtype === "error") {
    loggerWarn(`[ws-bridge] Control request ${pending.subtype} failed: ${msg.response.error}`);
    return;
  }
  pending.resolve(msg.response.response ?? {});
}

export function sendControlRequest(
  session: Session,
  request: Record<string, unknown>,
  sendToCLI: (session: Session, ndjson: string) => void,
  onResponse?: { subtype: string; resolve: (response: unknown) => void },
): void {
  const requestId = randomUUID();
  if (onResponse) {
    session.pendingControlRequests.set(requestId, onResponse);
  }
  const ndjson = JSON.stringify({
    type: "control_request",
    request_id: requestId,
    request,
  });
  sendToCLI(session, ndjson);
}

export function handleMcpGetStatus(
  session: Session,
  sendControlRequestFn: (
    request: Record<string, unknown>,
    onResponse?: { subtype: string; resolve: (response: unknown) => void },
  ) => void,
  broadcastToBrowsers: (session: Session, msg: BrowserIncomingMessage) => void,
): void {
  sendControlRequestFn({ subtype: "mcp_status" }, {
    subtype: "mcp_status",
    resolve: (response) => {
      const servers = (response as { mcpServers?: McpServerDetail[] }).mcpServers ?? [];
      broadcastToBrowsers(session, { type: "mcp_status", servers });
    },
  });
}

export function handleMcpToggle(
  sendControlRequestFn: (request: Record<string, unknown>) => void,
  serverName: string,
  enabled: boolean,
  refreshStatus: () => void,
): void {
  sendControlRequestFn({ subtype: "mcp_toggle", serverName, enabled });
  setTimeout(refreshStatus, 500);
}

export function handleMcpReconnect(
  sendControlRequestFn: (request: Record<string, unknown>) => void,
  serverName: string,
  refreshStatus: () => void,
): void {
  sendControlRequestFn({ subtype: "mcp_reconnect", serverName });
  setTimeout(refreshStatus, 1000);
}

export function handleMcpSetServers(
  sendControlRequestFn: (request: Record<string, unknown>) => void,
  servers: Record<string, unknown>,
  refreshStatus: () => void,
): void {
  sendControlRequestFn({ subtype: "mcp_set_servers", servers });
  setTimeout(refreshStatus, 2000);
}

/**
 * Handle a browser-initiated `mcp_message` request.
 *
 * Wraps the JSON-RPC message in a `control_request` and sends it to the CLI.
 * When the CLI responds, the response is forwarded back to browsers via
 * the `mcp_message` browser event.
 */
export function handleMcpMessage(
  session: Session,
  serverName: string,
  message: Record<string, unknown>,
  sendControlRequestFn: (
    request: Record<string, unknown>,
    onResponse?: { subtype: string; resolve: (response: unknown) => void },
  ) => void,
  broadcastToBrowsers: (session: Session, msg: BrowserIncomingMessage) => void,
): void {
  sendControlRequestFn(
    { subtype: "mcp_message", server_name: serverName, message },
    {
      subtype: "mcp_message",
      resolve: (response) => {
        const mcpResponse = (response as { mcp_response?: Record<string, unknown> }).mcp_response;
        if (mcpResponse) {
          broadcastToBrowsers(session, {
            type: "mcp_message",
            server_name: serverName,
            message: mcpResponse,
          });
        }
      },
    },
  );
}

/**
 * Handle a CLI-initiated `mcp_message` control_request.
 *
 * The CLI forwards an unsolicited MCP server notification/response to the
 * Zencode server.  We broadcast it to connected browsers and immediately
 * respond to the CLI with success so it is not blocked.
 */
export function handleMcpMessageFromCLI(
  session: Session,
  requestId: string,
  serverName: string,
  message: Record<string, unknown>,
  sendToCLI: (session: Session, ndjson: string) => void,
  broadcastToBrowsers: (session: Session, msg: BrowserIncomingMessage) => void,
): void {
  // Forward the MCP message to connected browsers
  broadcastToBrowsers(session, {
    type: "mcp_message",
    server_name: serverName,
    message,
  });

  // Acknowledge to the CLI immediately
  const ndjson = JSON.stringify({
    type: "control_response",
    response: {
      subtype: "success",
      request_id: requestId,
    },
  });
  sendToCLI(session, ndjson);
}

export function handleRewindFiles(
  _session: Session,
  userMessageId: string,
  dryRun: boolean,
  sendControlRequestFn: (
    request: Record<string, unknown>,
    onResponse?: { subtype: string; resolve: (response: unknown) => void },
  ) => void,
): Promise<RewindFilesResult> {
  return new Promise((resolve) => {
    sendControlRequestFn(
      { subtype: "rewind_files", user_message_id: userMessageId, dry_run: dryRun },
      {
        subtype: "rewind_files",
        resolve: (response) => resolve(response as RewindFilesResult),
      },
    );
  });
}

export interface InitializeOptions {
  systemPrompt?: string;
  appendSystemPrompt?: string;
}

/**
 * Send an `initialize` control request to the CLI.
 *
 * Per the SDK protocol (§7.1), this must be sent BEFORE the first `user` message.
 * The response contains available models, account info, commands, and output styles.
 */
export function sendInitializeRequest(
  session: Session,
  sendToCLI: (session: Session, ndjson: string) => void,
  options?: InitializeOptions,
  onResponse?: (data: InitializeResponseData) => void,
): void {
  const request: Record<string, unknown> = { subtype: "initialize" };
  if (options?.systemPrompt) {
    request.systemPrompt = options.systemPrompt;
  }
  if (options?.appendSystemPrompt) {
    request.appendSystemPrompt = options.appendSystemPrompt;
  }

  sendControlRequest(
    session,
    request,
    sendToCLI,
    onResponse
      ? {
        subtype: "initialize",
        resolve: (response) => onResponse(response as InitializeResponseData),
      }
      : undefined,
  );
}

/**
 * Handle a `hook_callback` control_request from the CLI.
 *
 * Hook callbacks are time-sensitive — the CLI blocks until it receives a
 * response.  The default policy is passthrough: respond with `{ continue: true }`
 * so Zencode never accidentally blocks normal CLI operation.
 *
 * The callback info is broadcast to connected browsers for observability.
 */
export function handleHookCallback(
  session: Session,
  requestId: string,
  callbackId: string,
  input: HookCallbackInput,
  toolUseId: string | undefined,
  sendToCLI: (session: Session, ndjson: string) => void,
  broadcastToBrowsers: (session: Session, msg: BrowserIncomingMessage) => void,
): void {
  // Broadcast to browsers for visibility (non-blocking, informational)
  broadcastToBrowsers(session, {
    type: "hook_callback",
    callback_id: callbackId,
    input,
    tool_use_id: toolUseId,
    timestamp: Date.now(),
  });

  // Extract teammate lifecycle info from hook callback inputs
  if (input.hook_event_name) {
    handleTeammateHookCallbackInput(
      session,
      input.hook_event_name,
      input as Record<string, unknown>,
      { broadcastToBrowsers, persistSession: () => { /* persisted below */ } },
    );
  }

  // Default passthrough policy: don't block the CLI
  const responseData: HookCallbackResponseData = { continue: true };

  const ndjson = JSON.stringify({
    type: "control_response",
    response: {
      subtype: "success",
      request_id: requestId,
      response: responseData,
    },
  });
  sendToCLI(session, ndjson);
}

/**
 * Validate and send an `update_environment_variables` message to the CLI.
 *
 * This is a direct message (not a control request). The CLI reads it and
 * updates its process environment variables at runtime.
 *
 * Only the variable keys are logged for auditability — values may contain secrets.
 */
export function handleUpdateEnv(
  session: Session,
  variables: Record<string, string>,
  sendToCLI: (session: Session, ndjson: string) => void,
): void {
  // Validate: every key and value must be a string (no nested objects, no nulls)
  for (const [key, value] of Object.entries(variables)) {
    if (typeof key !== "string" || typeof value !== "string") {
      log.warn(
        { sessionId: session.id },
        "update_env rejected: all keys and values must be strings",
      );
      return;
    }
  }

  log.info(
    { sessionId: session.id, keys: Object.keys(variables) },
    "Sending update_environment_variables to CLI",
  );

  const ndjson = JSON.stringify({
    type: "update_environment_variables",
    variables,
  });
  sendToCLI(session, ndjson);
}
