const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const UI = require('../utils/ui');
const toolAdapters = require('../utils/tool-adapters');
const LspInstaller = require('../utils/lsp-installer');
const lspAdapters = require('../utils/lsp-adapters');
const GraphifyInstaller = require('../utils/graphify-installer');

const CONFIG_FILE = 'baldart.config.yml';
// The subtree pull copies the entire BALDART repo (which itself has a
// top-level `framework/` directory) into `.framework/`. So the in-consumer
// path is `.framework/framework/templates/...` with the doubled segment.
const TEMPLATE_PATH = path.join('.framework', 'framework', 'templates', 'baldart.config.template.yml');
const OVERLAYS_DIR = path.join('.baldart', 'overlays');
const SCHEMA_VERSION = 1;

/**
 * Filesystem probes — autodetection heuristics aligned with
 * framework/agents/project-context.md § 6.
 *
 * Returns an object with the same shape as baldart.config.yml, populated
 * only with detected values. Empty / undetected keys remain at template
 * defaults and the user is prompted explicitly for them.
 */
const IGNORED_DIRS = new Set([
  'node_modules', '.git', '.next', '.nuxt', '.turbo', '.cache', '.svelte-kit',
  'dist', 'build', 'out', 'coverage', '.vercel', '.framework', '.baldart',
  '.expo', '.parcel-cache', 'tmp', '.idea', '.vscode', '.DS_Store'
]);

/**
 * Probe origin/develop branch protection to recommend a merge strategy.
 * Returns: { value, protected, reason }
 *   - 'pr'         when develop is protected (PR is the only viable path)
 *   - 'local-push' when develop is NOT protected (FF push is faster, no gh needed)
 *   - 'pr' as a safe default if we can't tell (gh missing, network down, no remote)
 *
 * Non-fatal: any failure returns `{ value: 'pr', protected: null }` so configure
 * keeps moving. The user can override in the interactive prompt.
 */
function detectMergeStrategy(cwd = process.cwd(), trunk = 'develop') {
  try {
    const { execSync } = require('child_process');
    const opts = { cwd, stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000, encoding: 'utf8' };
    // Need both git + gh. If either is absent or unauthenticated, default 'pr'.
    try { execSync('gh --version', opts); } catch (_) {
      return { value: 'pr', protected: null, reason: 'gh CLI not installed — defaulting to pr.' };
    }
    let nameWithOwner = '';
    try {
      nameWithOwner = execSync('gh repo view --json nameWithOwner -q .nameWithOwner', opts).trim();
    } catch (_) {
      return { value: 'pr', protected: null, reason: 'no GitHub remote detected — defaulting to pr.' };
    }
    // Probe branch protection on the resolved trunk. 404 means "not protected" → local-push viable.
    try {
      execSync(`gh api repos/${nameWithOwner}/branches/${trunk}/protection`, opts);
      return { value: 'pr', protected: true, reason: `${trunk} is protected on origin — PR strategy required.` };
    } catch (_) {
      // Could be 404 (not protected) OR branch doesn't exist. Check existence:
      try {
        execSync(`gh api repos/${nameWithOwner}/branches/${trunk} -q .name`, opts);
        return { value: 'local-push', protected: false, reason: `${trunk} exists on origin and is not protected — local-push viable.` };
      } catch (_) {
        return { value: 'pr', protected: null, reason: `${trunk} branch not found on origin — defaulting to pr.` };
      }
    }
  } catch (_) {
    return { value: 'pr', protected: null, reason: 'probe failed — defaulting to pr.' };
  }
}

/**
 * Autodetect the integration trunk branch (git.trunk_branch).
 * Order: origin/HEAD symbolic-ref → existing local `develop` → existing local
 * `main`/`master` → 'develop'. Non-fatal: any failure returns the 'develop'
 * back-compat default so configure keeps moving; the user can override.
 */
function detectTrunkBranch(cwd = process.cwd()) {
  try {
    const { execSync } = require('child_process');
    const opts = { cwd, stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000, encoding: 'utf8' };
    try {
      const ref = execSync('git symbolic-ref refs/remotes/origin/HEAD', opts).trim();
      const name = ref.replace(/^refs\/remotes\/origin\//, '');
      if (name) return { value: name, reason: `origin/HEAD points at ${name}.` };
    } catch (_) { /* fall through to local branch probes */ }
    const branches = (() => {
      try { return execSync('git branch --format=%(refname:short)', opts).split('\n').map((s) => s.trim()); }
      catch (_) { return []; }
    })();
    for (const cand of ['develop', 'main', 'master']) {
      if (branches.includes(cand)) return { value: cand, reason: `local '${cand}' branch found.` };
    }
    return { value: 'develop', reason: 'no trunk detected — defaulting to develop.' };
  } catch (_) {
    return { value: 'develop', reason: 'probe failed — defaulting to develop.' };
  }
}

/**
 * Autodetect whether this project develops in parallel git worktrees
 * (`git worktree list` shows linked worktrees beyond the main one). Used to
 * default `stack.schema_deploy_from_trunk_only`: parallel worktrees against a
 * single shared remote datastore are exactly the topology where a worktree-run
 * schema deploy causes migration-history desync / code-ahead-of-schema outages.
 * Non-fatal: any failure returns false (the back-compat default).
 */
function detectWorktreeUsage(cwd = process.cwd()) {
  try {
    const { execSync } = require('child_process');
    const opts = { cwd, stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000, encoding: 'utf8' };
    const out = execSync('git worktree list --porcelain', opts);
    const count = (out.match(/^worktree /gm) || []).length;
    return count > 1; // main worktree always counts as 1 — >1 means linked worktrees exist
  } catch (_) {
    return false;
  }
}

function detect(cwd = process.cwd()) {
  const exists = (p) => fs.existsSync(path.join(cwd, p));
  const findFirst = (...candidates) => candidates.find(exists) || '';
  const readJsonSafe = (p) => {
    try { return JSON.parse(fs.readFileSync(path.join(cwd, p), 'utf8')); } catch { return null; }
  };
  const readSafe = (p) => {
    try { return fs.readFileSync(path.join(cwd, p), 'utf8'); } catch { return ''; }
  };
  const isDir = (p) => {
    try { return fs.statSync(path.join(cwd, p)).isDirectory(); } catch { return false; }
  };
  const countMatches = (dir, regex) => {
    const abs = path.join(cwd, dir);
    if (!fs.existsSync(abs)) return 0;
    try { return fs.readdirSync(abs).filter((f) => regex.test(f)).length; }
    catch { return 0; }
  };

  // Shallow recursive walk (depth-limited, ignores known noise dirs).
  // Returns first relative path matching `predicate(name, fullRelPath)`, or ''.
  const walkFirst = (predicate, maxDepth = 3, startRel = '') => {
    const stack = [{ rel: startRel, depth: 0 }];
    while (stack.length) {
      const { rel, depth } = stack.pop();
      const abs = path.join(cwd, rel);
      let entries;
      try { entries = fs.readdirSync(abs, { withFileTypes: true }); }
      catch { continue; }
      for (const ent of entries) {
        if (ent.name.startsWith('.') && ent.name !== '.storybook') continue;
        if (IGNORED_DIRS.has(ent.name)) continue;
        const childRel = path.join(rel, ent.name);
        if (predicate(ent.name, childRel, ent)) return childRel;
        if (ent.isDirectory() && depth < maxDepth) {
          stack.push({ rel: childRel, depth: depth + 1 });
        }
      }
    }
    return '';
  };

  const pkg = readJsonSafe('package.json');
  const deps = pkg ? { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) } : {};
  const hasDep = (name) => Object.keys(deps).some((d) => d === name || d.startsWith(`${name}/`));
  const hasAnyDep = (...names) => names.some(hasDep);

  // ---- Monorepo awareness ------------------------------------------------
  const monorepoMarkers = ['pnpm-workspace.yaml', 'lerna.json', 'nx.json', 'turbo.json', 'rush.json'];
  const isMonorepo = monorepoMarkers.some(exists) || Array.isArray(pkg?.workspaces) ||
    !!(pkg?.workspaces && typeof pkg.workspaces === 'object');

  const monorepoRoots = []; // list of relative dirs to also search inside
  if (isMonorepo) {
    for (const root of ['packages', 'apps', 'services', 'libs']) {
      if (isDir(root)) {
        try {
          fs.readdirSync(path.join(cwd, root), { withFileTypes: true })
            .filter((d) => d.isDirectory() && !d.name.startsWith('.'))
            .forEach((d) => monorepoRoots.push(path.join(root, d.name)));
        } catch { /* ignore */ }
      }
    }
  }

  // Expand a candidate-relative path (e.g. "src/components/ui") into every
  // realistic search location: root + each monorepo package.
  const expandCandidates = (...rels) => {
    const out = [];
    for (const rel of rels) {
      out.push(rel);
      for (const mr of monorepoRoots) out.push(path.join(mr, rel));
    }
    return out;
  };

  // ---- Design system detection (multi-signal) ----------------------------
  // Path signals
  const dsPathCandidates = expandCandidates(
    'docs/design-system',
    'packages/ui',
    'packages/design-system',
    'packages/components',
    'packages/ds',
    'src/design-system',
    'src/ui'
  );
  let designSystemPath = dsPathCandidates.find((p) => isDir(p)) || '';

  // Storybook signal
  const hasStorybook = isDir('.storybook') || monorepoRoots.some((r) => isDir(path.join(r, '.storybook')));

  // shadcn marker
  const hasShadcn = exists('components.json') ||
    monorepoRoots.some((r) => exists(path.join(r, 'components.json')));

  // Tailwind with custom theme
  const tailwindCfg = findFirst(
    'tailwind.config.ts', 'tailwind.config.js', 'tailwind.config.mjs', 'tailwind.config.cjs'
  );
  let hasTailwindTheme = false;
  if (tailwindCfg) {
    const body = readSafe(tailwindCfg);
    hasTailwindTheme = /theme\s*:\s*\{[\s\S]*(extend|colors|spacing|fontFamily|borderRadius)/m.test(body);
  }

  // Design system packages
  const dsLibraries = [
    '@radix-ui', '@chakra-ui', '@mui/material', '@mui/joy', 'antd', '@mantine/core',
    '@nextui-org/react', 'daisyui', 'flowbite-react', '@headlessui/react', '@arco-design/web-react',
    '@blueprintjs/core', '@fluentui/react'
  ];
  const dsLibInDeps = dsLibraries.find((n) => hasDep(n)) || '';

  // Aggregate: a design system "exists" if ANY strong signal is present.
  const hasDesignSystem = !!(designSystemPath || hasStorybook || hasShadcn || hasTailwindTheme || dsLibInDeps);

  // If we found no explicit path but we know one exists by other means,
  // best-effort point to the most likely location for skills to read.
  if (!designSystemPath && hasDesignSystem) {
    designSystemPath = findFirst('docs/design-system', 'packages/ui', 'src/design-system') || '';
  }

  // ---- UI guidelines (widened) -------------------------------------------
  const uiGuidelines = findFirst(
    ...expandCandidates(
      'docs/references/ui-guidelines.md',
      'docs/ui-guidelines.md',
      'docs/references/brand-guidelines.md',
      'docs/brand-guidelines.md',
      'docs/style-guide.md',
      'docs/styleguide.md',
      'docs/brand/README.md',
      'STYLEGUIDE.md',
      'BRANDING.md',
      'BRAND.md'
    )
  );

  // ---- Components paths (monorepo-aware) ---------------------------------
  // Try the conventional UI primitive locations first; if none exist but a
  // design system path was detected, probe its common subdirs and only return
  // one that actually exists on disk.
  let componentsPrimitives = findFirst(
    ...expandCandidates('src/components/ui', 'app/components/ui', 'components/ui')
  );
  if (!componentsPrimitives && designSystemPath) {
    componentsPrimitives = findFirst(
      path.join(designSystemPath, 'src/components'),
      path.join(designSystemPath, 'components'),
      path.join(designSystemPath, 'src/ui'),
      designSystemPath // last-resort: the DS root itself
    );
  }

  const componentsRoot = findFirst(
    ...expandCandidates('src/components', 'app/components', 'components')
  );

  const globalStyles = findFirst(
    ...expandCandidates(
      'src/app/globals.css', 'app/globals.css',
      'src/styles/globals.css', 'styles/globals.css',
      'src/index.css', 'src/main.css', 'src/styles/index.css'
    )
  );

  // ---- API docs (widened — OpenAPI, GraphQL, populated dir) --------------
  const apiIndex = findFirst('docs/references/api/index.md', 'docs/api/index.md');
  const apiSchemas = findFirst('docs/references/api/schemas.md', 'docs/api/schemas.md');
  const apiErrors = findFirst('docs/references/errors.md', 'docs/errors.md');
  const openapiSpec = findFirst(
    ...expandCandidates(
      'openapi.yaml', 'openapi.yml', 'openapi.json',
      'api/openapi.yaml', 'api/openapi.yml', 'api/openapi.json',
      'docs/openapi.yaml', 'docs/openapi.yml', 'docs/openapi.json'
    )
  );
  const graphqlSchema = findFirst(
    ...expandCandidates('schema.graphql', 'schema.gql', 'src/schema.graphql')
  );
  // Many projects ship API docs as one markdown per resource (admin.md, push.md, …)
  // without a `schemas.md` or OpenAPI spec. Count any populated docs/.../api dir as
  // a positive signal.
  const apiDocsDir = findFirst('docs/references/api', 'docs/api');
  const apiDocsDirPopulated = apiDocsDir && countMatches(apiDocsDir, /\.md$/) > 0;

  // ---- Brand name (multi-fallback) ---------------------------------------
  let brandName = pkg?.name || '';
  // Strip scope from npm name (@org/foo → foo)
  if (brandName.startsWith('@') && brandName.includes('/')) brandName = brandName.split('/')[1];
  if (!brandName) {
    const readme = readSafe('README.md');
    const h1 = readme.split('\n').find((l) => /^#\s+\S/.test(l));
    if (h1) brandName = h1.replace(/^#\s+/, '').trim();
  }
  if (!brandName) brandName = path.basename(cwd);

  // ---- E2E framework -----------------------------------------------------
  const e2eTestsDir = findFirst(
    ...expandCandidates(
      'tests/e2e', 'e2e', 'tests/playwright', 'tests/cypress',
      'cypress/e2e', 'cypress/integration', 'playwright-tests'
    )
  );
  const e2eFramework = (
    exists('playwright.config.ts') || exists('playwright.config.js') ||
    exists('playwright.config.mjs') || hasDep('@playwright/test')
  ) ? 'playwright'
    : (exists('cypress.config.ts') || exists('cypress.config.js') || hasDep('cypress')) ? 'cypress'
    : '';

  const trunkBranchProbe = detectTrunkBranch(cwd);
  const mergeStrategyProbe = detectMergeStrategy(cwd, trunkBranchProbe.value);
  // Default schema_deploy_from_trunk_only ON when parallel worktrees are in use
  // (the topology prone to migration-history desync). Otherwise keep the
  // back-compat false default — the guard is a no-op unless explicitly enabled.
  const usesWorktrees = detectWorktreeUsage(cwd);

  // ---- Persistence layer (stack.database) --------------------------------
  let detectedDatabase = '';
  if (hasAnyDep('firebase', 'firebase-admin', '@google-cloud/firestore', '@firebase/firestore')) detectedDatabase = 'firestore';
  else if (hasAnyDep('@supabase/supabase-js', '@supabase/ssr', '@supabase/auth-helpers-nextjs')) detectedDatabase = 'supabase';
  else if (hasAnyDep('mongoose', 'mongodb')) detectedDatabase = 'mongodb';
  else if (hasAnyDep('pg', 'postgres', '@neondatabase/serverless', '@vercel/postgres')) detectedDatabase = 'postgres';
  else if (hasAnyDep('mysql', 'mysql2', '@planetscale/database')) detectedDatabase = 'mysql';
  else if (hasAnyDep('@aws-sdk/client-dynamodb', 'dynamodb-toolbox')) detectedDatabase = 'dynamodb';
  else if (hasAnyDep('better-sqlite3', 'sqlite3', '@libsql/client')) detectedDatabase = 'sqlite';

  // ---- Auth provider (stack.auth_provider) -------------------------------
  // Order matters: more specific deps win over the catch-all firebase/supabase.
  let detectedAuthProvider = '';
  if (hasAnyDep('@clerk/nextjs', '@clerk/clerk-sdk-node', '@clerk/clerk-js', '@clerk/backend')) detectedAuthProvider = 'clerk';
  else if (hasAnyDep('@auth0/nextjs-auth0', '@auth0/auth0-react', 'auth0')) detectedAuthProvider = 'auth0';
  else if (hasAnyDep('next-auth', '@auth/core')) detectedAuthProvider = 'nextauth';
  else if (hasAnyDep('lucia', '@lucia-auth/core', '@lucia-auth/adapter-prisma')) detectedAuthProvider = 'lucia';
  else if (hasAnyDep('amazon-cognito-identity-js', '@aws-sdk/client-cognito-identity-provider')) detectedAuthProvider = 'cognito';
  else if (hasAnyDep('@supabase/auth-helpers-nextjs', '@supabase/ssr')) detectedAuthProvider = 'supabase-auth';
  else if (hasAnyDep('firebase', '@firebase/auth', 'firebase-admin')) detectedAuthProvider = 'firebase-auth';

  // ---- Application framework (stack.framework) ---------------------------
  let detectedFramework = '';
  if (hasDep('next')) detectedFramework = 'nextjs';
  else if (hasAnyDep('@remix-run/react', '@remix-run/node', '@remix-run/server-runtime')) detectedFramework = 'remix';
  else if (hasAnyDep('@sveltejs/kit', 'svelte')) detectedFramework = 'sveltekit';
  else if (hasDep('astro')) detectedFramework = 'astro';
  else if (hasDep('nuxt')) detectedFramework = 'nuxt';
  else if (hasDep('express')) detectedFramework = 'express';
  else if (exists('Gemfile') && /\bgem\s+['"]rails['"]/.test((readSafe('Gemfile') || ''))) detectedFramework = 'rails';
  else if (exists('manage.py') || (exists('requirements.txt') && /\bdjango\b/i.test(readSafe('requirements.txt') || ''))) detectedFramework = 'django';
  else if (exists('requirements.txt') && /\bfastapi\b/i.test(readSafe('requirements.txt') || '')) detectedFramework = 'fastapi';

  // ---- Deployment target (stack.deployment) ------------------------------
  let detectedDeployment = '';
  if (exists('vercel.json') || hasDep('@vercel/node')) detectedDeployment = 'vercel';
  else if (exists('firebase.json')) detectedDeployment = 'firebase';
  else if (exists('wrangler.toml') || exists('wrangler.jsonc') || exists('wrangler.json')) detectedDeployment = 'cloudflare';
  else if (exists('fly.toml')) detectedDeployment = 'fly';
  else if (exists('render.yaml')) detectedDeployment = 'render';
  else if (exists('amplify.yml') || exists('serverless.yml')) detectedDeployment = 'aws';
  else if (exists('app.yaml') || exists('cloudbuild.yaml')) detectedDeployment = 'gcp';
  // Note: bare Dockerfile → leave empty; user picks "self-hosted" or actual target.

  const detected = {
    paths: {
      design_system: designSystemPath,
      ui_guidelines: uiGuidelines,
      api_index: apiIndex || (apiDocsDirPopulated ? path.join(apiDocsDir, 'index.md') : ''),
      api_schemas: apiSchemas || openapiSpec || graphqlSchema,
      api_errors: apiErrors,
      components_primitives: componentsPrimitives,
      components_root: componentsRoot,
      global_styles: globalStyles,
      backlog_dir: exists('backlog') ? 'backlog' : '',
      adrs_dir: countMatches('docs/decisions', /^ADR-.*\.md$/) > 0 ? 'docs/decisions' : '',
      prd_dir: exists('docs/prd') ? 'docs/prd' : '',
      docs_dir: exists('docs') ? 'docs' : '',
      references_dir: exists('docs/references') ? 'docs/references' : '',
      wiki_dir: exists('docs/wiki') ? 'docs/wiki' : '',
      e2e_tests_dir: e2eTestsDir,
      metrics: exists('docs/metrics') ? 'docs/metrics' : '',
      high_risk_modules: [],
    },
    identity: {
      brand_name: brandName,
      // Heuristic philosophy hint from stack (skill-readable; can still override).
      design_philosophy: hasShadcn ? 'Minimalist (shadcn/Tailwind)'
        : dsLibInDeps ? `Library-driven (${dsLibInDeps})`
        : '',
      language: 'en',
      audience_segments: [],
    },
    stack: {
      charting: {
        canonical: ['recharts', '@nivo/heatmap', '@nivo/bar', '@nivo/line', 'chart.js', 'd3', 'visx', '@tremor/react']
          .filter((n) => hasDep(n)),
        forbidden: [],
        wrappers_root: findFirst(...expandCandidates('src/components/charts', 'app/components/charts', 'components/charts')),
      },
      animation: {
        canonical: ['framer-motion', 'motion', 'lottie-react', 'gsap', '@react-spring/web', 'auto-animate']
          .filter((n) => hasDep(n)),
        forbidden: [],
      },
      testing: { e2e: e2eFramework },
      database: detectedDatabase,
      auth_provider: detectedAuthProvider,
      framework: detectedFramework,
      deployment: detectedDeployment,
      // Stack-agnostic schema-deploy safety invariant (v4.33.0). Defaulted ON when
      // parallel worktrees are detected (the desync-prone topology), else false.
      schema_deploy_from_trunk_only: usesWorktrees,
      // New: surface the detected monorepo + DS signals so skills can read them.
      monorepo: isMonorepo ? {
        detected: true,
        roots: monorepoRoots
      } : { detected: false, roots: [] },
      design_system_signals: {
        path: designSystemPath || null,
        storybook: hasStorybook,
        shadcn: hasShadcn,
        tailwind_theme: hasTailwindTheme,
        library: dsLibInDeps || null
      }
    },
    features: {
      has_design_system: hasDesignSystem,
      multi_tenant_theming: null,
      has_api_docs: !!(apiSchemas || openapiSpec || graphqlSchema || apiIndex || apiDocsDirPopulated),
      has_backlog: countMatches('backlog', /\.ya?ml$/) > 0,
      has_adrs: countMatches('docs/decisions', /^ADR-.*\.md$/) > 0,
      has_prd_workflow: exists('docs/prd'),
      has_wiki_overlay: exists('docs/wiki'),
      // LSP recommended when any supported language server adapter would
      // detect this project. Drives the default for the configure prompt.
      has_lsp_layer: lspAdapters.detectAll(cwd).length > 0,
      // Code graph recommended when the Graphify CLI is already on PATH.
      // Drives the default for the configure prompt; the install/build side
      // effects only run in the interactive branch (see below).
      has_code_graph: new GraphifyInstaller(cwd).detect(),
    },
    tools: {
      enabled: toolAdapters.defaultEnabled(cwd)
    },
    git: {
      // Set to plain strings — the structured probe details (`protected`,
      // `reason`) are kept on a sibling key so mergePreserving stays type-safe.
      trunk_branch: trunkBranchProbe.value,
      merge_strategy: mergeStrategyProbe.value,
    },
    _probes: {
      // Internal scratchpad — never serialized to YAML. Used by
      // interactivePrompts to surface the autodetection reasoning.
      merge_strategy: mergeStrategyProbe,
      trunk_branch: trunkBranchProbe,
    },
  };

  // Reference walkFirst once so the dead-code linter is happy and the helper
  // stays available for future probes (logo files, brand assets, …).
  void walkFirst;

  return detected;
}

/**
 * Deep merge: existing user values WIN over detected/proposed values.
 * Used by `baldart configure` to re-run idempotently without clobbering
 * customizations.
 *
 * IMPORTANT: empty string `''` is treated as a DELIBERATE user value
 * (the user said "this concept does not exist in my project"). Only
 * `null` / `undefined` / missing-key fall back to the source.
 */
function mergePreserving(target, source) {
  if (target === null || target === undefined) return source;
  if (Array.isArray(target)) return target.length ? target : source;
  if (typeof target !== 'object') return target;

  const out = { ...target };
  for (const key of Object.keys(source || {})) {
    if (!(key in out)) {
      out[key] = source[key];
    } else {
      out[key] = mergePreserving(out[key], source[key]);
    }
  }
  return out;
}

function loadExisting(cwd) {
  const full = path.join(cwd, CONFIG_FILE);
  if (!fs.existsSync(full)) return null;
  try {
    return yaml.load(fs.readFileSync(full, 'utf8'));
  } catch (err) {
    UI.warning(`Existing ${CONFIG_FILE} is not valid YAML: ${err.message}`);
    return null;
  }
}

function loadTemplateSkeleton(cwd) {
  const full = path.join(cwd, TEMPLATE_PATH);
  if (!fs.existsSync(full)) {
    throw new Error(
      `Template not found at ${TEMPLATE_PATH}. Run \`npx baldart add\` first.`
    );
  }
  return yaml.load(fs.readFileSync(full, 'utf8'));
}

async function promptForKey(label, defaultValue, kind = 'input') {
  if (kind === 'confirm') {
    return UI.confirm(label, defaultValue === true);
  }
  if (kind === 'select') {
    return UI.select(label, defaultValue);
  }
  const answer = await UI.input(label, defaultValue || '');
  return answer.trim();
}

async function interactivePrompts(merged, detected) {
  UI.section('Identity');
  merged.identity.brand_name = await promptForKey(
    'Brand / product name',
    merged.identity.brand_name || detected.identity.brand_name
  );
  // Skip the philosophy prompt if a design system was detected — the DS
  // already encodes the visual direction. The detected hint (e.g. "Minimalist
  // (shadcn/Tailwind)") is kept as metadata for skills that want a label.
  if (!detected.features.has_design_system) {
    merged.identity.design_philosophy = await promptForKey(
      'Design philosophy (e.g. "Neo-Brutalism", "Minimalist") — empty for neutral',
      merged.identity.design_philosophy
    );
  } else {
    UI.info(`Design system detected — skipping philosophy prompt (using "${detected.identity.design_philosophy || 'inherited from design system'}").`);
    merged.identity.design_philosophy = merged.identity.design_philosophy || detected.identity.design_philosophy;
  }
  merged.identity.language = await promptForKey(
    'Primary UI language (BCP-47, e.g. "en", "it")',
    merged.identity.language || 'en'
  );
  const segments = await promptForKey(
    'Audience segments (comma-separated, e.g. "merchant,customer") — empty for none',
    (merged.identity.audience_segments || []).join(',')
  );
  merged.identity.audience_segments = segments
    ? segments.split(',').map((s) => s.trim()).filter(Boolean)
    : [];

  // ---- AI tools (which CLI tools should this install target?) -----------
  UI.section('AI tools (which tools should consume the framework?)');
  merged.tools = merged.tools || {};
  const allTools = toolAdapters.listAdapters();
  const currentEnabled = (merged.tools.enabled && merged.tools.enabled.length)
    ? merged.tools.enabled
    : detected.tools.enabled;
  for (const toolName of allTools) {
    const adapter = toolAdapters.getAdapter(toolName);
    const currentlyOn = currentEnabled.includes(toolName);
    const detectedHint = (toolName !== 'claude' && detected.tools.enabled.includes(toolName))
      ? ' (detected on this machine)' : '';
    const want = await UI.confirm(`Install for ${adapter.label}?${detectedHint}`, currentlyOn);
    if (want && !currentEnabled.includes(toolName)) currentEnabled.push(toolName);
    if (!want) {
      const idx = currentEnabled.indexOf(toolName);
      if (idx >= 0) currentEnabled.splice(idx, 1);
    }
  }
  // Claude is the framework's primary target — refuse to disable it entirely.
  if (!currentEnabled.includes('claude')) {
    UI.warning('Claude Code cannot be disabled (primary target). Re-enabling.');
    currentEnabled.unshift('claude');
  }
  merged.tools.enabled = currentEnabled;

  // ---- Git merge strategy (since v3.9.0) --------------------------------
  UI.section('Git merge strategy (how `/mw` lands feature branches onto develop)');
  merged.git = merged.git || {};
  const probe = (detected._probes && detected._probes.merge_strategy)
    || { value: 'pr', protected: null };
  if (probe.reason) UI.info(probe.reason);
  const currentMerge = merged.git.merge_strategy || probe.value || 'pr';
  const chosenMerge = await UI.select(
    `Choose merge strategy (current: ${currentMerge})`,
    [
      { name: 'pr — open a GitHub PR, merge via `gh pr merge` (required if develop is protected)', value: 'pr' },
      { name: 'local-push — direct FF push to origin/develop (no PR, no `gh` dependency)', value: 'local-push' },
    ]
  );
  merged.git.merge_strategy = chosenMerge;
  if (chosenMerge === 'local-push' && probe.protected === true) {
    UI.warning('You picked `local-push` but develop appears protected on origin — pushes will be rejected. Switch back to `pr` if it fails.');
  }

  UI.section('Features (explicit yes/no — option A: always ask)');
  for (const flag of [
    ['has_design_system', 'Project has a documented design system?'],
    ['multi_tenant_theming', 'Project uses multi-tenant theming (per-tenant overrides)?'],
    ['has_api_docs', 'Project has canonical API docs?'],
    ['has_backlog', 'Project uses backlog-card workflow?'],
    ['has_adrs', 'Project uses ADR workflow?'],
    ['has_prd_workflow', 'Project uses PRD workflow (docs/prd/)?'],
    ['has_wiki_overlay', 'Project has LLM-wiki overlay (docs/wiki/)?'],
    ['has_lsp_layer', 'Enable LSP symbol-search layer? (recommended for large codebases)'],
    ['has_code_graph', 'Enable Graphify code-knowledge-graph layer? (structural queries + wiki feed; recommended for large codebases)'],
    ['has_e2e_review', 'Enable BLOCKING end-to-end review (Phase 2.6 of /new invokes /e2e-review — functional + visual fidelity gate)?'],
  ]) {
    const [key, question] = flag;
    const currentVal = merged.features[key];
    const detectedVal = detected.features[key];
    const proposedDefault = currentVal !== null && currentVal !== undefined
      ? currentVal
      : (detectedVal === true);
    merged.features[key] = await promptForKey(question, proposedDefault, 'confirm');
  }

  // ---- Design-system bootstrap hint (since v3.11.0) ---------------------
  // If the user said YES to has_design_system but autodetect found no path
  // (i.e. they WANT a registry but it doesn't exist yet), surface the
  // bootstrap skill so they don't end up with a flag that points at nothing.
  // If they said NO, plant a soft pointer for the future.
  if (merged.features.has_design_system === true && !detected.paths.design_system) {
    UI.info(
      'No design-system path was detected on disk. After configure, run\n' +
      '  /design-system-init  (or invoke the skill)\n' +
      'to scaffold INDEX.md + tokens-reference.md + per-component specs at\n' +
      'docs/design-system/. The registry-first protocol (ui-expert, ui-design,\n' +
      'frontend-design, /design-review) needs these files to enforce coherence.'
    );
  } else if (merged.features.has_design_system === false) {
    UI.info(
      'has_design_system=false — UI work will rely only on ui_guidelines.md.\n' +
      'When you want visual coherence + component reuse enforced project-wide,\n' +
      'run /design-system-init to scaffold a registry from your existing code.'
    );
  }

  // Path prompts — only ask for paths whose feature is enabled and value missing
  UI.section('Paths (autodetected — confirm or override)');
  const pathPrompts = [
    ['design_system', 'Design-system root', () => merged.features.has_design_system],
    ['ui_guidelines', 'UI guidelines doc', () => true],
    ['api_index', 'API index doc', () => merged.features.has_api_docs],
    ['api_schemas', 'API schemas doc', () => merged.features.has_api_docs],
    ['api_errors', 'API errors doc', () => merged.features.has_api_docs],
    ['components_primitives', 'UI primitives directory (e.g. src/components/ui)', () => true],
    ['components_root', 'Components root directory', () => true],
    ['global_styles', 'Global stylesheet path', () => true],
    ['backlog_dir', 'Backlog directory', () => merged.features.has_backlog],
    ['adrs_dir', 'ADR directory', () => merged.features.has_adrs],
    ['prd_dir', 'PRD directory', () => merged.features.has_prd_workflow],
    ['docs_dir', 'Docs root directory (rg-search umbrella)', () => true],
    ['references_dir', 'References docs root', () => true],
    ['wiki_dir', 'Wiki overlay directory', () => merged.features.has_wiki_overlay],
    ['e2e_tests_dir', 'E2E tests directory', () => true],
  ];

  for (const [key, label, gate] of pathPrompts) {
    if (!gate()) {
      merged.paths[key] = '';
      continue;
    }
    const proposed = merged.paths[key] || detected.paths[key] || '';
    merged.paths[key] = await promptForKey(`${label}`, proposed);
  }

  // ---- E2E review tuning (since v3.18.0) --------------------------------
  // Only runs when the user opted in above. Populates the e2e_review.*
  // sub-section with the severity floor, self-heal cap, pixel pre-filter
  // threshold, and override-reason requirement. Idempotent: re-running
  // configure on an already-installed project re-asks each value with the
  // current YAML value as default.
  merged.e2e_review = merged.e2e_review || {};
  if (merged.features.has_e2e_review === true) {
    UI.section('E2E review tuning (/e2e-review skill — Phase 2.6 of /new)');
    const tolDefault = merged.e2e_review.fidelity_tolerance || 'strict';
    const tolerance = await UI.select(
      `Severity floor that BLOCKS the commit (current: ${tolDefault})`,
      [
        { name: 'strict   — Critical + Major + Minor block (recommended for design-system-strict projects)', value: 'strict' },
        { name: 'balanced — Critical + Major block; Minor advisory', value: 'balanced' },
        { name: 'lenient  — Critical blocks; Major + Minor advisory', value: 'lenient' },
      ]
    );
    merged.e2e_review.fidelity_tolerance = tolerance;

    const maxIterDefault = merged.e2e_review.max_self_heal_iterations ?? 2;
    const maxIter = await promptForKey(
      'Max self-heal iterations before override path',
      String(maxIterDefault)
    );
    merged.e2e_review.max_self_heal_iterations = Number.isFinite(Number(maxIter)) ? Number(maxIter) : 2;

    const diffDefault = merged.e2e_review.pixel_diff_threshold ?? 0.02;
    const diffThr = await promptForKey(
      'Pixel-diff pre-filter threshold (0.0 = always invoke Vision, 1.0 = never)',
      String(diffDefault)
    );
    merged.e2e_review.pixel_diff_threshold = Number.isFinite(Number(diffThr)) ? Number(diffThr) : 0.02;

    const reqReasonDefault = merged.e2e_review.require_override_reason !== undefined
      ? merged.e2e_review.require_override_reason
      : true;
    merged.e2e_review.require_override_reason = await promptForKey(
      'Require a written reason when the user overrides the gate after self-heal exhaustion?',
      reqReasonDefault,
      'confirm'
    );

    if (!merged.paths.e2e_tests_dir) {
      UI.info(
        'E2E review requires `paths.e2e_tests_dir` set and `@playwright/test` installed.\n' +
        'The path prompt above asked for the directory — confirm it exists or run `npm i -D @playwright/test` later.'
      );
    }
  } else {
    UI.info(
      'has_e2e_review=false — /new Phase 2.6 silently skips the BLOCKING gate.\n' +
      'Re-run `baldart configure` to enable when you want functional + visual fidelity enforced per card.'
    );
  }

  // ---- LSP layer (since v3.10.0) ----------------------------------------
  // Only runs when the user opted in above. Idempotent: re-running configure
  // on an already-installed project re-detects, asks which servers to keep,
  // and updates lsp.installed_servers accordingly.
  merged.lsp = merged.lsp || { installed_servers: [], auto_verify: true };
  if (merged.features.has_lsp_layer === true) {
    UI.section('LSP layer (install language servers for symbol search)');
    const installer = new LspInstaller(process.cwd());
    const detectedLangs = installer.detectLanguages();
    if (detectedLangs.length === 0) {
      UI.warning('No supported languages detected (tsconfig.json, pyproject.toml, go.mod, Cargo.toml, Gemfile).');
      UI.info('Skipping LSP install. You can run `/lsp-bootstrap` or `npx baldart configure` later.');
    } else {
      const recs = installer.recommend(detectedLangs);
      const already = new Set(merged.lsp.installed_servers || []);
      const toInstall = [];
      for (const r of recs) {
        const note = already.has(r.name) ? ' (already recorded)' : '';
        UI.info(`${r.label} → ${r.binary} (${r.installMode})${note}`);
        const want = await UI.confirm(`Install ${r.label}?`, true);
        if (want) toInstall.push(r.name);
      }
      if (toInstall.length) {
        const { installed, skipped, manual } = await installer.installServers(toInstall, { interactive: false });
        for (const m of manual) {
          UI.warning(`System-level install required for ${m.label}:`);
          UI.code(m.command, `Recorded as pending — run \`baldart doctor\` after installing to verify.`);
        }
        const verify = installer.verifyServers(installed.map(i => i.name));
        const verified = verify.filter(v => v.ok).map(v => v.name);
        // Every known-adapter result that isn't cleanly `verified` is STILL
        // recorded (like system-mode pending installs): dropping it would make
        // every re-run reinstall and `baldart doctor` would never see the
        // mismatch. This covers `installed-but-unreachable` AND the npm-global
        // happy-path trap where `npm install -g` exited 0 but npm's global bin
        // dir isn't on the current `$PATH` (→ status `not-installed`, since npx
        // can't see the global prefix either). Surface the fix in both cases.
        const needsAttention = verify.filter(v => !v.ok && v.status !== 'unknown-adapter');
        for (const u of needsAttention) {
          UI.warning(`${u.name}: not reachable on $PATH (status: ${u.status}) — Claude Code's LSP tool can't spawn it.`);
          if (u.fix) {
            UI.code(u.fix, 'Run this, then ensure npm\'s global bin dir (`npm prefix -g` → /bin) is on $PATH; re-run `baldart doctor` to verify.');
          }
        }
        const newSet = new Set([
          ...(merged.lsp.installed_servers || []),
          ...verified,
          ...needsAttention.map(u => u.name),
          ...manual.map(m => m.name)
        ]);
        merged.lsp.installed_servers = Array.from(newSet);
        if (skipped.length) {
          UI.warning(`Skipped: ${skipped.map(s => `${s.name} (${s.reason})`).join(', ')}`);
        }
      }

      // ---- Native Claude Code companion plugins (since v3.14.0) ----------
      // Install the official LSP plugin alongside each language server. Without
      // this step, opting into the LSP layer only puts the binary on disk —
      // Claude itself never sees it and agents fall back to Grep silently.
      // Gated on `claude` being in `tools.enabled` and the `claude` CLI being
      // on PATH; degrades gracefully otherwise.
      const allLspServers = Array.from(new Set([
        ...(merged.lsp.installed_servers || [])
      ]));
      if (allLspServers.length) {
        const pluginResult = await installer.installClaudePlugins(allLspServers, {
          toolsEnabled: merged.tools.enabled,
          interactive: false,
          scope: 'user'
        });
        if (pluginResult.reason === 'claude not in tools.enabled') {
          UI.info('Skipping native Claude plugin install (claude not in tools.enabled).');
        } else if (pluginResult.reason === 'claude CLI not on PATH') {
          UI.warning('`claude` CLI not on PATH — native LSP plugins not installed.');
          UI.info('Install them manually after adding the CLI:');
          for (const m of pluginResult.skipped) {
            // We don't have manual[] entries here (no targets resolved past the
            // gate), so synthesize the command from the adapter directly.
            try {
              const a = lspAdapters.getAdapter(m.name, process.cwd());
              const pid = a.claudePluginId();
              if (pid) UI.code(`claude plugin install ${pid} --scope user`, m.name);
            } catch { /* unknown adapter */ }
          }
        } else {
          if (pluginResult.installed.length) {
            UI.success(`Installed Claude LSP plugin(s): ${pluginResult.installed.map(i => i.pluginId).join(', ')}`);
          }
          const alreadyOnes = pluginResult.skipped.filter(s => s.reason === 'plugin already installed');
          if (alreadyOnes.length) {
            UI.info(`Already installed: ${alreadyOnes.map(s => s.name).join(', ')}`);
          }
          for (const m of pluginResult.manual) {
            UI.warning(`Could not auto-install ${m.label} plugin — run manually:`);
            UI.code(m.command, '');
          }
        }
      }
    }
  } else {
    // Feature disabled — wipe the state so `installed_servers` doesn't lie.
    merged.lsp.installed_servers = [];
  }

  // ---- Code graph layer (Graphify, since v4.21.0) -----------------------
  // Single language-agnostic tool — no per-language adapters. This block lives
  // inside interactivePrompts (so it only runs in the interactive path; CI /
  // `--yes` writes the flag from autodetect and `baldart doctor` backfills the
  // install). Everything is a thin wrapper over Graphify's native commands.
  merged.graph = merged.graph || { output_dir: 'graphify-out', auto_rebuild_hook: true, register_mcp: false };
  if (merged.features.has_code_graph === true) {
    UI.section('Code graph layer (Graphify — structural retrieval + wiki feed)');
    const gi = new GraphifyInstaller(process.cwd());
    let present = gi.detect();

    if (!present) {
      const want = await UI.confirm('Graphify CLI not found. Install it now (`pipx install graphifyy`)?', true);
      if (want) {
        const res = gi.install();
        if (res.ok) { UI.success(`Graphify installed (${res.method}).`); present = true; }
        else { UI.warning(`Graphify install failed: ${res.error}`); UI.info('Run `/graphify-bootstrap` or `pipx install graphifyy` later, then `baldart doctor`.'); }
      } else {
        UI.info('Skipping install. Run `/graphify-bootstrap` later — `baldart doctor` will flag the missing binary.');
      }
    } else {
      const v = gi.verify();
      UI.info(`Graphify present (${v.version || 'version unknown'}).`);
    }

    if (present) {
      // Initial offline graph build (no API cost).
      if (await UI.confirm('Build the code graph now (`graphify update .`, offline)?', true)) {
        const b = gi.buildGraph(process.cwd());
        if (!b.ok) UI.warning(`Graph build failed: ${b.error}`);
      }
      // Always-on wiring for each enabled AI tool (graph-first instructions + pre-tool hook).
      const aw = gi.installAlwaysOn(merged.tools.enabled || ['claude']);
      if (aw.wired.length) UI.success(`Graphify always-on wired for: ${aw.wired.join(', ')}`);
      for (const s of aw.skipped) UI.info(`Always-on skipped for ${s.tool}: ${s.reason}`);
      // Native post-commit git hook (auto-rebuild) when opted in.
      if (merged.graph.auto_rebuild_hook !== false) {
        const h = gi.installGitHook();
        if (h.ok) UI.success('Graphify git hook installed (auto-rebuild on commit).');
        else UI.info(`Git hook not installed: ${h.error}`);
      }
      // Optional MCP registration (CLI path works without it).
      if (merged.graph.register_mcp === true) {
        const m = gi.registerMcp(process.cwd(), merged.graph.output_dir || 'graphify-out');
        if (m.ok) UI.success('Graphify MCP server registered (`graphify-mcp`).');
        else UI.info(`MCP registration skipped: ${m.reason}`);
      }

      // Certification — attest binary + first run + freshness + hook in one shot.
      const cert = gi.certify(process.cwd(), {
        outputDir: merged.graph.output_dir || 'graphify-out',
        expectHook: merged.graph.auto_rebuild_hook !== false,
      });
      if (cert.ok && cert.fresh && cert.hook) {
        UI.success(`Code graph CERTIFIED: graphify ${cert.version || ''} present, first graph built (${cert.builtAtCommit || 'commit n/a'}), auto-rebuild hook active.`);
      } else {
        UI.warning(`Code graph NOT fully certified — ${cert.details}${cert.hook ? '' : ' · auto-rebuild hook missing'}. Run \`baldart doctor\` or \`/graphify-bootstrap\` to finish.`);
      }
    }
  }

  UI.section('Stack (autodetected from package.json — confirm or override)');
  const charting = merged.stack.charting;
  const chartingCanonical = await promptForKey(
    'Approved charting libraries — comma-separated package names (empty if none)',
    (charting.canonical || detected.stack.charting.canonical).join(',')
  );
  charting.canonical = chartingCanonical
    ? chartingCanonical.split(',').map((s) => s.trim()).filter(Boolean)
    : [];
  const chartingForbidden = await promptForKey(
    'Forbidden charting libraries — comma-separated (empty if none)',
    (charting.forbidden || []).join(',')
  );
  charting.forbidden = chartingForbidden
    ? chartingForbidden.split(',').map((s) => s.trim()).filter(Boolean)
    : [];
  charting.wrappers_root = await promptForKey(
    'Custom chart wrappers directory (empty if none)',
    charting.wrappers_root || detected.stack.charting.wrappers_root || ''
  );

  const animation = merged.stack.animation;
  const animCanonical = await promptForKey(
    'Approved animation libraries — comma-separated package names (empty if none)',
    (animation.canonical || detected.stack.animation.canonical).join(',')
  );
  animation.canonical = animCanonical
    ? animCanonical.split(',').map((s) => s.trim()).filter(Boolean)
    : [];

  merged.stack.testing.e2e = await promptForKey(
    'E2E framework ("playwright", "cypress", or empty)',
    merged.stack.testing.e2e || detected.stack.testing.e2e || ''
  );

  merged.stack.database = await promptForKey(
    'Persistence layer ("firestore" / "supabase" / "postgres" / "mysql" / "mongodb" / "dynamodb" / "sqlite" / "none" — empty to leave unset)',
    merged.stack.database || detected.stack.database || ''
  );

  merged.stack.auth_provider = await promptForKey(
    'Auth provider ("firebase-auth" / "supabase-auth" / "clerk" / "auth0" / "cognito" / "nextauth" / "lucia" / "custom" / "none" — empty to leave unset)',
    merged.stack.auth_provider || detected.stack.auth_provider || ''
  );

  merged.stack.framework = await promptForKey(
    'Application framework ("nextjs" / "remix" / "sveltekit" / "astro" / "nuxt" / "rails" / "django" / "fastapi" / "express" / "none" — empty to leave unset)',
    merged.stack.framework || detected.stack.framework || ''
  );

  merged.stack.deployment = await promptForKey(
    'Deployment target ("vercel" / "firebase" / "aws" / "gcp" / "cloudflare" / "render" / "fly" / "self-hosted" / "none" — empty to leave unset)',
    merged.stack.deployment || detected.stack.deployment || ''
  );

  // Schema-deploy-from-trunk-only safety invariant (v4.33.0). Pre-checked when
  // configure detected parallel worktrees — the topology prone to schema desync.
  const sdftDefault = typeof merged.stack.schema_deploy_from_trunk_only === 'boolean'
    ? merged.stack.schema_deploy_from_trunk_only
    : (detected.stack.schema_deploy_from_trunk_only === true);
  merged.stack.schema_deploy_from_trunk_only = await promptForKey(
    'Only auto-deploy remote schema/migrations from the trunk branch? (recommended when cards are developed in parallel worktrees against one shared remote DB)',
    sdftDefault,
    'confirm'
  );

  return merged;
}

/**
 * Strip features.* flags whose value is null. We use null to signal
 * "detection inconclusive — never written, ask user on first use".
 * Honouring the always-ask contract (project-context.md § 3).
 */
function stripNullFeatures(config) {
  if (!config || !config.features) return config;
  const out = { ...config, features: { ...config.features } };
  for (const k of Object.keys(out.features)) {
    if (out.features[k] === null) {
      delete out.features[k];
    }
  }
  return out;
}

function serialize(config) {
  const header = `# BALDART Project Configuration
# Generated / updated by \`npx baldart configure\` — safe to edit by hand.
# Schema docs: framework/docs/PROJECT-CONFIGURATION.md
# Protocol:    framework/agents/project-context.md
# This file is a CUSTOMIZABLE COPY and is never overwritten by \`baldart update\`.
#
# NOTE: features.* flags MUST be explicit (true | false). Any flag absent
# from this file will prompt the user on first skill invocation.
`;
  const clean = stripNullFeatures(config);
  if (clean && clean._probes) delete clean._probes;
  return header + yaml.dump(clean, { lineWidth: 100, noRefs: true });
}

async function configure(opts = {}) {
  const cwd = process.cwd();
  UI.header('BALDART CONFIGURE — project context');

  if (!fs.existsSync(path.join(cwd, '.framework'))) {
    UI.error('Framework not installed. Run `npx baldart add` first.');
    process.exit(1);
  }

  const probeSpinner = UI.spinner('Probing project structure...').start();
  const detected = detect(cwd);
  probeSpinner.succeed('Project structure probed');

  const existing = loadExisting(cwd);
  const template = loadTemplateSkeleton(cwd);
  // Priority (highest wins): existing user values > detected > template defaults.
  // mergePreserving(target, source) keeps target where set, fills from source.
  let merged = mergePreserving(existing || {}, detected);
  merged = mergePreserving(merged, template);
  merged.version = SCHEMA_VERSION;

  const dsSignals = detected.stack.design_system_signals || {};
  const dsSignalLabels = [
    dsSignals.path && `path:${dsSignals.path}`,
    dsSignals.storybook && 'storybook',
    dsSignals.shadcn && 'shadcn',
    dsSignals.tailwind_theme && 'tailwind-theme',
    dsSignals.library && `lib:${dsSignals.library}`
  ].filter(Boolean).join(', ') || '—';
  const monorepo = detected.stack.monorepo || { detected: false, roots: [] };

  UI.box('AUTODETECTED', [
    `Brand name:      ${detected.identity.brand_name || '—'}`,
    `AI tools:        ${detected.tools.enabled.join(', ')}`,
    `Design system:   ${detected.features.has_design_system ? 'yes' : 'no'}`,
    `  └─ signals:    ${dsSignalLabels}`,
    `UI guidelines:   ${detected.paths.ui_guidelines || '— (none found)'}`,
    `Components UI:   ${detected.paths.components_primitives || '— (none found)'}`,
    `Monorepo:        ${monorepo.detected ? `yes (${monorepo.roots.length} package(s))` : 'no'}`,
    `Backlog:         ${detected.features.has_backlog ? 'yes' : 'no'}`,
    `ADRs:            ${detected.features.has_adrs ? 'yes' : 'no'}`,
    `API docs:        ${detected.features.has_api_docs ? 'yes' : 'no'}`,
    `E2E review:      ${detected.features.has_e2e_review === true ? 'yes' : detected.features.has_e2e_review === false ? 'no' : '— (will ask)'}`,
    `Charting libs:   ${detected.stack.charting.canonical.join(', ') || '—'}`,
    `Animation libs:  ${detected.stack.animation.canonical.join(', ') || '—'}`,
    `E2E framework:   ${detected.stack.testing.e2e || '—'}`,
    `Database:        ${detected.stack.database || '—'}`,
    `Auth provider:   ${detected.stack.auth_provider || '—'}`,
    `Framework:       ${detected.stack.framework || '—'}`,
    `Deployment:      ${detected.stack.deployment || '—'}`,
  ]);

  if (opts.nonInteractive) {
    UI.info('Non-interactive mode — writing autodetected values.');
    // For features.* keys where detection returned null (inconclusive), strip
    // them so the YAML omits the flag and the always-ask contract kicks in
    // on first skill invocation. Honors project-context.md § 3.
    if (merged.features && detected.features) {
      for (const k of Object.keys(detected.features)) {
        if (detected.features[k] === null && k in merged.features) {
          delete merged.features[k];
        }
      }
    }
  } else {
    const proceed = await UI.confirm(
      existing ? 'Re-run prompts and merge into existing config?' : 'Start configuration prompts?',
      true
    );
    if (!proceed) {
      UI.info('Configure cancelled.');
      return;
    }
    merged = await interactivePrompts(merged, detected);
  }

  const outPath = path.join(cwd, CONFIG_FILE);
  fs.writeFileSync(outPath, serialize(merged), 'utf8');
  UI.success(`Wrote ${CONFIG_FILE}`);

  // Ensure overlays dir exists (user-owned)
  const overlaysAbs = path.join(cwd, OVERLAYS_DIR);
  if (!fs.existsSync(overlaysAbs)) {
    fs.mkdirSync(overlaysAbs, { recursive: true });
    fs.writeFileSync(
      path.join(overlaysAbs, '.gitkeep'),
      '# Skill overlays — see framework/agents/project-context.md § 5\n',
      'utf8'
    );
    UI.success(`Created ${OVERLAYS_DIR}/`);
  }

  UI.box('NEXT STEPS', [
    '• Review baldart.config.yml — values are committed to your repo',
    '• Author skill overlays in .baldart/overlays/<skill-name>.md',
    '  Examples: framework/templates/overlays/',
    '• Re-run `npx baldart configure` any time to refresh',
  ]);
}

module.exports = configure;
module.exports.detect = detect;
module.exports.mergePreserving = mergePreserving;
