#!/usr/bin/env node
/**
 * pnpm work dispatch — orchestrator that picks the next ready task and
 * (with --execute) invokes sandcastle to run the implementer then reviewer.
 *
 * Default mode prints the dispatch plan without invoking sandcastle —
 * safe to run anywhere. --execute requires EITHER:
 *   1. Claude Code logged in on host (~/.claude/ — recommended for subscribers)
 *   2. ANTHROPIC_API_KEY or OPENAI_API_KEY in env (fallback)
 */
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { execSync } from "node:child_process";
import { buildState } from "./state-builder.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");

/**
 * Returns the first ready story's first unchecked AC bullet, or null if
 * there's no work to dispatch.
 *
 * Shape: { epic, story, title, storyPath, storyContent, bulletLine, bulletIndex }
 */
export function findNextTask(workRoot = WORK_ROOT) {
  const state = buildState(workRoot);
  if (state.ready.length === 0) return null;
  const next = state.ready[0];
  const storyPath = path.join(workRoot, next.epic, next.story, "_story.md");
  if (!fs.existsSync(storyPath)) return null;
  const storyContent = fs.readFileSync(storyPath, "utf8");
  const { bulletLine, bulletIndex } = findFirstUncheckedBullet(storyContent);
  if (bulletLine === null) return null;
  return {
    epic: next.epic,
    story: next.story,
    title: next.title,
    storyPath,
    storyContent,
    bulletLine,
    bulletIndex,
  };
}

/**
 * Scans the story content for the first `- [ ]` bullet INSIDE the `## Tasks`
 * section. Returns the matched line + its 0-based index within the file's
 * line array (used by the orchestrator if it later tick-edits the file).
 */
export function findFirstUncheckedBullet(content) {
  const lines = content.split("\n");
  let inTasks = false;
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];
    if (line.startsWith("## ")) {
      inTasks = /^##\s+Tasks\b/i.test(line);
      continue;
    }
    if (!inTasks) continue;
    if (/^[\s>-]*\[\s\]/.test(line)) {
      return { bulletLine: line, bulletIndex: i };
    }
  }
  return { bulletLine: null, bulletIndex: -1 };
}

/**
 * Builds the task spec string passed to sandcastle as TASK_FILE_CONTENT.
 * The implementer prompt template uses this verbatim.
 */
export function buildTaskSpec(next) {
  return `# Current task

## Epic
${next.epic}

## Story
${next.story} — ${next.title}

## Current bullet
${next.bulletLine.trim()}

## Full story for context

${next.storyContent}`;
}

/**
 * Resolve the auth method for sandcastle dispatch.
 *
 * Priority:
 *   1. Subscription (primary) — mount host's ~/.claude/ into the sandbox.
 *      Active when the host's Claude creds directory exists. The path
 *      defaults to ~/.claude/ and can be overridden via the
 *      SANDCASTLE_CLAUDE_CREDS_DIR env var.
 *   2. API key (fallback) — pass ANTHROPIC_API_KEY (or OPENAI_API_KEY)
 *      through to the sandbox env.
 *   3. Neither available → returns { mode: "missing" } and the dispatcher
 *      prints a clear error before exiting.
 *
 * Returns: { mode: "subscription", hostPath, sandboxPath }
 *        | { mode: "api-key", env }
 *        | { mode: "missing" }
 */
export function resolveClaudeAuth({
  env = process.env,
  home = os.homedir(),
} = {}) {
  // 1. Subscription path
  const credsHostPath =
    env.SANDCASTLE_CLAUDE_CREDS_DIR ?? path.join(home, ".claude");
  if (fs.existsSync(credsHostPath)) {
    return {
      mode: "subscription",
      hostPath: credsHostPath,
      // Inside the sandbox, claude looks at the agent user's home — tilde
      // expansion in MountConfig handles the actual /home/agent/.claude
      // resolution.
      sandboxPath: "~/.claude",
    };
  }
  // 2. API key fallback
  if (env.ANTHROPIC_API_KEY) {
    return {
      mode: "api-key",
      env: { ANTHROPIC_API_KEY: env.ANTHROPIC_API_KEY },
    };
  }
  if (env.OPENAI_API_KEY) {
    return { mode: "api-key", env: { OPENAI_API_KEY: env.OPENAI_API_KEY } };
  }
  // 3. Neither available
  return { mode: "missing" };
}

function printPlan() {
  const next = findNextTask();
  if (!next) {
    console.log("No ready task to dispatch.");
    console.log("Run `pnpm work blocked` to see what's waiting on what.");
    process.exit(0);
  }
  console.log("=== Dispatch plan ===");
  console.log(`  Epic:     ${next.epic}`);
  console.log(`  Story:    ${next.story} — ${next.title}`);
  console.log(`  Bullet:   ${next.bulletLine.trim()}`);
  console.log(`  Prompt:   .sandcastle/implementer.prompt.md`);
  console.log();
  console.log("To execute this dispatch:");
  console.log(
    "  - With Claude subscription: `claude login` (one-time) then `pnpm work dispatch --execute`",
  );
  console.log(
    "  - With API key:             `ANTHROPIC_API_KEY=... pnpm work dispatch --execute`",
  );
  console.log();
  console.log(
    "(Execute mode requires @ai-hero/sandcastle, a sandbox provider, and auth — see above.)",
  );
}

async function executeDispatch() {
  const next = findNextTask();
  if (!next) {
    console.log("No ready task to dispatch.");
    process.exit(0);
  }

  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(
    `Dispatching: ${next.epic} / ${next.story} / ${next.bulletLine.trim()}`,
  );
  const taskSpec = buildTaskSpec(next);

  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);
  }

  // Build sandbox + agent providers based on auth mode
  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);

  // Implementer
  const implementerPrompt = path.join(SANDCASTLE_DIR, "implementer.prompt.md");
  let implResult;
  try {
    implResult = await sandcastleRoot.run({
      agent,
      sandbox,
      promptFile: implementerPrompt,
      promptArgs: { TASK_FILE_CONTENT: taskSpec },
      cwd: REPO_ROOT,
    });
  } catch (e) {
    console.error("✗ Implementer dispatch failed:", e.message);
    console.error(
      "  See docs/guides/runbook.md → 'Using Sandcastle' for setup.",
    );
    process.exit(1);
  }
  console.log(
    `Implementer returned. Branch: ${implResult.branch}, Commits: ${implResult.commits.length}`,
  );

  // Reviewer
  let diff = "";
  try {
    diff = execSync(`git diff main..${implResult.branch}`, {
      encoding: "utf8",
      cwd: REPO_ROOT,
    });
  } catch {
    diff = "(diff unavailable)";
  }
  const reviewerPrompt = path.join(SANDCASTLE_DIR, "reviewer.prompt.md");
  const reviewResult = await sandcastleRoot.run({
    agent,
    sandbox,
    promptFile: reviewerPrompt,
    promptArgs: { TASK_FILE_CONTENT: taskSpec, DIFF: diff },
    cwd: REPO_ROOT,
  });
  console.log(`Reviewer returned. stdout follows:\n${reviewResult.stdout}`);

  // V1: orchestrator does NOT auto-mutate state. Print what should happen.
  console.log();
  console.log("=== Suggested state mutation ===");
  console.log(`  Edit ${next.storyPath} — tick the bullet:`);
  console.log(`    ${next.bulletLine.trim().replace("[ ]", "[x]")}`);
  console.log(
    `  Then: pnpm work rebuild-state && git add -A && git commit -m "feat(...): ..."`,
  );
  console.log();
  console.log("(Automatic state mutation by the orchestrator is v2.)");
}

const args = process.argv.slice(2);
if (args.includes("--execute")) {
  executeDispatch();
} else {
  printPlan();
}
