/**
 * GrokSession — a real AgentSession backed by xAI's **Grok** CLI, run inside
 * the task container:
 *
 *   docker exec -i task-<id>-app-1 ~/.grok/bin/grok -p "<prompt>" \
 *     --output-format streaming-json --permission-mode bypassPermissions \
 *     -m grok-4.5 --system-prompt-override "<briefing>" [-r <sessionId>]
 *
 * Like Kimi (and unlike Claude's persistent stdin / Codex's app-server), Grok
 * headless mode is **one-shot per turn**: `-p` runs one prompt to completion,
 * streams newline-delimited JSON, and exits. Each `send()` spawns a fresh
 * `docker exec`; sends are **serialized** so a second message can't race a
 * running turn. Continuity is `-r <sessionId>` — captured from the turn-end
 * event (per-agent isolation, since each session tracks its own id).
 *
 * Stream-json vocabulary (`--output-format streaming-json`, verified):
 *   {"type":"thought","data":"…"}                 → skipped (internal reasoning)
 *   {"type":"text","data":"…"}                     → message_delta (streamed)
 *   {"type":"end","stopReason","sessionId",…}      → message_complete + turn end
 * Tool calls happen (num_turns>1) but aren't surfaced as events in headless
 * mode, so there are no tool cards — the agent works, you get the streamed
 * answer.
 *
 * AUTH is the operator's Grok subscription: `~/.grok/{auth.json, config.toml}`
 * is docker-cp'd into the container at task-up (the Codex/Kimi config-dir
 * pattern; the macOS `bin/` is NOT copied — the image supplies the Linux
 * binary). auth.json is an OIDC token with a refresh_token, so the CLI renews
 * it in-container. Cloud never sees it (ADR-015). `--system-prompt-override`
 * carries the channel briefing (no fold-into-turn-1 needed).
 */
import { spawn, type ChildProcess } from "node:child_process";
import { existsSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";

import { register } from "./registry";
import type {
  AgentEvent,
  AgentEventHandler,
  AgentKind,
  AgentSession,
  RosterAgent,
} from "./types";

const GROK_BIN = "/home/node/.grok/bin/grok";

// `grok models` lists what the account can run. grok-4.5 is the current
// default; UPDATE WHEN xAI CHANGES the lineup.
const GROK_MODELS = ["grok-4.5"];
const GROK_DEFAULT_MODEL = "grok-4.5";
// Grok has no per-invocation reasoning-effort flag.
const GROK_EFFORTS: string[] = [];

function grokAuthPath(): string {
  return join(process.env.UAI_OWNER_HOME?.trim() || homedir(), ".grok", "auth.json");
}

// ---------------------------------------------------------------------------
// Pure protocol mapping — one streaming-json line → deltas / end.
// ---------------------------------------------------------------------------

export interface MappedGrokLine {
  /** A streamed text chunk (assistant answer), if this line carried one. */
  textDelta?: string;
  /** True on the turn-end event. */
  end?: boolean;
  /** The session id from the end event, for `-r` on the next turn. */
  sessionId?: string;
}

export function mapGrokLine(line: string): MappedGrokLine {
  const trimmed = line.trim();
  if (!trimmed.startsWith("{")) return {};
  let msg: Record<string, unknown>;
  try {
    msg = JSON.parse(trimmed) as Record<string, unknown>;
  } catch {
    return {};
  }
  if (msg.type === "text" && typeof msg.data === "string") {
    return { textDelta: msg.data };
  }
  if (msg.type === "end") {
    return {
      end: true,
      sessionId: typeof msg.sessionId === "string" ? msg.sessionId : undefined,
    };
  }
  // "thought" (reasoning) and any other types: no user-facing event.
  return {};
}

// ---------------------------------------------------------------------------
// Session
// ---------------------------------------------------------------------------

export type Spawner = (args: string[]) => ChildProcess;
const defaultSpawn: Spawner = (args) =>
  spawn("docker", args, { stdio: ["ignore", "pipe", "pipe"] });

export class GrokSession implements AgentSession {
  readonly agentId: string;
  readonly kind: AgentKind = "grok";

  private readonly containerName: string;
  private readonly model?: string;
  private readonly systemPreamble: string;
  private readonly agentEnv: Record<string, string>;
  private readonly spawner: Spawner;

  private readonly handlers = new Set<AgentEventHandler>();
  private sessionId: string | null = null;
  private current: ChildProcess | null = null;
  private queue: Promise<void> = Promise.resolve();
  private closed = false;

  constructor(args: {
    taskId: string;
    agent: RosterAgent;
    containerName: string;
    systemPreamble: string;
    agentEnv?: Record<string, string>;
    spawner?: Spawner;
  }) {
    this.agentId = args.agent.id;
    this.containerName = args.containerName;
    this.model = args.agent.model;
    this.systemPreamble = args.systemPreamble;
    this.agentEnv = args.agentEnv ?? {};
    this.spawner = args.spawner ?? defaultSpawn;
  }

  onEvent(handler: AgentEventHandler): () => void {
    this.handlers.add(handler);
    return () => this.handlers.delete(handler);
  }

  private emit(event: AgentEvent): void {
    if (this.closed && event.type !== "exit") return;
    for (const h of this.handlers) h(event);
  }

  async send(text: string): Promise<void> {
    if (this.closed) return;
    this.queue = this.queue.then(() => this.runTurn(text));
    return this.queue;
  }

  private buildArgs(prompt: string): string[] {
    const args = ["exec", "-i", "-u", "node"];
    for (const [k, v] of Object.entries(this.agentEnv)) {
      args.push("-e", `${k}=${v}`);
    }
    args.push(
      this.containerName,
      GROK_BIN,
      "-p",
      prompt,
      "--output-format",
      "streaming-json",
      // The container is the isolation boundary — auto-run tools.
      "--permission-mode",
      "bypassPermissions",
    );
    if (this.model) args.push("-m", this.model);
    if (this.systemPreamble.trim().length > 0) {
      args.push("--system-prompt-override", this.systemPreamble);
    }
    // Resume the same conversation across turns (captured from the last end).
    if (this.sessionId) args.push("-r", this.sessionId);
    return args;
  }

  private async runTurn(text: string): Promise<void> {
    if (this.closed) return;
    const child = this.spawner(this.buildArgs(text));
    this.current = child;

    let acc = "";
    let sawText = false;
    let buf = "";
    const consume = (chunk: string): void => {
      buf += chunk;
      let nl: number;
      while ((nl = buf.indexOf("\n")) >= 0) {
        const line = buf.slice(0, nl);
        buf = buf.slice(nl + 1);
        const m = mapGrokLine(line);
        if (m.sessionId) this.sessionId = m.sessionId;
        if (typeof m.textDelta === "string") {
          sawText = true;
          acc += m.textDelta;
          this.emit({ type: "message_delta", text: m.textDelta });
        }
      }
    };
    child.stdout?.on("data", (b: Buffer) => consume(b.toString("utf8")));
    let stderr = "";
    child.stderr?.on("data", (b: Buffer) => {
      stderr += b.toString("utf8");
    });

    await new Promise<void>((resolve) => {
      const finish = (code: number | null, spawnErr?: string): void => {
        this.current = null;
        if (buf.trim().length > 0) consume("\n"); // flush trailing line
        if (this.closed) return resolve();
        if (spawnErr) {
          this.emit({ type: "error", message: `grok spawn failed: ${spawnErr}` });
        } else if (code !== 0) {
          const tail = stderr.trim().slice(-500);
          this.emit({
            type: "error",
            message: `grok exited ${code ?? "null"}${tail ? `: ${tail}` : ""}`,
          });
        }
        // Finalize the streamed message (no-op text if the turn produced none).
        if (sawText) this.emit({ type: "message_complete", text: acc });
        this.emit({ type: "turn_complete" });
        resolve();
      };
      child.on("exit", (code) => finish(code));
      child.on("error", (err) => finish(null, err.message));
    });
  }

  async interrupt(): Promise<void> {
    this.current?.kill("SIGKILL");
  }

  // bypassPermissions auto-approves; no permission requests are emitted.
  async resolvePermission(): Promise<void> {
    /* no-op */
  }

  async close(): Promise<void> {
    if (this.closed) return;
    this.closed = true;
    this.current?.kill("SIGKILL");
    this.current = null;
    this.emit({ type: "exit", code: 0 });
    this.handlers.clear();
  }
}

register({
  kind: "grok",
  label: "Grok",
  supportedModels: () => [...GROK_MODELS],
  defaultModel: GROK_DEFAULT_MODEL,
  supportedEfforts: () => [...GROK_EFFORTS],
  available: () => existsSync(grokAuthPath()),
  create: async ({ taskId, agent, containerName, systemPreamble, agentEnv }) =>
    new GrokSession({ taskId, agent, containerName, systemPreamble, agentEnv }),
});
