import type { ChildProcessWithoutNullStreams } from "node:child_process";
import type { SessionRun } from "@codeflowmu/protocol";
import type { RunHandle, RuntimeEvent, RuntimeEventType, Unsubscribe } from "../types/state.ts";

/** Parsed subset of `claude -p --output-format json` stdout. */
export interface ParsedClaudeJson {
  result?: string;
  is_error?: boolean;
  usage?: unknown;
  modelUsage?: unknown;
  total_cost_usd?: number;
  duration_ms?: number;
  terminal_reason?: string;
  claude_session_id?: string;
  raw: Record<string, unknown>;
}

let _runSeq = 0;

function normalizeParsed(obj: Record<string, unknown>): ParsedClaudeJson {
  return {
    result: typeof obj.result === "string" ? obj.result : undefined,
    is_error: obj.is_error === true,
    usage: obj.usage,
    modelUsage: obj.modelUsage,
    total_cost_usd:
      typeof obj.total_cost_usd === "number" ? obj.total_cost_usd : undefined,
    duration_ms:
      typeof obj.duration_ms === "number" ? obj.duration_ms : undefined,
    terminal_reason:
      typeof obj.terminal_reason === "string" ? obj.terminal_reason : undefined,
    claude_session_id:
      typeof obj.session_id === "string" ? obj.session_id : undefined,
    raw: obj,
  };
}

/**
 * Parse Claude Code JSON stdout (single object or trailing NDJSON line).
 * Returns null when no valid JSON result object is found.
 */
export function parseClaudeStdout(stdout: string): ParsedClaudeJson | null {
  const trimmed = stdout.trim();
  if (!trimmed) return null;

  try {
    return normalizeParsed(JSON.parse(trimmed) as Record<string, unknown>);
  } catch {
    const lines = trimmed.split(/\r?\n/).filter((line) => line.trim());
    for (let i = lines.length - 1; i >= 0; i--) {
      try {
        const obj = JSON.parse(lines[i]!) as Record<string, unknown>;
        if (
          obj.type === "result" ||
          "result" in obj ||
          "is_error" in obj ||
          obj.subtype === "success" ||
          obj.subtype === "error"
        ) {
          return normalizeParsed(obj);
        }
      } catch {
        continue;
      }
    }
  }
  return null;
}

/**
 * Infer tool-call count from Claude JSON when possible.
 * On failed runs, returns `-1` when unknown so ZeroToolcallCircuitBreaker
 * does not treat consecutive CLI failures as zero-toolcall sessions (§14.4).
 */
export function inferToolCallsCount(
  parsed: ParsedClaudeJson | null,
  failed: boolean,
): number {
  if (parsed) {
    const raw = parsed.raw;
    const usage = raw.usage;
    if (usage && typeof usage === "object" && !Array.isArray(usage)) {
      const u = usage as Record<string, unknown>;
      for (const key of [
        "tool_calls",
        "tool_use_count",
        "num_tool_calls",
        "toolCalls",
      ] as const) {
        if (typeof u[key] === "number" && Number.isFinite(u[key])) {
          return u[key] as number;
        }
      }
    }
    if (typeof raw.tool_calls_count === "number") {
      return raw.tool_calls_count;
    }
    if (typeof raw.tool_use_count === "number") {
      return raw.tool_use_count;
    }
  }
  if (failed) return -1;
  return 0;
}

export interface ClaudeCodeRunHandleOptions {
  sessionId: string;
  agentId: string;
  runId?: string;
  maxToolRounds?: number;
  uiLang?: string;
}

/**
 * RunHandle for `claude -p --output-format json` subprocess runs.
 */
export class ClaudeCodeRunHandle implements RunHandle {
  readonly run_id: string;
  readonly session_id: string;
  readonly agent_id: string;

  private readonly _listeners = new Set<(event: RuntimeEvent) => void>();
  private readonly _eventBuffer: RuntimeEvent[] = [];
  private readonly _settlePromise: Promise<SessionRun>;
  private _resolveSettle!: (run: SessionRun) => void;
  private _rejectSettle!: (err: Error) => void;
  private _settled = false;
  private _cancelled = false;
  private _child: ChildProcessWithoutNullStreams | null = null;
  private readonly _startedAt: string;
  private readonly _maxToolRounds?: number;
  private readonly _uiLang?: string;

  constructor(opts: ClaudeCodeRunHandleOptions) {
    this.run_id = opts.runId ?? `run-cl-${(++_runSeq).toString(36)}`;
    this.session_id = opts.sessionId;
    this.agent_id = opts.agentId;
    this._startedAt = new Date().toISOString();
    this._maxToolRounds = opts.maxToolRounds;
    this._uiLang = opts.uiLang;
    this._settlePromise = new Promise((resolve, reject) => {
      this._resolveSettle = resolve;
      this._rejectSettle = reject;
    });
  }

  attachProcess(child: ChildProcessWithoutNullStreams): void {
    if (this._child) {
      throw new Error("ClaudeCodeRunHandle: process already attached");
    }
    this._child = child;

    this.emit("sdk.status", {
      status: "started",
      provider: "claude-code",
      ...(this._uiLang ? { uiLang: this._uiLang } : {}),
      ...(this._maxToolRounds != null
        ? { maxToolRounds: this._maxToolRounds }
        : {}),
    });

    let stdout = "";
    let stderr = "";

    child.stdout.on("data", (chunk: Buffer | string) => {
      stdout += chunk.toString();
    });
    child.stderr.on("data", (chunk: Buffer | string) => {
      const text = chunk.toString();
      stderr += text;
      const line = text.trim();
      if (line) {
        this.emit("sdk.status", { status: "stderr", text: line });
      }
    });

    child.on("error", (err) => {
      this._finalizeFailure({
        failureCode: "CLAUDE_SPAWN_ERROR",
        error: err.message,
        stderr,
        stdout,
        exitCode: null,
      });
    });

    child.on("close", (code) => {
      if (this._cancelled) {
        this._settleTerminal("cancelled", {
          toolCallsCount: -1,
          failureCode: "CLAUDE_CANCELLED",
        });
        return;
      }
      this._finalizeFromOutput(stdout, stderr, code ?? 1);
    });
  }

  /** Test hook: drive completion without a real child process. */
  _finalizeFromOutputForTest(
    stdout: string,
    stderr: string,
    exitCode: number,
  ): void {
    this._finalizeFromOutput(stdout, stderr, exitCode);
  }

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

  async cancel(reason: string): Promise<void> {
    void reason;
    if (this._settled) return;
    this._cancelled = true;
    const child = this._child;
    if (child && !child.killed) {
      try {
        child.kill("SIGTERM");
      } catch {
        // best-effort on Windows
      }
    }
    if (!child) {
      this._settleTerminal("cancelled", {
        toolCallsCount: -1,
        failureCode: "CLAUDE_CANCELLED",
      });
    }
  }

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

  onEvent(listener: (event: RuntimeEvent) => void): Unsubscribe {
    const wasEmpty = this._listeners.size === 0;
    this._listeners.add(listener);
    if (wasEmpty && this._eventBuffer.length > 0) {
      const buffered = this._eventBuffer.splice(0);
      for (const event of buffered) {
        this._deliverToListeners(event);
      }
    }
    return () => {
      this._listeners.delete(listener);
    };
  }

  emit(type: RuntimeEventType, payload?: unknown): void {
    const event: RuntimeEvent = {
      event_id: `evt-${Math.random().toString(36).slice(2, 10)}`,
      at: new Date().toISOString(),
      event_type: type,
      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);
      return;
    }
    this._deliverToListeners(event);
  }

  private _deliverToListeners(event: RuntimeEvent): void {
    for (const listener of this._listeners) {
      try {
        listener(event);
      } catch (err) {
        console.error(
          `[ClaudeCodeRunHandle] listener threw on run_id="${this.run_id}":`,
          err,
        );
      }
    }
  }

  private _finalizeFromOutput(
    stdout: string,
    stderr: string,
    exitCode: number,
  ): void {
    if (this._settled) return;

    if (!stdout.trim()) {
      this._finalizeFailure({
        failureCode: "CLAUDE_EMPTY_OUTPUT",
        error: "claude produced empty stdout",
        stderr,
        stdout,
        exitCode,
      });
      return;
    }

    const parsed = parseClaudeStdout(stdout);
    if (!parsed) {
      this._finalizeFailure({
        failureCode: "CLAUDE_JSON_PARSE_FAILED",
        error: "failed to parse claude JSON stdout",
        stderr,
        stdout,
        exitCode,
      });
      return;
    }

    if (parsed.is_error || exitCode !== 0) {
      this._finalizeFailure({
        failureCode: "CLAUDE_CODE_FAILED",
        error:
          parsed.result ??
          parsed.terminal_reason ??
          `claude exited with code ${exitCode}`,
        stderr,
        stdout,
        exitCode,
        parsed,
      });
      return;
    }

    if (parsed.result) {
      this.emit("sdk.assistant", { text: parsed.result });
    }

    this.emit("sdk.result", {
      status: "finished",
      raw: parsed.raw,
      usage: parsed.usage,
      modelUsage: parsed.modelUsage,
      total_cost_usd: parsed.total_cost_usd,
      duration_ms: parsed.duration_ms,
      terminal_reason: parsed.terminal_reason,
      claude_session_id: parsed.claude_session_id,
    });

    this._settleTerminal("finished", {
      toolCallsCount: inferToolCallsCount(parsed, false),
    });
  }

  private _finalizeFailure(opts: {
    failureCode: string;
    error: string;
    stderr: string;
    stdout: string;
    exitCode: number | null;
    parsed?: ParsedClaudeJson | null;
  }): void {
    this.emit("sdk.result", {
      status: "failed",
      error: opts.error,
      stderr: opts.stderr,
      failure_code: opts.failureCode,
      exit_code: opts.exitCode,
      raw: opts.parsed?.raw,
    });
    this._settleTerminal("failed", {
      toolCallsCount: inferToolCallsCount(opts.parsed ?? null, true),
      failureCode: opts.failureCode,
    });
  }

  private _settleTerminal(
    status: SessionRun["status"],
    opts: { toolCallsCount: number; failureCode?: string },
  ): void {
    if (this._settled) return;
    this._settled = true;
    this._resolveSettle({
      run_id: this.run_id,
      started_at: this._startedAt,
      ended_at: new Date().toISOString(),
      status,
      tool_calls_count: opts.toolCallsCount,
      ...(opts.failureCode ? { failure_code: opts.failureCode } : {}),
    });
  }
}
