/**
 * Codex App-Server Adapter
 *
 * Translates between the Codex app-server JSON-RPC protocol (stdin/stdout)
 * and Zencode's BrowserIncomingMessage/BrowserOutgoingMessage types.
 *
 * This allows the browser to be completely unaware of which backend is running —
 * it sees the same message types regardless of whether Claude Code or Codex is
 * the backend.
 *
 * Delegates to focused modules for:
 * - Types: ./codex-types.ts
 * - Plan parsing: ./codex-plan-parser.ts
 * - Plan state tracking: ./codex-plan-tracker.ts
 * - MCP config helpers: ./codex-mcp-helpers.ts
 * - Message construction: ./codex-message-utils.ts
 * - Approval request building: ./codex-approval-handlers.ts
 * - Incoming event handling: ./codex-event-handlers.ts
 * - Outgoing message translation: ./codex-message-translator.ts
 */

import type { Subprocess } from "bun";
import type {
  BrowserIncomingMessage,
  BrowserOutgoingMessage,
} from "./session-types.js";
import { createSessionLogger } from "./logger.js";
import type pino from "pino";
import { JsonRpcTransport } from "./json-rpc-transport.js";

import type { CodexAdapterOptions } from "./codex-types.js";
export type { CodexAdapterOptions };

import {
  buildInitialSessionState,
  mapApprovalPolicy,
  mapSandboxPolicy,
} from "./codex-session-helpers.js";

import { CodexPlanTracker } from "./codex-plan-tracker.js";
import {
  dispatchNotification,
  dispatchRequest,
  updateRateLimits,
} from "./codex-event-handlers.js";
import type { CodexAdapterMutableState, EventHandlerDeps } from "./codex-event-handlers.js";
import { dispatchOutgoingMessage } from "./codex-message-translator.js";
import type { OutgoingHandlerDeps } from "./codex-message-translator.js";

// ─── Codex Adapter ────────────────────────────────────────────────────────────
/**
 * Adapter that translates between the Codex app-server JSON-RPC protocol
 * (communicated via a spawned subprocess's stdin/stdout) and Zencode's internal
 * browser message types ({@link BrowserIncomingMessage}/{@link BrowserOutgoingMessage}).
 *
 * Key responsibilities:
 * - Manages the Codex subprocess lifecycle (spawn, initialize, disconnect)
 * - Performs the JSON-RPC `initialize` → `initialized` → `thread/start` handshake
 * - Queues outgoing browser messages during initialization, flushing once ready
 * - Delegates incoming JSON-RPC notifications and requests to {@link codex-event-handlers}
 * - Delegates outgoing browser messages to {@link codex-message-translator}
 * - Tracks mutable session state (streaming text, pending approvals, plan tracking)
 *
 * Connection lifecycle:
 * 1. Constructor spawns the transport and begins async `initialize()`
 * 2. On success, `connected` and `initialized` become true and queued messages flush
 * 3. On failure, `initFailed` is set and all subsequent messages are rejected
 * 4. `disconnect()` sends SIGTERM and awaits process exit (with timeout)
 *
 * @see codex-event-handlers.ts — Handles incoming Codex notifications and requests
 * @see codex-message-translator.ts — Translates outgoing browser messages to JSON-RPC
 */export class CodexAdapter {
  private transport: JsonRpcTransport;
  private proc: Subprocess;
  private sessionId: string;
  private options: CodexAdapterOptions;
  private log: pino.Logger;

  private browserMessageCb: ((msg: BrowserIncomingMessage) => void) | null = null;
  private sessionMetaCb: ((meta: { cliSessionId?: string; model?: string; cwd?: string }) => void) | null = null;
  private disconnectCb: (() => void) | null = null;
  private initErrorCb: ((error: string) => void) | null = null;

  // State
  private threadId: string | null = null;
  private connected = false;
  private initialized = false;
  private initFailed = false;

  // Track what we last sent to Codex so we only send on transitions.
  // null = nothing sent yet (first turn needs to send if starting in plan).
  private lastSentCollaborationModeKind: "default" | "plan" | null = null;

  // Queue messages received before initialization completes
  private pendingOutgoing: BrowserOutgoingMessage[] = [];

  // Mutable state shared with event handlers and message translator
  private mutableState: CodexAdapterMutableState;

  private static readonly DYNAMIC_TOOL_CALL_TIMEOUT_MS = 120_000;

  private getExecutionCwd(): string {
    return this.options.executionCwd || this.options.cwd || "";
  }

  /**
   * @param proc - The spawned Codex subprocess with stdio pipes.
   * @param sessionId - Unique session identifier.
   * @param options - Configuration options (model, cwd, approval mode, sandbox, etc.).
   * @throws If the subprocess lacks stdio pipes.
   */
  constructor(proc: Subprocess, sessionId: string, options: CodexAdapterOptions = {}) {
    this.proc = proc;
    this.sessionId = sessionId;
    this.options = options;
    this.log = createSessionLogger("codex-adapter", sessionId);

    const currentPermissionMode = options.approvalMode || "default";
    const lastNonPlanPermissionMode = currentPermissionMode === "plan"
      ? "acceptEdits"
      : currentPermissionMode;
    const currentCollaborationModeKind = currentPermissionMode === "plan"
      ? "plan" as const
      : "default" as const;

    this.mutableState = {
      streamingText: "",
      streamingItemId: null,
      commandStartTimes: new Map(),
      reasoningTextByItemId: new Map(),
      emittedToolUseIds: new Set(),
      pendingApprovals: new Map(),
      pendingUserInputQuestionIds: new Map(),
      pendingReviewDecisions: new Set(),
      pendingDynamicToolCalls: new Map(),
      currentTurnId: null,
      currentCollaborationModeKind,
      currentPermissionMode,
      lastNonPlanPermissionMode,
      _rateLimits: null,
      planTracker: new CodexPlanTracker(),
    };

    const stdout = proc.stdout;
    const stdin = proc.stdin;
    if (!stdout || !stdin || typeof stdout === "number" || typeof stdin === "number") {
      throw new Error("Codex process must have stdio pipes");
    }

    this.transport = new JsonRpcTransport(
      stdin as WritableStream<Uint8Array> | { write(data: Uint8Array): number },
      stdout as ReadableStream<Uint8Array>,
    );
    this.transport.onNotification((method, params) => {
      dispatchNotification(method, params, this.eventHandlerDeps());
    });
    this.transport.onRequest((method, id, params) => {
      dispatchRequest(method, id, params, this.eventHandlerDeps());
    });

    // Wire raw message recording if a recorder is provided
    if (options.recorder) {
      const recorder = options.recorder;
      const cwd = options.cwd || "";
      this.transport.onRawIncoming((line) => {
        recorder.record(sessionId, "in", line, "cli", "codex", cwd);
      });
      this.transport.onRawOutgoing((data) => {
        recorder.record(sessionId, "out", data.trimEnd(), "cli", "codex", cwd);
      });
    }

    // Monitor process exit
    proc.exited.then(() => {
      this.connected = false;
      for (const pending of this.mutableState.pendingDynamicToolCalls.values()) {
        clearTimeout(pending.timeout);
      }
      this.mutableState.pendingDynamicToolCalls.clear();
      this.disconnectCb?.();
    });

    // Start initialization
    this.initialize();
  }

  // ── Public API ──────────────────────────────────────────────────────────

  /** @returns Current Codex rate limit information, or null if not yet fetched. */
  getRateLimits() {
    return this.mutableState._rateLimits;
  }

  /**
   * Send a browser-originated message to the Codex backend.
   * Messages are queued if initialization has not yet completed.
   *
   * @param msg - The outgoing browser message to translate and send.
   * @returns `true` if the message was accepted (sent or queued), `false` if rejected.
   */
  sendBrowserMessage(msg: BrowserOutgoingMessage): boolean {
    // If initialization failed, reject all new messages
    if (this.initFailed) {
      return false;
    }

    // Queue messages if not yet initialized (init is async)
    if (!this.initialized || !this.threadId) {
      if (
        msg.type === "user_message"
        || msg.type === "permission_response"
        || msg.type === "mcp_get_status"
        || msg.type === "mcp_toggle"
        || msg.type === "mcp_reconnect"
        || msg.type === "mcp_set_servers"
      ) {
        this.log.info({ msgType: msg.type }, "Queuing message — adapter not yet initialized");
        this.pendingOutgoing.push(msg);
        return true; // accepted, will be sent after init
      }
      // Non-queueable messages are dropped if not connected
      if (!this.connected) return false;
    }

    return this.dispatchOutgoing(msg);
  }

  private dispatchOutgoing(msg: BrowserOutgoingMessage): boolean {
    return dispatchOutgoingMessage(msg, this.outgoingHandlerDeps());
  }

  /** Register a callback for browser-bound messages emitted by the adapter. */
  onBrowserMessage(cb: (msg: BrowserIncomingMessage) => void): void {
    this.browserMessageCb = cb;
  }

  /** Register a callback for session metadata (CLI session ID, model, cwd). */
  onSessionMeta(cb: (meta: { cliSessionId?: string; model?: string; cwd?: string }) => void): void {
    this.sessionMetaCb = cb;
  }

  /** Register a callback invoked when the Codex process exits. */
  onDisconnect(cb: () => void): void {
    this.disconnectCb = cb;
  }

  /** Register a callback invoked when initialization fails. */
  onInitError(cb: (error: string) => void): void {
    this.initErrorCb = cb;
  }

  /** @returns `true` if the Codex process is alive and initialization succeeded. */
  isConnected(): boolean {
    return this.connected;
  }

  /**
   * Gracefully disconnect from the Codex process.
   * Sends SIGTERM and waits up to 5 seconds for the process to exit.
   */
  async disconnect(): Promise<void> {
    this.connected = false;
    try {
      this.proc.kill("SIGTERM");
      await Promise.race([
        this.proc.exited,
        new Promise((r) => setTimeout(r, 5000)),
      ]);
    } catch (err) {
      this.log.debug({ err }, "Failed to terminate Codex process during disconnect");
    }
  }

  /** @returns The Codex thread ID, or null if not yet initialized. */
  getThreadId(): string | null {
    return this.threadId;
  }

  // ── Initialization ──────────────────────────────────────────────────────

  private async initialize(): Promise<void> {
    try {
      // Step 1: Send initialize request
      await this.transport.call("initialize", {
        clientInfo: {
          name: "zencode",
          title: "Zencode",
          version: "1.0.0",
        },
        capabilities: {
          experimentalApi: true,
        },
      }) as Record<string, unknown>;

      // Step 2: Send initialized notification
      await this.transport.notify("initialized", {});

      this.connected = true;
      this.initialized = true;

      // Step 3: Start or resume a thread
      // Note: thread/start and thread/resume use `sandbox` (SandboxMode string),
      // while turn/start uses `sandboxPolicy` (SandboxPolicy object) — these are
      // different Codex API fields by design.
      if (this.options.threadId) {
        // Resume an existing thread
        const resumeResult = await this.transport.call("thread/resume", {
          threadId: this.options.threadId,
          model: this.options.model,
          cwd: this.getExecutionCwd(),
          approvalPolicy: mapApprovalPolicy(this.mutableState.currentPermissionMode),
          sandbox: this.options.sandbox || mapSandboxPolicy(this.mutableState.currentPermissionMode),
        }) as { thread: { id: string } };
        this.threadId = resumeResult.thread.id;
      } else {
        // Start a new thread
        const threadResult = await this.transport.call("thread/start", {
          model: this.options.model,
          cwd: this.getExecutionCwd(),
          approvalPolicy: mapApprovalPolicy(this.mutableState.currentPermissionMode),
          sandbox: this.options.sandbox || mapSandboxPolicy(this.mutableState.currentPermissionMode),
        }) as { thread: { id: string } };
        this.threadId = threadResult.thread.id;
      }

      // Notify session metadata
      this.sessionMetaCb?.({
        cliSessionId: this.threadId,
        model: this.options.model,
        cwd: this.options.cwd,
      });

      // Send session_init to browser
      const state = buildInitialSessionState(
        this.sessionId,
        this.options.model || "",
        this.options.cwd || "",
        this.mutableState.currentPermissionMode,
      );
      this.emit({ type: "session_init", session: state });

      // Fetch initial rate limits (non-blocking — don't fail init if this errors)
      this.transport.call("account/rateLimits/read", {}).then((result) => {
        updateRateLimits(result as Record<string, unknown>, this.eventHandlerDeps());
      }).catch((err) => {
        this.log.debug({ err }, "Failed to fetch initial Codex rate limits");
      });

      // Flush any messages that were queued during initialization
      if (this.pendingOutgoing.length > 0) {
        this.log.info({ count: this.pendingOutgoing.length }, "Flushing queued messages");
        const queued = this.pendingOutgoing.splice(0);
        for (const msg of queued) {
          this.dispatchOutgoing(msg);
        }
      }
    } catch (err) {
      const errorMsg = `Codex initialization failed: ${err}`;
      this.log.error({ errorMsg }, "Codex initialization failed");
      this.initFailed = true;
      this.connected = false;
      // Discard any messages queued during the failed init attempt
      this.pendingOutgoing.length = 0;
      this.emit({ type: "error", message: errorMsg });
      this.initErrorCb?.(errorMsg);
    }
  }

  // ── Helpers ─────────────────────────────────────────────────────────────

  private emit(msg: BrowserIncomingMessage): void {
    this.browserMessageCb?.(msg);
  }

  /** Build the EventHandlerDeps object for dispatching to codex-event-handlers. */
  private eventHandlerDeps(): EventHandlerDeps {
    return {
      state: this.mutableState,
      transport: this.transport,
      log: this.log,
      sessionId: this.sessionId,
      model: this.options.model || "",
      getExecutionCwd: () => this.getExecutionCwd(),
      emit: (msg) => this.emit(msg),
      dynamicToolCallTimeoutMs: CodexAdapter.DYNAMIC_TOOL_CALL_TIMEOUT_MS,
    };
  }

  /** Build the OutgoingHandlerDeps object for dispatching to codex-message-translator. */
  private outgoingHandlerDeps(): OutgoingHandlerDeps {
    return {
      state: this.mutableState,
      transport: this.transport,
      log: this.log,
      threadId: this.threadId,
      model: this.options.model || "",
      getExecutionCwd: () => this.getExecutionCwd(),
      emit: (msg) => this.emit(msg),
      lastSentCollaborationModeKind: this.lastSentCollaborationModeKind,
      setLastSentCollaborationModeKind: (kind) => {
        this.lastSentCollaborationModeKind = kind;
      },
      setCurrentTurnId: (turnId) => {
        this.mutableState.currentTurnId = turnId;
      },
    };
  }
}
