/**
 * scripts/work/prd-ship.mjs — flip a PRD's status to `shipped`.
 *
 * Invoked when an epic completes and its seed PRD's implementation is fully
 * landed. Writes back to the PRD's frontmatter:
 *   - status: <approved|in-review> -> shipped
 *   - shipped: <ISO date> (today, UTC)
 *   - shipping-commits: [<sha1>, <sha2>, ...] (optional)
 *
 * Refuses to flip from `draft` (must go through human review first —
 * draft -> approved is the human step, NOT automated by this command).
 * Refuses to flip if already `shipped` (idempotent fail-soft).
 *
 * Usage:
 *   pnpm work prd-ship <prd-id>
 *   pnpm work prd-ship <prd-id> --commits sha1,sha2,sha3
 *   pnpm work prd-ship <prd-id> --auto-commits  # derive from `git log` since the PRD's
 *                                                # created date on the PRD file's first
 *                                                # appearance in git history
 */
import fs from "node:fs";
import path from "node:path";
import { execSync } from "node:child_process";

/**
 * Parse the top YAML frontmatter block of a markdown file. Returns
 * { frontmatter: Record<string, string|string[]>, body: string,
 *   raw: { frontmatterText, frontmatterStart, frontmatterEnd } }
 *
 * Hand-rolled YAML-ish parser — handles scalar lines and "- item" lists,
 * the only two shapes the PRD/epic/story frontmatter uses. Sufficient for
 * this use case; not a general YAML parser.
 */
export function parseFrontmatter(text) {
  if (!text.startsWith("---\n")) {
    return { frontmatter: {}, body: text, raw: null };
  }
  const end = text.indexOf("\n---\n", 4);
  if (end === -1) return { frontmatter: {}, body: text, raw: null };
  const fmText = text.slice(4, end);
  const body = text.slice(end + 5);

  const fm = {};
  let currentList = null;
  for (const line of fmText.split("\n")) {
    if (line.startsWith("  - ")) {
      if (currentList) currentList.push(line.slice(4).trim());
      continue;
    }
    currentList = null;
    const m = /^([a-zA-Z_-]+):\s*(.*)$/.exec(line);
    if (!m) continue;
    const [, key, value] = m;
    if (value === "") {
      // Empty value — could be the start of a list
      fm[key] = [];
      currentList = fm[key];
    } else {
      fm[key] = value;
    }
  }
  return {
    frontmatter: fm,
    body,
    raw: { frontmatterText: fmText, frontmatterStart: 4, frontmatterEnd: end },
  };
}

/**
 * Serialize a parsed frontmatter back to YAML text. Preserves declared key
 * order for keys that were in the original; appends new keys at the end.
 */
export function serializeFrontmatter(originalText, newFrontmatter) {
  const lines = [];
  const originalKeyOrder = [];
  if (originalText) {
    for (const line of originalText.split("\n")) {
      const m = /^([a-zA-Z_-]+):/.exec(line);
      if (m) originalKeyOrder.push(m[1]);
    }
  }
  const seen = new Set();
  for (const key of originalKeyOrder) {
    if (seen.has(key)) continue;
    seen.add(key);
    if (!(key in newFrontmatter)) continue;
    emitKey(lines, key, newFrontmatter[key]);
  }
  for (const key of Object.keys(newFrontmatter)) {
    if (seen.has(key)) continue;
    emitKey(lines, key, newFrontmatter[key]);
  }
  return lines.join("\n");
}

function emitKey(lines, key, value) {
  if (Array.isArray(value)) {
    lines.push(`${key}:`);
    for (const item of value) lines.push(`  - ${item}`);
  } else {
    lines.push(`${key}: ${value}`);
  }
}

/**
 * Flip a PRD's status to "shipped". Pure function over the file's text.
 * Returns the new file text. Throws on illegal transitions.
 */
export function flipPrdStatus(text, { shippedDate, commits } = {}) {
  const { frontmatter, body, raw } = parseFrontmatter(text);
  if (!raw) {
    throw new Error("PRD has no parseable frontmatter");
  }
  const current = frontmatter.status;
  if (current === "shipped") {
    throw new Error("PRD is already marked shipped (idempotent fail-soft)");
  }
  if (current === "draft") {
    throw new Error(
      "PRD is still draft — flip to approved (human review) before shipping",
    );
  }
  if (current !== "approved" && current !== "in-review") {
    throw new Error(
      `Unexpected PRD status "${current}" — expected approved or in-review`,
    );
  }

  const newFm = { ...frontmatter };
  newFm.status = "shipped";
  newFm.shipped = shippedDate ?? new Date().toISOString().slice(0, 10);
  if (commits && commits.length > 0) {
    newFm["shipping-commits"] = commits;
  }

  const newFmText = serializeFrontmatter(raw.frontmatterText, newFm);
  return `---\n${newFmText}\n---\n${body}`;
}

/**
 * Derive shipping commits for a PRD by walking `git log` for the PRD's
 * linked epic folder. Best-effort; returns empty array if the epic folder
 * doesn't exist or git isn't available.
 */
export function deriveShippingCommits(repoRoot, prdId, workRoot) {
  // Find the epic whose `prd:` matches this prdId
  const epicsRoot = path.join(workRoot, "epics");
  if (!fs.existsSync(epicsRoot)) return [];
  const entries = fs.readdirSync(epicsRoot, { withFileTypes: true });
  let epicDir = null;
  for (const entry of entries) {
    if (!entry.isDirectory()) continue;
    const epicFile = path.join(epicsRoot, entry.name, "_epic.md");
    if (!fs.existsSync(epicFile)) continue;
    const { frontmatter } = parseFrontmatter(fs.readFileSync(epicFile, "utf8"));
    if (frontmatter.prd === prdId) {
      epicDir = entry.name;
      break;
    }
  }
  if (!epicDir) return [];

  try {
    const log = execSync(
      `git log --format=%h --reverse -- docs/work/epics/${epicDir}/`,
      { cwd: repoRoot, encoding: "utf8" },
    );
    return log.trim().split("\n").filter(Boolean);
  } catch {
    return [];
  }
}

export function findPrdPath(workRoot, prdId) {
  const prdsDir = path.join(workRoot, "prds");
  if (!fs.existsSync(prdsDir)) return null;
  for (const file of fs.readdirSync(prdsDir)) {
    if (!file.endsWith(".prd.md")) continue;
    const text = fs.readFileSync(path.join(prdsDir, file), "utf8");
    const { frontmatter } = parseFrontmatter(text);
    if (frontmatter.id === prdId) {
      return path.join(prdsDir, file);
    }
  }
  return null;
}

// ---- CLI ----

export function runCli(args, { repoRoot, workRoot }) {
  const prdId = args[0];
  if (!prdId) {
    process.stderr.write(
      "Usage: pnpm work prd-ship <prd-id> [--commits sha1,sha2,...] [--auto-commits]\n",
    );
    return 2;
  }

  let commits;
  let autoCommits = false;
  for (let i = 1; i < args.length; i++) {
    if (args[i] === "--commits") {
      commits = args[++i]
        .split(",")
        .map((s) => s.trim())
        .filter(Boolean);
    } else if (args[i] === "--auto-commits") {
      autoCommits = true;
    }
  }

  const prdPath = findPrdPath(workRoot, prdId);
  if (!prdPath) {
    process.stderr.write(
      `PRD with id="${prdId}" not found under docs/work/prds/\n`,
    );
    return 1;
  }

  if (autoCommits && !commits) {
    commits = deriveShippingCommits(repoRoot, prdId, workRoot);
  }

  const text = fs.readFileSync(prdPath, "utf8");
  let newText;
  try {
    newText = flipPrdStatus(text, { commits });
  } catch (err) {
    process.stderr.write(`Cannot ship PRD ${prdId}: ${err.message}\n`);
    return 1;
  }

  fs.writeFileSync(prdPath, newText);
  process.stdout.write(
    `Shipped ${prdId} (${path.relative(repoRoot, prdPath)})` +
      (commits ? ` with ${commits.length} commit(s)` : "") +
      "\n",
  );
  return 0;
}
