#!/usr/bin/env node
// Blueprint harness CLI — orchestrates the scaffold-test pipeline.

import { cpSync, mkdirSync, existsSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
import { spawn, execFileSync } from 'node:child_process';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createInterface } from 'node:readline';

import { prepWorkspace } from './lib/prep.mjs';
import { synthesizeReport } from './lib/synthesize.mjs';

const __dirname = dirname(fileURLToPath(import.meta.url));
const MONOREPO_ROOT = resolve(__dirname, '../..');
const COLD_PROMPT_TEMPLATE = resolve(__dirname, 'templates/cold-prompt.md');

const USAGE = `Usage:
  blueprint-harness run <blueprint>
  blueprint-harness prep <blueprint> [--workspace=<dir>]
  blueprint-harness prompt <workspace>
  blueprint-harness smoke <workspace> [--port=5173] [--output=<dir>]
  blueprint-harness synthesize <workspace> --subagent-report=<file> [--output=<dir>]
  blueprint-harness promote <workspace> [--slug=<blueprint>] [--force]

Options:
  --workspace=<dir>          Workspace path (default: /tmp/harness-<date>-<blueprint>/my-app)
  --output=<dir>             Output dir for artifacts (default: .claude/harness-runs/<date>-<blueprint>)
  --port=<n>                 Dev server port (default: 5173)
  --subagent-report=<file>   Path to the cold-subagent report markdown
  --force                    Replace an existing showcase-host capsule during promote
`;

// Derive a short set of representative routes from the scaffolded essence.
// The mobile-smoke default (landing / login / #/agents / #/marketplace) works
// only for agent-marketplace; other blueprints use different primary routes.
// Strategy: always probe `/`, always probe the first route whose path or id
// suggests auth, then the first "primary-looking" route, then the first
// "auxiliary-looking" route, falling back to just the first two routes.
function detectProbeRoutes(workspace) {
  const essencePath = join(workspace, 'decantr.essence.json');
  if (!existsSync(essencePath)) return null;
  const e = JSON.parse(readFileSync(essencePath, 'utf8'));
  const routes = e?.blueprint?.routes ?? [];
  if (!Array.isArray(routes) || routes.length === 0) return null;

  const norm = routes.map((r) => ({
    path: r.path ?? r.route ?? '/',
    section: r.section ?? '',
    id: r.id ?? r.page_id ?? '',
    role: r.role ?? '',
  }));
  const isAuthy = (r) => /auth|login|sign.?in/i.test(`${r.path} ${r.section} ${r.id}`);
  const authRoute = norm.find(isAuthy);

  const nonAuth = norm.filter((r) => !isAuthy(r));
  const primary = nonAuth.find((r) => r.role === 'primary' || /dashboard|home|overview|index/i.test(r.id)) ?? nonAuth[0];
  const secondary = nonAuth.find((r) => r !== primary) ?? nonAuth[1] ?? null;

  const hashify = (p) => (p === '/' ? '/' : `/#${p}`);
  const out = [{ name: 'landing', path: '/' }];
  if (authRoute) out.push({ name: 'login', path: hashify(authRoute.path) });
  if (primary && primary.path !== '/') out.push({ name: 'primary', path: hashify(primary.path) });
  if (secondary && secondary.path !== '/') out.push({ name: 'secondary', path: hashify(secondary.path) });
  return out.slice(0, 4);
}

// Blueprint slug reliably lives at `.decantr/project.json#blueprintId`.
// essence.json's `blueprint` key is an object of (sections, features, routes)
// without its own slug — another docs-vs-reality gap worth noting.
function detectBlueprintSlug(workspace) {
  const projectPath = join(workspace, '.decantr/project.json');
  if (existsSync(projectPath)) {
    const p = JSON.parse(readFileSync(projectPath, 'utf8'));
    if (typeof p.blueprintId === 'string') return p.blueprintId;
    if (typeof p.blueprint === 'string') return p.blueprint;
  }
  const essencePath = join(workspace, 'decantr.essence.json');
  if (existsSync(essencePath)) {
    const e = JSON.parse(readFileSync(essencePath, 'utf8'));
    if (typeof e.blueprint === 'string') return e.blueprint;
    if (typeof e.blueprint_id === 'string') return e.blueprint_id;
    if (typeof e?.meta?.blueprint === 'string') return e.meta.blueprint;
  }
  return 'unknown';
}

function parseArgs(argv) {
  const out = { _: [] };
  for (const a of argv) {
    if (a.startsWith('--')) {
      const [k, v] = a.slice(2).split('=');
      out[k] = v ?? true;
    } else {
      out._.push(a);
    }
  }
  return out;
}

function today() {
  // LOCAL date, not UTC. The harness writes paths like
  // /tmp/harness-YYYY-MM-DD-{slug}/ for prep, prompt, smoke, synthesize.
  // If `today()` returned UTC, evening-PT sessions would prep into
  // /tmp/harness-2026-04-26-... while a same-session dispatch would still
  // resolve /tmp/harness-2026-04-25-... → empty workspace, void run.
  // Local-date keeps every phase aligned for the duration of the operator's
  // working session.
  const d = new Date();
  const yyyy = d.getFullYear();
  const mm = String(d.getMonth() + 1).padStart(2, '0');
  const dd = String(d.getDate()).padStart(2, '0');
  return `${yyyy}-${mm}-${dd}`;
}

function defaultWorkspace(blueprint) {
  return `/tmp/harness-${today()}-${blueprint}/my-app`;
}

function defaultOutputDir(blueprint) {
  return resolve(MONOREPO_ROOT, `.claude/harness-runs/${today()}-${blueprint}`);
}

function renderColdPrompt({ blueprint, workspace }) {
  const template = readFileSync(COLD_PROMPT_TEMPLATE, 'utf8');
  return template
    .replaceAll('{{BLUEPRINT}}', blueprint)
    .replaceAll('{{WORKSPACE}}', workspace);
}

async function waitForDevServer({ port, timeoutMs = 15000 }) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    try {
      const res = await fetch(`http://localhost:${port}/`);
      if (res.ok) return true;
    } catch {
      /* not up yet */
    }
    await new Promise((r) => setTimeout(r, 300));
  }
  return false;
}

async function startDevServer(workspace, port) {
  const child = spawn('npm', ['run', 'dev', '--', '--port', String(port), '--strictPort'], {
    cwd: workspace,
    stdio: ['ignore', 'pipe', 'pipe'],
    env: { ...process.env, FORCE_COLOR: '0' },
  });
  const ok = await waitForDevServer({ port });
  if (!ok) {
    child.kill('SIGTERM');
    throw new Error(`Dev server did not come up on :${port} within timeout.`);
  }
  return child;
}

function prompt(question) {
  return new Promise((res) => {
    const rl = createInterface({ input: process.stdin, output: process.stdout });
    rl.question(question, (a) => {
      rl.close();
      res(a);
    });
  });
}

async function cmdPrep(args) {
  const blueprint = args._[0];
  if (!blueprint) throw new Error('missing blueprint slug');
  const workspace = args.workspace ?? defaultWorkspace(blueprint);
  console.log(`Preparing workspace at ${workspace}...`);
  const result = prepWorkspace({ blueprint, workspace });
  console.log(`✓ Scaffold ready at ${workspace}`);
  console.log(`  contract shape:    ${result.contractShape}`);
  console.log(`  context files:     ${result.contextFileCount}`);
  console.log(`  scaffold-pack.md:  ${result.hasScaffoldPack ? 'yes' : 'NO'}`);
  console.log(`  pack-manifest:     ${result.hasPackManifest ? 'yes' : 'NO'}`);
  console.log(`  section packs:     ${result.hasAnySectionPack ? 'yes' : 'NO'}`);
  console.log(`  page packs:        ${result.hasAnyPagePack ? 'yes' : 'NO'}`);
  if (result.contractShape !== 'pack-style') {
    console.log(
      `\n⚠  This blueprint did NOT compile execution packs. The cold-prompt's` +
        `\n   "treat compiled execution-pack files as primary source of truth"` +
        `\n   instruction will not apply as written. Record this as a finding.`,
    );
  }
  return { workspace, blueprint, ...result };
}

async function cmdPrompt(args) {
  const workspace = args._[0];
  if (!workspace) throw new Error('missing workspace path');
  const blueprint = detectBlueprintSlug(workspace);
  const text = renderColdPrompt({ blueprint, workspace });
  const harnessDir = join(workspace, '_harness');
  mkdirSync(harnessDir, { recursive: true });
  const file = join(harnessDir, 'cold-prompt.md');
  writeFileSync(file, text);
  console.log(text);
  console.log(`\n✓ Prompt written to ${file}`);
  console.log(`\nNext: dispatch a cold Agent (general-purpose, model: opus, background) with the prompt above.`);
  console.log(`When the agent returns, save its full report to:\n  ${join(harnessDir, 'subagent-report.md')}`);
  return { file, blueprint };
}

async function cmdSmoke(args) {
  const workspace = args._[0];
  if (!workspace) throw new Error('missing workspace path');
  const port = Number(args.port ?? 5173);
  const outputDir = args.output ?? join(workspace, '_harness/mobile-shots');

  console.log(`Starting dev server on :${port}...`);
  const dev = await startDevServer(workspace, port);
  console.log(`✓ Dev server up. Running mobile smoke...`);
  const { runMobileSmoke } = await import('./lib/mobile-smoke.mjs');
  const routes = detectProbeRoutes(workspace);
  if (routes) {
    console.log(`  probing ${routes.length} blueprint-derived routes: ${routes.map((r) => r.path).join(', ')}`);
  } else {
    console.log(`  using default routes (couldn't derive from essence)`);
  }
  try {
    const results = await runMobileSmoke({
      baseUrl: `http://localhost:${port}`,
      outputDir,
      ...(routes ? { routes } : {}),
    });
    console.log(`✓ ${results.filter((r) => r.ok).length}/${results.length} shots captured`);
    writeFileSync(
      join(dirname(outputDir), 'smoke-results.json'),
      JSON.stringify(results, null, 2),
    );
    return { results, outputDir };
  } finally {
    dev.kill('SIGTERM');
  }
}

async function cmdSynthesize(args) {
  const workspace = args._[0];
  if (!workspace) throw new Error('missing workspace path');
  const subagentReportPath =
    args['subagent-report'] ?? join(workspace, '_harness/subagent-report.md');
  const harnessDir = join(workspace, '_harness');
  const smokeResultsPath = join(harnessDir, 'smoke-results.json');
  const smokeResults = existsSync(smokeResultsPath)
    ? JSON.parse(readFileSync(smokeResultsPath, 'utf8'))
    : [];
  const blueprint = detectBlueprintSlug(workspace);

  const outputDir = args.output ?? defaultOutputDir(blueprint);
  mkdirSync(outputDir, { recursive: true });
  const outputPath = join(outputDir, 'report.md');

  const result = synthesizeReport({
    workspace,
    blueprint,
    subagentReportPath,
    mobileShotsDir: join(harnessDir, 'mobile-shots'),
    smokeResults,
    outputPath,
  });

  console.log(`✓ Report: ${outputPath}`);
  console.log(`  Source files: ${result.src.files} (${result.src.lines} lines)`);
  console.log(`  Inline styles (auto-counted): ${result.inlineStyles}`);
  return result;
}

function assertPromotableWorkspace(workspace) {
  const required = [
    'src',
    'DECANTR.md',
    'decantr.essence.json',
    '.decantr/project.json',
  ];
  const missing = required.filter((relativePath) => !existsSync(join(workspace, relativePath)));
  if (missing.length > 0) {
    throw new Error(`workspace is missing required scaffold artifacts: ${missing.join(', ')}`);
  }
}

async function cmdPromote(args) {
  const workspace = args._[0];
  if (!workspace) throw new Error('missing workspace path');
  assertPromotableWorkspace(workspace);

  const slug = args.slug ?? detectBlueprintSlug(workspace);
  if (!slug || slug === 'unknown') {
    throw new Error('unable to detect blueprint slug; pass --slug=<blueprint>');
  }

  const target = resolve(MONOREPO_ROOT, 'apps/showcase-host/src/capsules', slug);
  if (existsSync(target)) {
    if (args.force !== true) {
      throw new Error(`capsule already exists at ${target}; pass --force to replace it`);
    }
    rmSync(target, { recursive: true, force: true });
  }

  mkdirSync(target, { recursive: true });
  cpSync(join(workspace, 'src'), join(target, 'src'), { recursive: true });
  cpSync(join(workspace, 'DECANTR.md'), join(target, 'DECANTR.md'));
  cpSync(join(workspace, 'decantr.essence.json'), join(target, 'decantr.essence.json'));
  cpSync(join(workspace, '.decantr'), join(target, '.decantr'), { recursive: true });

  console.log(`✓ Promoted ${slug} -> ${target}`);
  console.log('Next: run `pnpm run showcase:verify:shortlist` from the monorepo root.');
  return { slug, target };
}

async function cmdRun(args) {
  const blueprint = args._[0];
  if (!blueprint) throw new Error('missing blueprint slug');
  const workspace = args.workspace ?? defaultWorkspace(blueprint);

  await cmdPrep({ _: [blueprint], workspace });
  await cmdPrompt({ _: [workspace] });

  console.log('\n' + '─'.repeat(60));
  console.log('OPERATOR STEP: dispatch the cold subagent now.');
  console.log('─'.repeat(60));
  console.log(`  1. Copy the prompt from ${join(workspace, '_harness/cold-prompt.md')}`);
  console.log(`  2. Invoke Agent tool: subagent_type=general-purpose, model=opus, run_in_background=true`);
  console.log(`  3. When it returns, save the full response to ${join(workspace, '_harness/subagent-report.md')}`);

  const answer = await prompt('\nPress [Enter] when the subagent report is saved, or type "skip" to stop here: ');
  if (String(answer).trim().toLowerCase() === 'skip') {
    console.log('Stopped. Run `blueprint-harness smoke` and `synthesize` later.');
    return;
  }

  await cmdSmoke({ _: [workspace] });
  await cmdSynthesize({ _: [workspace] });
  console.log('\n✓ Harness run complete.');
}

async function main() {
  const [, , cmd, ...rest] = process.argv;
  if (!cmd || cmd === '-h' || cmd === '--help') {
    console.log(USAGE);
    return;
  }
  const args = parseArgs(rest);
  const dispatch = {
    run: cmdRun,
    prep: cmdPrep,
    prompt: cmdPrompt,
    smoke: cmdSmoke,
    synthesize: cmdSynthesize,
    promote: cmdPromote,
  };
  const fn = dispatch[cmd];
  if (!fn) {
    console.error(`unknown command: ${cmd}\n\n${USAGE}`);
    process.exit(2);
  }
  try {
    await fn(args);
  } catch (err) {
    console.error(`error: ${err.message}`);
    process.exit(1);
  }
}

main();
