import { readFile, writeFile } from 'node:fs/promises'
import { resolve } from 'node:path'

const root = resolve(new URL('..', import.meta.url).pathname)
const load = async (file) => JSON.parse(await readFile(resolve(root, file), 'utf8'))
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, child]) => [key, canonical(child)]))
    : value
const stable = (value) => JSON.stringify(canonical(value))
const plan = await load('config/freecad-parameter-mutation-plan.json')
const coverage = await load(plan.coveragePlan)
const fail = (message) => { throw new Error(`FreeCAD parameter mutation report: ${message}`) }
if (plan.schemaVersion !== 1 || coverage.schemaVersion !== 1 || coverage.baselineId !== plan.baselineId || coverage.sourceCommit !== plan.sourceCommit) fail('baseline is not locked to FreeCAD 1.1.1.')

const familyByType = new Map()
for (const family of coverage.families) {
  familyByType.set(family.id, family.id)
  for (const type of family.operationTypes) familyByType.set(type, family.id)
}
const typeAliases = new Map([
  ['PartDesign::Revolution', 'partdesign-revolution'],
  ['PartDesign::AdditiveLoft', 'additive-loft'],
  ['PartDesign::SubtractiveLoft', 'subtractive-loft'],
  ['PartDesign::AdditivePipe', 'additive-pipe'],
  ['PartDesign::SubtractivePipe', 'subtractive-pipe'],
  ['PartDesign::Fillet', 'partdesign-fillet'],
  ['PartDesign::Chamfer', 'partdesign-chamfer'],
])
for (const [typeId, alias] of typeAliases) familyByType.set(alias, familyByType.get(typeId) || typeId)
const records = new Map(coverage.families.map((family) => [family.id, {
  nominal: [],
  invalid: [],
  parameterVariants: new Map(),
  editRecovery: [],
  roundtrip: { required: family.roundtripIds.length, passed: 0 },
  sources: new Set(),
}]))
const addRecord = (familyId, dimension, record, source) => {
  const bucket = records.get(familyId)
  if (!bucket) return
  bucket[dimension].push(record)
  bucket.sources.add(source)
}
const addVariant = (familyId, snapshot, source) => {
  const bucket = records.get(familyId)
  if (!bucket || snapshot === undefined) return
  bucket.parameterVariants.set(stable(snapshot), { snapshot, source })
  bucket.sources.add(source)
}
const addMutation = (mutation, source) => {
  if (!mutation || typeof mutation !== 'object') return
  const familyId = familyByType.get(mutation.familyId)
  if (!familyId || mutation.status !== 'pass' || mutation.parameterChanged !== true || mutation.shapeChanged !== true || mutation.restoredExactly !== true) return
  addRecord(familyId, 'editRecovery', { transaction: `native-parameter:${mutation.featureName}:${mutation.propertyPath}`, ...mutation }, source)
  addVariant(familyId, mutation.before.parameterSnapshot, `${source}#before`)
  addVariant(familyId, mutation.edited.parameterSnapshot, `${source}#edited`)
}
const manifestCases = async (manifestPath, entryKey, directory) => {
  const manifest = await load(manifestPath)
  const base = resolve(root, directory)
  return Promise.all((manifest[entryKey] || []).map(async (entry) => ({
    ...await load(resolve(base, entry.file)),
    source: `${manifestPath}/${entry.id}`,
  })))
}
const [successFixtures, failureFixtures, supplementalSuccessFixtures, supplementalFailureFixtures] = await Promise.all([
  manifestCases('fixtures/freecad-golden/manifest.json', 'scenarios', 'fixtures/freecad-golden'),
  manifestCases('fixtures/freecad-golden/failures/manifest.json', 'failures', 'fixtures/freecad-golden/failures'),
  manifestCases('fixtures/freecad-golden/feature-families/manifest.json', 'scenarios', 'fixtures/freecad-golden/feature-families'),
  manifestCases('fixtures/freecad-golden/feature-families/failures/manifest.json', 'failures', 'fixtures/freecad-golden/feature-families/failures'),
])
for (const fixture of [...successFixtures, ...supplementalSuccessFixtures]) {
  const familyId = familyByType.get(fixture.operation?.type)
  if (!familyId) continue
  addRecord(familyId, 'nominal', { id: fixture.id, parameterSnapshot: fixture.operation }, fixture.source)
  addVariant(familyId, fixture.operation, fixture.source)
}
for (const fixture of [...failureFixtures, ...supplementalFailureFixtures]) {
  const familyId = familyByType.get(fixture.operation?.type)
  if (!familyId) continue
  addRecord(familyId, 'invalid', { id: fixture.id, parameterSnapshot: fixture.operation }, fixture.source)
}

const addOracleCases = async (file, successKey, failureKey) => {
  const report = await load(file)
  const source = file
  for (const key of [successKey, failureKey]) for (const fixture of report[key] || []) {
    const familyId = familyByType.get(fixture.typeId)
    if (!familyId) continue
    if (key === successKey) {
      addRecord(familyId, 'nominal', { id: fixture.id, typeId: fixture.typeId }, source)
      addMutation(fixture.mutation, source)
    }
    else addRecord(familyId, 'invalid', { id: fixture.id, typeId: fixture.typeId }, source)
  }
}
await addOracleCases('config/freecad-part-builders-oracle.json', 'successCases', 'failureCases')
const coreMutationOracle = await load('config/freecad-core-parameter-mutation-oracle.json')
for (const mutation of coreMutationOracle.cases || []) addMutation(mutation, 'config/freecad-core-parameter-mutation-oracle.json')
for (const file of ['config/freecad-partdesign-failure-oracle.json', 'config/freecad-partdesign-revolution-groove-oracle.json', 'config/freecad-partdesign-transform-oracle.json']) {
  const report = await load(file)
  for (const fixture of report.cases || []) {
    const familyId = familyByType.get(fixture.typeId)
    if (!familyId) continue
    if (fixture.passed === true && fixture.status === 'Valid' && fixture.shapeValid !== false && fixture.solids > 0) addRecord(familyId, 'nominal', { id: fixture.id, typeId: fixture.typeId }, file)
    if (fixture.passed === true && (fixture.observed === 'rejected' || fixture.state?.includes('Invalid'))) addRecord(familyId, 'invalid', { id: fixture.id, typeId: fixture.typeId }, file)
    addMutation(fixture.mutation, file)
  }
}
const baseOracle = await load('config/freecad-partdesign-base-oracle.json')
for (const fixture of baseOracle.cases || []) {
  const familyId = fixture.id.startsWith('pad-') ? 'PartDesign::Pad' : fixture.id.startsWith('pocket-') ? 'PartDesign::Pocket' : undefined
  if (familyId && fixture.passed === true && fixture.status === 'Valid' && fixture.solids > 0) addRecord(familyId, 'nominal', { id: fixture.id, typeId: familyId, outcome: { volume: fixture.volume, faces: fixture.faces } }, 'config/freecad-partdesign-base-oracle.json')
  if (familyId) addMutation(fixture.mutation, 'config/freecad-partdesign-base-oracle.json')
}
for (const file of ['config/freecad-partdesign-loft-oracle.json', 'config/freecad-partdesign-dressup-oracle.json']) {
  const report = await load(file)
  for (const fixture of report.cases || []) {
    const familyId = familyByType.get(fixture.typeId)
    if (familyId && fixture.passed === true && fixture.status === 'Valid' && fixture.solids > 0) addRecord(familyId, 'nominal', { id: fixture.id, typeId: fixture.typeId, outcome: { volume: fixture.volume, faces: fixture.faces } }, file)
    addMutation(fixture.mutation, file)
  }
}

const lifecycle = await load('config/chrome-partdesign-lifecycle-verification.json')
if (lifecycle.status === 'pass' && lifecycle.initial && lifecycle.edited && lifecycle.undone && lifecycle.reopened) {
  for (const familyId of ['PartDesign::Pad', 'PartDesign::Pocket']) addRecord(familyId, 'editRecovery', { transaction: 'pad-length-edit', before: lifecycle.initial, edited: lifecycle.edited, restored: lifecycle.undone, reopened: lifecycle.reopened }, 'config/chrome-partdesign-lifecycle-verification.json')
}
const loft = await load('config/chrome-partdesign-loft-verification.json')
if (loft.status === 'pass') for (const [operation, recovery] of Object.entries(loft.failureRecovery || {})) {
  for (const familyId of operation === 'loft' ? ['PartDesign::AdditiveLoft', 'PartDesign::SubtractiveLoft'] : ['PartDesign::AdditivePipe', 'PartDesign::SubtractivePipe']) addRecord(familyId, 'editRecovery', { transaction: `${operation}-failure-recovery`, recovery }, 'config/chrome-partdesign-loft-verification.json')
}

const omitRuntimeFields = (value) => {
  if (!value || typeof value !== 'object' || Array.isArray(value)) return value
  const ignored = new Set(['objectId', 'tip', 'shapeId', 'volume', 'area', 'shapeCount', 'kernelReferenceCount', 'recompute', 'status', 'solids', 'faces', 'edges', 'vertices', 'structuralValid', 'base'])
  return Object.fromEntries(Object.entries(value).filter(([key]) => !ignored.has(key)))
}
const visitBrowserEvidence = (value, source) => {
  if (!value || typeof value !== 'object') return
  if (typeof value.command === 'string') {
    const familyId = familyByType.get(value.command)
    if (familyId) addVariant(familyId, omitRuntimeFields(value), source)
  }
  for (const child of Object.values(value)) visitBrowserEvidence(child, source)
}
for (const file of ['config/chrome-part-primitives-verification.json', 'config/chrome-partdesign-transform-verification.json', 'config/chrome-partdesign-loft-verification.json', 'config/chrome-partdesign-lifecycle-verification.json']) {
  visitBrowserEvidence(await load(file), file)
}
if (lifecycle.initial?.length !== undefined && lifecycle.edited?.length !== undefined && lifecycle.undone?.length !== undefined) {
  addVariant('PartDesign::Pad', { length: lifecycle.initial.length }, 'config/chrome-partdesign-lifecycle-verification.json#initial')
  addVariant('PartDesign::Pad', { length: lifecycle.edited.length }, 'config/chrome-partdesign-lifecycle-verification.json#edited')
  addVariant('PartDesign::Pad', { length: lifecycle.undone.length }, 'config/chrome-partdesign-lifecycle-verification.json#restored')
}

for (const family of coverage.families) {
  const bucket = records.get(family.id)
  const roundtrip = await load(coverage.fcstdRoundtrip)
  const requiredIds = new Set(family.roundtripIds)
  bucket.roundtrip.passed = roundtrip.scenarios.filter((scenario) => requiredIds.has(scenario.id) && scenario.status === 'pass' && (scenario.differences || []).length === 0).length
}

const families = coverage.families.map((family) => {
  const bucket = records.get(family.id)
  const evidence = {
    'nominal-parameter-set': bucket.nominal.length > 0,
    'invalid-boundary': bucket.invalid.length > 0,
    'parameter-variant': bucket.parameterVariants.size >= 2,
    'edit-recompute-recovery': bucket.editRecovery.length > 0,
    'fcstd-roundtrip': bucket.roundtrip.required === 0 || bucket.roundtrip.passed === bucket.roundtrip.required,
  }
  const missing = Object.entries(evidence).filter(([, present]) => !present).map(([dimension]) => dimension)
  return {
    id: family.id,
    operationTypes: family.operationTypes,
    evidence,
    counts: {
      nominal: bucket.nominal.length,
      invalid: bucket.invalid.length,
      parameterVariants: bucket.parameterVariants.size,
      editRecovery: bucket.editRecovery.length,
      roundtripRequired: bucket.roundtrip.required,
      roundtripPassed: bucket.roundtrip.passed,
    },
    parameterVariantSources: [...bucket.parameterVariants.values()].slice(0, 12),
    editRecoverySources: bucket.editRecovery.map((entry) => entry.transaction),
    sources: [...bucket.sources].sort(),
    complete: missing.length === 0,
    missing,
  }
})
const complete = families.filter((family) => family.complete).length
const missingDimensions = Object.fromEntries([...new Set(families.flatMap((family) => family.missing))].sort().map((dimension) => [dimension, families.filter((family) => family.missing.includes(dimension)).length]))
const report = {
  schemaVersion: 1,
  baselineId: plan.baselineId,
  sourceCommit: plan.sourceCommit,
  status: complete === families.length ? 'pass' : 'fail-closed',
  exactPromotionReady: false,
  dimensions: plan.dimensions,
  summary: {
    families: families.length,
    completeFamilies: complete,
    incompleteFamilies: families.length - complete,
    nominalFamilies: families.filter((family) => family.evidence['nominal-parameter-set']).length,
    invalidBoundaryFamilies: families.filter((family) => family.evidence['invalid-boundary']).length,
    parameterVariantFamilies: families.filter((family) => family.evidence['parameter-variant']).length,
    editRecoveryFamilies: families.filter((family) => family.evidence['edit-recompute-recovery']).length,
    roundtripFamilies: families.filter((family) => family.evidence['fcstd-roundtrip']).length,
  },
  blockers: [
    `${missingDimensions['parameter-variant'] || 0} families lack two distinct parameter snapshots.`,
    `${missingDimensions['edit-recompute-recovery'] || 0} families lack a real edited/recomputed/restored transaction.`,
  ].filter((message) => !message.startsWith('0 ')),
  missingDimensions,
  families,
}
const outputPath = resolve(root, 'config/freecad-parameter-mutation-report.json')
const serialized = `${JSON.stringify(report, null, 2)}\n`
if (process.argv.includes('--check')) {
  const existing = await readFile(outputPath, 'utf8')
  if (existing !== serialized) fail('config/freecad-parameter-mutation-report.json is stale; run npm run generate:freecad-parameter-mutations.')
  console.log(JSON.stringify({ status: report.status === 'pass' ? 'freecad-parameter-mutation-pass' : 'freecad-parameter-mutation-fail-closed', ...report.summary, missingDimensions: report.missingDimensions, blockers: report.blockers, exactPromotionReady: false }, null, 2))
} else {
  await writeFile(outputPath, serialized)
  console.log(JSON.stringify({ status: 'freecad-parameter-mutation-report-generated', ...report.summary, blockers: report.blockers }, null, 2))
}
