/**
 * Shared parser for `platform/neo4j/schema.cypher`.
 *
 * Two consumers today:
 *   1. The admin SCHEMA prompt block (`platform/ui/app/lib/admin-schema-block.ts`)
 *      renders labels declared in schema.cypher into the agent's
 *      system prompt.
 *   2. The memory-plugin schema validator treats schema.cypher
 *      declarations as the "promise" half of the recognised-label set —
 *      labels that exist as constraints/indexes but may not yet have a node
 *      in the graph (fresh-install bootstrap).
 *
 * The previous home was admin-schema-block.ts; that owner was wrong as
 * soon as a second consumer arrived. Lifting here keeps a single source of
 * truth and removes the cross-package shape coupling.
 *
 * Also exports a compact `levenshtein` for "did you mean?" suggestions on
 * unknown labels — used by the memory-plugin validator. The graph-mcp
 * cypher validator has its own copy in schema-cache.ts (private to the
 * stale-miss heuristic); the duplication is intentional, since this
 * exported one targets human-readable error UX, not refresh debouncing.
 */

const LABEL_PATTERN = /FOR\s*\(\s*\w+\s*:\s*(\w+)\s*\)/g;

/**
 * Extract every Neo4j label declared in a `schema.cypher` file by scanning
 * `FOR (alias:Label)` constraint and index forms. Returns a sorted, de-duped
 * list. Pure function — no I/O.
 */
export function parseLabelsFromSchemaCypher(schemaCypher: string): string[] {
  const found = new Set<string>();
  for (const match of schemaCypher.matchAll(LABEL_PATTERN)) {
    found.add(match[1]);
  }
  return [...found].sort();
}

/**
 * Standard iterative-DP Levenshtein. Used to suggest a near-match when an
 * agent submits an unknown label — small alphabets, short strings, runs in
 * microseconds. Identical algorithm to the private copy in schema-cache.ts;
 * exported here so callers outside graph-mcp don't have to roll their own.
 */
export function levenshtein(a: string, b: string): number {
  if (a === b) return 0;
  if (a.length === 0) return b.length;
  if (b.length === 0) return a.length;
  let prev = new Array<number>(b.length + 1);
  let curr = new Array<number>(b.length + 1);
  for (let j = 0; j <= b.length; j++) prev[j] = j;
  for (let i = 1; i <= a.length; i++) {
    curr[0] = i;
    for (let j = 1; j <= b.length; j++) {
      const cost = a[i - 1] === b[j - 1] ? 0 : 1;
      curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
    }
    [prev, curr] = [curr, prev];
  }
  return prev[b.length];
}

/**
 * Find the closest label in `candidates` to `unknown` by edit distance.
 * Returns null when the closest match is further than `maxDistance` (default
 * 3) — beyond that a suggestion is more confusing than helpful.
 */
export function nearestLabel(
  unknown: string,
  candidates: Iterable<string>,
  maxDistance = 3,
): string | null {
  let best: string | null = null;
  let bestDist = Infinity;
  for (const candidate of candidates) {
    const d = levenshtein(unknown, candidate);
    if (d < bestDist) {
      bestDist = d;
      best = candidate;
    }
  }
  if (best === null || bestDist > maxDistance) return null;
  return best;
}
