/**
 * SET 38b — delta-debugging over module-IIFE sets (pure logic).
 *
 * 31a isolation makes every module-IIFE subset a syntactically meaningful
 * program → ddmin is sound. Complexity O(n log n) interesting-tests.
 * Integration wires probe/oracle; this module stays import-cycle free.
 */

/** Partition `set` into `n` contiguous deltas (last may be larger). */
export function partitionSet<T>(set: T[], n: number): T[][] {
  const size = Math.max(1, Math.floor(set.length / n));
  const deltas: T[][] = [];
  let i = 0;
  for (let d = 0; d < n && i < set.length; d++) {
    const end = d === n - 1 ? set.length : Math.min(set.length, i + size);
    deltas.push(set.slice(i, end));
    i = end;
  }
  return deltas.filter(d => d.length > 0);
}

/**
 * Zeller ddmin: return a 1-minimal subset that is still interesting.
 * `interesting(subset)` is true when the subset FAILS (reproduces the defect).
 * Returns [] when the universe itself is not interesting.
 */
export function ddmin<T>(
  universe: T[],
  interesting: (subset: T[]) => boolean,
): T[] {
  if (universe.length === 0) return [];
  if (!interesting(universe)) return [];

  const ddminRec = (set: T[], n: number): T[] => {
    if (set.length <= 1) return set;
    const deltas = partitionSet(set, Math.min(n, set.length));
    for (const delta of deltas) {
      if (delta.length > 0 && interesting(delta)) {
        return ddminRec(delta, 2);
      }
    }
    for (let i = 0; i < deltas.length; i++) {
      const complement = set.filter(x => !deltas[i]!.includes(x));
      if (complement.length > 0 && complement.length < set.length && interesting(complement)) {
        return ddminRec(complement, Math.max(deltas.length - 1, 2));
      }
    }
    if (n < set.length) {
      return ddminRec(set, Math.min(set.length, 2 * n));
    }
    return set;
  };

  return ddminRec(universe, 2);
}

/** Event string: `minimal failing set: [A, B]`. */
export function formatMinimalFailingSetEvent(paths: string[]): string {
  return `minimal failing set: [${paths.join(', ')}]`;
}

/** Tripwire counterpart when ddmin cannot name a set. */
export function ddminFailureEvent(reason?: string): string {
  return reason && reason.length > 0 ? `ddmin-failure: ${reason}` : 'ddmin-failure';
}

/**
 * SET 38b — find the minimal failing module-path set.
 *
 * `evaluate(subset)` returns a crash message when the subset is interesting
 * (fails), or null when clean. Paths in the result preserve `files` order.
 *
 * Exported as the eval-injected core; testExec wraps with programParseError.
 */
export function ddminMinimalFailingSetWithEval(
  files: { path: string }[],
  evaluate: (subset: { path: string }[]) => string | null,
  crashMessage?: string,
): { set: string[]; event: string } {
  try {
    if (files.length === 0) {
      return { set: [], event: ddminFailureEvent('empty module set') };
    }
    const interesting = (subset: { path: string }[]): boolean => {
      const err = evaluate(subset);
      if (err == null) return false;
      if (!crashMessage) return true;
      return sameCrashClassLoose(err, crashMessage);
    };
    if (!interesting(files)) {
      return { set: [], event: ddminFailureEvent('universe not interesting') };
    }
    const minimal = ddmin(files, interesting);
    if (minimal.length === 0) {
      return { set: [], event: ddminFailureEvent('empty result') };
    }
    const set = minimal.map(f => f.path);
    return { set, event: formatMinimalFailingSetEvent(set) };
  } catch (e) {
    const msg = e instanceof Error ? e.message : String(e);
    return { set: [], event: ddminFailureEvent(msg.slice(0, 120)) };
  }
}

function sameCrashClassLoose(a: string, b: string): boolean {
  if (/Invalid regular expression/i.test(a) && /Invalid regular expression/i.test(b)) return true;
  const idA = /Identifier '([A-Za-z_$][\w$]*)' has already been declared/i.exec(a);
  const idB = /Identifier '([A-Za-z_$][\w$]*)' has already been declared/i.exec(b);
  if (idA && idB) return idA[1] === idB[1];
  return (a.split('\n')[0] ?? '').slice(0, 60) === (b.split('\n')[0] ?? '').slice(0, 60);
}

export type DdminOracle = (
  content: string,
  errorHint?: string,
) => { content: string; classKey: string; edits: number } | null;

/**
 * SET 38b — apply delimiter-oracle over each minimal-set member's FULL text
 * (not ±120 seam window). Returns candidate fixes (caller re-probes).
 */
export function ddminHealMinimalSet(
  files: { path: string; content: string }[],
  minimalSet: string[],
  oracle: DdminOracle,
  crashMessage?: string,
): { path: string; content: string; note: string }[] {
  const fixes: { path: string; content: string; note: string }[] = [];
  for (const path of minimalSet) {
    const file = files.find(f => f.path === path);
    if (!file) continue;
    const healed = oracle(file.content, crashMessage);
    if (!healed || healed.content === file.content) continue;
    fixes.push({
      path,
      content: healed.content,
      note: `ddmin delimiter-oracle full-text heal (${healed.classKey}, ${healed.edits} edit${healed.edits === 1 ? '' : 's'}) on ${path}`,
    });
  }
  return fixes;
}

/**
 * SET 38c — report line for parse-poison recurrence tracking.
 * Exact shape: `parse-poison incidents: N (prior run: M)`.
 */
export function formatParsePoisonIncidents(n: number, priorM: number): string {
  return `parse-poison incidents: ${n} (prior run: ${priorM})`;
}

/**
 * SET 38c — detect regex-literal poison patterns that warrant a delimiter-
 * oracle entry / build-prompt guard. Returns suggestion keys (empty if clean).
 */
export function detectRegexLiteralPoisonPatterns(content: string): string[] {
  const hits: string[] = [];
  // Classic combat poison: `= /(/;` — open group, next `/` is not a closer.
  if (/\/\(\//.test(content) || /=\s*\/\([^/\n;]*;/.test(content)) {
    hits.push('unterminated-regex-literal');
  }
  const lines = content.split('\n');
  for (const line of lines) {
    if (/\/\([^/\n]*$/.test(line) || (/\/\([^)]*$/.test(line) && !/\/\([^)]*\)\/[gimsuy]*/.test(line))) {
      if (!hits.includes('unterminated-regex-group')) hits.push('unterminated-regex-group');
    }
    if (/\/\[[^\]]*$/.test(line)) {
      if (!hits.includes('unterminated-regex-charclass')) hits.push('unterminated-regex-charclass');
    }
    if (oddUnescapedSlashes(line) && !hits.includes('unterminated-regex-literal')) {
      hits.push('unterminated-regex-literal');
    }
  }
  return hits;
}

function oddUnescapedSlashes(line: string): boolean {
  let count = 0;
  let inStr: string | null = null;
  for (let i = 0; i < line.length; i++) {
    const c = line[i]!;
    if (inStr) {
      if (c === '\\') { i++; continue; }
      if (c === inStr) inStr = null;
      continue;
    }
    if (c === '"' || c === "'" || c === '`') { inStr = c; continue; }
    if (c === '/' && line[i + 1] === '/') break;
    if (c === '/' && line[i + 1] === '*') break;
    if (c === '\\') { i++; continue; }
    if (c === '/') count++;
  }
  return count > 0 && count % 2 === 1;
}
