import { spawn, execFileSync } from "node:child_process";
import { readFileSync, writeFileSync, readdirSync, mkdirSync, openSync } from "node:fs";
import { createServer } from "node:net";
import { join } from "node:path";
import { pathToFileURL } from "node:url";

// Run from a current-main checkout with scenario tooling installed.
// T3_SCENARIO_APP_DIR may point to the base or head checkout being proved.
const tooling = process.cwd();
const scratch = process.env.PROOF_SCRATCH;
if (!scratch) throw new Error("Set PROOF_SCRATCH to an empty scratch directory");
mkdirSync(scratch, { recursive: true });
const { makeState } = await import(pathToFileURL(join(tooling, "scripts/scenarios/t3.ts")).href);
const { APP_DIR, T3_HOME } = await import(pathToFileURL(join(tooling, "scripts/e2e.config.ts")).href);

const port = () => new Promise<number>((resolve) => {
  const server = createServer();
  server.listen(0, "127.0.0.1", () => {
    const value = (server.address() as { port: number }).port;
    server.close(() => resolve(value));
  });
});
const cli = (home: string, args: string[]) => execFileSync(process.execPath,
  ["apps/server/src/bin.ts", ...args, "--base-dir", home],
  { cwd: APP_DIR, env: { ...process.env, T3CODE_DEV_SEED: "0" }, encoding: "utf8" });
const settings = (home: string) => {
  const path = join(home, "userdata/settings.json");
  const data = JSON.parse(readFileSync(path, "utf8"));
  writeFileSync(path, JSON.stringify({ ...data, linkedEnvironmentsEnabled: true }));
};
const ready = async (url: string) => {
  for (let i = 0; i < 180; i++) {
    try { const res = await fetch(url); if (res.ok) return; } catch {}
    await new Promise((resolve) => setTimeout(resolve, 500));
  }
  throw new Error("Synthetic linked server did not become ready");
};

// Uses only the replay process belonging to our synthetic sender thread. Credentials stay in memory.
const senderCredential = (threadId: string) => {
  for (const pid of readdirSync("/proc").filter((s) => /^\d+$/.test(s))) {
    try {
      const env = Object.fromEntries(readFileSync(`/proc/${pid}/environ`, "utf8").split("\0")
        .filter(Boolean).map((entry) => { const i = entry.indexOf("="); return [entry.slice(0, i), entry.slice(i + 1)]; }));
      if (env.T3CODE_THREAD_ID === threadId && env.T3_ACP_MCP_AUTHORIZATION)
        return { endpoint: env.T3_ACP_MCP_ENDPOINT!, authorization: env.T3_ACP_MCP_AUTHORIZATION };
    } catch {}
  }
  throw new Error("No live replay credential for the synthetic sender");
};
const send = async (credential: ReturnType<typeof senderCredential>, args: Record<string, unknown>) => {
  let session = "";
  const rpc = async (body: object) => {
    const response = await fetch(credential.endpoint, { method: "POST", headers: {
      "Content-Type": "application/json", Accept: "application/json, text/event-stream",
      Authorization: credential.authorization, ...(session ? { "Mcp-Session-Id": session } : {}),
    }, body: JSON.stringify(body) });
    session = response.headers.get("mcp-session-id") ?? session;
    const raw = await response.text();
    if (!response.ok) throw new Error(`Synthetic MCP request failed (${response.status})`);
    const line = raw.split("\n").find((line) => line.startsWith("data: "));
    return raw ? JSON.parse(line ? line.slice(6) : raw) : undefined;
  };
  await rpc({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "scenario-replay-driver", version: "1" } } });
  await rpc({ jsonrpc: "2.0", method: "notifications/initialized" });
  const result = await rpc({ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "t3_thread_send", arguments: args } });
  if (result.error || result.result?.isError) throw new Error(`Linked send failed: ${JSON.stringify(result)}`);
};


const receiverPort = await port();
const receiverOrigin = `http://127.0.0.1:${receiverPort}`;
const receiverLog = openSync(join(scratch, "receiver-manual.log"), "a");
const receiverChild = spawn(process.execPath, [join(tooling, "scripts/scenarios/start-app.ts"), "web", T3_HOME, "127.0.0.1", String(receiverPort)], { cwd: APP_DIR, env: { ...process.env, T3CODE_DEV_SEED: "0" }, stdio: ["ignore", receiverLog, receiverLog] });
await ready(`${receiverOrigin}/.well-known/t3/environment`);
const t3 = makeState(T3_HOME).state;
const app = { baseUrl: receiverOrigin };
const runDir = join(scratch, `manual-${Date.now()}`);
    mkdirSync(runDir, { recursive: true });
    const senderHome = join(runDir, "sender");
    const senderPort = await port();
    const senderOrigin = `http://127.0.0.1:${senderPort}`;
    const senderChild = spawn(process.execPath, [join(tooling, "scripts/scenarios/start-app.ts"), "web", senderHome, "127.0.0.1", String(senderPort)], {
      cwd: APP_DIR, env: { ...process.env, T3CODE_DEV_SEED: "0" },
      stdio: ["ignore", openSync(join(runDir, "sender.log"), "w"), openSync(join(runDir, "sender-error.log"), "w")],
    });

      await ready(`${senderOrigin}/.well-known/t3/environment`);
      settings(T3_HOME); settings(senderHome);
      const a = makeState(senderHome).state;
      const senderProject = await a.project("Dispatch desk");
      await a.thread({ project: senderProject, title: "Release coordinator", messages: ["Prepare the release handoff"], model: "question" });
      const receiverProject = await t3.project("Release inbox");
      await t3.thread({ project: receiverProject, title: "Receiving thread", messages: ["Ready for the handoff"], model: "multi-turn" });
      const senderList = JSON.parse(cli(senderHome, ["thread", "list", "--project", senderProject, "--json"]));
      await t3.thread({ project: receiverProject, title: "Queued receiving thread", messages: ["Keep the release turn active"], model: "question" });
      const receiverList = JSON.parse(cli(T3_HOME, ["thread", "list", "--project", receiverProject, "--json"]));
      const senderThread = (Array.isArray(senderList) ? senderList : senderList.threads).find((t: any) => t.title === "Release coordinator");
      const receiverThread = (Array.isArray(receiverList) ? receiverList : receiverList.threads).find((t: any) => t.title === "Receiving thread");
      const queuedThread = (Array.isArray(receiverList) ? receiverList : receiverList.threads).find((t: any) => t.title === "Queued receiving thread");
      const paired = JSON.parse(cli(T3_HOME, ["auth", "pairing", "create", "--json"]));
      cli(senderHome, ["environment", "link", app.baseUrl, "--pairing-code", paired.credential, "--access", "full-access"]);
      const receiverDescriptor = await (await fetch(`${app.baseUrl}/.well-known/t3/environment`)).json();
      const credential = senderCredential(senderThread.threadId);
      await send(credential, { environmentId: receiverDescriptor.environmentId, threadId: receiverThread.threadId, message: "Release handoff: the build is ready for verification." });
      await send(credential, { environmentId: receiverDescriptor.environmentId, threadId: queuedThread.threadId, message: "Queued handoff: verify the release after this turn.", mode: "queue" });

const info = { receiverOrigin, senderOrigin, receiverThread, senderThread, queuedThread, receiverDescriptor, senderHome, receiverHome: T3_HOME, receiverPid: receiverChild.pid, senderPid: senderChild.pid };
writeFileSync(join(scratch, "manual-info.json"), JSON.stringify(info));
console.log("Synthetic linked environments ready");
for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => { receiverChild.kill("SIGTERM"); senderChild.kill("SIGTERM"); });
await Promise.all([new Promise(r=>receiverChild.on("exit",r)), new Promise(r=>senderChild.on("exit",r))]);
