/**
 * SET 65 — synthesis grammar for IR fill (flagged: gates.grammarEnabled).
 *
 * An invariant taxonomy generalizes the clade templates: deterministic gates
 * (case kind + signature + spec tokens) choose the invariant, the skeleton is
 * a parseable test body with typed holes, and the LLM fills holes ONLY. A
 * conformant draw is re-emitted by deterministic reassembly (skeleton +
 * validated fills), so stray prose, imports, and parse poison cannot survive
 * even when the model emits them. Born-red vacuousness is impossible by
 * construction: every skeleton calls the symbol under test.
 */
import { isLiteralArgs } from './traceCapture';

export type InvariantId =
  | 'existence'
  | 'roundtrip'
  | 'idempotence'
  | 'determinism'
  | 'boundary'
  | 'rejection'
  | 'ordering'
  | 'referential-integrity'
  | 'error-contract'
  | 'state-transition'
  | 'uses-dep'
  | 'rate-window';

/** Hole marker: ⟨NAME⟩ — outside any legal TS token space. */
const HOLE = (name: string) => `⟨${name}⟩`;

/**
 * Structural skeleton per invariant. SYM is substituted at render; each hole
 * is validated at conform time (ARGS/ARGS2 literal-only, EXPECT literal-only).
 * Every skeleton invokes SYM — a filled skeleton can never be vacuous.
 */
const SKELETONS: Record<InvariantId, string> = {
  existence: `expect(typeof SYM(${HOLE('ARGS')})).toBe(${HOLE('EXPECT')});`,
  determinism: `expect(SYM(${HOLE('ARGS')})).toEqual(SYM(${HOLE('ARGS')}));`,
  idempotence: `expect(SYM(SYM(${HOLE('ARGS')}))).toEqual(SYM(${HOLE('ARGS')}));`,
  boundary: `expect(SYM(${HOLE('ARGS')})).toEqual(${HOLE('EXPECT')});`,
  rejection: `expect(() => SYM(${HOLE('ARGS')})).toThrow();`,
  'error-contract': `expect(() => SYM(${HOLE('ARGS')})).toThrow(${HOLE('EXPECT')});`,
  ordering: `expect(SYM(${HOLE('ARGS')})).toEqual(${HOLE('EXPECT')});`,
  'referential-integrity': `const input = ${HOLE('ARGS')};\nexpect(SYM(input)).not.toBe(input);`,
  'state-transition': `expect(SYM(${HOLE('ARGS')})).toEqual(${HOLE('EXPECT')});\nexpect(SYM(${HOLE('ARGS2')})).toEqual(${HOLE('EXPECT2')});`,
  roundtrip: `expect(SYM(${HOLE('ARGS')})).toEqual(${HOLE('EXPECT')});`,
  'uses-dep': `expect(SYM(${HOLE('ARGS')})).toEqual(${HOLE('EXPECT')});`,
  // SET 69 — time-boundary invariant with injected-clock scaffolding: the
  // clock is a plain closure, never a real timer. The window boundary is the
  // assertion: allowed before the advance, decided again after it.
  'rate-window': [
    `let now = 0;`,
    `const clock = () => now;`,
    `const limited = SYM(${HOLE('ARGS')}, clock);`,
    `expect(limited(${HOLE('KEY')})).toEqual(${HOLE('EXPECT')});`,
    `now += ${HOLE('ADVANCE')};`,
    `expect(limited(${HOLE('KEY')})).toEqual(${HOLE('EXPECT2')});`,
  ].join('\n'),
};

/**
 * Deterministic invariant gate: case kind + name/license tokens. First entry
 * is the rendered skeleton; the full set is reported for provenance.
 */
export function chooseInvariants(opts: {
  name: string;
  kind: string;
  license?: string | null;
}): InvariantId[] {
  const t = `${opts.name} ${opts.license ?? ''}`.toLowerCase();
  const out: InvariantId[] = [];
  // SET 69 — time-boundary semantics gate first: rate limits are counters +
  // clocks, maximally testable under an injected clock.
  if (/rate.?limit|sliding\s+window|throttl|per\s+(second|minute|hour|day)/.test(t)) out.push('rate-window');
  if (/throw|invalid|reject|error|fail|refuse/.test(t)) out.push('rejection', 'error-contract');
  if (/idempotent|twice|re-appl|stable under/.test(t)) out.push('idempotence');
  if (/sort|order|sequence|rank/.test(t)) out.push('ordering');
  if (/determinis|same input|consistent|repeatab/.test(t)) out.push('determinism');
  if (/clone|copy|mutat|immutab|reference/.test(t)) out.push('referential-integrity');
  if (/round.?trip|encode.*decode|parse.*format/.test(t)) out.push('roundtrip');
  if (/state|transition|then|after/.test(t) && /sequence|second|next/.test(t)) out.push('state-transition');
  if (opts.kind === 'edge') out.push('boundary');
  out.push('boundary');
  return [...new Set(out)];
}

export interface RenderedSkeleton {
  invariant: InvariantId;
  symbol: string;
  /** Parseable body with ⟨HOLE⟩ markers. */
  body: string;
  holes: string[];
}

export function renderSkeleton(invariant: InvariantId, symbol: string): RenderedSkeleton {
  const body = SKELETONS[invariant].replace(/SYM/g, symbol);
  const holes = [...body.matchAll(/⟨([A-Z0-9]+)⟩/g)].map(m => m[1]!);
  return { invariant, symbol, body, holes };
}

/** Prompt block instructing a holes-only fill. */
export function skeletonPrompt(sk: RenderedSkeleton): string {
  return [
    `Synthesis grammar — invariant "${sk.invariant}". Emit EXACTLY this skeleton with each ⟨HOLE⟩ replaced by a literal value; every other character verbatim. No imports, no prose.`,
    sk.body,
  ].join('\n');
}

/**
 * Conform a drawn body to the skeleton: flexible whitespace, holes captured,
 * fills validated (literal-only), then DETERMINISTIC REASSEMBLY — the
 * accepted body is the skeleton with validated fills, never the raw draw.
 */
export type ConformMiss =
  | 'shape-mismatch'      // the draw is not this invariant's assertion at all
  | 'non-literal-fill'    // holes filled with calls/identifiers, not literals
  | 'inconsistent-fill';  // a repeated hole disagreed with itself

export interface ConformResult {
  fills: Record<string, string>;
  /** Setup statements preserved ahead of the conformed assertion. */
  setup: string[];
  body: string;
}

/**
 * Conform a drawn body to the skeleton. SET 78 — a real case is often
 * `const x = f(); expect(...)`: the assertion conforms, the setup is
 * legitimate. Setup lines are preserved verbatim (they already passed the
 * conformance and accept gates) and only the ASSERTION is reassembled from
 * the skeleton, so richness no longer forces a freeform fallback.
 */
export function conformDrawToSkeleton(
  draw: string,
  sk: RenderedSkeleton,
): ConformResult | null {
  const attempt = (text: string): { fills: Record<string, string>; body: string } | { miss: ConformMiss } => {
    const pattern = sk.body
      .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
      .replace(/\s+/g, '\\s*')
      .replace(/(\\\(|\\\)|\\\.|;|,)/g, m => `\\s*${m}\\s*`)
      .replace(/⟨([A-Z0-9]+)⟩/g, '([\\s\\S]{1,160}?)');
    // ANCHORED: an unanchored match would find the assertion inside a larger
    // body and silently discard the surrounding setup — deleting real test
    // code. Whole-text match only; multi-statement is handled explicitly.
    const m = new RegExp(`^${pattern}$`).exec(text.trim());
    if (!m) return { miss: 'shape-mismatch' };
    const fills: Record<string, string> = {};
    for (let i = 0; i < sk.holes.length; i++) {
      const name = sk.holes[i]!;
      const val = (m[i + 1] ?? '').trim();
      if (!val || !isLiteralArgs(val)) return { miss: 'non-literal-fill' };
      if (fills[name] != null && fills[name] !== val) return { miss: 'inconsistent-fill' };
      fills[name] = val;
    }
    let body = sk.body;
    for (const [name, val] of Object.entries(fills)) body = body.split(`⟨${name}⟩`).join(val);
    return { fills, body };
  };

  const whole = attempt(draw);
  if ('body' in whole) return { fills: whole.fills, setup: [], body: whole.body };

  // Multi-statement: conform the LAST statement, keep the rest as setup.
  const stmts = draw.split('\n').map(l => l.trim()).filter(Boolean);
  if (stmts.length > 1) {
    const tail = stmts[stmts.length - 1]!;
    const tailTry = attempt(tail);
    if ('body' in tailTry) {
      const setup = stmts.slice(0, -1);
      return { fills: tailTry.fills, setup, body: [...setup, tailTry.body].join('\n') };
    }
  }
  return null;
}

/** Why a draw failed to conform — for the event line, never silent. */
export function explainConformMiss(draw: string, sk: RenderedSkeleton): ConformMiss {
  const stmts = draw.split('\n').map(l => l.trim()).filter(Boolean);
  const probe = stmts.length > 1 ? stmts[stmts.length - 1]! : draw;
  const loose = sk.body.replace(/⟨[A-Z0-9]+⟩/g, '').replace(/[^A-Za-z(]/g, '');
  const head = loose.slice(0, 12);
  if (head && !probe.replace(/[^A-Za-z(]/g, '').includes(head.slice(0, 8))) return 'shape-mismatch';
  return 'non-literal-fill';
}

export function formatGrammarConform(caseName: string, invariant: InvariantId): string {
  return `grammar: case "${caseName}" conformed to ${invariant} skeleton (holes-only, reassembled)`;
}

// SET 95e — `formatGrammarFallback` (the retired "grammar fallback: ..."
// event) is gone. SET 95's hole-only contract replaces shell-lock's soft
// reassembly-or-passthrough: a non-conformant draw is no longer narrated as
// a freeform pass-through. `explainConformMiss` below stays — it is a pure
// diagnostic (why a draw didn't match a skeleton), not fallback narration.
