// cli/commands/codevelop.js
// Windows Terminal installer + launcher for Claude/Codex co-development.

import fs from "fs";
import os from "os";
import http from "http";
import https from "https";
import path from "path";
import { spawn, spawnSync } from "child_process";
import { fileURLToPath } from "url";
import chalk from "chalk";

const DEFAULT_REPO = "C:\\Dev\\regen-root";
const DEFAULT_PORT = 53137;
const RESERVED_PORTS = new Set([52437, 52438]);
const CLAUDE_PROFILE = "Co Develop Claude";
const CODEX_PROFILE = "Co Develop Codex";
const ASSET_DIR = path.resolve(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "assets", "codevelop"));

function asBool(value) {
  return value === true || value === "true";
}

function getStateDir(repo) {
  return path.join(repo, ".rdc", "co-develop");
}

function getClauthAppDir() {
  return path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "clauth");
}

function getLauncherPath(peer) {
  return path.join(getClauthAppDir(), `codevelop-${peer}.cmd`);
}

function getActiveLauncherPath(peer) {
  return path.join(getClauthAppDir(), `codevelop-${peer}-active.cmd`);
}

function getContextPath(peer) {
  return path.join(getClauthAppDir(), `codevelop-${peer}-context.txt`);
}

function getRequestPath() {
  return path.join(getClauthAppDir(), "codevelop-request.json");
}

function getAdhocStatePath() {
  return path.join(getClauthAppDir(), "codevelop-channel-active.json");
}

function getAdhocChannelsPath() {
  return path.join(getClauthAppDir(), "codevelop-channels.json");
}

function cmdValue(value) {
  return String(value ?? "").replace(/%/g, "%%").replace(/"/g, "'");
}

function renderTemplate(template, values) {
  return template.replace(/\$\{([A-Z_]+)\}/g, (_, key) => values[key] ?? "");
}

function renderValue(value, values) {
  if (typeof value === "string") return renderTemplate(value, values);
  if (Array.isArray(value)) return value.map(item => renderValue(item, values));
  if (value && typeof value === "object") {
    return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, renderValue(child, values)]));
  }
  return value;
}

function readCodevelopConfig() {
  return readJsonFile(path.join(ASSET_DIR, "windows-terminal.profiles.json"));
}

function writeLauncherScripts(repo) {
  const appDir = getClauthAppDir();
  fs.mkdirSync(appDir, { recursive: true });

  const template = fs.readFileSync(path.join(ASSET_DIR, "launcher-static.cmd.template"), "utf8");
  const launchers = {
    claude: getLauncherPath("claude"),
    codex: getLauncherPath("codex"),
  };

  for (const [peer, launcherPath] of Object.entries(launchers)) {
    fs.writeFileSync(launcherPath, renderTemplate(template, {
      ACTIVE_LAUNCHER: getActiveLauncherPath(peer),
      REPO: repo,
      PORT: String(DEFAULT_PORT),
    }).replace(/\n/g, "\r\n"), "utf8");
  }

  return launchers;
}

function writeActiveLauncherScripts(manifest) {
  const appDir = getClauthAppDir();
  fs.mkdirSync(appDir, { recursive: true });

  const codexCmd = path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "npm", "codex.cmd");
  const claudeCmd = "claude";
  const cliPath = path.resolve(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "index.js"));
  const template = fs.readFileSync(path.join(ASSET_DIR, "launcher-active.cmd.template"), "utf8");

  for (const peer of ["claude", "codex"]) {
    const peerConfig = manifest.peers[peer];
    const prompt = [
      `You are peer=${peer} in co-development session ${manifest.session_id}.`,
      `Your default partner is ${peerConfig.target_peer}.`,
      `Use clauth codevelop tools at ${manifest.transport.base_url} to send and receive partner messages.`,
      `When you need partner help, run: clauth codevelop ask --to ${peerConfig.target_peer} --wait --task "<specific task>".`,
      "When you complete a partner request, run: clauth codevelop reply --turn <turn_id> --summary \"...\" --evidence \"...\" --files-changed \"...\".",
      "Do not hand-write curl JSON unless the CLI command fails.",
      "On startup, acknowledge co-development readiness, poll your inbox, and report any queued partner messages.",
      "Reply with evidence, files changed, commits, blockers, and next action.",
      `Manifest: ${path.join(getStateDir(manifest.repo), `${manifest.session_id}.json`)}`,
    ].join(" ");
    const contextPath = getContextPath(peer);
    fs.writeFileSync(contextPath, `${prompt}\n`, "utf8");

    const command = peer === "claude"
      ? `${claudeCmd} -n "Co Develop Claude" --append-system-prompt "${cmdValue(prompt)}" %*`
      : `call "${codexCmd}" -C "${cmdValue(peerConfig.cwd)}" %*`;

    fs.writeFileSync(getActiveLauncherPath(peer), renderTemplate(template, {
      SESSION_ID: cmdValue(manifest.session_id),
      PEER: peer,
      TARGET_PEER: cmdValue(peerConfig.target_peer),
      BASE_URL: cmdValue(manifest.transport.base_url),
      MANIFEST: cmdValue(path.join(getStateDir(manifest.repo), `${manifest.session_id}.json`)),
      CONTEXT_FILE: cmdValue(contextPath),
      CLAUTH_CLI: cmdValue(cliPath),
      REPO: cmdValue(manifest.repo),
      CWD: cmdValue(peerConfig.cwd),
      PRINT_COMMAND: peer === "claude" ? "claude -n \"Co Develop Claude\" --append-system-prompt [co-develop system prompt]" : `codex -C "${cmdValue(peerConfig.cwd)}"`,
      COMMAND: command,
    }).replace(/\n/g, "\r\n"), "utf8");
  }
}

function getWindowsTerminalSettingsPath() {
  const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
  const candidates = [
    path.join(localAppData, "Packages", "Microsoft.WindowsTerminal_8wekyb3d8bbwe", "LocalState", "settings.json"),
    path.join(localAppData, "Packages", "Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe", "LocalState", "settings.json"),
    path.join(localAppData, "Microsoft", "Windows Terminal", "settings.json"),
  ];
  return candidates.find(p => fs.existsSync(p)) || candidates[0];
}

function readJsonFile(filePath) {
  return JSON.parse(fs.readFileSync(filePath, "utf8").replace(/^\uFEFF/, ""));
}

function upsertProfile(list, profile) {
  const existing = list.find(p => p.name === profile.name || p.guid === profile.guid);
  if (existing) {
    Object.assign(existing, profile);
  } else {
    list.push(profile);
  }
}

function installWindowsTerminalProfiles({ repo = DEFAULT_REPO, dryRun = false } = {}) {
  if (process.platform !== "win32") {
    throw new Error("Windows Terminal profile install is only supported on Windows.");
  }

  const settingsPath = getWindowsTerminalSettingsPath();
  if (!fs.existsSync(settingsPath)) {
    throw new Error(`Windows Terminal settings not found at ${settingsPath}`);
  }

  const settings = readJsonFile(settingsPath);
  settings.profiles ||= {};
  settings.profiles.list ||= [];
  const launchers = dryRun
    ? { claude: getLauncherPath("claude"), codex: getLauncherPath("codex") }
    : writeLauncherScripts(repo);
  const config = readCodevelopConfig();
  const profiles = config.profiles.map(profile => renderValue(profile, {
    REPO: repo,
    CLAUTH_APPDATA: getClauthAppDir(),
  }));

  for (const profile of profiles) upsertProfile(settings.profiles.list, profile);

  const backupPath = `${settingsPath}.bak-codevelop-${new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "").replace("T", "-")}`;
  if (!dryRun) {
    fs.copyFileSync(settingsPath, backupPath);
    fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
  }

  return { settingsPath, backupPath: dryRun ? null : backupPath, profiles, launchers };
}

async function requestJson(method, url, body) {
  const opts = { method, headers: {} };
  if (body !== undefined) {
    opts.headers["content-type"] = "application/json";
    opts.body = JSON.stringify(body);
  }
  const response = await fetch(url, opts);
  const text = await response.text();
  let data = {};
  try { data = text ? JSON.parse(text) : {}; } catch { data = { raw: text }; }
  if (!response.ok) throw new Error(`${method} ${url} failed: HTTP ${response.status} ${text}`);
  return data;
}

async function testPing(baseUrl) {
  try {
    const result = await requestJson("GET", `${baseUrl}/ping`);
    return result?.status === "ok";
  } catch {
    return false;
  }
}

function startIsolatedClauth(port) {
  const cliPath = path.resolve(path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "index.js"));
  const child = spawn(process.execPath, [cliPath, "serve", "foreground", "--port", String(port), "--isolated"], {
    detached: true,
    stdio: "ignore",
    windowsHide: true,
  });
  child.unref();
  return child.pid;
}

async function waitForPing(baseUrl) {
  for (let i = 0; i < 30; i += 1) {
    if (await testPing(baseUrl)) return true;
    await new Promise(resolve => setTimeout(resolve, 250));
  }
  return false;
}

function ensureWorktree(repo, sessionId, dryRun) {
  const codexRepo = `C:\\Dev\\regen-root-codex-${sessionId}`;
  const branch = `codex/${sessionId}`;
  if (!dryRun && !fs.existsSync(codexRepo)) {
    const result = spawnSync("git", ["-C", repo, "worktree", "add", "-b", branch, codexRepo, "develop"], {
      stdio: "inherit",
      shell: false,
    });
    if (result.status !== 0) throw new Error(`git worktree add failed with exit code ${result.status}`);
  }
  return { codexRepo, branch };
}

function writeManifest({ repo, sessionId, name, baseUrl, status, codexRepo, branch, startedClauthPid, dryRun }) {
  const stateDir = getStateDir(repo);
  fs.mkdirSync(stateDir, { recursive: true });
  const manifestPath = path.join(stateDir, `${sessionId}.json`);
  const manifest = {
    session_id: sessionId,
    name,
    repo,
    mode: "start-pair",
    transport: { type: "clauth-codevelop", base_url: baseUrl },
    peers: {
      claude: {
        role: "supervisor",
        cwd: repo,
        branch: "develop",
        target_peer: "codex",
        stream_url: `${baseUrl}/codevelop/${sessionId}/claude/stream`,
      },
      codex: {
        role: "implementation_partner",
        cwd: codexRepo,
        branch,
        target_peer: "claude",
        stream_url: `${baseUrl}/codevelop/${sessionId}/codex/stream`,
      },
    },
    clauth_status: status,
    clauth_process: startedClauthPid
      ? { pid: startedClauthPid, started_by_launcher: true, isolated: true }
      : { started_by_launcher: false, isolated: true },
    commands: {
      claude: `Windows Terminal profile: ${CLAUDE_PROFILE}`,
      codex: `Windows Terminal profile: ${CODEX_PROFILE}`,
      wt: buildWtArgs().display,
    },
    manual_prompt_required: false,
    context_contract: {
      context_file: null,
      required_reads: [],
      instruction: "The initiating peer should provide a context file or plan/architecture path with each substantive ask.",
    },
    created_at: new Date().toISOString(),
  };

  fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
  if (!dryRun) {
    fs.writeFileSync(path.join(stateDir, "active.json"), `${JSON.stringify({
      session_id: sessionId,
      manifest: manifestPath,
      updated_at: new Date().toISOString(),
    }, null, 2)}\n`, "utf8");
    writeActiveLauncherScripts(manifest);
  }
  return manifestPath;
}

function buildWtArgs() {
  const args = readCodevelopConfig().layout.wtArgs;
  return { args, display: `wt.exe ${args.map(a => a.includes(" ") ? `"${a}"` : a).join(" ")}` };
}

async function startCodevelop(opts = {}) {
  const repo = opts.repo || DEFAULT_REPO;
  const port = Number(opts.port || DEFAULT_PORT);
  if (RESERVED_PORTS.has(port)) throw new Error(`Refusing to use reserved clauth port ${port}`);
  const baseUrl = opts.baseUrl || `http://127.0.0.1:${port}`;
  const dryRun = asBool(opts.dryRun);
  const noOpen = asBool(opts.noOpen);
  const name = opts.name || `codev-${path.basename(repo)}-${new Date().toISOString().replace(/[-:]/g, "").slice(0, 15)}`;
  const contextFile = opts.context || opts.contextFile || null;

  let startedClauthPid = null;
  if (!(await testPing(baseUrl))) {
    if (!asBool(opts.startIsolatedClauth)) {
      throw new Error(`clauth is not reachable at ${baseUrl}. Re-run with --start-isolated-clauth.`);
    }
    startedClauthPid = startIsolatedClauth(port);
    if (!(await waitForPing(baseUrl))) throw new Error(`isolated clauth did not become ready at ${baseUrl}`);
  }

  const started = await requestJson("POST", `${baseUrl}/codevelop/start`, { name, repo });
  const sessionId = started.session_id;
  await requestJson("POST", `${baseUrl}/codevelop/join`, { session_id: sessionId, peer_id: "claude", role: "supervisor", target_peer: "codex" });
  await requestJson("POST", `${baseUrl}/codevelop/join`, { session_id: sessionId, peer_id: "codex", role: "implementation_partner", target_peer: "claude" });
  const status = await requestJson("GET", `${baseUrl}/codevelop/${sessionId}/status`);
  await sendStartupHandshake(baseUrl, sessionId);

  const { codexRepo, branch } = ensureWorktree(repo, sessionId, dryRun);
  const manifestPath = writeManifest({ repo, sessionId, name, baseUrl, status, codexRepo, branch, startedClauthPid, dryRun });
  if (contextFile) {
    const manifest = readJsonFile(manifestPath);
    manifest.context_contract.context_file = contextFile;
    manifest.context_contract.required_reads = [contextFile];
    fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
    if (!dryRun) writeActiveLauncherScripts(manifest);
  }
  const wt = buildWtArgs();

  console.log(`CODEVELOP_SESSION=${sessionId}`);
  console.log(`MANIFEST=${manifestPath}`);
  if (!dryRun) console.log(`ACTIVE=${path.join(getStateDir(repo), "active.json")}`);
  console.log(`CLAUDE_STREAM=${baseUrl}/codevelop/${sessionId}/claude/stream`);
  console.log(`CODEX_STREAM=${baseUrl}/codevelop/${sessionId}/codex/stream`);
  if (startedClauthPid) console.log(`CLAUTH_PID=${startedClauthPid}`);
  console.log("");
  console.log("Windows Terminal command:");
  console.log(wt.display);

  if (!dryRun && !noOpen) {
    spawn("wt.exe", wt.args, { detached: true, stdio: "ignore", windowsHide: false }).unref();
  }
}

async function sendStartupHandshake(baseUrl, sessionId) {
  const messages = [
    {
      session_id: sessionId,
      from: "system",
      to: "claude",
      type: "ready",
      task: "Co-develop transport is ready. Partner codex is registered. Standard operating procedure: check partner status before assigning work.",
      context: { startup: true, partner: "codex" },
      expect: { action: "acknowledge when you begin collaboration" },
    },
    {
      session_id: sessionId,
      from: "system",
      to: "codex",
      type: "ready",
      task: "Co-develop transport is ready. Partner claude is registered. Standard operating procedure: check partner status before accepting work.",
      context: { startup: true, partner: "claude" },
      expect: { action: "remain idle until addressed" },
    },
  ];

  for (const message of messages) {
    await requestJson("POST", `${baseUrl}/codevelop/send`, message);
  }
}

function getActiveManifest(repo, session = null) {
  if (session) {
    const directPath = path.resolve(String(session));
    const manifestPath = fs.existsSync(directPath)
      ? directPath
      : path.join(getStateDir(repo), `${session}.json`);
    if (!fs.existsSync(manifestPath)) throw new Error(`Co-develop manifest not found for --session ${session}: ${manifestPath}`);
    return readJsonFile(manifestPath);
  }
  if (process.env.CODEVELOP_MANIFEST && fs.existsSync(process.env.CODEVELOP_MANIFEST)) {
    return readJsonFile(process.env.CODEVELOP_MANIFEST);
  }
  const activePath = path.join(getStateDir(repo), "active.json");
  if (!fs.existsSync(activePath)) throw new Error(`No active co-develop session found at ${activePath}. Run clauth codevelop start first.`);
  const active = readJsonFile(activePath);
  if (!active.manifest || !fs.existsSync(active.manifest)) throw new Error(`Active co-develop manifest is missing: ${active.manifest}`);
  return readJsonFile(active.manifest);
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function parseList(value) {
  if (value === undefined || value === null || value === "") return [];
  if (Array.isArray(value)) return value.flatMap(item => parseList(item));
  const text = String(value).trim();
  if (!text) return [];
  if (text.startsWith("[")) {
    try {
      const parsed = JSON.parse(text);
      if (Array.isArray(parsed)) return parsed.map(item => String(item));
    } catch {}
  }
  return text
    .split(/;|\|\|/)
    .map(item => item.trim())
    .filter(Boolean);
}

function normalizeOptionalPeer(peer) {
  const value = String(peer || "").toLowerCase().trim();
  if (!value) return null;
  if (!["claude", "codex"].includes(value)) throw new Error("peer must be claude or codex");
  return value;
}

function normalizeAdhocId(value, label) {
  const text = String(value || "").trim().toLowerCase();
  if (!text) throw new Error(`${label} is required`);
  const normalized = text.replace(/[^a-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "");
  if (!normalized) throw new Error(`${label} must contain letters, numbers, dots, underscores, or hyphens`);
  if (normalized.length > 64) throw new Error(`${label} is too long; keep it under 64 characters`);
  return normalized;
}

function normalizeOptionalAdhocId(value) {
  const text = String(value || "").trim();
  return text ? normalizeAdhocId(text, "peer") : null;
}

function getCodevelopBaseUrl(opts = {}) {
  const port = Number(opts.port || DEFAULT_PORT);
  if (RESERVED_PORTS.has(port)) throw new Error(`Refusing to use reserved clauth port ${port}`);
  return opts.baseUrl || process.env.CODEVELOP_BASE_URL || `http://127.0.0.1:${port}`;
}

async function ensureCodevelopReachable(opts = {}) {
  const baseUrl = getCodevelopBaseUrl(opts);
  if (await testPing(baseUrl)) return { baseUrl, startedClauthPid: null };
  if (!asBool(opts.startIsolatedClauth)) {
    throw new Error(`clauth is not reachable at ${baseUrl}. Re-run with --start-isolated-clauth.`);
  }
  const port = Number(opts.port || DEFAULT_PORT);
  const startedClauthPid = startIsolatedClauth(port);
  if (!(await waitForPing(baseUrl))) throw new Error(`isolated clauth did not become ready at ${baseUrl}`);
  return { baseUrl, startedClauthPid };
}

function writeAdhocState(state) {
  const appDir = getClauthAppDir();
  fs.mkdirSync(appDir, { recursive: true });
  fs.writeFileSync(getAdhocStatePath(), `${JSON.stringify({
    ...state,
    updated_at: new Date().toISOString(),
  }, null, 2)}\n`, "utf8");
}

function readAdhocState() {
  const statePath = getAdhocStatePath();
  if (!fs.existsSync(statePath)) return null;
  return readJsonFile(statePath);
}

function readAdhocChannels() {
  const channelsPath = getAdhocChannelsPath();
  if (!fs.existsSync(channelsPath)) return {};
  const parsed = readJsonFile(channelsPath);
  return parsed && typeof parsed === "object" ? parsed : {};
}

function writeAdhocChannels(channels) {
  const appDir = getClauthAppDir();
  fs.mkdirSync(appDir, { recursive: true });
  fs.writeFileSync(getAdhocChannelsPath(), `${JSON.stringify(channels, null, 2)}\n`, "utf8");
}

async function getCodevelopStatus(baseUrl, sessionId) {
  return requestJson("GET", `${baseUrl}/codevelop/${encodeURIComponent(sessionId)}/status`);
}

async function findOrCreateAdhocChannel(opts = {}) {
  const channel = normalizeAdhocId(opts.channel || opts.name || process.env.CODEVELOP_CHANNEL || "1", "channel");
  const sessionName = `channel-${channel}`;
  const { baseUrl, startedClauthPid } = await ensureCodevelopReachable(opts);
  const channels = readAdhocChannels();
  const existing = channels[channel];
  if (existing?.session_id && existing?.base_url === baseUrl) {
    try {
      const session = await getCodevelopStatus(baseUrl, existing.session_id);
      if (session.status !== "stopped") return { baseUrl, channel, session, created: false, startedClauthPid };
    } catch {}
  }

  const session = await requestJson("POST", `${baseUrl}/codevelop/start`, {
    name: sessionName,
    repo: opts.repo || DEFAULT_REPO,
    metadata: {
      mode: "adhoc-channel",
      channel,
      created_by: opts.peer || process.env.CODEVELOP_PEER || null,
    },
  });
  channels[channel] = {
    channel,
    session_id: session.session_id,
    base_url: baseUrl,
    name: sessionName,
    created_at: new Date().toISOString(),
    updated_at: new Date().toISOString(),
  };
  writeAdhocChannels(channels);
  return { baseUrl, channel, session, created: true, startedClauthPid };
}

async function loadAdhocContext(opts = {}) {
  if (opts.session && opts.peer) {
    return {
      baseUrl: getCodevelopBaseUrl(opts),
      channel: normalizeAdhocId(opts.channel || opts.name || "custom", "channel"),
      sessionId: opts.session,
      peer: normalizeAdhocId(opts.peer, "peer"),
    };
  }

  const state = readAdhocState();
  const channel = opts.channel || state?.channel || process.env.CODEVELOP_CHANNEL;
  const peer = opts.peer || opts.from || state?.peer || process.env.CODEVELOP_PEER;
  if (!channel || !peer) {
    throw new Error("No active ad hoc channel. Run: clauth codevelop join --channel 1 --peer codex-1");
  }

  if (opts.channel || opts.peer || !state?.session_id) {
    const joined = await joinAdhocChannel({ ...opts, channel, peer, silent: true });
    return {
      baseUrl: joined.baseUrl,
      channel: joined.channel,
      sessionId: joined.session_id,
      peer: joined.peer,
    };
  }

  return {
    baseUrl: opts.baseUrl || state.base_url || getCodevelopBaseUrl(opts),
    channel: normalizeAdhocId(channel, "channel"),
    sessionId: state.session_id,
    peer: normalizeAdhocId(peer, "peer"),
  };
}

function formatAdhocMessage(message) {
  const body = message.task || message.message || message.summary || "";
  return `[${message.turn_id}] ${message.from} -> ${message.to} ${message.type || "message"}: ${body}`;
}

async function joinAdhocChannel(opts = {}) {
  const peer = normalizeAdhocId(opts.peer || opts.from || process.env.CODEVELOP_PEER, "peer");
  const { baseUrl, channel, session, created, startedClauthPid } = await findOrCreateAdhocChannel(opts);
  const sessionId = session.session_id;
  const joined = await requestJson("POST", `${baseUrl}/codevelop/join`, {
    session_id: sessionId,
    peer_id: peer,
    role: opts.role || "participant",
    metadata: {
      mode: "adhoc-channel",
      channel,
      cwd: process.cwd(),
    },
  });

  writeAdhocState({
    channel,
    peer,
    session_id: sessionId,
    base_url: baseUrl,
    state_path: getAdhocStatePath(),
  });

  if (!opts.silent) {
    console.log(`CHANNEL=${channel}`);
    console.log(`PEER=${peer}`);
    console.log(`CODEVELOP_SESSION=${sessionId}`);
    console.log(`BASE_URL=${baseUrl}`);
    console.log(`CREATED=${created ? "yes" : "no"}`);
    if (startedClauthPid) console.log(`CLAUTH_PID=${startedClauthPid}`);
    console.log(`STATE=${getAdhocStatePath()}`);
    console.log("");
    console.log(`Send:  clauth codevelop say --message "hello"`);
    console.log(`Read:  clauth codevelop read`);
    console.log(`Watch: clauth codevelop watch`);
  }

  return { ...joined, baseUrl, channel, peer, session_id: sessionId };
}

async function listAdhocPeers(opts = {}) {
  const ctx = await loadAdhocContext(opts);
  const status = await requestJson("GET", `${ctx.baseUrl}/codevelop/${encodeURIComponent(ctx.sessionId)}/status`);
  console.log(`channel ${ctx.channel} (${ctx.sessionId})`);
  for (const peer of Object.values(status.peers || {})) {
    console.log(`${peer.peer_id}\trole=${peer.role || "participant"}\tqueued=${peer.queued}\tstreams=${peer.streams}\tlast_seen=${peer.last_seen_at || ""}`);
  }
}

async function sayAdhoc(opts = {}) {
  const ctx = await loadAdhocContext(opts);
  const message = opts.message || opts.task || opts.args?.join(" ") || "";
  if (!String(message).trim()) throw new Error("--message is required for codevelop say");

  const status = await requestJson("GET", `${ctx.baseUrl}/codevelop/${encodeURIComponent(ctx.sessionId)}/status`);
  const explicitTo = normalizeOptionalAdhocId(opts.to);
  const targets = explicitTo
    ? [explicitTo]
    : Object.keys(status.peers || {}).filter(peer => peer !== ctx.peer && peer !== "system");
  if (!targets.length) {
    throw new Error(`No other peers are in channel ${ctx.channel}. Have the other pane run: clauth codevelop join --channel ${ctx.channel} --peer claude-1`);
  }

  const sent = [];
  for (const target of targets) {
    const result = await requestJson("POST", `${ctx.baseUrl}/codevelop/send`, {
      session_id: ctx.sessionId,
      from: ctx.peer,
      to: target,
      type: "chat",
      task: message,
      message,
      context: {
        mode: "adhoc-channel",
        channel: ctx.channel,
      },
    });
    sent.push(result);
  }

  console.log(JSON.stringify({
    channel: ctx.channel,
    session_id: ctx.sessionId,
    from: ctx.peer,
    to: targets,
    sent,
  }, null, 2));
}

async function readAdhoc(opts = {}) {
  const ctx = await loadAdhocContext(opts);
  const inbox = await requestJson("POST", `${ctx.baseUrl}/codevelop/poll`, {
    session_id: ctx.sessionId,
    peer_id: ctx.peer,
  });
  if (asBool(opts.json)) {
    console.log(JSON.stringify(inbox, null, 2));
    return;
  }
  if (!inbox.count) {
    console.log(`channel ${ctx.channel}: no messages for ${ctx.peer}`);
    return;
  }
  for (const message of inbox.messages || []) console.log(formatAdhocMessage(message));
}

async function watchAdhoc(opts = {}) {
  const ctx = await loadAdhocContext(opts);
  const streamUrl = `${ctx.baseUrl}/codevelop/${encodeURIComponent(ctx.sessionId)}/${encodeURIComponent(ctx.peer)}/stream`;
  const once = asBool(opts.once);
  console.log(`Watching channel ${ctx.channel} as ${ctx.peer}`);
  console.log(`stream ${streamUrl}`);

  connectSse(streamUrl, {
    onEvent: ({ event, data }) => {
      if (event !== "message" && event !== "stop") return;
      console.log(formatAdhocMessage(data || {}));
      if (once) process.exit(0);
    },
    onError: err => {
      console.error(`watch error: ${err.message}`);
      process.exitCode = 1;
    },
  });
  await new Promise(() => {});
}

function inferFromPeer({ from, to, peer } = {}) {
  const explicit = normalizeOptionalPeer(from) || normalizeOptionalPeer(peer) || normalizeOptionalPeer(process.env.CODEVELOP_PEER);
  if (explicit) return explicit;
  const target = normalizeOptionalPeer(to);
  if (target === "claude") return "codex";
  if (target === "codex") return "claude";
  throw new Error("Unable to infer sender peer. Pass --from claude|codex or set CODEVELOP_PEER.");
}

function inferTargetPeer({ to, from } = {}) {
  const explicit = normalizeOptionalPeer(to);
  if (explicit) return explicit;
  const sender = normalizeOptionalPeer(from);
  if (sender === "claude") return "codex";
  if (sender === "codex") return "claude";
  throw new Error("Unable to infer target peer. Pass --to claude|codex.");
}

async function pollPeer(manifest, peer) {
  return requestJson("POST", `${manifest.transport.base_url}/codevelop/poll`, {
    session_id: manifest.session_id,
    peer_id: peer,
  });
}

async function waitForReply(manifest, peer, turnId, { timeoutMs = 300_000, intervalMs = 2_000 } = {}) {
  const started = Date.now();
  const seen = [];
  while (Date.now() - started < timeoutMs) {
    const inbox = await pollPeer(manifest, peer);
    for (const message of inbox.messages || []) {
      if (message.turn_id === turnId && message.type === "reply") {
        return { reply: message, inbox, seen };
      }
      seen.push(message);
    }
    await sleep(intervalMs);
  }
  throw new Error(`Timed out waiting ${timeoutMs}ms for reply turn ${turnId} addressed to ${peer}`);
}

function launchPeer(opts = {}) {
  const repo = opts.repo || DEFAULT_REPO;
  const peer = String(opts.peer || "").toLowerCase();
  if (!["claude", "codex"].includes(peer)) throw new Error("--peer must be claude or codex");

  const manifest = getActiveManifest(repo, opts.session);
  const peerConfig = manifest.peers?.[peer];
  if (!peerConfig) throw new Error(`Manifest does not define peer '${peer}'`);

  const env = {
    ...process.env,
    CODEVELOP_SESSION: manifest.session_id,
    CODEVELOP_PEER: peer,
    CODEVELOP_TARGET: peerConfig.target_peer,
    CODEVELOP_BASE_URL: manifest.transport.base_url,
    CODEVELOP_MANIFEST: path.join(getStateDir(repo), `${manifest.session_id}.json`),
    CELL_ROLE: `codevelop-${peer}`,
  };

  const prompt = `You are peer=${peer} in co-development session ${manifest.session_id}.
Your default partner is ${peerConfig.target_peer}.
Use clauth codevelop tools at ${manifest.transport.base_url} to send and receive partner messages.
Honor per-turn role and skill requests when provided.
When you need partner help, run: clauth codevelop ask --to ${peerConfig.target_peer} --wait --task "<specific task>".
When you complete a partner request, run: clauth codevelop reply --turn <turn_id> --summary "..." --evidence "..." --files-changed "...".
Do not hand-write curl JSON unless the CLI command fails.
On startup, acknowledge co-development readiness, poll your inbox, and report any queued partner messages.
Reply with evidence, files changed, commits, blockers, and next action.
Manifest: ${env.CODEVELOP_MANIFEST}`;
  const contextPath = getContextPath(peer);
  fs.writeFileSync(contextPath, `${prompt}\n`, "utf8");
  env.CODEVELOP_CONTEXT = contextPath;

  console.log("");
  console.log(`Co-develop ${peer}`);
  console.log(`  session:  ${manifest.session_id}`);
  console.log(`  partner:  ${peerConfig.target_peer}`);
  console.log(`  base_url: ${manifest.transport.base_url}`);
  console.log(`  cwd:      ${peerConfig.cwd}`);
  console.log(`  manifest: ${env.CODEVELOP_MANIFEST}`);
  console.log("");

  if (asBool(opts.printOnly)) {
    console.log(peer === "claude"
      ? 'command: claude -n "Co Develop Claude" --append-system-prompt [co-develop system prompt]'
      : `command: codex -C "${peerConfig.cwd}"`);
    return;
  }

  const cmd = peer === "claude" ? "claude" : "codex";
  const args = peer === "claude"
    ? ["-n", CLAUDE_PROFILE, "--append-system-prompt", prompt]
    : ["-C", peerConfig.cwd];
  const child = spawn(cmd, args, { cwd: peerConfig.cwd, env, stdio: "inherit", shell: process.platform === "win32" && peer === "codex" });
  child.on("exit", code => process.exit(code ?? 0));
}

async function checkPartner(opts = {}) {
  const repo = opts.repo || DEFAULT_REPO;
  const peer = normalizePeerArg(opts.peer);
  const manifest = getActiveManifest(repo, opts.session);
  const peerConfig = manifest.peers?.[peer];
  if (!peerConfig) throw new Error(`Manifest does not define peer '${peer}'`);

  const partner = peerConfig.target_peer;
  const status = await requestJson("GET", `${manifest.transport.base_url}/codevelop/${manifest.session_id}/status`);
  const current = status.peers?.[peer];
  const target = status.peers?.[partner];
  const pending = await requestJson("POST", `${manifest.transport.base_url}/codevelop/poll`, {
    session_id: manifest.session_id,
    peer_id: peer,
  });

  console.log(`[co-develop] peer=${peer} partner=${partner}`);
  console.log(`[co-develop] partner_status=${target ? "registered" : "missing"} partner_queue=${target?.queued ?? "n/a"}`);
  console.log(`[co-develop] my_queue=${current?.queued ?? 0} startup_messages=${pending.count || 0}`);
  for (const message of pending.messages || []) {
    console.log(`[co-develop] ${message.from}->${message.to} ${message.type}: ${message.task || message.message || ""}`);
  }
}

async function syncPartner(opts = {}) {
  const repo = opts.repo || DEFAULT_REPO;
  const peer = normalizePeerArg(opts.peer);
  const manifest = getActiveManifest(repo, opts.session);
  const peerConfig = manifest.peers?.[peer];
  if (!peerConfig) throw new Error(`Manifest does not define peer '${peer}'`);

  const result = await requestJson("POST", `${manifest.transport.base_url}/codevelop/send`, {
    session_id: manifest.session_id,
    from: peer,
    to: peerConfig.target_peer,
    type: "ready",
    task: `${peer} is ready to collaborate and listening.`,
    context: {
      cwd: peerConfig.cwd,
      manifest: path.join(getStateDir(repo), `${manifest.session_id}.json`),
    },
    expect: { action: "acknowledge readiness" },
  });
  console.log(JSON.stringify(result, null, 2));
}

async function askPartner(opts = {}) {
  const repo = opts.repo || DEFAULT_REPO;
  const manifest = getActiveManifest(repo, opts.session);
  const to = inferTargetPeer(opts);
  const from = inferFromPeer({ ...opts, to });
  const task = opts.task || opts.args?.join(" ") || "";
  if (!task.trim()) throw new Error("--task is required for codevelop ask");
  const senderConfig = manifest.peers?.[from];
  const targetConfig = manifest.peers?.[to];
  if (!senderConfig) throw new Error(`Manifest does not define sender peer '${from}'`);
  if (!targetConfig) throw new Error(`Manifest does not define target peer '${to}'`);

  const message = {
    session_id: manifest.session_id,
    from,
    to,
    type: "build_request",
    role: opts.role || "partner",
    skill: opts.skill || "rdc:co-develop",
    task,
    context: {
      repo: targetConfig.cwd || manifest.repo,
      manifest: path.join(getStateDir(repo), `${manifest.session_id}.json`),
      context_file: opts.context || null,
    },
    expect: {
      response_format: "CO_DEVELOP_REPLY",
      evidence_required: true,
      commit_allowed: false,
    },
  };

  const sent = await requestJson("POST", `${manifest.transport.base_url}/codevelop/send`, message);
  const output = {
    sent,
    session_id: manifest.session_id,
    from,
    to,
    turn_id: sent.turn_id,
    waiting: !!opts.wait,
  };

  if (opts.wait) {
    const timeoutMs = Number(opts.timeoutMs || 300_000);
    const intervalMs = Number(opts.intervalMs || 2_000);
    const waited = await waitForReply(manifest, from, sent.turn_id, { timeoutMs, intervalMs });
    output.reply = waited.reply;
    output.seen_while_waiting = waited.seen.length;
  }

  console.log(JSON.stringify(output, null, 2));
}

async function replyPartner(opts = {}) {
  const repo = opts.repo || DEFAULT_REPO;
  const manifest = getActiveManifest(repo, opts.session);
  const from = inferFromPeer(opts);
  const to = inferTargetPeer({ to: opts.to, from });
  if (!opts.turn) throw new Error("--turn is required for codevelop reply");
  const senderConfig = manifest.peers?.[from];
  if (!senderConfig) throw new Error(`Manifest does not define sender peer '${from}'`);

  const result = await requestJson("POST", `${manifest.transport.base_url}/codevelop/send`, {
    session_id: manifest.session_id,
    turn_id: opts.turn,
    from,
    to,
    type: "reply",
    verdict: opts.verdict || "pass",
    summary: opts.summary || "Partner task completed.",
    evidence: parseList(opts.evidence),
    files_changed: parseList(opts.filesChanged),
    commits: parseList(opts.commits),
    blockers: parseList(opts.blockers),
    next: parseList(opts.next),
  });

  console.log(JSON.stringify({
    queued: result.queued,
    session_id: manifest.session_id,
    turn_id: opts.turn,
    from,
    to,
    verdict: opts.verdict || "pass",
  }, null, 2));
}

async function inboxPeer(opts = {}) {
  const repo = opts.repo || DEFAULT_REPO;
  const manifest = getActiveManifest(repo, opts.session);
  const peer = normalizeOptionalPeer(opts.peer) || normalizeOptionalPeer(opts.from) || normalizeOptionalPeer(process.env.CODEVELOP_PEER);
  if (!peer) throw new Error("--peer is required for codevelop inbox");
  const inbox = await pollPeer(manifest, peer);
  console.log(JSON.stringify(inbox, null, 2));
}

function emitMonitorLine(payload) {
  process.stdout.write(`${JSON.stringify(payload)}\n`);
}

function connectSse(url, { onEvent, onError }) {
  const parsed = new URL(url);
  const client = parsed.protocol === "https:" ? https : http;
  const req = client.get(parsed, {
    headers: {
      accept: "text/event-stream",
      "cache-control": "no-cache",
    },
  }, res => {
    if (res.statusCode !== 200) {
      onError?.(new Error(`SSE ${url} failed: HTTP ${res.statusCode}`));
      res.resume();
      return;
    }

    res.setEncoding("utf8");
    let buffer = "";
    let current = { event: "message", data: [] };

    function flushEvent() {
      if (!current.data.length) {
        current = { event: "message", data: [] };
        return;
      }
      const text = current.data.join("\n");
      try {
        onEvent?.({ event: current.event || "message", data: JSON.parse(text) });
      } catch {
        onEvent?.({ event: current.event || "message", data: text });
      }
      current = { event: "message", data: [] };
    }

    res.on("data", chunk => {
      buffer += chunk;
      const lines = buffer.split(/\r?\n/);
      buffer = lines.pop() || "";
      for (const line of lines) {
        if (line === "") {
          flushEvent();
        } else if (line.startsWith("event:")) {
          current.event = line.slice(6).trim();
        } else if (line.startsWith("data:")) {
          current.data.push(line.slice(5).trimStart());
        }
      }
    });
    res.on("end", () => onError?.(new Error("SSE stream ended")));
  });

  req.on("error", err => onError?.(err));
  return req;
}

async function listenPeer(opts = {}) {
  const repo = opts.repo || DEFAULT_REPO;
  const manifest = getActiveManifest(repo, opts.session);
  const peer = normalizeOptionalPeer(opts.peer) || normalizeOptionalPeer(opts.from) || normalizeOptionalPeer(process.env.CODEVELOP_PEER);
  if (!peer) throw new Error("--peer is required for codevelop listen");

  const streamUrl = manifest.peers?.[peer]?.stream_url
    || `${manifest.transport.base_url}/codevelop/${encodeURIComponent(manifest.session_id)}/${encodeURIComponent(peer)}/stream`;
  const once = asBool(opts.once);
  const reconnectMs = Number(opts.intervalMs || 2_000);
  let stopped = false;

  emitMonitorLine({
    event: "codevelop_listen_ready",
    session_id: manifest.session_id,
    peer,
    partner: manifest.peers?.[peer]?.target_peer || null,
    stream_url: streamUrl,
    action: "Claude/Codex should run clauth codevelop inbox --peer " + peer + " when codevelop_message arrives, then complete and reply.",
  });

  function connect() {
    if (stopped) return;
    connectSse(streamUrl, {
      onEvent: ({ event, data }) => {
        if (event !== "message" && event !== "stop") return;
        emitMonitorLine({
          event: event === "stop" ? "codevelop_stop" : "codevelop_message",
          session_id: manifest.session_id,
          peer,
          turn_id: data?.turn_id || null,
          from: data?.from || null,
          to: data?.to || peer,
          type: data?.type || event,
          task: data?.task || null,
          message: data?.message || null,
          payload: data,
          next: data?.type === "reply"
            ? "Read the reply and continue the waiting task."
            : `Run clauth codevelop inbox --peer ${peer}, do the requested work, then run clauth codevelop reply --turn ${data?.turn_id || "<turn_id>"} ...`,
        });
        if (once) {
          stopped = true;
          process.exit(0);
        }
      },
      onError: err => {
        if (stopped) return;
        emitMonitorLine({
          event: "codevelop_listen_reconnect",
          session_id: manifest.session_id,
          peer,
          error: err.message,
          retry_ms: reconnectMs,
        });
        setTimeout(connect, reconnectMs);
      },
    });
  }

  connect();
  await new Promise(() => {});
}

async function requestPartner(opts = {}) {
  const repo = opts.repo || DEFAULT_REPO;
  const port = Number(opts.port || DEFAULT_PORT);
  if (RESERVED_PORTS.has(port)) throw new Error(`Refusing to use reserved clauth port ${port}`);
  const baseUrl = opts.baseUrl || `http://127.0.0.1:${port}`;
  const peer = normalizePeerArg(opts.peer);
  const partner = peer === "codex" ? "claude" : "codex";
  const task = opts.task || "I need a co-development partner. Confirm you are ready, read the context file if provided, and wait for the first concrete assignment.";
  const contextFile = opts.context || opts.contextFile || null;
  const name = opts.name || `assist-${peer}-${new Date().toISOString().replace(/[-:]/g, "").slice(0, 15)}`;

  if (!(await testPing(baseUrl))) {
    if (!asBool(opts.startIsolatedClauth)) throw new Error(`clauth is not reachable at ${baseUrl}. Re-run with --start-isolated-clauth.`);
    startIsolatedClauth(port);
    if (!(await waitForPing(baseUrl))) throw new Error(`isolated clauth did not become ready at ${baseUrl}`);
  }

  const started = await requestJson("POST", `${baseUrl}/codevelop/start`, {
    name,
    repo,
    metadata: {
      mode: "request-partner",
      initiator: peer,
      partner,
      context_file: contextFile,
    },
  });
  const sessionId = started.session_id;
  await requestJson("POST", `${baseUrl}/codevelop/join`, { session_id: sessionId, peer_id: peer, role: "initiator", target_peer: partner });
  await requestJson("POST", `${baseUrl}/codevelop/join`, { session_id: sessionId, peer_id: partner, role: "co_developer", target_peer: peer });
  const status = await requestJson("GET", `${baseUrl}/codevelop/${sessionId}/status`);

  const { codexRepo, branch } = ensureWorktree(repo, sessionId, false);
  const manifestPath = writeManifest({ repo, sessionId, name, baseUrl, status, codexRepo, branch, startedClauthPid: null, dryRun: false });
  const manifest = readJsonFile(manifestPath);
  manifest.mode = "request-partner";
  manifest.initiator = peer;
  manifest.context_contract.context_file = contextFile;
  manifest.context_contract.required_reads = contextFile ? [contextFile] : [];
  fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
  writeActiveLauncherScripts(manifest);

  const ask = [
    "Co-development partner requested.",
    `Initiator: ${peer}.`,
    contextFile ? `Context file to read first: ${contextFile}.` : "No context file supplied.",
    `Task: ${task}`,
  ].join(" ");
  await requestJson("POST", `${baseUrl}/codevelop/send`, {
    session_id: sessionId,
    from: peer,
    to: partner,
    type: "partner_request",
    task: ask,
    context: {
      mode: "request-partner",
      context_file: contextFile,
      manifest: manifestPath,
      repo,
    },
    expect: {
      response: "ready_ack",
      instruction: "Acknowledge readiness before doing implementation work.",
    },
  });

  const wt = buildWtArgs();
  console.log(`CODEVELOP_SESSION=${sessionId}`);
  console.log(`MANIFEST=${manifestPath}`);
  console.log(`PARTNER=${partner}`);
  console.log(`CONTEXT=${contextFile || ""}`);
  console.log("Windows Terminal command:");
  console.log(wt.display);
  if (!asBool(opts.noOpen)) spawn("wt.exe", wt.args, { detached: true, stdio: "ignore", windowsHide: false }).unref();
}

function normalizePeerArg(peer) {
  const value = String(peer || process.env.CODEVELOP_PEER || "").toLowerCase();
  if (!["claude", "codex"].includes(value)) throw new Error("--peer must be claude or codex");
  return value;
}

export async function runCodevelop(opts = {}) {
  const action = opts.action || "help";
  if (action === "install-terminal") {
    const result = installWindowsTerminalProfiles(opts);
    console.log(chalk.green(`Installed Windows Terminal profiles: ${CLAUDE_PROFILE}, ${CODEX_PROFILE}`));
    console.log(chalk.gray(`Settings: ${result.settingsPath}`));
    if (result.backupPath) console.log(chalk.gray(`Backup: ${result.backupPath}`));
    return;
  }
  if (action === "start") return startCodevelop(opts);
  if (action === "join") return joinAdhocChannel(opts);
  if (action === "say") return sayAdhoc(opts);
  if (action === "read") return readAdhoc(opts);
  if (action === "watch") return watchAdhoc(opts);
  if (action === "who") return listAdhocPeers(opts);
  if (action === "launch-peer") return launchPeer(opts);
  if (action === "check-partner") return checkPartner(opts);
  if (action === "sync") return syncPartner(opts);
  if (action === "request-partner") return requestPartner(opts);
  if (action === "ask") return askPartner(opts);
  if (action === "reply") return replyPartner(opts);
  if (action === "inbox") return inboxPeer(opts);
  if (action === "listen") return listenPeer(opts);

  console.log(chalk.cyan("\nclauth codevelop\n"));
  console.log("  clauth codevelop install-terminal [--repo C:\\Dev\\regen-root]");
  console.log("  clauth codevelop start --port 53137 --start-isolated-clauth [--repo C:\\Dev\\regen-root]");
  console.log("  clauth codevelop join --channel 1 --peer codex-1 [--start-isolated-clauth]");
  console.log("  clauth codevelop say --message \"claude-1 look at this doc and report back\" [--to claude-1]");
  console.log("  clauth codevelop read");
  console.log("  clauth codevelop watch [--once]");
  console.log("  clauth codevelop who");
  console.log("  clauth codevelop ask --to claude|codex --task \"...\" --wait");
  console.log("  clauth codevelop reply --from claude|codex --turn turn-0001 --summary \"...\" --evidence \"...\"");
  console.log("  clauth codevelop inbox --peer claude|codex");
  console.log("  clauth codevelop listen --peer claude|codex [--once]");
  console.log("  clauth codevelop request-partner --peer codex|claude --task \"...\" [--context path] [--repo C:\\Dev\\regen-root]");
  console.log("  clauth codevelop launch-peer --peer claude|codex [--repo C:\\Dev\\regen-root]\n");
  console.log("  clauth codevelop check-partner --peer claude|codex [--repo C:\\Dev\\regen-root]");
  console.log("  clauth codevelop sync --peer claude|codex [--repo C:\\Dev\\regen-root]\n");
}
