/**
 * SET 89 §§10.3–10.5 — canonical append-only unique-defect resolution registry.
 *
 * Authoritative findings are observed EXACTLY where a supergit `error` node is
 * created. Speculative probes, quarantined candidates, and helper classifications
 * never create occurrences.
 *
 * Resolutions require a confirming authoritative measurement that no longer
 * contains the defect identity within the measured scope. Applying an edit alone
 * never resolves a defect.
 */
export type FindingCategory =
  | 'parse'
  | 'conformance'
  | 'execution'
  | 'crash'
  | 'test-failure';

export type DefectOccurrence = {
  occurrenceId: string;
  defectId: string;
  canonicalKey: string;
  measurementId: string;
  resultNodeId: string;
  errorNodeId: string;
  featureId: string;
  caseId: string;
  scopeKey: string;
  category: FindingCategory;
  classKey: string;
  artifactPath?: string;
  normalizedSignature: string;
  actionId?: string;
  candidateId?: string;
  editIds?: string[];
  ts: string;
};

export type DefectResolution = {
  resolutionId: string;
  defectId: string;
  canonicalKey: string;
  confirmingMeasurementId: string;
  confirmingResultNodeId: string;
  causeErrorNodeId: string;
  scopeKey: string;
  actionId?: string;
  candidateId?: string;
  editIds?: string[];
  ts: string;
};

export type DefectIdentityState = {
  defectId: string;
  canonicalKey: string;
  featureId: string;
  /** Measurement scope owner — normally the feature id. */
  scopeKey: string;
  caseId: string;
  category: FindingCategory;
  classKey: string;
  artifactPath?: string;
  firstSeenAt: string;
  lastSeenAt: string;
  occurrenceCount: number;
  unresolved: boolean;
  latestErrorNodeId: string;
  latestOccurrenceId: string;
};

export type DefectRegistrySnapshot = {
  occurrences: DefectOccurrence[];
  resolutions: DefectResolution[];
  identities: DefectIdentityState[];
  metrics: DefectRegistryMetrics;
};

export type DefectRegistryMetrics = {
  findingOccurrencesTotal: number;
  findingOccurrencesParse: number;
  findingOccurrencesConformance: number;
  findingOccurrencesExecution: number;
  findingOccurrencesCrash: number;
  findingOccurrencesTestFailure: number;
  uniqueDefectsObserved: number;
  uniqueDefectsResolved: number;
  uniqueDefectsUnresolved: number;
};

export type AuthoritativeFindingInput = {
  featureId: string;
  /** Exact case name, or suite sentinel `@suite`. */
  caseId: string;
  classKey: string;
  category: FindingCategory;
  artifactPath?: string | null;
  /** Expected/received pair OR crash signature text. */
  expected?: string | null;
  received?: string | null;
  crashSignature?: string | null;
  message: string;
  measurementId: string;
  resultNodeId: string;
  errorNodeId: string;
  actionId?: string;
  candidateId?: string;
  editIds?: string[];
  ts?: string;
};

const SUITE_SENTINEL = '@suite';

function pad(n: number, width: number): string {
  return String(n).padStart(width, '0');
}

/** Normalize volatile path noise while preserving owned relative paths. */
export function normalizeArtifactPath(path: string | null | undefined): string {
  if (!path) return '';
  let p = path.replace(/\\/g, '/');
  // Strip absolute prefixes that vary across machines / sandboxes.
  p = p.replace(/^.*?\/(src|tests)\//, '$1/');
  p = p.replace(/^\.\//, '');
  return p;
}

export function normalizeSignatureText(text: string | null | undefined): string {
  if (text == null) return '';
  return String(text)
    .replace(/\r\n/g, '\n')
    .replace(/\\/g, '/')
    .replace(/\/Users\/[^/\s]+/g, '~')
    .replace(/\/home\/[^/\s]+/g, '~')
    .replace(/file:\/\/\/[^\s)]+/g, 'file://…')
    .replace(/\s+/g, ' ')
    .trim()
    .slice(0, 240);
}

export function buildCanonicalKey(input: {
  featureId: string;
  caseId: string;
  classKey: string;
  artifactPath?: string | null;
  expected?: string | null;
  received?: string | null;
  crashSignature?: string | null;
}): string {
  const caseId = input.caseId === '__suite__' || !input.caseId ? SUITE_SENTINEL : input.caseId;
  const parts = [
    `f=${normalizeSignatureText(input.featureId).toLowerCase()}`,
    `c=${normalizeSignatureText(caseId)}`,
    `k=${normalizeSignatureText(input.classKey)}`,
    `p=${normalizeArtifactPath(input.artifactPath)}`,
  ];
  if (input.crashSignature) {
    parts.push(`x=${normalizeSignatureText(input.crashSignature)}`);
  } else {
    parts.push(`e=${normalizeSignatureText(input.expected)}`);
    parts.push(`r=${normalizeSignatureText(input.received)}`);
  }
  return parts.join('|');
}

/**
 * Deterministic category mapping from classified failure facts.
 * Categories are disjoint and partition all authoritative findings.
 */
export function classifyFindingCategory(input: {
  classKey?: string | null;
  crash?: boolean;
  message?: string;
  kindHint?: 'parse' | 'conformance' | 'execution' | 'crash' | 'test-failure' | null;
}): FindingCategory {
  if (input.kindHint) return input.kindHint;
  if (input.crash) return 'crash';
  const ck = (input.classKey ?? '').toLowerCase();
  const msg = (input.message ?? '').toLowerCase();
  if (
    ck.includes('parse') || ck.includes('syntax') || ck.includes('transpile')
    || msg.includes('syntaxerror') || msg.includes('parse error') || msg.includes('unterminated')
  ) return 'parse';
  if (
    ck.includes('conformance') || ck.includes('export') || ck.includes('manifest')
    || ck.includes('registration') || ck.includes('contract') || ck.includes('module-shape')
    || msg.includes('is not a function') || msg.includes('export')
  ) return 'conformance';
  if (
    ck.includes('throw-mismatch') || ck.includes('assertion') || ck.includes('expect')
    || msg.includes('expected') || msg.includes('received') || msg.includes('assertionerror')
    || msg.includes('tobe(') || msg.includes('tothrow')
  ) return 'test-failure';
  if (ck.includes('crash') || msg.includes('uncaught') || msg.includes('unexpected throw')) {
    return 'crash';
  }
  return 'execution';
}

export class DefectRegistry {
  private occurrenceSeq = 0;
  private defectSeq = 0;
  private resolutionSeq = 0;
  private readonly occurrences: DefectOccurrence[] = [];
  private readonly resolutions: DefectResolution[] = [];
  /** canonicalKey → defectId */
  private readonly keyToDefectId = new Map<string, string>();
  private readonly identities = new Map<string, DefectIdentityState>();
  private readonly clock: () => string;

  constructor(clock: () => string = () => new Date().toISOString()) {
    this.clock = clock;
  }

  observeAuthoritativeFinding(input: AuthoritativeFindingInput): DefectOccurrence {
    const caseId = !input.caseId || input.caseId === '__suite__' ? SUITE_SENTINEL : input.caseId;
    const canonicalKey = buildCanonicalKey({
      featureId: input.featureId,
      caseId,
      classKey: input.classKey,
      artifactPath: input.artifactPath,
      expected: input.expected,
      received: input.received,
      crashSignature: input.crashSignature,
    });
    const normalizedSignature = input.crashSignature
      ? normalizeSignatureText(input.crashSignature)
      : normalizeSignatureText(
        `expected=${input.expected ?? ''};received=${input.received ?? ''};msg=${input.message}`,
      );
    const ts = input.ts ?? this.clock();
    let defectId = this.keyToDefectId.get(canonicalKey);
    if (!defectId) {
      this.defectSeq += 1;
      defectId = `D${pad(this.defectSeq, 4)}`;
      this.keyToDefectId.set(canonicalKey, defectId);
      this.identities.set(defectId, {
        defectId,
        canonicalKey,
        featureId: input.featureId,
        scopeKey: input.featureId,
        caseId,
        category: input.category,
        classKey: input.classKey,
        artifactPath: normalizeArtifactPath(input.artifactPath) || undefined,
        firstSeenAt: ts,
        lastSeenAt: ts,
        occurrenceCount: 0,
        unresolved: true,
        latestErrorNodeId: input.errorNodeId,
        latestOccurrenceId: '',
      });
    }
    this.occurrenceSeq += 1;
    const occurrenceId = `O${pad(this.occurrenceSeq, 6)}`;
    const occurrence: DefectOccurrence = {
      occurrenceId,
      defectId,
      canonicalKey,
      measurementId: input.measurementId,
      resultNodeId: input.resultNodeId,
      errorNodeId: input.errorNodeId,
      featureId: input.featureId,
      caseId,
      scopeKey: input.featureId,
      category: input.category,
      classKey: input.classKey,
      artifactPath: normalizeArtifactPath(input.artifactPath) || undefined,
      normalizedSignature,
      actionId: input.actionId,
      candidateId: input.candidateId,
      editIds: input.editIds ? [...input.editIds] : undefined,
      ts,
    };
    this.occurrences.push(occurrence);
    const identity = this.identities.get(defectId)!;
    identity.occurrenceCount += 1;
    identity.lastSeenAt = ts;
    identity.unresolved = true;
    identity.latestErrorNodeId = input.errorNodeId;
    identity.latestOccurrenceId = occurrenceId;
    identity.category = input.category;
    identity.classKey = input.classKey;
    return occurrence;
  }

  /**
   * After an authoritative measurement of `scopeKey` (typically a feature id),
   * resolve every previously open identity in that scope whose canonical key
   * is absent from `presentKeys`.
   */
  reconcileMeasurementScope(opts: {
    scopeKey: string;
    presentKeys: Iterable<string>;
    confirmingMeasurementId: string;
    confirmingResultNodeId: string;
    actionId?: string;
    candidateId?: string;
    editIds?: string[];
    ts?: string;
  }): DefectResolution[] {
    const present = new Set(opts.presentKeys);
    const ts = opts.ts ?? this.clock();
    const out: DefectResolution[] = [];
    for (const identity of this.identities.values()) {
      if (!identity.unresolved) continue;
      if (identity.scopeKey !== opts.scopeKey && identity.featureId !== opts.scopeKey) continue;
      if (present.has(identity.canonicalKey)) continue;
      out.push(this.appendResolution(identity, {
        confirmingMeasurementId: opts.confirmingMeasurementId,
        confirmingResultNodeId: opts.confirmingResultNodeId,
        actionId: opts.actionId,
        candidateId: opts.candidateId,
        editIds: opts.editIds,
        ts,
        scopeKey: opts.scopeKey,
      }));
    }
    return out;
  }

  /**
   * Terminal full-artifact reconciliation — may resolve any open identity
   * absent from the complete final finding set.
   */
  reconcileTerminalMeasurement(opts: {
    presentKeys: Iterable<string>;
    confirmingMeasurementId: string;
    confirmingResultNodeId: string;
    actionId?: string;
    candidateId?: string;
    editIds?: string[];
    ts?: string;
  }): DefectResolution[] {
    const present = new Set(opts.presentKeys);
    const ts = opts.ts ?? this.clock();
    const out: DefectResolution[] = [];
    for (const identity of this.identities.values()) {
      if (!identity.unresolved) continue;
      if (present.has(identity.canonicalKey)) continue;
      out.push(this.appendResolution(identity, {
        confirmingMeasurementId: opts.confirmingMeasurementId,
        confirmingResultNodeId: opts.confirmingResultNodeId,
        actionId: opts.actionId,
        candidateId: opts.candidateId,
        editIds: opts.editIds,
        ts,
        scopeKey: '*',
      }));
    }
    return out;
  }

  private appendResolution(
    identity: DefectIdentityState,
    opts: {
      confirmingMeasurementId: string;
      confirmingResultNodeId: string;
      actionId?: string;
      candidateId?: string;
      editIds?: string[];
      ts: string;
      scopeKey: string;
    },
  ): DefectResolution {
    this.resolutionSeq += 1;
    const resolution: DefectResolution = {
      resolutionId: `R${pad(this.resolutionSeq, 6)}`,
      defectId: identity.defectId,
      canonicalKey: identity.canonicalKey,
      confirmingMeasurementId: opts.confirmingMeasurementId,
      confirmingResultNodeId: opts.confirmingResultNodeId,
      causeErrorNodeId: identity.latestErrorNodeId,
      scopeKey: opts.scopeKey,
      actionId: opts.actionId,
      candidateId: opts.candidateId,
      editIds: opts.editIds ? [...opts.editIds] : undefined,
      ts: opts.ts,
    };
    this.resolutions.push(resolution);
    identity.unresolved = false;
    return resolution;
  }

  metrics(): DefectRegistryMetrics {
    let parse = 0, conformance = 0, execution = 0, crash = 0, testFailure = 0;
    for (const o of this.occurrences) {
      if (o.category === 'parse') parse++;
      else if (o.category === 'conformance') conformance++;
      else if (o.category === 'execution') execution++;
      else if (o.category === 'crash') crash++;
      else testFailure++;
    }
    let unresolved = 0;
    let resolved = 0;
    for (const id of this.identities.values()) {
      if (id.unresolved) unresolved++;
      else resolved++;
    }
    return {
      findingOccurrencesTotal: this.occurrences.length,
      findingOccurrencesParse: parse,
      findingOccurrencesConformance: conformance,
      findingOccurrencesExecution: execution,
      findingOccurrencesCrash: crash,
      findingOccurrencesTestFailure: testFailure,
      uniqueDefectsObserved: this.identities.size,
      uniqueDefectsResolved: resolved,
      uniqueDefectsUnresolved: unresolved,
    };
  }

  snapshot(): DefectRegistrySnapshot {
    return {
      occurrences: this.occurrences.map(o => ({ ...o, editIds: o.editIds ? [...o.editIds] : undefined })),
      resolutions: this.resolutions.map(r => ({ ...r, editIds: r.editIds ? [...r.editIds] : undefined })),
      identities: [...this.identities.values()].map(i => ({ ...i })),
      metrics: this.metrics(),
    };
  }

  assertInvariants(): void {
    const m = this.metrics();
    const catSum =
      m.findingOccurrencesParse
      + m.findingOccurrencesConformance
      + m.findingOccurrencesExecution
      + m.findingOccurrencesCrash
      + m.findingOccurrencesTestFailure;
    if (catSum !== m.findingOccurrencesTotal) {
      throw new Error(`defectRegistry: category sum ${catSum} !== total ${m.findingOccurrencesTotal}`);
    }
    if (m.uniqueDefectsResolved + m.uniqueDefectsUnresolved !== m.uniqueDefectsObserved) {
      throw new Error('defectRegistry: resolved + unresolved !== observed');
    }
  }

  toJSONL(): string {
    const lines: string[] = [];
    for (const o of this.occurrences) {
      lines.push(JSON.stringify({ kind: 'occurrence', ...o }));
    }
    for (const r of this.resolutions) {
      lines.push(JSON.stringify({ kind: 'resolution', ...r }));
    }
    return lines.join('\n') + (lines.length ? '\n' : '');
  }
}

export { SUITE_SENTINEL };
