const sha256 = (value) => typeof value === 'string' && /^[0-9a-f]{64}$/.test(value)
const canonical = (value) => Array.isArray(value)
  ? value.map(canonical)
  : value && typeof value === 'object'
    ? Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, canonical(entry)]))
    : value

export const recoveredNamingDecisions = [
  {
    taskId: 'TSN-DRIFT-000',
    caseId: 'partdesign-plain',
    classification: 'allowed_evolution',
    reasonCode: 'native-history-stabilizes-after-edit-restore',
    rationale: 'FreeCAD changes the Body and Pad naming history after the first Length edit/restore while restoring geometry exactly; the evolved names repeat deterministically and remain stable through save, reopen, and resave.',
  },
  {
    taskId: 'TSN-DRIFT-001',
    caseId: 'partdesign-midplane',
    classification: 'allowed_evolution',
    reasonCode: 'native-history-stabilizes-after-edit-restore',
    rationale: 'FreeCAD applies the same deterministic history evolution to a Midplane Pad Length edit/restore: Body and Pad geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
  },
  {
    taskId: 'TSN-DRIFT-002',
    caseId: 'partdesign-reverse',
    classification: 'allowed_evolution',
    reasonCode: 'native-history-stabilizes-after-edit-restore',
    rationale: 'FreeCAD applies the same deterministic history evolution to a Reversed Pad Length edit/restore: Body and Pad geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
  },
  {
    taskId: 'TSN-DRIFT-003',
    caseId: 'partdesign-taper',
    classification: 'allowed_evolution',
    reasonCode: 'native-history-stabilizes-after-edit-restore',
    rationale: 'FreeCAD applies the same deterministic history evolution to a Tapered Pad Length edit/restore: Body and Pad geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
  },
  {
    taskId: 'TSN-DRIFT-004',
    caseId: 'partdesign-twoside',
    classification: 'allowed_evolution',
    reasonCode: 'native-history-stabilizes-after-edit-restore',
    rationale: 'FreeCAD applies the same deterministic history evolution to a Two Lengths Pad Length edit/restore: Body and Pad geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
  },
  {
    taskId: 'TSN-DRIFT-005',
    caseId: 'partdesign-pocket',
    classification: 'allowed_evolution',
    reasonCode: 'native-history-stabilizes-after-edit-restore',
    rationale: 'FreeCAD applies deterministic history evolution to an upstream Pad Length edit/restore: Body, Pad, and downstream Pocket geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
  },
  {
    taskId: 'TSN-DRIFT-006',
    caseId: 'partdesign-pocket-through',
    classification: 'allowed_evolution',
    reasonCode: 'native-history-stabilizes-after-edit-restore',
    rationale: 'FreeCAD applies deterministic history evolution through a Through All Pocket after an upstream Pad Length edit/restore: Body, Pad, and Pocket geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
  },
  {
    taskId: 'TSN-DRIFT-007',
    caseId: 'partdesign-pocket-midplane',
    classification: 'allowed_evolution',
    reasonCode: 'native-history-stabilizes-after-edit-restore',
    rationale: 'FreeCAD applies deterministic history evolution through a Midplane Pocket after an upstream Pad Length edit/restore: Body, Pad, and Pocket geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
  },
  {
    taskId: 'TSN-DRIFT-008',
    caseId: 'partdesign-pocket-twoside',
    classification: 'allowed_evolution',
    reasonCode: 'native-history-stabilizes-after-edit-restore',
    rationale: 'FreeCAD applies deterministic history evolution through a Two Lengths Pocket after an upstream Pad Length edit/restore: Body, Pad, and Pocket geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
  },
  {
    taskId: 'TSN-DRIFT-009',
    caseId: 'partdesign-pocket-taper',
    classification: 'allowed_evolution',
    reasonCode: 'native-history-stabilizes-after-edit-restore',
    rationale: 'FreeCAD applies deterministic history evolution through a Tapered Pocket after an upstream Pad Length edit/restore: Body, Pad, and Pocket geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
  },
  {
    taskId: 'TSN-DRIFT-010',
    caseId: 'partdesign-pocket-up-to-face',
    classification: 'allowed_evolution',
    reasonCode: 'native-history-stabilizes-after-edit-restore',
    rationale: 'FreeCAD applies deterministic history evolution through an Up To Face Pocket after an upstream Pad Length edit/restore: Body, Pad, and Pocket geometry returns exactly, and the evolved names remain stable through save, reopen, and resave.',
  },
]

export const same = (left, right) => JSON.stringify(canonical(left)) === JSON.stringify(canonical(right))

const namingFingerprint = (record) => ({
  relationDigest: record.relationDigest,
  semanticNameDigest: record.semanticNameDigest,
})

const phaseFingerprint = (record) => ({
  geometryDigest: record.shape.geometryDigest,
  relationDigest: record.relationDigest,
  semanticNameDigest: record.semanticNameDigest,
})

export const buildRecoveredNamingSnapshot = (fixture, resaveFixture) => {
  const driftOrdinals = fixture?.mutation?.metrics?.namingRestorationDriftOrdinals
  if (!Array.isArray(driftOrdinals) || driftOrdinals.length === 0) throw new Error(`${fixture?.id ?? 'unknown case'} has no recovered naming drift to classify.`)
  if (!resaveFixture || !['initial', 'reopened', 'resaved'].every((phase) => Array.isArray(resaveFixture.stageCorrelations?.[phase]))) throw new Error(`${fixture.id} lacks native save/reopen/resave evidence.`)

  const stages = driftOrdinals.map((ordinal) => {
    const nominal = fixture.stages?.[ordinal]
    const before = fixture.mutation.phases?.before?.[ordinal]
    const edited = fixture.mutation.phases?.edited?.[ordinal]
    const restored = fixture.mutation.phases?.restored?.[ordinal]
    if (![nominal, before, edited, restored].every(Boolean)) throw new Error(`${fixture.id}/${ordinal} lacks a complete mutation phase.`)
    const persistence = Object.fromEntries(['initial', 'reopened', 'resaved'].map((phase) => {
      const record = resaveFixture.stageCorrelations[phase][ordinal]
      if (!record) throw new Error(`${fixture.id}/${ordinal} lacks ${phase} persistence evidence.`)
      return [phase, namingFingerprint(record)]
    }))
    return {
      ordinal,
      name: nominal.name,
      typeId: nominal.typeId,
      nominal: phaseFingerprint(nominal),
      before: phaseFingerprint(before),
      edited: phaseFingerprint(edited),
      restored: phaseFingerprint(restored),
      persistence,
    }
  })

  return {
    caseId: fixture.id,
    contract: {
      targetObject: fixture.mutation.contract.targetObject,
      targetTypeId: fixture.mutation.contract.targetTypeId,
      propertyPath: fixture.mutation.contract.propertyPath,
      propertyType: fixture.mutation.contract.propertyType,
      finalObject: fixture.mutation.contract.finalObject,
    },
    values: fixture.mutation.values,
    driftOrdinals,
    stages,
    checks: {
      propertyRestored: fixture.mutation.metrics.propertyRestored === true,
      geometryRestored: stages.every(({ before, restored }) => before.geometryDigest === restored.geometryDigest),
      namingEvolved: stages.every(({ before, restored }) => !same({ relationDigest: before.relationDigest, semanticNameDigest: before.semanticNameDigest }, { relationDigest: restored.relationDigest, semanticNameDigest: restored.semanticNameDigest })),
      restoredMatchesNominal: stages.every(({ nominal, restored }) => same(nominal, restored)),
      persistenceStable: resaveFixture.roundtripNameDrift === 0
        && resaveFixture.resaveNameDrift === 0
        && resaveFixture.nativeDesktopResaveCovered === true
        && stages.every(({ restored, persistence }) => ['initial', 'reopened', 'resaved'].every((phase) => same({ relationDigest: restored.relationDigest, semanticNameDigest: restored.semanticNameDigest }, persistence[phase]))),
    },
  }
}

export const validateRecoveredNamingSnapshot = (snapshot) => {
  if (!snapshot || typeof snapshot.caseId !== 'string' || !snapshot.caseId) throw new Error('classification snapshot has no case identity.')
  if (!Array.isArray(snapshot.driftOrdinals) || snapshot.driftOrdinals.length === 0 || snapshot.stages?.length !== snapshot.driftOrdinals.length) throw new Error(`${snapshot.caseId} has an invalid drift-stage set.`)
  if (!same(snapshot.stages.map(({ ordinal }) => ordinal), snapshot.driftOrdinals)) throw new Error(`${snapshot.caseId} drift-stage identities are inconsistent.`)
  for (const stage of snapshot.stages) {
    if (!Number.isInteger(stage.ordinal) || !stage.name || !stage.typeId) throw new Error(`${snapshot.caseId} has an invalid stage identity.`)
    for (const phase of ['nominal', 'before', 'edited', 'restored']) {
      const value = stage[phase]
      if (!sha256(value?.geometryDigest) || !sha256(value?.relationDigest) || !sha256(value?.semanticNameDigest)) throw new Error(`${snapshot.caseId}/${stage.name}/${phase} has an invalid fingerprint.`)
    }
    for (const phase of ['initial', 'reopened', 'resaved']) {
      const value = stage.persistence?.[phase]
      if (!sha256(value?.relationDigest) || !sha256(value?.semanticNameDigest)) throw new Error(`${snapshot.caseId}/${stage.name}/${phase} has invalid persistence evidence.`)
    }
  }
  if (!Object.values(snapshot.checks ?? {}).every((value) => value === true)) throw new Error(`${snapshot.caseId} does not satisfy the allowed-evolution evidence contract.`)
}
