/**
 * Rust Agent SDK adapter — spawns the rust agent binary as a subprocess
 * and streams NDJSON events back as an async generator.
 */

import { type ChildProcess, spawn } from "node:child_process";
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { resolve as resolvePath } from "node:path";
import { createInterface } from "node:readline";
import { serverConfig } from "./config";

// ─── NDJSON event types from the Rust binary ─────────────────────────────────

export interface NdjsonStarted {
  type: "started";
  tools: string[];
}

export interface NdjsonThinking {
  type: "thinking";
  content: string;
  iteration: number;
}

export interface NdjsonToolCall {
  type: "tool_call";
  tool_call_id: string;
  tool_name: string;
  arguments: string;
  iteration: number;
}

export interface NdjsonToolResult {
  type: "tool_result";
  tool_call_id: string;
  tool_name: string;
  content: string;
  iteration: number;
}

export interface NdjsonTextDelta {
  type: "text_delta";
  content: string;
}

export interface NdjsonCompleted {
  type: "completed";
  final_content: string;
  tokens_used: number;
  iterations: number;
  tool_calls: number;
}

export interface NdjsonFailed {
  type: "failed";
  error: string;
}

export type NdjsonEvent =
  | NdjsonStarted
  | NdjsonThinking
  | NdjsonToolCall
  | NdjsonToolResult
  | NdjsonTextDelta
  | NdjsonCompleted
  | NdjsonFailed;

// ─── Options ─────────────────────────────────────────────────────────────────

export interface RustAgentOptions {
  prompt: string;
  cwd: string;
  systemPrompt?: string;
  model?: string;
  tools?: string[];
  maxIterations?: number;
  maxTokens?: number;
  abortSignal?: AbortSignal;
  env?: Record<string, string>;
}

// ─── Adapter ─────────────────────────────────────────────────────────────────

export async function* runRustAgent(
  opts: RustAgentOptions,
): AsyncGenerator<NdjsonEvent> {
  const binaryPath = resolvePath(serverConfig.rustAgent.binaryPath);

  const resolvedCwd = resolvePath(opts.cwd);
  const args: string[] = [];
  // Flags
  args.push("--json");
  args.push("--dir", resolvedCwd);

  if (opts.model) {
    args.push("--model", opts.model);
  }
  if (opts.systemPrompt) {
    args.push("--system", opts.systemPrompt);
  }
  if (opts.maxIterations !== undefined) {
    args.push("--max-iterations", String(opts.maxIterations));
  }
  if (opts.maxTokens !== undefined) {
    args.push("--max-tokens", String(opts.maxTokens));
  }
  if (opts.tools) {
    args.push("--tools", opts.tools.join(","));
  }

  // Write the prompt to a temp file and pass via --prompt-file to avoid
  // OS argument size limits (E2BIG) on large prompts.
  const promptTempDir = mkdtempSync(resolvePath(tmpdir(), "optage-agent-prompt-"));
  const promptPath = resolvePath(promptTempDir, "prompt.txt");
  writeFileSync(promptPath, opts.prompt, "utf-8");
  args.push("--prompt-file", promptPath);

  const env = {
    ...process.env,
    ...opts.env,
  };

  const spawnCwd = existsSync(resolvedCwd) ? resolvedCwd : undefined;

  const child: ChildProcess = spawn(binaryPath, args, {
    env,
    stdio: ["ignore", "pipe", "pipe"] as const,
    cwd: spawnCwd,
  });

  // Convert readline to async iterator with a queue
  const queue: Array<NdjsonEvent | null> = [];
  let waiting: (() => void) | null = null;

  const push = (item: NdjsonEvent | null) => {
    queue.push(item);
    if (waiting) {
      waiting();
      waiting = null;
    }
  };

  // Handle spawn errors (e.g. ENOENT)
  child.on("error", (err: Error) => {
    console.error("rust-agent: spawn error:", err.message);
    push({ type: "failed", error: `Failed to spawn rust agent: ${err.message}` });
    push(null);
  });

  // Handle abort signal
  if (opts.abortSignal) {
    const onAbort = () => {
      child.kill("SIGTERM");
    };
    if (opts.abortSignal.aborted) {
      child.kill("SIGTERM");
    } else {
      opts.abortSignal.addEventListener("abort", onAbort, { once: true });
      child.on("exit", () => {
        opts.abortSignal!.removeEventListener("abort", onAbort);
      });
    }
  }

  // Log stderr
  if (child.stderr) {
    const stderrRl = createInterface({ input: child.stderr });
    stderrRl.on("line", (line: string) => {
      console.error("rust-agent:", line);
    });
  }

  // Stream stdout as NDJSON
  if (child.stdout) {
    const rl = createInterface({ input: child.stdout });
    rl.on("line", (line: string) => {
      const trimmed = line.trim();
      if (!trimmed) return;
      try {
        const event = JSON.parse(trimmed) as NdjsonEvent;
        push(event);
      } catch {
        console.error("rust-agent: failed to parse NDJSON line:", trimmed);
      }
    });
  }

  const exitPromise = new Promise<number | null>((res) => {
    child.on("exit", (code: number | null) => {
      push(null); // sentinel
      res(code);
    });
  });

  // Yield events as they arrive
  let hadFailedEvent = false;
  while (true) {
    if (queue.length > 0) {
      const event = queue.shift()!;
      if (event === null) break; // child exited
      if (event.type === "failed") hadFailedEvent = true;
      yield event;
    } else {
      await new Promise<void>((r) => {
        waiting = r;
      });
    }
  }

  const exitCode = await exitPromise;
  try {
    rmSync(promptTempDir, { recursive: true, force: true });
  } catch {
    // Ignore cleanup failures for ephemeral prompt temp dirs.
  }
  if (exitCode !== 0 && exitCode !== null && !hadFailedEvent) {
    yield {
      type: "failed",
      error: `Rust agent process exited with code ${exitCode}`,
    };
  }
}
