#!/usr/bin/env node
/**
 * scripts/work/decompose.mjs
 *
 * Decomposer dispatcher — takes an approved PRD and invokes the decomposer
 * agent to write the epic folder + per-requirement story files under
 * docs/work/epics/<epic-slug>/.
 *
 * Default mode (no --execute): print the dispatch plan + validate the PRD
 * (refuses to proceed on draft / in-review / shipped). Safe anywhere.
 *
 * --execute mode: requires @ai-hero/sandcastle + auth (Claude subscription
 * via ~/.claude OR ANTHROPIC_API_KEY). Mirrors `pnpm work dispatch --execute`.
 *
 * Usage:
 *   pnpm work decompose <prd-id>
 *   pnpm work decompose <prd-id> --execute
 */
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { findPrdPath, parseFrontmatter } from "./prd-ship.mjs";
import { resolveClaudeAuth } from "./dispatch.mjs";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, "..", "..");
const WORK_ROOT = path.join(REPO_ROOT, "docs", "work");
const SANDCASTLE_DIR = path.join(REPO_ROOT, ".sandcastle");

/**
 * Validate that a PRD is in a decomposable state. Throws on draft (must go
 * through human review), in-review (review not yet complete), shipped (epic
 * already exists), or missing.
 *
 * Returns the parsed frontmatter + body for the caller to pass into the
 * decomposer.
 */
export function validatePrdForDecompose(prdPath) {
  if (!fs.existsSync(prdPath)) {
    throw new Error(`PRD file not found: ${prdPath}`);
  }
  const text = fs.readFileSync(prdPath, "utf8");
  const { frontmatter } = parseFrontmatter(text);
  const status = frontmatter.status;

  if (status === "draft") {
    throw new Error(
      `PRD status is "draft" — human review required before decomposing. ` +
        `Flip status to "approved" in the PRD frontmatter, then re-run.`,
    );
  }
  if (status === "in-review") {
    throw new Error(
      `PRD status is "in-review" — review must complete (status -> approved) before decomposing.`,
    );
  }
  if (status === "shipped") {
    throw new Error(
      `PRD status is "shipped" — its epic should already exist under docs/work/. ` +
        `Re-decomposing a shipped PRD is not supported.`,
    );
  }
  if (status !== "approved") {
    throw new Error(
      `Unexpected PRD status "${status}" — expected "approved" to decompose.`,
    );
  }

  return { frontmatter, text };
}

/**
 * Print what would happen on --execute. Validation runs in both modes; this
 * is the "preview" companion of executeDecompose().
 */
export function printDecomposePlan(prdId, prdPath, frontmatter) {
  console.log("=== Decompose plan ===");
  console.log(`  PRD:      ${path.relative(REPO_ROOT, prdPath)}`);
  console.log(`  Id:       ${prdId}`);
  console.log(`  Title:    ${frontmatter.title ?? "(no title)"}`);
  console.log(`  Status:   ${frontmatter.status} (eligible to decompose)`);
  console.log();
  console.log(`  Decomposer prompt: .sandcastle/decomposer.prompt.md`);
  console.log(
    `  Output:           docs/work/epics/<epic-slug>/_epic.md + per-requirement story files`,
  );
  console.log();
  console.log("To run for real:");
  console.log(
    "  - With Claude subscription: `claude login` (one-time) then `pnpm work decompose <id> --execute`",
  );
  console.log(
    "  - With API key:             `ANTHROPIC_API_KEY=... pnpm work decompose <id> --execute`",
  );
  console.log();
  console.log(
    "(Execute mode requires @ai-hero/sandcastle, a sandbox provider, and auth — see docs/guides/runbook.md.)",
  );
}

/**
 * Invoke sandcastle with the decomposer prompt + the PRD file content. The
 * decomposer agent writes the epic + stories to disk inside the sandbox; the
 * orchestrator then has those files in a branch the human can review.
 */
export async function executeDecompose(prdId, prdPath, prdText) {
  const auth = resolveClaudeAuth();
  if (auth.mode === "missing") {
    console.error("✗ --execute requires either:");
    console.error(
      "  1. Claude Code logged in on host (run `claude login` first; ~/.claude/ becomes the auth source — this is the recommended path for Pro/Max subscribers)",
    );
    console.error("  2. ANTHROPIC_API_KEY or OPENAI_API_KEY in env (fallback)");
    console.error("");
    console.error(
      "  Override Claude creds path via SANDCASTLE_CLAUDE_CREDS_DIR.",
    );
    process.exit(1);
  }
  console.log(
    `Auth mode: ${auth.mode === "subscription" ? `subscription (mounting ${auth.hostPath})` : "api-key"}`,
  );
  console.log(`Decomposing PRD: ${prdId}`);

  let sandcastleRoot;
  let dockerProvider;
  try {
    sandcastleRoot = await import("@ai-hero/sandcastle");
    const dockerModule = await import("@ai-hero/sandcastle/sandboxes/docker");
    dockerProvider = dockerModule.docker;
  } catch {
    console.error(
      "✗ @ai-hero/sandcastle is not installed. Run `pnpm install` first.",
    );
    process.exit(1);
  }

  const dockerOpts = {};
  const agentOpts = {};
  if (auth.mode === "subscription") {
    dockerOpts.mounts = [
      {
        hostPath: auth.hostPath,
        sandboxPath: auth.sandboxPath,
        readonly: false,
      },
    ];
  } else if (auth.mode === "api-key") {
    agentOpts.env = auth.env;
  }
  const sandbox = dockerProvider(dockerOpts);
  const agent = sandcastleRoot.claudeCode("claude-sonnet-4-6", agentOpts);

  const decomposerPrompt = path.join(SANDCASTLE_DIR, "decomposer.prompt.md");
  let result;
  try {
    result = await sandcastleRoot.run({
      agent,
      sandbox,
      promptFile: decomposerPrompt,
      promptArgs: { PRD_FILE_CONTENT: prdText },
      cwd: REPO_ROOT,
      // Sandcastle's default maxIterations: 1 cut the agent off after its
      // first response — files were written inside the sandbox but never
      // captured as commits. Decompose is a small authoring task (read
      // context, write epic + stories, commit); 10 iterations is enough
      // room. Tune via env SANDCASTLE_DECOMPOSE_ITERATIONS.
      maxIterations: Number(process.env.SANDCASTLE_DECOMPOSE_ITERATIONS ?? 10),
      // Stop iterating the moment the agent emits this marker. Without it,
      // sandcastle re-invokes the model up to maxIterations even when the
      // work is already done — the prompt instructs the agent to emit
      // <promise>COMPLETE</promise> on its final line.
      completionSignal: "<promise>COMPLETE</promise>",
    });
  } catch (e) {
    console.error("✗ Decomposer dispatch failed:", e.message);
    if (/Image '.+' not found locally/.test(e.message ?? "")) {
      console.error(
        "  One-time setup: pnpm exec sandcastle docker build-image",
      );
    }
    if (
      /Not logged in|Please run \/login/.test(e.message ?? "") &&
      process.platform === "darwin"
    ) {
      console.error(
        "  macOS users: Claude Code stores credentials in the Keychain, not in ~/.claude/. Extract once:",
      );
      console.error(
        `    security find-generic-password -s "Claude Code-credentials" -a "$USER" -w > ~/.claude/.credentials.json`,
      );
      console.error("    chmod 600 ~/.claude/.credentials.json");
      console.error(
        "  OR fall back to API key: export ANTHROPIC_API_KEY=sk-ant-...",
      );
    }
    console.error(
      "  See docs/guides/runbook.md → 'Using Sandcastle' for setup.",
    );
    process.exit(1);
  }
  console.log(
    `Decomposer returned. Branch: ${result.branch}, Commits: ${result.commits.length}`,
  );
  console.log();
  console.log("=== Suggested next steps ===");
  console.log(
    `  1. Inspect the new epic folder under docs/work/epics/<epic-slug>/ on branch ${result.branch}`,
  );
  console.log(
    `  2. Review the generated stories + tasks; edit anything that should change`,
  );
  console.log(`  3. Merge the branch to main`);
  console.log(
    `  4. pnpm work rebuild-state && pnpm work next  # see the first ready task`,
  );
  console.log(`  5. pnpm work dispatch --execute               # dispatch it`);
}

// ---- CLI ----

function usage() {
  console.error("Usage: pnpm work decompose <prd-id> [--execute]");
  process.exit(2);
}

export async function runCli(args, { workRoot }) {
  const positional = args.filter((a) => !a.startsWith("--"));
  const prdId = positional[0];
  if (!prdId) {
    usage();
  }

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

  let frontmatter;
  let prdText;
  try {
    const result = validatePrdForDecompose(prdPath);
    frontmatter = result.frontmatter;
    prdText = result.text;
  } catch (err) {
    console.error(`Cannot decompose ${prdId}: ${err.message}`);
    return 1;
  }

  if (args.includes("--execute")) {
    await executeDecompose(prdId, prdPath, prdText);
    return 0;
  }
  printDecomposePlan(prdId, prdPath, frontmatter);
  return 0;
}

const invokedDirectly = import.meta.url === `file://${process.argv[1]}`;
if (invokedDirectly) {
  runCli(process.argv.slice(2), {
    repoRoot: REPO_ROOT,
    workRoot: WORK_ROOT,
  }).then((code) => process.exit(code));
}
