/**
 * @file Gives a lane the same MCP servers as its source repo: reads the
 * source repo's already-configured mcpServers from ~/.claude.json (normal
 * Claude Code project-scope config — the human sets this up once, the same
 * way they would for any project), relocates any absolute path under the
 * source repo to the lane's own directory, pins a Playwright server's proof
 * output dir, and writes <lane.cwd>/.mcp.json. Also seeds the lane's
 * Chromium browser profiles from the source repo's own (preserves saved
 * logins — a QA account only needs to log in once per machine).
 *
 * Deliberately does NOT write any permission or settings.local.json content
 * — see the F1 design spec's Scope section for why. Plain file I/O; no git
 * calls, unlike lane-sync.js (E2) or lane-agents.js (E3).
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */

const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");

/** Read the source repo's mcpServers from ~/.claude.json. Throws
 *  ENOMCPCONFIG if the file is missing or the project has none declared —
 *  a lane with zero MCP servers synced would silently break Stage 3/6 much
 *  later, at a far less debuggable point, so this fails loud and early. */
function readSourceMcpServers(sourceRepo) {
  const claudeJsonPath = path.join(os.homedir(), ".claude.json");
  let cfg;
  try {
    cfg = JSON.parse(fs.readFileSync(claudeJsonPath, "utf8"));
  } catch {
    cfg = null;
  }
  const servers = cfg?.projects?.[sourceRepo]?.mcpServers;
  if (!servers || !Object.keys(servers).length) {
    throw Object.assign(
      new Error(
        `no mcpServers configured for source repo ${sourceRepo} in ~/.claude.json — ` +
          `configure them there first (see the ship-feature-lane skill's Setup section)`
      ),
      { code: "ENOMCPCONFIG" }
    );
  }
  return servers;
}

/** Deep-walk a server config, replacing every occurrence of `sourceRepo`
 *  inside a string with `laneDir`. Strings, arrays, and plain objects only
 *  — an MCP server def never contains anything else. */
function relocate(value, sourceRepo, laneDir) {
  if (typeof value === "string") return value.split(sourceRepo).join(laneDir);
  if (Array.isArray(value)) return value.map((v) => relocate(v, sourceRepo, laneDir));
  if (value && typeof value === "object") {
    const out = {};
    for (const [k, v] of Object.entries(value)) out[k] = relocate(v, sourceRepo, laneDir);
    return out;
  }
  return value;
}

/** Pin --output-dir for a @playwright/mcp server that doesn't already
 *  declare one, so relative proof filenames (proof/<feature>/...) always
 *  land under <laneDir>/.playwright-mcp — the one place the dashboard's
 *  proof gallery reads. */
function pinPlaywrightOutputDir(servers, laneDir) {
  for (const server of Object.values(servers)) {
    const args = server.args;
    if (!Array.isArray(args)) continue;
    const isPlaywright = args.some((a) => typeof a === "string" && a.startsWith("@playwright/mcp"));
    if (isPlaywright && !args.includes("--output-dir")) {
      server.args = [...args, "--output-dir", path.join(laneDir, ".playwright-mcp")];
    }
  }
}

/** Copy each <sourceRepo>/.playwright-mcp/profiles/<name>/ into the lane
 *  ONLY if that name doesn't already exist there — an existing profile
 *  means a session already logged in with it; never clobber that. Strips
 *  Singleton* lock files from the freshly-seeded copy (stale locks from the
 *  source's own last browser process would make the lane's browser refuse
 *  to start, thinking another instance already holds the profile). */
function seedProfiles(sourceRepo, laneDir) {
  const srcProfiles = path.join(sourceRepo, ".playwright-mcp", "profiles");
  if (!fs.existsSync(srcProfiles)) return [];

  const seeded = [];
  for (const name of fs.readdirSync(srcProfiles)) {
    const src = path.join(srcProfiles, name);
    if (!fs.statSync(src).isDirectory()) continue;
    const dest = path.join(laneDir, ".playwright-mcp", "profiles", name);
    if (fs.existsSync(dest)) continue;

    fs.mkdirSync(path.dirname(dest), { recursive: true });
    fs.cpSync(src, dest, { recursive: true });
    for (const entry of fs.readdirSync(dest)) {
      if (entry.startsWith("Singleton")) fs.rmSync(path.join(dest, entry), { force: true });
    }
    seeded.push(name);
  }
  return seeded;
}

/** Idempotently append a line to <laneDir>/.git/info/exclude — same
 *  read-existing-then-append-if-missing pattern E2/E3 already use for
 *  .git/info/attributes and .git/info/exclude. .mcp.json sits at the lane
 *  root (not inside .git), so this needs no --git-common-dir resolution —
 *  the exclude file itself is always local to this lane's own working copy. */
function excludeFromGit(laneDir, line) {
  const excludePath = path.join(laneDir, ".git", "info", "exclude");
  fs.mkdirSync(path.dirname(excludePath), { recursive: true });
  const existing = fs.existsSync(excludePath) ? fs.readFileSync(excludePath, "utf8") : "";
  const lines = existing.split("\n").filter(Boolean);
  if (!lines.includes(line)) {
    lines.push(line);
    fs.writeFileSync(excludePath, lines.join("\n") + "\n");
  }
}

/**
 * @param {{cwd: string, source_repo: string}} lane
 * @returns {{servers: string[], profilesSeeded: string[]}}
 */
async function syncMcp(lane) {
  const sourceRepo = lane.source_repo || lane.cwd;
  const sourceServers = readSourceMcpServers(sourceRepo);
  const relocated = relocate(sourceServers, sourceRepo, lane.cwd);
  pinPlaywrightOutputDir(relocated, lane.cwd);

  fs.writeFileSync(
    path.join(lane.cwd, ".mcp.json"),
    JSON.stringify({ mcpServers: relocated }, null, 2) + "\n"
  );
  excludeFromGit(lane.cwd, ".mcp.json");

  const profilesSeeded = seedProfiles(sourceRepo, lane.cwd);

  return { servers: Object.keys(relocated), profilesSeeded };
}

module.exports = { syncMcp };
