/**
 * Codex Event Handlers
 *
 * Handles incoming Codex JSON-RPC notifications and requests, translating
 * them into BrowserIncomingMessage events. Extracted from codex-adapter.ts
 * following the ws-bridge-* decomposition pattern.
 */

import { randomUUID } from "node:crypto";
import type pino from "pino";
import type { BrowserIncomingMessage } from "./session-types.js";
import type { JsonRpcTransport } from "./json-rpc-transport.js";
import type {
  CodexItem,
  CodexAgentMessageItem,
  CodexCommandExecutionItem,
  CodexFileChangeItem,
  CodexMcpToolCallItem,
  CodexWebSearchItem,
  CodexReasoningItem,
} from "./codex-types.js";
import type { CodexPlanTracker } from "./codex-plan-tracker.js";
import type { RateLimits } from "./codex-session-helpers.js";
import {
  getStartedItemToolUse,
  getReasoningStartText,
  buildCommandCompletionResult,
  buildFileChangeCompletionResult,
  getMcpToolCallToolUse,
  getWebSearchToolUse,
  getCommandString,
} from "./codex-item-handlers.js";
import {
  buildToolUseMessage,
  buildToolResultMessage,
  buildToolUseStartEvent,
  buildMessageStartEvent,
  buildTextBlockStartEvent,
  buildTextDeltaEvent,
  buildContentBlockStopEvent,
  buildMessageDeltaEvent,
  buildAssistantTextMessage,
  buildThinkingBlockStartEvent,
  buildThinkingMessage,
} from "./codex-message-utils.js";
import {
  buildCommandApprovalRequest,
  buildFileChangeApprovalRequest,
  buildMcpToolCallApprovalRequest,
  buildDynamicToolCallApprovalRequest,
  buildUserInputApprovalRequest,
  buildApplyPatchApprovalRequest,
  buildExecCommandApprovalRequest,
} from "./codex-approval-handlers.js";
import {
  buildTurnCompletedResult,
  normalizeRateLimits,
  buildTokenUsageSessionUpdate,
  resolveCollaborationModeFromTurn,
} from "./codex-session-helpers.js";

// ─── Shared mutable state passed by reference ─────────────────────────────────

export interface DynamicToolCallEntry {
  jsonRpcId: number;
  callId: string;
  toolName: string;
  timeout: ReturnType<typeof setTimeout>;
}

export interface CodexAdapterMutableState {
  streamingText: string;
  streamingItemId: string | null;
  commandStartTimes: Map<string, number>;
  reasoningTextByItemId: Map<string, string>;
  emittedToolUseIds: Set<string>;
  pendingApprovals: Map<string, number>;
  pendingUserInputQuestionIds: Map<string, string[]>;
  pendingReviewDecisions: Set<string>;
  pendingDynamicToolCalls: Map<string, DynamicToolCallEntry>;
  currentTurnId: string | null;
  currentCollaborationModeKind: "default" | "plan";
  currentPermissionMode: string;
  lastNonPlanPermissionMode: string;
  _rateLimits: RateLimits | null;
  planTracker: CodexPlanTracker;
}

// ─── Dependencies injected by the adapter class ──────────────────────────────

export interface EventHandlerDeps {
  state: CodexAdapterMutableState;
  transport: JsonRpcTransport;
  log: pino.Logger;
  sessionId: string;
  model: string;
  getExecutionCwd: () => string;
  emit: (msg: BrowserIncomingMessage) => void;
  dynamicToolCallTimeoutMs: number;
}

// ─── Emit helpers (keep tool_use tracking logic local) ────────────────────────

function emitToolUse(deps: EventHandlerDeps, toolUseId: string, toolName: string, input: Record<string, unknown>): void {
  deps.log.debug({ toolName, toolUseId }, "Emitting tool_use");
  deps.emit(buildToolUseMessage(toolUseId, toolName, input, deps.model));
}

function emitToolUseTracked(deps: EventHandlerDeps, toolUseId: string, toolName: string, input: Record<string, unknown>): void {
  deps.state.emittedToolUseIds.add(toolUseId);
  emitToolUse(deps, toolUseId, toolName, input);
}

function emitToolUseStart(deps: EventHandlerDeps, toolUseId: string, toolName: string, input: Record<string, unknown>): void {
  deps.emit(buildToolUseStartEvent(toolUseId, toolName));
  emitToolUseTracked(deps, toolUseId, toolName, input);
}

function ensureToolUseEmitted(deps: EventHandlerDeps, toolUseId: string, toolName: string, input: Record<string, unknown>): void {
  if (!deps.state.emittedToolUseIds.has(toolUseId)) {
    deps.log.debug({ toolName, toolUseId }, "Backfilling tool_use — item/started was missing");
    emitToolUseTracked(deps, toolUseId, toolName, input);
  }
}

function emitToolResult(deps: EventHandlerDeps, toolUseId: string, content: unknown, isError: boolean): void {
  deps.emit(buildToolResultMessage(toolUseId, content, isError, deps.model));
}

// ─── Notification dispatcher ─────────────────────────────────────────────────

export function dispatchNotification(
  method: string,
  params: Record<string, unknown>,
  deps: EventHandlerDeps,
): void {
  if (method.startsWith("item/") || method.startsWith("turn/") || method.startsWith("thread/")) {
    const item = params.item as { type?: string; id?: string } | undefined;
    deps.log.debug({
      method,
      itemType: item?.type,
      itemId: item?.id,
      paramKeys: !item && Object.keys(params).length > 0 ? Object.keys(params) : undefined,
    }, "Incoming notification");
  }

  try {
    switch (method) {
      case "item/started":
        handleItemStarted(params, deps);
        break;
      case "item/agentMessage/delta":
        handleAgentMessageDelta(params, deps);
        break;
      case "item/commandExecution/outputDelta":
        emitCommandProgress(params, deps);
        break;
      case "item/fileChange/outputDelta":
        break;
      case "item/reasoning/textDelta":
      case "item/reasoning/summaryTextDelta":
      case "item/reasoning/summaryPartAdded":
        handleReasoningDelta(params, deps);
        break;
      case "item/mcpToolCall/progress": {
        const itemId = params.itemId as string | undefined;
        if (itemId) {
          deps.emit({
            type: "tool_progress",
            tool_use_id: itemId,
            tool_name: "mcp_tool_call",
            elapsed_time_seconds: 0,
          });
        }
        break;
      }
      case "item/plan/delta":
        deps.state.planTracker.handlePlanDelta(params);
        break;
      case "item/updated":
        break;
      case "item/completed":
        handleItemCompleted(params, deps);
        break;
      case "rawResponseItem/completed":
        break;
      case "turn/started":
        handleTurnStarted(params, deps);
        break;
      case "turn/completed":
        handleTurnCompleted(params, deps);
        break;
      case "turn/plan/updated":
        handleTurnPlanUpdated(params, deps);
        break;
      case "turn/diff/updated":
        break;
      case "thread/started":
        break;
      case "thread/tokenUsage/updated":
        handleTokenUsageUpdated(params, deps);
        break;
      case "account/updated":
      case "account/login/completed":
        break;
      case "account/rateLimits/updated":
        updateRateLimits(params, deps);
        break;
      case "codex/event/stream_error": {
        const msg = params.msg as { message?: string } | undefined;
        if (msg?.message) {
          deps.log.info({ message: msg.message }, "Stream error");
        }
        break;
      }
      case "codex/event/error": {
        const msg = params.msg as { message?: string } | undefined;
        if (msg?.message) {
          deps.log.error({ message: msg.message }, "Codex error");
          deps.emit({ type: "error", message: msg.message });
        }
        break;
      }
      default:
        if (!method.startsWith("account/") && !method.startsWith("codex/event/")) {
          deps.log.info({ method }, "Unhandled notification");
        }
        break;
    }
  } catch (err) {
    deps.log.error({ method, err }, "Error handling notification");
  }
}

// ─── Request dispatcher ──────────────────────────────────────────────────────

export function dispatchRequest(
  method: string,
  id: number,
  params: Record<string, unknown>,
  deps: EventHandlerDeps,
): void {
  try {
    switch (method) {
      case "item/commandExecution/requestApproval":
        handleCommandApproval(id, params, deps);
        break;
      case "item/fileChange/requestApproval":
        handleFileChangeApproval(id, params, deps);
        break;
      case "item/mcpToolCall/requestApproval":
        handleMcpToolCallApproval(id, params, deps);
        break;
      case "item/tool/call":
        handleDynamicToolCall(id, params, deps);
        break;
      case "item/tool/requestUserInput":
        handleUserInputRequest(id, params, deps);
        break;
      case "applyPatchApproval":
        handleApplyPatchApproval(id, params, deps);
        break;
      case "execCommandApproval":
        handleExecCommandApproval(id, params, deps);
        break;
      case "account/chatgptAuthTokens/refresh":
        deps.log.warn("Auth token refresh not supported");
        deps.transport.respond(id, { error: "not supported" });
        break;
      default:
        deps.log.info({ method }, "Unhandled request");
        deps.transport.respond(id, { decision: "accept" });
        break;
    }
  } catch (err) {
    deps.log.error({ method, err }, "Error handling request");
  }
}

// ─── Item event handlers ─────────────────────────────────────────────────────

function handleItemStarted(params: Record<string, unknown>, deps: EventHandlerDeps): void {
  const item = params.item as CodexItem;
  if (!item) return;

  const toolUse = getStartedItemToolUse(item);
  if (toolUse) {
    if (item.type === "commandExecution") {
      deps.state.commandStartTimes.set(item.id, Date.now());
    }
    emitToolUseStart(deps, toolUse.toolUseId, toolUse.toolName, toolUse.input);
    return;
  }

  switch (item.type) {
    case "agentMessage":
      deps.state.streamingItemId = item.id;
      deps.state.streamingText = "";
      deps.emit(buildMessageStartEvent(item.id, deps.model));
      deps.emit(buildTextBlockStartEvent());
      break;

    case "reasoning": {
      const text = getReasoningStartText(item as CodexReasoningItem);
      deps.state.reasoningTextByItemId.set(item.id, text);
      if (text) {
        deps.emit(buildThinkingBlockStartEvent(text));
      }
      break;
    }

    case "contextCompaction":
      deps.emit({ type: "status_change", status: "compacting" });
      break;

    default:
      if (item.type !== "userMessage") {
        deps.log.info({ itemType: item.type, preview: JSON.stringify(item).substring(0, 300) }, "Unhandled item/started type");
      }
      break;
  }
}

function handleAgentMessageDelta(params: Record<string, unknown>, deps: EventHandlerDeps): void {
  const delta = params.delta as string;
  if (!delta) return;

  deps.state.streamingText += delta;
  deps.emit(buildTextDeltaEvent(delta));
}

function handleReasoningDelta(params: Record<string, unknown>, deps: EventHandlerDeps): void {
  const itemId = params.itemId as string | undefined;
  if (!itemId) return;

  if (!deps.state.reasoningTextByItemId.has(itemId)) {
    deps.state.reasoningTextByItemId.set(itemId, "");
  }

  const delta = params.delta as string | undefined;
  if (delta) {
    const current = deps.state.reasoningTextByItemId.get(itemId) || "";
    deps.state.reasoningTextByItemId.set(itemId, current + delta);
  }
}

function handleItemCompleted(params: Record<string, unknown>, deps: EventHandlerDeps): void {
  const item = params.item as CodexItem;
  if (!item) return;

  switch (item.type) {
    case "agentMessage": {
      const agentMsg = item as CodexAgentMessageItem;
      const text = agentMsg.text || deps.state.streamingText;
      deps.emit(buildContentBlockStopEvent());
      deps.emit(buildMessageDeltaEvent());
      deps.emit(buildAssistantTextMessage(item.id, text, deps.model));
      deps.state.streamingText = "";
      deps.state.streamingItemId = null;
      break;
    }

    case "commandExecution": {
      const cmd = item as CodexCommandExecutionItem;
      ensureToolUseEmitted(deps, item.id, "Bash", { command: getCommandString(cmd) });
      deps.state.commandStartTimes.delete(item.id);
      const result = buildCommandCompletionResult(cmd);
      if (result) {
        emitToolResult(deps, item.id, result.text, result.isError);
      }
      break;
    }

    case "fileChange": {
      const fc = buildFileChangeCompletionResult(item as CodexFileChangeItem);
      ensureToolUseEmitted(deps, item.id, fc.toolName, fc.toolInput);
      emitToolResult(deps, item.id, fc.summary, fc.isError);
      break;
    }

    case "mcpToolCall": {
      const mcp = getMcpToolCallToolUse(item as CodexMcpToolCallItem);
      ensureToolUseEmitted(deps, item.id, mcp.toolName, mcp.input);
      const mcpItem = item as CodexMcpToolCallItem;
      emitToolResult(deps, item.id, mcpItem.result || mcpItem.error || "MCP tool call completed", mcpItem.status === "failed");
      break;
    }

    case "webSearch": {
      const ws = getWebSearchToolUse(item as CodexWebSearchItem);
      ensureToolUseEmitted(deps, item.id, ws.toolName, ws.input);
      const wsItem = item as CodexWebSearchItem;
      emitToolResult(deps, item.id, wsItem.action?.url || wsItem.query || "Web search completed", false);
      break;
    }

    case "reasoning": {
      const r = item as CodexReasoningItem;
      const thinkingText = (
        deps.state.reasoningTextByItemId.get(item.id)
        || r.summary
        || r.content
        || ""
      ).trim();

      if (thinkingText) {
        deps.emit(buildThinkingMessage(item.id, thinkingText, deps.model));
      }

      deps.state.reasoningTextByItemId.delete(item.id);
      deps.emit(buildContentBlockStopEvent());
      break;
    }

    case "contextCompaction":
      deps.emit({ type: "status_change", status: null });
      break;

    default:
      if (item.type !== "userMessage") {
        deps.log.info({ itemType: item.type, preview: JSON.stringify(item).substring(0, 300) }, "Unhandled item/completed type");
      }
      break;
  }
}

// ─── Turn handlers ────────────────────────────────────────────────────────────

function handleTurnStarted(params: Record<string, unknown>, deps: EventHandlerDeps): void {
  const kind = resolveCollaborationModeFromTurn(params);
  if (!kind) return;

  deps.state.currentCollaborationModeKind = kind;
  const nextMode = kind === "plan" ? "plan" : deps.state.lastNonPlanPermissionMode;
  if (nextMode === deps.state.currentPermissionMode) return;

  deps.state.currentPermissionMode = nextMode;
  deps.emit({
    type: "session_update",
    session: { permissionMode: deps.state.currentPermissionMode },
  });
}

function handleTurnCompleted(params: Record<string, unknown>, deps: EventHandlerDeps): void {
  const turn = params.turn as { id: string; status: string; error?: { message: string } } | undefined;
  const result = buildTurnCompletedResult(turn, deps.sessionId);
  deps.emit({ type: "result", data: result });

  if (turn?.id) {
    deps.state.planTracker.clearTurn(turn.id);
  }
  deps.state.currentTurnId = null;
}

function handleTurnPlanUpdated(params: Record<string, unknown>, deps: EventHandlerDeps): void {
  const result = deps.state.planTracker.handleTurnPlanUpdated(params, deps.state.currentTurnId);
  if (result) {
    emitToolUseTracked(deps, result.toolUseId, "TodoWrite", { todos: result.todos });
  }
}

// ─── Token & rate limit handlers ─────────────────────────────────────────────

export function updateRateLimits(data: Record<string, unknown>, deps: EventHandlerDeps): void {
  const rateLimits = normalizeRateLimits(data);
  if (!rateLimits) return;
  deps.state._rateLimits = rateLimits;
  deps.emit({
    type: "session_update",
    session: {
      codex_rate_limits: {
        primary: rateLimits.primary,
        secondary: rateLimits.secondary,
      },
    },
  });
}

function handleTokenUsageUpdated(params: Record<string, unknown>, deps: EventHandlerDeps): void {
  const updates = buildTokenUsageSessionUpdate(params);
  if (updates) {
    deps.emit({ type: "session_update", session: updates });
  }
}

// ─── Command progress tracking ───────────────────────────────────────────────

function emitCommandProgress(params: Record<string, unknown>, deps: EventHandlerDeps): void {
  const itemId = params.itemId as string | undefined;
  if (!itemId) return;
  const startTime = deps.state.commandStartTimes.get(itemId);
  const elapsed = startTime ? Math.round((Date.now() - startTime) / 1000) : 0;
  deps.emit({
    type: "tool_progress",
    tool_use_id: itemId,
    tool_name: "Bash",
    elapsed_time_seconds: elapsed,
  });
}

// ─── Approval request handlers ───────────────────────────────────────────────

function handleCommandApproval(jsonRpcId: number, params: Record<string, unknown>, deps: EventHandlerDeps): void {
  const { requestId, permission } = buildCommandApprovalRequest(params, deps.getExecutionCwd());
  deps.state.pendingApprovals.set(requestId, jsonRpcId);
  deps.emit({ type: "permission_request", request: permission });
}

function handleFileChangeApproval(jsonRpcId: number, params: Record<string, unknown>, deps: EventHandlerDeps): void {
  const { requestId, permission } = buildFileChangeApprovalRequest(params);
  deps.state.pendingApprovals.set(requestId, jsonRpcId);
  deps.emit({ type: "permission_request", request: permission });
}

function handleMcpToolCallApproval(jsonRpcId: number, params: Record<string, unknown>, deps: EventHandlerDeps): void {
  const { requestId, permission } = buildMcpToolCallApprovalRequest(params);
  deps.state.pendingApprovals.set(requestId, jsonRpcId);
  deps.emit({ type: "permission_request", request: permission });
}

function handleDynamicToolCall(jsonRpcId: number, params: Record<string, unknown>, deps: EventHandlerDeps): void {
  const callId = params.callId as string || `dynamic-${randomUUID()}`;
  const toolName = params.tool as string || "unknown_dynamic_tool";
  const toolArgs = params.arguments as Record<string, unknown> || {};

  deps.log.info({ toolName, callId }, "Dynamic tool call received");

  emitToolUseTracked(deps, callId, `dynamic:${toolName}`, toolArgs);

  const { requestId, permission } = buildDynamicToolCallApprovalRequest(params, toolName, callId);
  deps.state.pendingApprovals.set(requestId, jsonRpcId);
  const timeout = setTimeout(() => {
    resolveDynamicToolCallTimeout(requestId, deps);
  }, deps.dynamicToolCallTimeoutMs);

  deps.state.pendingDynamicToolCalls.set(requestId, {
    jsonRpcId,
    callId,
    toolName,
    timeout,
  });

  deps.emit({ type: "permission_request", request: permission });
}

async function resolveDynamicToolCallTimeout(requestId: string, deps: EventHandlerDeps): Promise<void> {
  const pending = deps.state.pendingDynamicToolCalls.get(requestId);
  if (!pending) return;

  deps.state.pendingDynamicToolCalls.delete(requestId);
  deps.state.pendingApprovals.delete(requestId);

  emitToolResult(
    deps,
    pending.callId,
    `Dynamic tool "${pending.toolName}" timed out waiting for output.`,
    true,
  );

  try {
    await deps.transport.respond(pending.jsonRpcId, {
      contentItems: [{ type: "inputText", text: `Timed out waiting for dynamic tool output: ${pending.toolName}` }],
      success: false,
    });
  } catch (err) {
    deps.log.warn({ toolName: pending.toolName, err: String(err) }, "Failed to send dynamic tool timeout response");
  }
}

function handleUserInputRequest(jsonRpcId: number, params: Record<string, unknown>, deps: EventHandlerDeps): void {
  const { requestId, questionIds, permission } = buildUserInputApprovalRequest(params);
  deps.state.pendingApprovals.set(requestId, jsonRpcId);
  deps.state.pendingUserInputQuestionIds.set(requestId, questionIds);
  deps.emit({ type: "permission_request", request: permission });
}

function handleApplyPatchApproval(jsonRpcId: number, params: Record<string, unknown>, deps: EventHandlerDeps): void {
  const { requestId, permission } = buildApplyPatchApprovalRequest(params);
  deps.state.pendingApprovals.set(requestId, jsonRpcId);
  deps.state.pendingReviewDecisions.add(requestId);
  deps.emit({ type: "permission_request", request: permission });
}

function handleExecCommandApproval(jsonRpcId: number, params: Record<string, unknown>, deps: EventHandlerDeps): void {
  const { requestId, permission } = buildExecCommandApprovalRequest(params, deps.getExecutionCwd());
  deps.state.pendingApprovals.set(requestId, jsonRpcId);
  deps.state.pendingReviewDecisions.add(requestId);
  deps.emit({ type: "permission_request", request: permission });
}
