/**
 * Pure structural gates for the feature-coded spec and frozen IR.
 *
 * Everything here is deterministic string/graph work with zero Node coupling —
 * no fs, no child processes, no typescript compiler — so the Tier 1 browser
 * runner can bundle these gates unchanged. pipeline.ts re-exports them; the
 * heavyweight gates (tsc typecheck, vitest execution) stay in pipeline/sandbox.
 */
import type { FeatureNode, FeatureSpec, GeneratedFile, IRTestCase } from './types';

export function normalizePath(p: string): string {
  return p.replace(/\\/g, '/').replace(/^\.\//, '');
}

// ---------------------------------------------------------------------------
// Feature-tree utilities
// ---------------------------------------------------------------------------

export function topoOrder(features: FeatureNode[]): FeatureNode[] {
  const byId = new Map(features.map(f => [f.id, f]));
  const seen = new Set<string>();
  const out: FeatureNode[] = [];
  const visit = (f: FeatureNode, stack: Set<string>) => {
    if (seen.has(f.id)) return;
    if (stack.has(f.id)) throw new Error(`dependency cycle involving ${f.id}`);
    stack.add(f.id);
    for (const d of f.deps) {
      const dep = byId.get(d);
      if (!dep) throw new Error(`${f.id} depends on unknown feature ${d}`);
      visit(dep, stack);
    }
    stack.delete(f.id);
    seen.add(f.id);
    out.push(f);
  };
  for (const f of features) visit(f, new Set());
  return out;
}

/** Dependency depth: 0 for leaves, 1 + max(dep depths) otherwise. */
export function depthOf(features: FeatureNode[]): Map<string, number> {
  const byId = new Map(features.map(f => [f.id, f]));
  const memo = new Map<string, number>();
  const depth = (id: string): number => {
    if (memo.has(id)) return memo.get(id)!;
    const f = byId.get(id)!;
    const d = f.deps.length === 0 ? 0 : 1 + Math.max(...f.deps.map(depth));
    memo.set(id, d);
    return d;
  };
  for (const f of features) depth(f.id);
  return memo;
}

/** Features that directly depend on `id` — the parent-recheck set. */
export function dependentsOf(id: string, features: FeatureNode[]): FeatureNode[] {
  return features.filter(f => f.deps.includes(id));
}

/**
 * Collapse duplicate stub paths into one entry (concatenated content).
 * Invariant: ONE stub entry per path — see T2 / validateSpec duplicate check.
 */
export function mergeStubsByPath(stubs: GeneratedFile[]): GeneratedFile[] {
  const byPath = new Map<string, GeneratedFile>();
  for (const s of stubs) {
    const p = normalizePath(s.path);
    const prev = byPath.get(p);
    byPath.set(p, prev ? { path: s.path, content: `${prev.content.trimEnd()}\n\n${s.content}` } : s);
  }
  return [...byPath.values()];
}

/**
 * Deterministic write-boundary enforcement: a generation/repair step for a
 * feature may only produce files that feature owns, and never anything under
 * tests/ (the IR is frozen). Returns the list of violations.
 */
export function boundaryViolations(files: GeneratedFile[], feature: FeatureNode): string[] {
  const owned = new Set(feature.files.map(normalizePath));
  const violations: string[] = [];
  for (const f of files) {
    const p = normalizePath(f.path);
    if (p.startsWith('tests/') || p.includes('/tests/')) {
      violations.push(`${p}: writes into the frozen IR (tests/)`);
    } else if (!owned.has(p)) {
      violations.push(`${p}: outside the ownership boundary of ${feature.id} (${[...owned].join(', ')})`);
    }
  }
  return violations;
}

// ---------------------------------------------------------------------------
// Spec validation (Phase A gate)
// ---------------------------------------------------------------------------

/** Features whose IR would be thin (no edge cases) — warn, never block. */
export function specThinFeatures(spec: FeatureSpec): string[] {
  return spec.features.filter(f => (f.edgeCases ?? []).length === 0).map(f => f.id);
}

/**
 * Part P2 — deterministic normalizers that kill spurious re-encodes.
 * Run before the first validateSpec and before every measure/repair merge.
 */
export function preSanitizeSpec(spec: FeatureSpec): void {
  // Deterministic dependency-id resolution (zero tokens). Models routinely
  // write a dep as the bare "F2" while the feature's id is "F2_application_
  // shell" — same feature, different string. Resolve by exact id, then by
  // unique leading F<number> token; drop self-deps, duplicates, and deps that
  // remain unresolvable (a dep pointing at nothing is a broken edge, not a
  // reason to regenerate or exclude anything).
  const ids = new Set(spec.features.map(f => f.id));
  const byToken = new Map<string, string | null>(); // token -> id, null = ambiguous
  for (const f of spec.features) {
    const t = /^F?\d+/i.exec(f.id)?.[0]?.toUpperCase();
    if (!t) continue;
    byToken.set(t, byToken.has(t) ? null : f.id);
  }
  const resolveDep = (d: string): string | null => {
    if (ids.has(d)) return d;
    const t = /^F?\d+/i.exec(d.trim())?.[0]?.toUpperCase();
    const hit = t ? byToken.get(t) : undefined;
    return hit ?? null;
  };
  for (const f of spec.features) {
    f.deps = [...new Set(
      (f.deps ?? [])
        .map(resolveDep)
        .filter((d): d is string => !!d && d !== f.id),
    )];
  }
  for (const f of spec.features) {
    f.files = [...new Set(
      f.files
        .map(normalizePath)
        .filter(p => p.startsWith('src/')),
    )];
    let tf = normalizePath(f.testFile || '');
    if (!tf.startsWith('tests/')) {
      const slug = (f.name || f.id).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40) || 'feature';
      tf = `tests/${f.id.toLowerCase().replace(/[^a-z0-9.]+/g, '-')}-${slug}.test.ts`;
    }
    f.testFile = tf;
    f.stubs = mergeStubsByPath(f.stubs ?? []);
    const stubPaths = new Set(f.stubs.map(s => normalizePath(s.path)));
    for (const file of f.files) {
      const p = normalizePath(file);
      if (!stubPaths.has(p)) {
        const sym = `__rhaStub_${f.id.replace(/\W/g, '_')}`;
        f.stubs.push({
          path: p,
          content: [
            `/** auto-stub for ${p} — mechanical fill for missing interface */`,
            `export function ${sym}(): never {`,
            `  throw new Error('stub: not implemented');`,
            `}`,
            '',
          ].join('\n'),
        });
        stubPaths.add(p);
      }
    }
    f.stubs = f.stubs.filter(s => f.files.map(normalizePath).includes(normalizePath(s.path)));
  }

  // Reconcile dependency ids: models often write "F2" when the feature is
  // "F2_application_shell". Token → unique feature is rewritten; ambiguous
  // tokens and genuine unknowns are left for validateSpec.
  resolveDependencyIds(spec);
}

/** Leading F?/digits token of a feature id ("F2" from "F2_application_shell"). */
function leadingFeatureToken(id: string): string | null {
  const m = /^F?\d+/i.exec(id);
  return m ? m[0] : null;
}

/**
 * Rewrite deps onto exact feature ids when a bare token uniquely identifies one.
 * Mutates each feature's `deps` in place (deduped, no self-deps).
 */
export function resolveDependencyIds(spec: FeatureSpec): void {
  const byExact = new Map<string, string>();
  /** token → feature id, or null when ambiguous. */
  const byToken = new Map<string, string | null>();

  for (const f of spec.features) {
    byExact.set(f.id, f.id);
    const token = leadingFeatureToken(f.id);
    if (!token) continue;
    if (byToken.has(token)) {
      byToken.set(token, null); // ambiguous — do not resolve via this token
    } else {
      byToken.set(token, f.id);
    }
  }

  const resolveOne = (d: string): string | null => {
    if (byExact.has(d)) return byExact.get(d)!;
    if (byToken.has(d) && byToken.get(d) != null) return byToken.get(d)!;
    const token = leadingFeatureToken(d);
    if (token && byToken.has(token) && byToken.get(token) != null) {
      return byToken.get(token)!;
    }
    return null; // unresolvable — leave unchanged for validateSpec
  };

  for (const f of spec.features) {
    const next: string[] = [];
    const seen = new Set<string>();
    for (const d of f.deps ?? []) {
      const resolved = resolveOne(d);
      const id = resolved ?? d;
      if (id === f.id) continue; // drop self-deps
      if (seen.has(id)) continue;
      seen.add(id);
      next.push(id);
    }
    f.deps = next;
  }
}

/** Part P1 — true when a candidate feature set collapsed vs the floor. */
export function isSpecCollapse(floor: number, nextCount: number): boolean {
  return nextCount < floor;
}

/** Map validateSpec problem strings back to the feature ids they name. */
export function offendingFeatureIds(problems: string[], knownIds: string[]): string[] {
  const known = new Set(knownIds);
  const hit = new Set<string>();
  for (const p of problems) {
    for (const id of known) {
      if (
        p.startsWith(`${id}:`)
        || p.startsWith(`${id} `)
        || p.includes(` ${id} `)
        || p.includes(` ${id}:`)
        || p.endsWith(` ${id}`)
        || p.includes(`both ${id} `)
        || p.includes(`and ${id}`)
      ) {
        hit.add(id);
      }
    }
  }
  return [...hit];
}

export function validateSpec(spec: FeatureSpec): string[] {
  const problems: string[] = [];
  const ids = new Set<string>();
  for (const f of spec.features) {
    if (ids.has(f.id)) problems.push(`duplicate feature id ${f.id}`);
    ids.add(f.id);
  }
  for (const f of spec.features) {
    for (const d of f.deps) if (!ids.has(d)) problems.push(`${f.id} depends on unknown ${d}`);
    if (f.files.length === 0) problems.push(`${f.id} owns no implementation files`);
    for (const file of f.files) {
      if (!normalizePath(file).startsWith('src/')) problems.push(`${f.id}: file ${file} must live under src/`);
    }
    if (!normalizePath(f.testFile).startsWith('tests/')) problems.push(`${f.id}: test file must live under tests/`);
    // Thin edge cases are a QUALITY warning, not an unusable contract — they
    // must never block the gate or seed a cascade (a "no edge cases" flag once
    // amputated 19 healthy dependents). Surfaced via specThinFeatures().
    // header files: every owned file needs a stub at that exact path
    const stubPaths = new Set(f.stubs.map(s => normalizePath(s.path)));
    for (const file of f.files) {
      if (!stubPaths.has(normalizePath(file))) problems.push(`${f.id}: no interface stub for owned file ${file}`);
    }
    for (const s of f.stubs) {
      if (!f.files.map(normalizePath).includes(normalizePath(s.path))) {
        problems.push(`${f.id}: stub ${s.path} is not an owned file`);
      }
    }
    const seenStubPaths = new Set<string>();
    for (const s of f.stubs) {
      const p = normalizePath(s.path);
      if (seenStubPaths.has(p)) {
        problems.push(`${f.id}: duplicate stub for ${p} — ONE file must have ONE stub entry; merge all of the file's types/classes into a single stub content, each type imported or defined in-file`);
      }
      seenStubPaths.add(p);
    }
  }
  // ownership must be disjoint
  const owner = new Map<string, string>();
  for (const f of spec.features) {
    for (const file of f.files) {
      const p = normalizePath(file);
      if (owner.has(p)) problems.push(`file ${p} owned by both ${owner.get(p)} and ${f.id}`);
      owner.set(p, f.id);
    }
  }
  try {
    topoOrder(spec.features);
  } catch (e) {
    problems.push((e as Error).message);
  }
  return problems;
}

// ---------------------------------------------------------------------------
// IR validation (Phase B gate) — shape checks only; the syntax check via the
// typescript compiler lives in pipeline.validateTestFile (Tier 2).
// ---------------------------------------------------------------------------

/** Regex-level suite checks shared by Tier 1 (browser) and validateTestFile. */
export function validateSuiteShape(content: string): string[] {
  const problems: string[] = [];
  if (!/from ['"]vitest['"]/.test(content)) problems.push('does not import from vitest');
  if (!/\bdescribe\s*\(/.test(content)) problems.push('has no describe block');
  if (!/\bit\s*\(/.test(content)) problems.push('has no test cases');
  return problems;
}

export function extractCases(featureId: string, content: string): IRTestCase[] {
  const cases: IRTestCase[] = [];
  const re = /\bit\s*\(\s*(['"`])((?:\\.|(?!\1).)*)\1/g;
  let m: RegExpExecArray | null;
  while ((m = re.exec(content)) !== null) {
    const name = m[2];
    const kind = name.startsWith('edge:') ? 'edge' : name.startsWith('integration:') ? 'integration' : 'behavior';
    cases.push({ featureId, name, kind });
  }
  return cases;
}
