/**
 * SET 3b — clade tree. Template-born cases carry their family; model bodies
 * get a deterministic post-hoc classification; unmatched → freeform.
 */
import type { IRCaseGen, IRClade } from '../core/irScaffold';
import type { TestgenTemplate } from './testgenOracle.generated';
import { draftCladePromotion, nameTokenOverlap } from './superbuffer';
import type { CladePromotionProposal } from './superbuffer';
import { matchDraft43Family } from './cladeDrafts43';

export const FAMILY_BRANCH: Record<string, IRClade['branch']> = {
  roundtrip: 'algebraic',
  idempotency: 'algebraic',
  determinism: 'algebraic',
  involution: 'algebraic',
  commutativity: 'algebraic',
  'throws-on-invalid': 'contract',
  'rejects-empty': 'contract',
  'null-safety': 'contract',
  'uses-dep': 'contract',
  'method-anchor': 'contract',
  // SET 43d — draft families (bodies in cladeDrafts43; mutation-oracle admission pending)
  'response-shape': 'contract',
  'crud-lifecycle': 'state',
  expiry: 'state',
  'collision-uniqueness': 'state',
  ordering: 'state',
  'model-schema': 'state',
  pagination: 'state',
  'empty-input': 'representation',
  'boundary-values': 'representation',
  unicode: 'representation',
  'format-string': 'representation',
};

/** SET 31d — template/planner family for *Model class anchors. */
export const MODEL_SCHEMA_CLADE = 'model-schema';
/** SET 31d — template/planner family for `uses <dependency>` integration cases. */
export const USES_DEP_CLADE = 'uses-dep';
/**
 * SET 37d — method-anchor clade (registerUser / createArticle / checkRateLimit).
 * Substrate-templated: invoke stub-signature + return-shape assert. Runner wiring
 * is Integration; planSuite/testgen belong to Stream C.
 */
export const METHOD_ANCHOR_CLADE = 'method-anchor';
/**
 * SET 43d — draft clade families from the empty-draw drop ledger.
 * Draft bodies live in cladeDrafts43.ts; full admission still requires the
 * mutation-fixture oracle (do not edit testgenOracle.generated.ts by hand).
 */
export const RESPONSE_SHAPE_CLADE = 'response-shape';
export const FORMAT_STRING_CLADE = 'format-string';
export const PAGINATION_CLADE = 'pagination';

const MODEL_NAME_RE = /^[A-Z][A-Za-z0-9_]*Model$/;
/** camelCase verb+Noun method anchors that concentrate empty-draws. */
const METHOD_ANCHOR_RE =
  /^(register|create|check|update|delete|get|set|find|list|add|remove|validate|authenticate|authorize)[A-Z][A-Za-z0-9_]*$/;

/** True when a planned/case name is an XModel class-anchor. */
export function isModelSchemaName(name: string): boolean {
  return MODEL_NAME_RE.test(name);
}

/** SET 37d — true for method-name anchors (registerUser, createArticle, …). */
export function isMethodAnchorName(name: string): boolean {
  return METHOD_ANCHOR_RE.test(name);
}

/**
 * SET 37d — pure helper: substrate-templated case body for a method anchor.
 * Invokes the stub symbol with a contract placeholder arg and asserts a
 * defined return shape (optionally narrowed by a feature-text hint).
 */
export function methodAnchorBody(symbol: string, returnShapeHint?: string): string {
  const sym = symbol || 'fn';
  const shape = (returnShapeHint ?? '').toLowerCase();
  if (/string|slug|id|token/.test(shape)) {
    return `const __got = ${sym}({} as never);\nexpect(__got).toBeDefined();\nexpect(typeof __got).toBe('string');`;
  }
  if (/number|count|limit/.test(shape)) {
    return `const __got = ${sym}({} as never);\nexpect(__got).toBeDefined();\nexpect(typeof __got).toBe('number');`;
  }
  if (/bool|ok|valid/.test(shape)) {
    return `const __got = ${sym}({} as never);\nexpect(__got).toBeDefined();\nexpect(typeof __got).toBe('boolean');`;
  }
  if (/object|record|article|user|session/.test(shape)) {
    return `const __got = ${sym}({} as never);\nexpect(__got).toBeDefined();\nexpect(typeof __got).toBe('object');`;
  }
  return `const __got = ${sym}({} as never);\nexpect(__got).toBeDefined();`;
}

/**
 * SET 31d planner half — which structural clade family a planned case belongs
 * to (if any). Used by planSuite tagging and applyTemplates matching so
 * model-schema / uses-dep can be substrate-templated without edge licenses.
 */
export function plannedCladeFamily(p: {
  name: string;
  kind?: string;
  source?: string;
  symbol?: string;
  cladeFamily?: string;
}): string | null {
  if (p.cladeFamily) return p.cladeFamily;
  if (isModelSchemaName(p.name) || (p.symbol != null && isModelSchemaName(p.symbol))) {
    return MODEL_SCHEMA_CLADE;
  }
  if (p.kind === 'integration' || p.source === 'dep' || /^uses\s+/i.test(p.name)) {
    return USES_DEP_CLADE;
  }
  if (isMethodAnchorName(p.name) || (p.symbol != null && isMethodAnchorName(p.symbol))) {
    return METHOD_ANCHOR_CLADE;
  }
  // SET 43d — draft family name patterns (Status/ErrorBody, LogFormat, Page/Offset/Link).
  const draft43 = matchDraft43Family(p.name)
    ?? (p.symbol != null ? matchDraft43Family(p.symbol) : null);
  if (draft43) return draft43;
  return null;
}

export function cladeFromTemplate(t: TestgenTemplate, symbol: string): IRClade {
  return {
    branch: t.branch,
    family: t.family,
    bindings: { symbol },
  };
}

/** Post-hoc classification of a model-authored body. */
export function classifyCaseClade(c: IRCaseGen, symbol?: string): IRClade {
  const body = c.body;
  const name = `${c.name} ${c.kind}`.toLowerCase();
  const bind: Record<string, string> = symbol ? { symbol } : {};

  // SET 31d — structural families (planner half); name/kind beat body heuristics.
  if (isModelSchemaName(c.name) || c.clade?.family === MODEL_SCHEMA_CLADE) {
    return { branch: 'state', family: MODEL_SCHEMA_CLADE, bindings: bind };
  }
  if (c.kind === 'integration' || /^uses\s+/i.test(c.name) || c.clade?.family === USES_DEP_CLADE) {
    return { branch: 'contract', family: USES_DEP_CLADE, bindings: bind };
  }
  // SET 37d — method-anchor (registerUser / createArticle / checkRateLimit).
  if (isMethodAnchorName(c.name) || c.clade?.family === METHOD_ANCHOR_CLADE) {
    return { branch: 'contract', family: METHOD_ANCHOR_CLADE, bindings: bind };
  }

  if (/decode\s*\(\s*encode|encode\s*\(\s*decode|SYM\s*\(\s*SYM/.test(body)
    || /round.?trip/.test(name)) {
    return { branch: 'algebraic', family: 'roundtrip', bindings: bind };
  }
  if (/\.toThrow\s*\(/.test(body) && /invalid|reject|throw/.test(name)) {
    return { branch: 'contract', family: 'throws-on-invalid', bindings: bind };
  }
  if (/\.toThrow\s*\(/.test(body) && /empty|''|""/.test(body + name)) {
    return { branch: 'contract', family: 'rejects-empty', bindings: bind };
  }
  if (/null|undefined/.test(name) && /\.toThrow|\.toBeNull|\.toBeUndefined/.test(body)) {
    return { branch: 'contract', family: 'null-safety', bindings: bind };
  }
  if (/expir|ttl|clock/.test(name)) {
    return { branch: 'state', family: 'expiry', bindings: bind };
  }
  if (/unique|collision|duplicate/.test(name)) {
    return { branch: 'state', family: 'collision-uniqueness', bindings: bind };
  }
  if (/order|sort|lexicograph/.test(name)) {
    return { branch: 'state', family: 'ordering', bindings: bind };
  }
  if (/empty/.test(name)) {
    return { branch: 'representation', family: 'empty-input', bindings: bind };
  }
  if (/boundary|zero|negative|max/.test(name)) {
    return { branch: 'representation', family: 'boundary-values', bindings: bind };
  }
  if (/unicode|emoji|utf/.test(name)) {
    return { branch: 'representation', family: 'unicode', bindings: bind };
  }
  if (FAMILY_BRANCH[c.clade?.family ?? '']) {
    return c.clade!;
  }
  return { branch: 'freeform', family: 'freeform', bindings: bind };
}

/**
 * SET 3b rebind — for a schema'd failing case, produce a fresh body candidate
 * from the clade family before any model call. Returns null for freeform.
 */
export function rebindCladeBody(c: IRCaseGen): string | null {
  const clade = c.clade ?? classifyCaseClade(c);
  const sym = clade.bindings.symbol || 'fn';
  switch (clade.family) {
    case 'rejects-empty':
      return `expect(() => ${sym}('')).toThrow();`;
    case 'roundtrip':
      return `expect(${sym}(${sym}('abc'))).toBe(${sym}('abc'));`;
    case 'determinism':
      return `expect(${sym}(1, 2)).toBe(${sym}(1, 2));`;
    case 'throws-on-invalid':
      return `expect(() => ${sym}(null as unknown as string)).toThrow();`;
    case 'empty-input':
      return `expect(() => ${sym}('')).toThrow();`;
    case MODEL_SCHEMA_CLADE:
      return `expect(() => new ${sym}({})).not.toThrow();\nexpect(new ${sym}({})).toBeInstanceOf(${sym});`;
    case USES_DEP_CLADE:
      return `expect(${sym}).toBeDefined();\nexpect(typeof ${sym}).toBe('function');`;
    case METHOD_ANCHOR_CLADE:
      return methodAnchorBody(sym);
    default:
      return null; // freeform / unrebindable — model path
  }
}

export function cladeRatio(cases: IRCaseGen[]): { schemad: number; freeform: number } {
  let schemad = 0;
  let freeform = 0;
  for (const c of cases) {
    const cl = c.clade ?? classifyCaseClade(c);
    if (cl.branch === 'freeform') freeform++;
    else schemad++;
  }
  return { schemad, freeform };
}

// ---------------------------------------------------------------------------
// SET 31d — cross-run recurrence scan. The corpus does not compound when the
// SAME planned case dies run after run and nothing ever escalates it: SET 17
// clade-promotion only ever watched a single run. This scans stored run
// records for planned cases that failed/dropped ≥N runs in a row within the
// same lineage (same projectId, or a near-identical prompt) and proposes the
// same CladePromotionProposal shape SET 17 already persists — never
// auto-admitted, just made visible. 0 tokens: pure string/array arithmetic
// over records the host already has in storage.
// ---------------------------------------------------------------------------

/** Minimal shape the scan needs from a persisted tier1Run:* record. */
export interface RunFailureSnapshot {
  id: string;
  ts: string;
  prompt?: string | null;
  projectId?: string | null;
  /** Tier1Result.problems for that run (case-level dropped/unfilled lines live here). */
  problems?: readonly string[] | null;
}

/** The single largest recurring coverage loss (SET 31d postmortem): model-schema
 *  bodies (ArticleModel, UserModel, SessionModel, ...) all collapse into one
 *  anchor family instead of competing as unrelated one-off freeform cases. */
const MODEL_SCHEMA_FAMILY = 'model-schema:*Model';

// Matches both drop-site wordings runner.ts emits:
//   case "NAME" dropped — unparseable (coverage reduced)
//   planned case "NAME" unfilled after retries — dropped (coverage reduced)
const DROPPED_CASE_RE = /case "([^"]+)"\s+(?:dropped|unfilled)/;

/** Extract planned-case names that died (dropped/unfilled) from a run's problems[]. */
export function extractDroppedCaseNames(problems: readonly string[] | null | undefined): string[] {
  if (!problems?.length) return [];
  const out: string[] = [];
  for (const p of problems) {
    const m = DROPPED_CASE_RE.exec(p);
    if (m) out.push(m[1]!);
  }
  return out;
}

/** Group a dropped/unfilled case name into a recurring family key. */
export function caseFamily(name: string): string {
  if (isModelSchemaName(name)) return MODEL_SCHEMA_FAMILY;
  // Per-run recurring loss (5abf390b): `uses <dep>` integration cases share one clade.
  if (/^uses\s+/i.test(name)) return USES_DEP_CLADE;
  if (isMethodAnchorName(name)) return METHOD_ANCHOR_CLADE;
  const norm = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
  return norm || 'freeform';
}

function samePromptLineage(a: string | null | undefined, b: string | null | undefined): boolean {
  if (!a || !b) return false;
  if (a === b) return true;
  return nameTokenOverlap(a, b) >= 0.5;
}

/** Cluster run snapshots into lineages: same projectId, else near-identical prompt. */
function groupLineages(records: readonly RunFailureSnapshot[]): RunFailureSnapshot[][] {
  const byProject = new Map<string, RunFailureSnapshot[]>();
  const loose: RunFailureSnapshot[] = [];
  for (const r of records) {
    if (r.projectId) {
      const arr = byProject.get(r.projectId) ?? [];
      arr.push(r);
      byProject.set(r.projectId, arr);
    } else {
      loose.push(r);
    }
  }
  const clusters: RunFailureSnapshot[][] = [];
  for (const r of loose) {
    const home = clusters.find(c => samePromptLineage(c[0]!.prompt, r.prompt));
    if (home) home.push(r);
    else clusters.push([r]);
  }
  const groups = [...byProject.values(), ...clusters];
  for (const g of groups) g.sort((a, b) => (a.ts || '').localeCompare(b.ts || ''));
  return groups;
}

/**
 * Scan stored run records for a case family that failed/dropped in the last
 * ≥minConsecutive runs of the same lineage, in a row (the streak must reach
 * the most recent run — a family that used to fail but has since healed does
 * not re-propose). Returns CladePromotionProposal[], honesty-formatted via
 * formatCladeCandidateMessage; callers persist via the existing SET 17
 * clade-candidates storage (never auto-admitted).
 */
export function scanRecurringFailures(
  records: readonly RunFailureSnapshot[],
  opts: { minConsecutive?: number } = {},
): CladePromotionProposal[] {
  const minConsecutive = opts.minConsecutive ?? 3;
  const proposals: CladePromotionProposal[] = [];
  for (const group of groupLineages(records)) {
    if (group.length < minConsecutive) continue;
    const perRun = group.map(r => {
      const names = extractDroppedCaseNames(r.problems);
      const byFamily = new Map<string, string>();
      for (const n of names) if (!byFamily.has(caseFamily(n))) byFamily.set(caseFamily(n), n);
      return byFamily;
    });
    const allFamilies = new Set<string>();
    for (const byFamily of perRun) for (const f of byFamily.keys()) allFamilies.add(f);
    for (const family of allFamilies) {
      let streak = 0;
      let exampleName = family;
      const runIds: string[] = [];
      for (let i = perRun.length - 1; i >= 0; i--) {
        const hit = perRun[i]!.get(family);
        if (!hit) break;
        if (streak === 0) exampleName = hit; // most-recent occurrence — current shape
        streak++;
        runIds.unshift(group[i]!.id);
      }
      if (streak < minConsecutive) continue;
      const isModelSchema = family === MODEL_SCHEMA_FAMILY;
      proposals.push(draftCladePromotion({
        patternKey: `family:${family}`,
        featureId: group[group.length - 1]!.id,
        wins: streak,
        source: 'cross-run-failure',
        runIds,
        stubSketch: isModelSchema
          ? `export class ${exampleName} { constructor(_fields: Record<string, unknown>) { throw new Error('stub'); } }`
          : undefined,
        caseBody: isModelSchema
          ? `expect(() => new ${exampleName}({} as never)).not.toThrow();`
          : undefined,
      }));
    }
  }
  return proposals;
}
