/**
 * Manifest + spec-fingerprint — make testcases reflect the SOFTWARE, not chase it.
 *
 * Records, per scenario, which spec section it was derived from + a hash of that
 * section. On re-run, diff current spec hashes vs the manifest to classify each
 * scenario: keep / regenerate / retire — so the orchestrator regenerates only the
 * parts whose spec changed (stable + cheap), and never silently drifts.
 *
 * Deterministic. Manifest lives at .sungen/manifest/<screen>.json.
 */
import * as fs from 'fs';
import * as path from 'path';
import { createHash } from 'crypto';
import { featureBasename, reportSlug } from './unit-paths';

export interface SpecSection { name: string; hash: string }
export interface ManifestEntry { scenario: string; vpCode?: string; section: string; specHash: string }
export interface Manifest {
  screen: string;
  builtAt: string;
  specSections: Record<string, string>;   // section name → hash (snapshot at build time)
  entries: ManifestEntry[];                // scenario ↔ section ↔ hash
}

export type ChangeKind = 'keep' | 'regenerate' | 'retire';
export interface ChangePlan {
  screen: string;
  scenarios: { scenario: string; section: string; change: ChangeKind; reason: string }[];
  newSections: string[];     // spec sections with no scenario yet → generate
  removedSections: string[]; // sections in manifest no longer in spec
  summary: { keep: number; regenerate: number; retire: number; newSections: number };
}

function norm(s: string): string {
  return s.replace(/\(Tier[^)]*\)/i, '').replace(/[#*-]/g, '').trim().toLowerCase().replace(/\s+/g, ' ');
}
function shortHash(s: string): string {
  return createHash('sha1').update(s).digest('hex').slice(0, 12);
}

const NO_SPEC = '(no-spec-link)';

/**
 * Match a feature section name to a spec section name. Feature comments often
 * combine spec sections (e.g. "features items / product cards" → spec "features
 * items"), so fall back to longest substring match in either direction.
 */
function matchSection(featureSection: string, specNames: string[]): string | null {
  if (specNames.includes(featureSection)) return featureSection;
  let best: string | null = null;
  for (const s of specNames) {
    if (featureSection.includes(s) || s.includes(featureSection)) {
      if (!best || s.length > best.length) best = s;
    }
  }
  return best;
}

/** Parse spec.md into hashable sections (## and ### headings; "Section:" prefix stripped). */
export function parseSpecSections(specPath: string): SpecSection[] {
  if (!fs.existsSync(specPath)) return [];
  const lines = fs.readFileSync(specPath, 'utf-8').split('\n');
  const sections: { name: string; body: string[] }[] = [];
  let cur: { name: string; body: string[] } | null = null;
  for (const line of lines) {
    const h = line.match(/^#{2,3}\s+(.*)$/);
    if (h) {
      if (cur) sections.push(cur);
      const name = h[1].replace(/^Section:\s*/i, '').trim();
      cur = { name, body: [] };
    } else if (cur) {
      cur.body.push(line);
    }
  }
  if (cur) sections.push(cur);
  return sections.map((s) => ({ name: norm(s.name), hash: shortHash(s.body.join('\n').trim()) }));
}

/** Parse feature into scenario → section mapping using `# --- Section: X ---` comments. */
function parseFeatureSections(featurePath: string): { scenario: string; vpCode?: string; section: string }[] {
  if (!fs.existsSync(featurePath)) return [];
  const lines = fs.readFileSync(featurePath, 'utf-8').split('\n');
  const out: { scenario: string; vpCode?: string; section: string }[] = [];
  let section = '(unsectioned)';
  for (const raw of lines) {
    const line = raw.trim();
    const sc = line.match(/^#\s*-{2,}\s*Section:\s*(.*?)\s*-{2,}\s*$/i);
    if (sc) { section = norm(sc[1]); continue; }
    const s = line.match(/^Scenario:\s*(.*)$/);
    if (s) {
      const name = s[1].trim();
      const code = name.match(/\bVP-[A-Z]+-\d+/i)?.[0];
      out.push({ scenario: name, vpCode: code, section });
    }
  }
  return out;
}

export function buildManifest(screenDir: string, screenName: string): Manifest {
  const specPath = path.join(screenDir, 'requirements', 'spec.md');
  const featurePath = path.join(screenDir, 'features', `${featureBasename(screenName)}.feature`);
  const specSections = parseSpecSections(specPath);
  const specMap: Record<string, string> = {};
  for (const s of specSections) specMap[s.name] = s.hash;

  const specNames = Object.keys(specMap);
  const featScenarios = parseFeatureSections(featurePath);
  const entries: ManifestEntry[] = featScenarios.map((f) => {
    const matched = matchSection(f.section, specNames);
    return {
      scenario: f.scenario,
      vpCode: f.vpCode,
      section: matched ?? f.section,         // canonical spec section when matched
      specHash: matched ? specMap[matched] : NO_SPEC,
    };
  });

  return {
    screen: screenName,
    builtAt: new Date().toISOString(),
    specSections: specMap,
    entries,
  };
}

export function diffManifest(screenDir: string, screenName: string, manifest: Manifest): ChangePlan {
  const specPath = path.join(screenDir, 'requirements', 'spec.md');
  const current = parseSpecSections(specPath);
  const currentMap: Record<string, string> = {};
  for (const s of current) currentMap[s.name] = s.hash;

  const scenarios = manifest.entries.map((e) => {
    let change: ChangeKind; let reason: string;
    if (e.specHash === NO_SPEC) { change = 'keep'; reason = 'not linked to a spec section (manual/cross-cutting)'; }
    else {
      const now = currentMap[e.section];
      if (now === undefined) { change = 'retire'; reason = `spec section "${e.section}" no longer exists`; }
      else if (now !== e.specHash) { change = 'regenerate'; reason = `spec section "${e.section}" changed (${e.specHash} → ${now})`; }
      else { change = 'keep'; reason = 'spec section unchanged'; }
    }
    return { scenario: e.scenario, section: e.section, change, reason };
  });

  const manifestSections = new Set(manifest.entries.map((e) => e.section));
  const newSections = Object.keys(currentMap).filter((s) => !manifestSections.has(s) && !manifest.specSections[s]);
  const removedSections = Object.keys(manifest.specSections).filter((s) => currentMap[s] === undefined);

  return {
    screen: screenName,
    scenarios,
    newSections,
    removedSections,
    summary: {
      keep: scenarios.filter((s) => s.change === 'keep').length,
      regenerate: scenarios.filter((s) => s.change === 'regenerate').length,
      retire: scenarios.filter((s) => s.change === 'retire').length,
      newSections: newSections.length,
    },
  };
}

export function manifestPath(screenName: string): string {
  return path.join(process.cwd(), '.sungen', 'manifest', `${reportSlug(screenName)}.json`);
}
export function loadManifest(screenName: string): Manifest | null {
  const p = manifestPath(screenName);
  return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf-8')) : null;
}
export function saveManifest(m: Manifest): string {
  const p = manifestPath(m.screen);
  fs.mkdirSync(path.dirname(p), { recursive: true });
  fs.writeFileSync(p, JSON.stringify(m, null, 2), 'utf-8');
  return p;
}
