/**
 * Embedded Pi agent host.
 *
 * Runs **inside** the app: this process imports the pi SDK (not the `pi` CLI),
 * builds an agent session for the open vault, and registers the TaskLens vault
 * tools straight into it (`customTools`). It is started by the Rust core
 * (`tasklens-core::agent`) as a child process and speaks the same newline-JSON
 * protocol the rest of the app already understands:
 *
 *   in : {"type":"start","config":{…}} | {"type":"prompt","message":…}
 *        | {"type":"steer","message":…} | {"type":"abort"} | {"type":"stop"}
 *   out: {"event":"status","state":"ready",…}
 *        {"event":"agent","data":<pi session event>}
 *        {"event":"error","message":…}
 *
 * Session events are forwarded verbatim, so the renderer keeps working with
 * whatever events the installed pi version emits.
 */

import {
  createAgentSession,
  DefaultResourceLoader,
  getAgentDir,
  ModelRuntime,
  SessionManager,
} from "@earendil-works/pi-coding-agent";
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";

import { VAULT_TOOLS } from "../pi-extension/tasklens.ts";

interface HostConfig {
  vault: string;
  provider: string;
  model: string;
  thinking: string;
  apiKey: string;
  baseUrl: string;
  contextFiles: boolean;
  sessionDir: string;
  /** Extra extension files to load alongside the built-in vault tools. */
  extensions: string[];
}

interface AgentSessionLike {
  subscribe(listener: (event: unknown) => void): () => void;
  prompt(text: string, options?: Record<string, unknown>): Promise<void>;
  steer?(text: string): Promise<void>;
  abort(): Promise<void>;
  dispose(): void;
  model?: { provider?: string; id?: string; name?: string };
}

const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];

function emit(value: unknown): void {
  process.stdout.write(`${JSON.stringify(value)}\n`);
}

function emitAgent(event: unknown): void {
  emit(event);
}

/** Diagnostics reach the UI as notices (they are not agent events). */
function note(text: string): void {
  emit({ type: "tasklens_log", stream: "host", text });
}

function fail(message: string): void {
  emit({ type: "error", message });
}

function emptyConfig(): HostConfig {
  return {
    vault: process.cwd(),
    provider: "",
    model: "",
    thinking: "",
    apiKey: "",
    baseUrl: "",
    contextFiles: true,
    sessionDir: "",
    extensions: [],
  };
}

function readConfig(raw: unknown): HostConfig {
  const config = { ...emptyConfig(), ...(raw as Partial<HostConfig> | undefined) };
  if (!config.vault.trim()) config.vault = process.cwd();
  return config;
}

let session: AgentSessionLike | null = null;
let unsubscribe: (() => void) | null = null;
/** Resolves when the current `start` has finished building the session. */
let pendingStart: Promise<void> | null = null;

async function start(raw: unknown): Promise<void> {
  stop();
  const config = readConfig(raw);

  // A custom endpoint (self-hosted or proxied OpenAI-compatible host) is picked
  // up by pi-ai from the environment.
  if (config.baseUrl.trim()) {
    process.env.OPENAI_BASE_URL = config.baseUrl.trim();
  }

  let runtime: ModelRuntime;
  try {
    runtime = await ModelRuntime.create();
  } catch (error) {
    fail(`无法初始化模型运行时:${error instanceof Error ? error.message : String(error)}`);
    return;
  }

  if (config.provider.trim() && config.apiKey.trim()) {
    try {
      await runtime.setRuntimeApiKey(config.provider.trim(), config.apiKey.trim());
    } catch (error) {
      fail(`API Key 设置失败(${config.provider}):${error instanceof Error ? error.message : String(error)}`);
    }
  }

  let model;
  if (config.provider.trim() && config.model.trim()) {
    model = runtime.getModel(config.provider.trim(), config.model.trim()) ?? undefined;
    if (!model) {
      const available = await runtime.getAvailable().catch(() => []);
      const names = available
        .map((entry) => `${entry.provider}/${entry.id}`)
        .slice(0, 20)
        .join("、");
      fail(`找不到模型 ${config.provider}/${config.model}。可用:${names || "(无,先运行 pi --list-models)"}`);
      return;
    }
  }

  const loader = new DefaultResourceLoader({
    cwd: config.vault,
    agentDir: getAgentDir(),
    // The vault tools arrive as `customTools` below, so the installed
    // `.pi/extensions/tasklens.ts` must not be discovered as well — registering
    // the same tool names twice would break the session. Explicit extra
    // extensions keep working.
    noExtensions: true,
    additionalExtensionPaths: config.extensions.length > 0 ? config.extensions : undefined,
    noContextFiles: !config.contextFiles,
  });

  const requestedThinking = config.thinking.trim();
  // Checked against the allowlist, so the narrowing is safe.
  const thinking: ThinkingLevel | undefined = THINKING_LEVELS.includes(requestedThinking)
    ? (requestedThinking as ThinkingLevel)
    : undefined;

  try {
    const created = await createAgentSession({
      cwd: config.vault,
      model,
      thinkingLevel: thinking,
      modelRuntime: runtime,
      resourceLoader: loader,
      customTools: VAULT_TOOLS,
      sessionManager: config.sessionDir.trim()
        ? SessionManager.create(config.vault, config.sessionDir.trim())
        : SessionManager.create(config.vault),
    });

    session = created.session as unknown as AgentSessionLike;
    unsubscribe = session.subscribe((event) => emitAgent(event));
    if (created.modelFallbackMessage) fail(created.modelFallbackMessage);

    note(
      `内置 Pi 会话就绪(${VAULT_TOOLS.length} 个知识库工具)· ${session.model?.provider ?? "?"}/${session.model?.id ?? "?"}`,
    );
  } catch (error) {
    fail(`无法启动 Pi 会话:${error instanceof Error ? error.message : String(error)}`);
  }
}

function stop(): void {
  try {
    unsubscribe?.();
  } catch {
    // Ignore: the session may already be gone.
  }
  unsubscribe = null;
  try {
    session?.dispose();
  } catch {
    // Ignore disposal errors on a dead session.
  }
  session = null;
}

async function handle(line: string): Promise<void> {
  let request: Record<string, unknown>;
  try {
    request = JSON.parse(line) as Record<string, unknown>;
  } catch (error) {
    fail(`不是合法 JSON:${error instanceof Error ? error.message : String(error)}`);
    return;
  }

  const type = typeof request.type === "string" ? request.type : "";
  if (type === "start") {
    pendingStart = start(request.config);
    await pendingStart;
    return;
  }
  if (type === "stop") {
    await pendingStart?.catch(() => undefined);
    stop();
    note("内置 Pi 会话已停止");
    return;
  }
  // A prompt can arrive while the session is still being built; the app sends
  // `start` and the first message back to back.
  if (pendingStart) await pendingStart.catch(() => undefined);
  if (!session) {
    fail("agent 尚未启动:先发送 {\"type\":\"start\",\"config\":{…}}");
    return;
  }

  try {
    if (type === "prompt") {
      const message = String(request.message ?? "");
      if (!message.trim()) return;
      await session.prompt(message, { streamingBehavior: "steer" });
      return;
    }
    if (type === "steer" && session.steer) {
      await session.steer(String(request.message ?? ""));
      return;
    }
    if (type === "abort") {
      await session.abort();
      return;
    }
    fail(`未知指令:${type}`);
  } catch (error) {
    fail(`执行失败:${error instanceof Error ? error.message : String(error)}`);
  }
}

process.stdin.setEncoding("utf8");
let buffer = "";
process.stdin.on("data", (chunk: string) => {
  buffer += chunk;
  let index = buffer.indexOf("\n");
  while (index >= 0) {
    const line = buffer.slice(0, index).trim();
    buffer = buffer.slice(index + 1);
    index = buffer.indexOf("\n");
    if (line) void handle(line);
  }
});

process.stdin.on("end", () => {
  stop();
  process.exit(0);
});

process.on("uncaughtException", (error) => {
  fail(`宿主异常:${error.message}`);
});

process.on("unhandledRejection", (reason) => {
  fail(`宿主未处理的拒绝:${reason instanceof Error ? reason.message : String(reason)}`);
});