/**
 * SET 2a — deterministic IR suite plan. The substrate authors the case list
 * and imports; the model only fills bodies for holes the template bank cannot.
 *
 * SET 38d — tag edge cases with planner-consumable family hints
 * (determinism, rejects-empty, roundtrip, model-schema) so applyTemplates
 * can substrate-fill them (no false planner-bypass alarms).
 */
import type { FeatureNode, FeatureSpec } from '../core/types';
import type { IRImport } from '../core/irScaffold';
import {
  isModelSchemaName,
  MODEL_SCHEMA_CLADE,
  plannedCladeFamily,
  USES_DEP_CLADE,
} from './clades';
// clades.ts is Stream-A owned for method-anchor; 38d family wiring stays here.

/** Families the planner can consume via applyTemplates (38d). */
export const PLANNER_CONSUMABLE_FAMILIES = [
  'determinism',
  'rejects-empty',
  'roundtrip',
  'model-schema',
  'uses-dep',
] as const;

export type PlannerConsumableFamily = (typeof PLANNER_CONSUMABLE_FAMILIES)[number];

/**
 * SET 38d — map edge-case / license text → consumable clade family.
 * Kept in planSuite (not clades.ts) so Stream C owns planner wiring.
 */
export function edgeCladeFamilyHint(text: string): PlannerConsumableFamily | undefined {
  const t = text.toLowerCase();
  if (/round.?trip/.test(t)) return 'roundtrip';
  if (/rejects?-?empty|empty\s*input|\bempty\b/.test(t) && !/boundary/.test(t)) {
    return 'rejects-empty';
  }
  if (/determin|idempot|same\s*input|repeatable/.test(t)) return 'determinism';
  if (isModelSchemaName(text.trim())) return 'model-schema';
  return undefined;
}

export type PlannedCaseKind = 'behavior' | 'edge' | 'integration';

export interface PlannedCase {
  name: string;
  kind: PlannedCaseKind;
  /** Licensing text: export symbol, edge-case string, or dep id. */
  license: string;
  source: 'export' | 'edge' | 'dep';
  symbol?: string;
  /** Parsed from the owning stub when available. */
  signature?: ParsedSig | null;
  /**
   * SET 31d planner half — structural clade family hint so applyTemplates can
   * substrate-template model-schema / uses-dep without edge-text licenses.
   */
  cladeFamily?: string;
}

export interface ParsedSig {
  name: string;
  params: { name: string; type: string }[];
  returnType: string;
}

export interface SuitePlan {
  imports: IRImport[];
  cases: PlannedCase[];
}

const SIG_RE =
  /export\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*:\s*([^{;]+)/;

/**
 * Extract a simple function signature from feature stubs, OR (SET 32f) a
 * class-constructor signature when the symbol is declared as `export class`.
 * Class anchors seed the model-schema clade the runner leads batch 1 with.
 */
export function parseExportSignature(feature: FeatureNode, symbol: string): ParsedSig | null {
  const esc = symbol.replace(/[$]/g, '\\$&');
  for (const stub of feature.stubs) {
    const fnRe = new RegExp(
      `export\\s+(?:async\\s+)?function\\s+(${esc})\\s*\\(([^)]*)\\)\\s*:\\s*([^{;]+)`,
    );
    const fnHit = fnRe.exec(stub.content);
    if (fnHit) {
      const params = fnHit[2]!.split(',').map(p => p.trim()).filter(Boolean).map(p => {
        const [n, t] = p.split(':').map(s => s.trim());
        return { name: n || 'arg', type: (t || 'unknown').replace(/\s+/g, ' ') };
      });
      return { name: fnHit[1]!, params, returnType: fnHit[3]!.trim() };
    }
    // SET 32f — class stub: parse the constructor(...) signature; when absent,
    // return a zero-argument constructor so anchors can still be generated
    // from `new X()`. Return type is the class itself.
    const classRe = new RegExp(
      `export\\s+class\\s+(${esc})\\b([^]*?)(?:\\n\\}|$)`,
    );
    const classHit = classRe.exec(stub.content);
    if (classHit) {
      const body = classHit[2] ?? '';
      const ctorRe = /constructor\s*\(([^)]*)\)/;
      const ctor = ctorRe.exec(body);
      const paramText = ctor ? ctor[1]! : '';
      const params = paramText.split(',').map(p => p.trim()).filter(Boolean).map(p => {
        const [n, t] = p.split(':').map(s => s.trim());
        return { name: n || 'arg', type: (t || 'unknown').replace(/\s+/g, ' ') };
      });
      return { name: classHit[1]!, params, returnType: classHit[1]! };
    }
  }
  return null;
}

/** SET 32f — the set of exported symbols the feature OWNS across its stubs.
 *  Function / class / const / let. Anchors are always drawn from this set. */
export function ownedExportSymbols(feature: FeatureNode): string[] {
  const out = new Set<string>();
  const re = /export\s+(?:async\s+)?(?:function|class|const|let|interface|type)\s+([A-Za-z_$][\w$]*)/g;
  for (const stub of feature.stubs) {
    for (const m of stub.content.matchAll(re)) out.add(m[1]!);
  }
  for (const exp of feature.exports) out.add(exp.symbol);
  return [...out];
}

/** SET 32f — true when the symbol is declared as `export class` in any stub. */
export function isExportedClass(feature: FeatureNode, symbol: string): boolean {
  const esc = symbol.replace(/[$]/g, '\\$&');
  const re = new RegExp(`export\\s+class\\s+${esc}\\b`);
  return feature.stubs.some(s => re.test(s.content));
}

/** Wiring imports — substrate-authored from this feature's exports + deps. */
export function wiringImports(spec: FeatureSpec, feature: FeatureNode): IRImport[] {
  const byModule = new Map<string, Set<string>>();
  const add = (file: string, symbol: string) => {
    const mod = file.replace(/\.(ts|js)$/, '');
    if (!byModule.has(mod)) byModule.set(mod, new Set());
    byModule.get(mod)!.add(symbol);
  };
  for (const exp of feature.exports) add(exp.file, exp.symbol);
  for (const depId of feature.deps) {
    const dep = spec.features.find(f => f.id === depId);
    if (!dep) continue;
    for (const exp of dep.exports) add(exp.file, exp.symbol);
  }
  return [...byModule.entries()].map(([from, symbols]) => ({
    from,
    symbols: [...symbols],
  }));
}

/**
 * Binding plan: 1 behavior per export, 1 edge per edgeCases entry (name
 * verbatim), 1 integration per dep. Imports come from the wiring contract.
 *
 * SET 32f — every plan MUST include ≥1 anchor case named after an owned
 * export (class/function/const). When feature.exports is empty but a stub
 * still exports an anchor symbol, injectClassAnchor (below) fills it in
 * deterministically after this call.
 */
export function planSuite(spec: FeatureSpec, feature: FeatureNode): SuitePlan {
  const cases: PlannedCase[] = [];
  for (const exp of feature.exports) {
    const cladeFamily = isModelSchemaName(exp.symbol) ? MODEL_SCHEMA_CLADE : undefined;
    cases.push({
      name: exp.symbol,
      kind: 'behavior',
      license: exp.symbol,
      source: 'export',
      symbol: exp.symbol,
      signature: parseExportSignature(feature, exp.symbol),
      cladeFamily,
    });
  }
  const primary = feature.exports[0];
  const primarySig = primary ? parseExportSignature(feature, primary.symbol) : null;
  for (const edge of feature.edgeCases) {
    const name = edge.trim();
    if (!name) continue;
    // SET 38d — prefer edge-text family hint, then structural plannedCladeFamily.
    const cladeFamily =
      edgeCladeFamilyHint(name)
      ?? plannedCladeFamily({ name, kind: 'edge', source: 'edge', symbol: primary?.symbol })
      ?? undefined;
    cases.push({
      name,
      kind: 'edge',
      license: name,
      source: 'edge',
      symbol: primary?.symbol,
      signature: primarySig,
      cladeFamily,
    });
  }
  for (const depId of feature.deps) {
    const dep = spec.features.find(f => f.id === depId);
    if (!dep) continue;
    cases.push({
      name: `uses ${dep.name}`,
      kind: 'integration',
      license: dep.id,
      source: 'dep',
      symbol: dep.exports[0]?.symbol,
      cladeFamily: USES_DEP_CLADE,
    });
  }
  return { imports: wiringImports(spec, feature), cases };
}

/**
 * SET 32f — anchor invariant: every plan MUST contain ≥1 case named after
 * an owned export (class/function/const). When the plan already has one
 * (which is the common case — feature.exports produced behavior cases with
 * matching names), returns the plan unchanged. Otherwise picks the first
 * owned export symbol found in the stubs and prepends a behavior case for
 * it — so `applyTemplates` may template it and it LEADS batch 1 downstream.
 *
 * Returns { plan, injected } — `injected` is the anchor case if one was
 * added, else null (caller decides whether to bump event/metrics).
 */
/**
 * SET 83 — DETERMINISTIC ANCHOR FILL (Andy: "F1 always has a really rough
 * time"). F1 is structurally the coldest node in every run: it is first, so
 * it has no frozen siblings, no in-run lessons, and an empty corpus page —
 * every warm input the superbuffer exists to supply is empty. On top of that
 * its injected class-anchor was DRAWN, and in the observed runs it burned six
 * model draws and still failed (`0/1 passing` ×3, then a corpse).
 *
 * The anchor is the one case we never needed a model for: it asserts that the
 * owned export is reachable and constructible, which is derivable from the
 * stub alone. Filling it deterministically costs 0 tokens, always lands, and
 * gives the cold feature a frozen exemplar for the very next batch.
 */
export function deterministicAnchorBody(
  feature: FeatureNode,
  symbol: string,
): string | null {
  const stub = (feature.stubs ?? []).map(s => s.content).join('\n');
  const esc = symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  if (new RegExp(`export\\s+class\\s+${esc}\\b`).test(stub)) {
    const ctor = new RegExp(`class\\s+${esc}[^{]*\\{[\\s\\S]*?constructor\\s*\\(([^)]*)\\)`).exec(stub);
    const args = argListFor(ctor?.[1] ?? '');
    return `expect(() => new ${symbol}(${args})).not.toThrow();`;
  }
  const fn = new RegExp(`export\\s+(?:async\\s+)?function\\s+${esc}\\s*\\(([^)]*)\\)`).exec(stub);
  if (fn) return `expect(() => ${symbol}(${argListFor(fn[1] ?? '')})).not.toThrow();`;
  if (new RegExp(`export\\s+const\\s+${esc}\\b`).test(stub)) {
    return `expect(${symbol}).toBeDefined();`;
  }
  return null;
}

/** Typed placeholder arguments from a parameter list. Deterministic. */
function argListFor(params: string): string {
  const p = params.trim();
  if (!p) return '';
  return p.split(',').map(one => {
    const t = (one.split(':')[1] ?? '').trim().toLowerCase();
    if (t.startsWith('number')) return '1';
    if (t.startsWith('string')) return "'x'";
    if (t.startsWith('boolean')) return 'true';
    if (t.endsWith('[]') || t.startsWith('array')) return '[]';
    if (t.startsWith('date')) return 'new Date(0)';
    return '{} as never';
  }).join(', ');
}

export function injectClassAnchor(
  plan: SuitePlan,
  feature: FeatureNode,
): { plan: SuitePlan; injected: PlannedCase | null } {
  const owned = ownedExportSymbols(feature);
  if (!owned.length) return { plan, injected: null };
  const have = new Set(plan.cases.map(c => c.name));
  const alreadyAnchored = owned.some(s => have.has(s));
  if (alreadyAnchored) return { plan, injected: null };
  const symbol = owned[0]!;
  const sig = parseExportSignature(feature, symbol);
  const anchor: PlannedCase = {
    name: symbol,
    kind: 'behavior',
    license: symbol,
    source: 'export',
    symbol,
    signature: sig,
    cladeFamily: isModelSchemaName(symbol) ? MODEL_SCHEMA_CLADE : undefined,
  };
  // Prepend so the anchor LEADS batch 1 — anchor-led first batches went
  // 3/3 across most of F4–F12 in the postmortem run and seeded the
  // frozen-exemplar cascade; F1's anchor-less plan filled zero.
  return {
    plan: { imports: plan.imports, cases: [anchor, ...plan.cases] },
    injected: anchor,
  };
}

/** Every identifier a body calls must exist in wiring imports or stub exports. */
export function surfaceConformanceErrors(
  body: string,
  allowed: Set<string>,
): string[] {
  // Rough call sites: foo(  — skip expect/describe/it and keywords.
  const reserved = new Set([
    'expect', 'describe', 'it', 'test', 'beforeEach', 'afterEach',
    'async', 'await', 'return', 'throw', 'new', 'typeof', 'instanceof',
    'if', 'else', 'for', 'while', 'const', 'let', 'var', 'function',
    'true', 'false', 'null', 'undefined', 'String', 'Number', 'Boolean',
    'Array', 'Object', 'Error', 'Math', 'JSON', 'Promise', 'Date',
    // Vitest matchers / chainers — not wiring symbols.
    'toBe', 'toEqual', 'toThrow', 'toMatch', 'toContain', 'toBeTruthy',
    'toBeFalsy', 'toBeNull', 'toBeUndefined', 'toBeDefined', 'toBeCloseTo',
    'toHaveLength', 'toBeInstanceOf', 'toStrictEqual', 'not', 'resolves', 'rejects',
  ]);
  const errs: string[] = [];
  // Only flag bare calls, not `obj.method(` — wiring symbols are free functions.
  const re = /(?<![\w$.])([A-Za-z_$][\w$]*)\s*\(/g;
  let m: RegExpExecArray | null;
  const seen = new Set<string>();
  while ((m = re.exec(body))) {
    const sym = m[1]!;
    if (reserved.has(sym) || seen.has(sym)) continue;
    seen.add(sym);
    if (!allowed.has(sym)) {
      errs.push(`undeclared symbol "${sym}" — not in wiring contract`);
    }
  }
  return errs;
}

export function allowedSymbols(plan: SuitePlan, feature: FeatureNode): Set<string> {
  const s = new Set<string>();
  for (const imp of plan.imports) for (const sym of imp.symbols) s.add(sym);
  for (const exp of feature.exports) s.add(exp.symbol);
  for (const stub of feature.stubs) {
    for (const m of stub.content.matchAll(/export\s+(?:async\s+)?(?:function|class|const)\s+([A-Za-z_$][\w$]*)/g)) {
      s.add(m[1]!);
    }
  }
  return s;
}
