import type { ChildProcessWithoutNullStreams } from "node:child_process";
import { createInterface } from "node:readline";

import type { SessionRun } from "@codeflowmu/protocol";

import type {
  RunHandle,
  RuntimeEvent,
  RuntimeEventType,
  Unsubscribe,
} from "../types/state.ts";

export type CodexJsonEvent = Record<string, unknown> & {
  type?: string;
  item?: Record<string, unknown>;
  error?: unknown;
};

function itemText(item: Record<string, unknown> | undefined): string {
  if (!item) return "";
  for (const key of ["text", "content", "message", "output"] as const) {
    const value = item[key];
    if (typeof value === "string" && value.trim()) return value;
  }
  return "";
}

function itemId(item: Record<string, unknown> | undefined): string {
  const value = item?.["id"] ?? item?.["call_id"];
  return typeof value === "string" && value.trim()
    ? value
    : `codex-item-${Math.random().toString(36).slice(2, 10)}`;
}

function itemKind(item: Record<string, unknown> | undefined): string {
  const value = item?.["type"];
  return typeof value === "string" ? value : "unknown";
}

export interface CodexCliRunHandleOptions {
  sessionId: string;
  agentId: string;
  provider: string;
  model: string;
  runId?: string;
  maxToolRounds?: number;
}

/**
 * Runtime RunHandle for `codex exec --json`.
 *
 * Codex JSONL exposes observable reasoning summaries and tool lifecycle
 * events. They are mapped to status/tool events; hidden chain-of-thought is
 * never fabricated or surfaced as `sdk.thinking`.
 */
export class CodexCliRunHandle implements RunHandle {
  readonly run_id: string;
  readonly session_id: string;
  readonly agent_id: string;

  readonly #provider: string;
  readonly #model: string;
  readonly #maxToolRounds?: number;
  readonly #listeners = new Set<(event: RuntimeEvent) => void>();
  readonly #eventBuffer: RuntimeEvent[] = [];
  readonly #startedToolItems = new Set<string>();
  readonly #settledPromise: Promise<SessionRun>;
  readonly #startedAt = new Date().toISOString();
  #resolveSettled!: (run: SessionRun) => void;
  #child: ChildProcessWithoutNullStreams | null = null;
  #settled = false;
  #cancelled = false;
  #toolCalls = 0;
  #stderr = "";

  constructor(options: CodexCliRunHandleOptions) {
    this.run_id = options.runId ?? `run-codex-${Math.random().toString(36).slice(2, 12)}`;
    this.session_id = options.sessionId;
    this.agent_id = options.agentId;
    this.#provider = options.provider;
    this.#model = options.model;
    this.#maxToolRounds = options.maxToolRounds;
    this.#settledPromise = new Promise((resolve) => {
      this.#resolveSettled = resolve;
    });
  }

  attachProcess(child: ChildProcessWithoutNullStreams): void {
    if (this.#child) throw new Error("CodexCliRunHandle: process already attached");
    this.#child = child;
    this.#emit("runtime.session_started", {
      provider: this.#provider,
      model: this.#model,
      transport: "codex-cli-jsonl",
    });

    const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
    lines.on("line", (line) => this.#acceptLine(line));
    child.stderr.on("data", (chunk: Buffer | string) => {
      const text = chunk.toString();
      this.#stderr += text;
      if (text.trim()) this.#emit("sdk.status", { status: "stderr", text: text.trim() });
    });
    child.on("error", (error) => {
      this.#fail("CODEX_CLI_SPAWN_ERROR", error.message, null);
    });
    child.on("close", (code) => {
      lines.close();
      if (this.#settled) return;
      if (this.#cancelled) {
        this.#finish("cancelled", "CODEX_CLI_CANCELLED");
        return;
      }
      if (code === 0) {
        this.#emit("sdk.result", {
          status: "finished",
          provider: this.#provider,
          model: this.#model,
          terminal_reason: "process_exit",
        });
        this.#finish("finished");
        return;
      }
      this.#fail(
        "CODEX_CLI_EXIT_NONZERO",
        this.#stderr.trim() || `codex exited with code ${code ?? "unknown"}`,
        code,
      );
    });
  }

  /** Unit-test seam: consume one JSONL event without a child process. */
  acceptEventForTest(event: CodexJsonEvent): void {
    this.#acceptEvent(event);
  }

  /** Unit-test seam: terminate without a child process. */
  finishForTest(exitCode = 0): void {
    if (exitCode === 0) this.#finish("finished");
    else this.#fail("CODEX_CLI_EXIT_NONZERO", `exit ${exitCode}`, exitCode);
  }

  isActive(): boolean {
    return !this.#settled;
  }

  async cancel(reason: string): Promise<void> {
    if (this.#settled) return;
    this.#cancelled = true;
    this.#emit("runtime.session_cancelled", { reason });
    if (this.#child && !this.#child.killed) {
      try { this.#child.kill("SIGTERM"); } catch { /* best effort on Windows */ }
    } else {
      this.#finish("cancelled", "CODEX_CLI_CANCELLED");
    }
  }

  whenSettled(): Promise<SessionRun> {
    return this.#settledPromise;
  }

  onEvent(listener: (event: RuntimeEvent) => void): Unsubscribe {
    const first = this.#listeners.size === 0;
    this.#listeners.add(listener);
    if (first && this.#eventBuffer.length > 0) {
      const buffered = this.#eventBuffer.splice(0);
      for (const event of buffered) this.#deliver(event);
    }
    return () => this.#listeners.delete(listener);
  }

  #acceptLine(line: string): void {
    const trimmed = line.trim();
    if (!trimmed) return;
    try {
      this.#acceptEvent(JSON.parse(trimmed) as CodexJsonEvent);
    } catch {
      this.#emit("sdk.status", { status: "stdout", text: trimmed });
    }
  }

  #acceptEvent(event: CodexJsonEvent): void {
    const type = typeof event.type === "string" ? event.type : "unknown";
    const item = event.item;
    const kind = itemKind(item);
    const id = itemId(item);

    if (type === "thread.started") {
      this.#emit("sdk.status", { status: "thread_started", thread_id: event["thread_id"] });
      return;
    }
    if (type === "turn.started") {
      this.#emit("sdk.status", { status: "turn_started" });
      return;
    }
    if (type === "turn.completed") {
      this.#emit("sdk.result", {
        status: "finished",
        provider: this.#provider,
        model: this.#model,
        usage: event["usage"],
        raw: event,
      });
      this.#finish("finished");
      return;
    }
    if (type === "turn.failed" || type === "error") {
      const message = typeof event.error === "string"
        ? event.error
        : JSON.stringify(event.error ?? event);
      this.#fail("CODEX_CLI_TURN_FAILED", message, null);
      return;
    }

    if (!type.startsWith("item.")) {
      this.#emit("sdk.status", { status: "codex_event", raw: event });
      return;
    }

    if (kind === "agent_message" && type === "item.completed") {
      const text = itemText(item);
      if (text) this.#emit("sdk.assistant", { text, item_id: id });
      return;
    }
    if (kind === "reasoning") {
      const text = itemText(item);
      if (text) {
        this.#emit("sdk.status", {
          status: "reasoning_summary",
          text,
          item_id: id,
          observable_summary: true,
        });
      }
      return;
    }

    if (["command_execution", "mcp_tool_call", "web_search", "file_change"].includes(kind)) {
      if (!this.#startedToolItems.has(id)) {
        this.#startedToolItems.add(id);
        this.#toolCalls += 1;
        this.#emit("sdk.tool_call", {
          call_id: id,
          name: kind,
          item,
        });
      }
      if (type === "item.completed") {
        this.#emit("sdk.tool_result", {
          call_id: id,
          name: kind,
          item,
          ok: item?.["status"] !== "failed",
        });
      }
      if (this.#maxToolRounds != null && this.#toolCalls > this.#maxToolRounds) {
        void this.cancel(`Codex CLI tool round limit exceeded (${this.#maxToolRounds})`);
      }
      return;
    }

    this.#emit("sdk.status", { status: type, item_id: id, item });
  }

  #fail(failureCode: string, error: string, exitCode: number | null): void {
    if (this.#settled) return;
    this.#emit("sdk.result", {
      status: "failed",
      failure_code: failureCode,
      error,
      exit_code: exitCode,
      stderr: this.#stderr,
      provider: this.#provider,
      model: this.#model,
    });
    this.#finish("failed", failureCode);
  }

  #finish(status: SessionRun["status"], failureCode?: string): void {
    if (this.#settled) return;
    this.#settled = true;
    this.#resolveSettled({
      run_id: this.run_id,
      started_at: this.#startedAt,
      ended_at: new Date().toISOString(),
      status,
      tool_calls_count: this.#toolCalls,
      ...(failureCode ? { failure_code: failureCode } : {}),
    });
  }

  #emit(eventType: RuntimeEventType, payload?: unknown): void {
    const event: RuntimeEvent = {
      event_id: `evt-${Math.random().toString(36).slice(2, 10)}`,
      at: new Date().toISOString(),
      event_type: eventType,
      session_id: this.session_id,
      run_id: this.run_id,
      agent_id: this.agent_id,
      payload,
    };
    if (this.#listeners.size === 0) this.#eventBuffer.push(event);
    else this.#deliver(event);
  }

  #deliver(event: RuntimeEvent): void {
    for (const listener of this.#listeners) {
      try { listener(event); } catch { /* listeners are isolated */ }
    }
  }
}

