export const FREECAD_NAMING_CALLBACKS = [
  'freecadNamingAbiVersion',
  'freecadNamingCapabilitiesJson',
  'freecadNamingEvidenceJson',
]

export const FREECAD_NAMING_OPERATIONS = [
  'fuse', 'cut', 'common', 'rotate', 'pad', 'pocket', 'loft', 'pipe',
  'revolution', 'groove', 'fillet', 'chamfer', 'hole', 'draft', 'thickness',
  'linear-pattern', 'polar-pattern', 'mirrored', 'multi-transform',
]

const shapeStep = (module, method, ...args) => {
  const shape = module[method](...args)
  try {
    return module.shapeToStep(shape)
  } finally {
    shape.delete?.()
  }
}

const box = (module, ...args) => shapeStep(module, 'makeBox', ...args)
const placedBox = (module, ...args) => shapeStep(module, 'makeBoxPlaced', ...args)
const rectangle = (module, ...args) => shapeStep(module, 'makeRectangleFace', ...args)
const placedRectangle = (module, ...args) => shapeStep(module, 'makeRectangleFacePlaced', ...args)
const line = (module, ...args) => shapeStep(module, 'makeLineWire', ...args)

const input = (inputId, role, objectTag, step) => ({
  inputId,
  objectId: `production-${inputId}`,
  role,
  objectTag,
  step,
})

const fixtures = {
  fuse(module) {
    const objectStep = box(module, 10, 10, 10)
    const toolStep = placedBox(module, 5, 5, 5, 8, 0, 0)
    return { response: module.booleanHistoryFromStep(objectStep, toolStep, 'fuse'), inputs: [input('object', 'object', 101, objectStep), input('tool', 'tool', 102, toolStep)] }
  },
  cut(module) {
    const objectStep = box(module, 10, 10, 10)
    const toolStep = box(module, 5, 5, 5)
    return { response: module.booleanHistoryFromStep(objectStep, toolStep, 'cut'), inputs: [input('object', 'object', 111, objectStep), input('tool', 'tool', 112, toolStep)] }
  },
  common(module) {
    const objectStep = box(module, 10, 10, 10)
    const toolStep = placedBox(module, 5, 5, 5, 8, 2, 2)
    return { response: module.booleanHistoryFromStep(objectStep, toolStep, 'common'), inputs: [input('object', 'object', 121, objectStep), input('tool', 'tool', 122, toolStep)] }
  },
  rotate(module) {
    const objectStep = placedBox(module, 2, 1, 1, 1, 0, 0)
    return { response: module.rotateHistoryFromStep(objectStep, 0, 0, 0, 0, 0, 1, 30), inputs: [input('object', 'object', 131, objectStep)] }
  },
  pad(module) {
    const objectStep = rectangle(module, 2, 3)
    return { response: module.prismHistoryFromStep(objectStep, 0, 0, 5), inputs: [input('object', 'object', 141, objectStep)] }
  },
  pocket(module) {
    const objectStep = box(module, 10, 10, 10)
    const toolStep = rectangle(module, 2, 3)
    return { response: module.pocketHistoryFromStep(objectStep, toolStep, 0, 0, 5), inputs: [input('object', 'object', 151, objectStep), input('tool', 'tool', 152, toolStep)] }
  },
  loft(module) {
    const objectStep = placedRectangle(module, 2, 2, 0, 0, 0)
    const toolStep = placedRectangle(module, 2, 2, 0, 0, 5)
    return { response: module.loftHistoryFromStep(objectStep, toolStep, false), inputs: [input('object', 'object', 161, objectStep), input('tool', 'tool', 162, toolStep)] }
  },
  pipe(module) {
    const objectStep = rectangle(module, 2, 2)
    const toolStep = line(module, 0, 0, 0, 0, 0, 5)
    return { response: module.pipeHistoryFromStep(objectStep, toolStep), inputs: [input('object', 'object', 171, objectStep), input('tool', 'tool', 172, toolStep)] }
  },
  revolution(module) {
    const objectStep = rectangle(module, 2, 3)
    return { response: module.revolutionHistoryFromStep(objectStep, -1, 0, 0, 0, 1, 0, 360), inputs: [input('object', 'object', 181, objectStep)] }
  },
  groove(module) {
    const objectStep = placedBox(module, 10, 10, 8, -5, 0, -4)
    const toolStep = placedRectangle(module, 1, 3, 0.5, 2, 0)
    return { response: module.grooveHistoryFromStep(objectStep, toolStep, 0, 0, 0, 0, 1, 0, 360), inputs: [input('object', 'object', 191, objectStep), input('tool', 'tool', 192, toolStep)] }
  },
  fillet(module) {
    const objectStep = box(module, 6, 6, 6)
    return { response: module.filletHistoryFromStep(objectStep, 0.4), inputs: [input('object', 'object', 201, objectStep)] }
  },
  chamfer(module) {
    const objectStep = box(module, 6, 6, 6)
    return { response: module.chamferHistoryFromStep(objectStep, 0.4), inputs: [input('object', 'object', 211, objectStep)] }
  },
  hole(module) {
    const objectStep = box(module, 10, 10, 10)
    return { response: module.holeHistoryFromStep(objectStep, 1, 10, 0, 0, -5, 0, 0, 1), inputs: [input('object', 'object', 221, objectStep)] }
  },
  draft(module) {
    const objectStep = box(module, 10, 10, 10)
    return { response: module.draftHistoryFromStep(objectStep, 0, 5, 0, 0, 1, 0, 0, 0, 0, 0, 1, false), inputs: [input('object', 'object', 231, objectStep)] }
  },
  thickness(module) {
    const objectStep = box(module, 6, 6, 6)
    return { response: module.thicknessHistoryFromStep(objectStep, 1, -0.4, false), inputs: [input('object', 'object', 241, objectStep)] }
  },
  'linear-pattern'(module) {
    const objectStep = box(module, 2, 2, 2)
    return { response: module.linearPatternHistoryFromStep(objectStep, 2, 0, 0), inputs: [input('object', 'object', 251, objectStep)] }
  },
  'polar-pattern'(module) {
    const objectStep = placedBox(module, 2, 1, 1, -1, -0.5, -0.5)
    return { response: module.polarPatternHistoryFromStep(objectStep, 0, 0, 0, 0, 0, 1, 90), inputs: [input('object', 'object', 261, objectStep)] }
  },
  mirrored(module) {
    const objectStep = placedBox(module, 2, 1, 1, -0.5, -0.5, -0.5)
    return { response: module.mirroredHistoryFromStep(objectStep, 0, 0, 0, 1, 0, 0), inputs: [input('object', 'object', 271, objectStep)] }
  },
  'multi-transform'(module) {
    const objectStep = placedBox(module, 2, 1, 1, -0.5, -0.5, -0.5)
    return {
      response: module.multiTransformHistoryFromStep(objectStep, [
        { type: 'linear', direction: [1, 0, 0] },
        { type: 'mirrored', axisOrigin: [0, 0, 0], direction: [1, 0, 0] },
      ]),
      inputs: [input('object', 'object', 281, objectStep)],
    }
  },
}

const serializableHistory = (response) => ({
  provider: response.provider,
  occtVersion: response.occtVersion,
  records: response.records,
  hasModified: response.hasModified,
  hasGenerated: response.hasGenerated,
  hasDeleted: response.hasDeleted,
  resultStep: response.resultStep,
  resultBrep: response.resultBrep,
})

const externallySourcedRecords = (records, inputs) => {
  const sources = new Set(inputs.flatMap(({ inputId, role }) => [inputId, role]).filter(Boolean))
  if (inputs.length > 0) sources.add('object')
  if (inputs.length > 1) sources.add('tool')
  return records.filter((record) => sources.has(record.sourceId ?? record.source))
}

const resolveInput = (record, inputs) => {
  const source = record.sourceId ?? record.source
  return inputs.find((entry) => entry.inputId === source)
    ?? inputs.find((entry) => entry.role === source)
    ?? (source === 'object' ? inputs[0] : undefined)
    ?? (source === 'tool' ? inputs[1] : undefined)
}

const persistentSourceName = (record, sourceInput) => {
  const prior = sourceInput.namingEvidence?.mappedNames?.find((mapped) => mapped.kind === record.kind && mapped.resultIndex === record.sourceIndex)
  const title = `${record.kind[0].toUpperCase()}${record.kind.slice(1)}`
  return prior?.resultPersistentId ?? `${title}${record.sourceIndex + 1}`
}

export const classifyFreeCadNamingProvenance = (records, inputs) => {
  const externalRecords = externallySourcedRecords(records, inputs)
  const grouped = new Map()
  for (const record of externalRecords) {
    if (record.relation === 'deleted') continue
    const sourceInput = resolveInput(record, inputs)
    if (!sourceInput) throw new Error(`Native source ${record.sourceId ?? record.source} has no transport input.`)
    const resultKind = record.resultKind || record.kind
    const indexes = Array.isArray(record.resultIndexes) ? record.resultIndexes : [record.resultIndex]
    for (const resultIndex of indexes) {
      if (!Number.isSafeInteger(resultIndex) || resultIndex < 0) throw new Error(`Native result ${resultKind}:${String(resultIndex)} is invalid.`)
      const key = `${resultKind}:${resultIndex}`
      const entries = grouped.get(key) ?? []
      entries.push({
        record: { ...record, resultKind, resultIndex, resultIndexes: undefined },
        resultKind,
        resultIndex,
        candidate: {
          inputId: sourceInput.inputId,
          objectId: sourceInput.objectId || sourceInput.inputId,
          persistentId: persistentSourceName(record, sourceInput),
          ...(sourceInput.stageId ? { stageId: sourceInput.stageId } : {}),
          relation: record.relation,
          sourceKind: record.kind,
          sourceIndex: record.sourceIndex,
        },
      })
      grouped.set(key, entries)
    }
  }
  const selected = externalRecords.filter((record) => record.relation === 'deleted').map((record) => ({ ...record }))
  const ambiguities = []
  let duplicateRecordCount = 0
  let relationConflictCount = 0
  for (const entries of grouped.values()) {
    const unique = new Map()
    for (const entry of entries) {
      const candidate = entry.candidate
      const key = `${candidate.objectId}\u0000${candidate.persistentId}\u0000${candidate.stageId || ''}\u0000${candidate.relation}`
      if (!unique.has(key)) unique.set(key, entry)
    }
    duplicateRecordCount += entries.length - unique.size
    const distinctSources = new Set([...unique.values()].map(({ candidate }) => `${candidate.objectId}\u0000${candidate.persistentId}\u0000${candidate.stageId || ''}`))
    if (distinctSources.size > 1) {
      ambiguities.push({ resultKind: entries[0].resultKind, resultIndex: entries[0].resultIndex, candidates: [...unique.values()].map(({ candidate }) => candidate) })
      continue
    }
    const relations = new Set([...unique.values()].map(({ candidate }) => candidate.relation))
    if (relations.size > 1) relationConflictCount += 1
    const stable = [...unique.values()].find(({ candidate }) => candidate.relation === 'modified') ?? [...unique.values()][0]
    selected.push(stable.record)
  }
  return {
    records: selected,
    ambiguities,
    externalRecordCount: externalRecords.length,
    internalRecordCount: records.length - externalRecords.length,
    duplicateRecordCount,
    relationConflictCount,
  }
}

const validateEvidence = (operation, evidence, recordCount) => {
  if (evidence.status !== 'native-evidence') throw new Error(`${operation}: callback did not return native evidence.`)
  if (evidence.mappedNames?.length !== recordCount || recordCount < 1) throw new Error(`${operation}: callback did not map every selected native history result.`)
  // A first-generation mapping from plain Face1/Edge1 names legitimately has
  // no interned strings. Chained mappings below must restore and grow the table.
  if (!Array.isArray(evidence.stringHasher?.entries)) throw new Error(`${operation}: StringHasher evidence is absent.`)
  if (!Array.isArray(evidence.elementMap2?.maps) || evidence.elementMap2.maps.length < 1) throw new Error(`${operation}: ElementMap2 evidence is empty.`)
  const hasherIds = new Set(evidence.stringHasher.entries.map((entry) => entry.id))
  const tokens = evidence.elementMap2.maps.flatMap((map) => map.sections.flatMap((section) => section.names.flatMap((name) => name.tokens)))
  for (const mapped of evidence.mappedNames) {
    const reference = mapped.reference
    if (typeof reference?.name !== 'string' || !reference.name) throw new Error(`${operation}: MappedNameRef is malformed.`)
    if (reference.name.startsWith('#')) {
      if (!Number.isSafeInteger(reference.prefixStringId) || !reference.stringIds?.includes(reference.prefixStringId) || !reference.stringIds.every((id) => hasherIds.has(id))) throw new Error(`${operation}: MappedNameRef is not closed over StringHasher.`)
      if (!tokens.some((token) => token.marker === '$' && token.name === reference.name)) throw new Error(`${operation}: MappedNameRef is absent from ElementMap2.`)
    }
  }
}

export const runFreeCadNamingProductionMatrix = async (module) => {
  if (!FREECAD_NAMING_CALLBACKS.every((name) => typeof module[name] === 'function')) throw new Error('Production Worker omits a required FreeCAD naming callback.')
  if (module.freecadNamingAbiVersion() !== 1) throw new Error('Production Worker FreeCAD naming ABI version is not 1.')
  const descriptor = JSON.parse(module.freecadNamingCapabilitiesJson())
  if (descriptor.freecadVersion !== '1.1.1' || descriptor.sourceCommit !== '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d') throw new Error('Production Worker naming descriptor is not source locked.')
  if (descriptor.operations?.length !== FREECAD_NAMING_OPERATIONS.length || FREECAD_NAMING_OPERATIONS.some((operation) => !descriptor.operations.includes(operation))) throw new Error('Production Worker naming descriptor does not cover all native builders.')

  const cases = []
  let chainedSource
  for (let index = 0; index < FREECAD_NAMING_OPERATIONS.length; index += 1) {
    const operation = FREECAD_NAMING_OPERATIONS[index]
    const { response, inputs } = fixtures[operation](module)
    try {
      if (response.provider !== 'occt-native' || !/^8\./.test(response.occtVersion) || !response.resultStep?.startsWith('ISO-10303-21;')) throw new Error(`${operation}: native builder response is invalid.`)
      const provenance = classifyFreeCadNamingProvenance(response.records, inputs)
      const records = provenance.records
      const stableResultCount = records.filter((record) => record.relation !== 'deleted').length
      if (stableResultCount < 1 && provenance.ambiguities.length < 1) throw new Error(`${operation}: native builder returned no usable result history.`)
      const request = {
        schemaVersion: 1,
        requestId: `production-${operation}-request`,
        documentId: 'production-naming-matrix',
        documentVersion: index + 1,
        operationId: `production-${operation}`,
        operation,
        stageId: `production:${operation}:stage`,
        resultObjectId: `production:${operation}:result`,
        resultObjectTag: 1000 + index,
        inputs,
        stages: [{ stageId: `production:${operation}:stage`, operation, inputIds: inputs.map(({ inputId }) => inputId), ordinal: 0 }],
        resultStep: response.resultStep,
        resultBrep: response.resultBrep,
        ...(provenance.ambiguities.length > 0 ? { historyAmbiguities: provenance.ambiguities } : {}),
        history: { ...serializableHistory(response), records },
      }
      const evidence = stableResultCount > 0 ? JSON.parse(module.freecadNamingEvidenceJson(JSON.stringify(request))) : undefined
      if (evidence) validateEvidence(operation, evidence, stableResultCount)
      cases.push({ operation, status: 'pass', namingStatus: evidence ? (provenance.ambiguities.length > 0 ? 'mixed-native-and-ambiguous' : 'native-evidence') : 'ambiguous', callbackExecuted: Boolean(evidence), historyRecords: response.records.length, externallySourcedRecords: provenance.externalRecordCount, internalRecordsExcluded: provenance.internalRecordCount, duplicateRecordsCollapsed: provenance.duplicateRecordCount, modifiedRelationPrecedence: provenance.relationConflictCount, selectedRecords: stableResultCount, ambiguousResults: provenance.ambiguities.length, provenanceResults: stableResultCount + provenance.ambiguities.length, mappedNames: evidence?.mappedNames.length ?? 0, stringHasherEntries: evidence?.stringHasher.entries.length ?? 0, elementMaps: evidence?.elementMap2.maps.length ?? 0 })
      if (operation === 'cut' && evidence) chainedSource = { request, evidence, record: records.find((record) => record.relation !== 'deleted') }
    } finally {
      response.result?.delete?.()
    }
  }

  const symmetricObjectStep = box(module, 10, 10, 10)
  const symmetricToolStep = box(module, 10, 10, 10)
  const symmetricResponse = module.booleanHistoryFromStep(symmetricObjectStep, symmetricToolStep, 'common')
  let isomorphicCorpus
  try {
    const symmetricInputs = [input('symmetric-object', 'object', 301, symmetricObjectStep), input('symmetric-tool', 'tool', 302, symmetricToolStep)]
    const provenance = classifyFreeCadNamingProvenance(symmetricResponse.records, symmetricInputs)
    if (provenance.ambiguities.length < 1 || provenance.ambiguities.some(({ candidates }) => candidates.length < 2)) throw new Error('Symmetric Common did not retain its non-unique native source candidates.')
    const candidateSets = provenance.ambiguities.map(({ resultKind, resultIndex, candidates }) => ({ result: `${resultKind}:${resultIndex}`, candidates: candidates.map(({ objectId, persistentId, stageId }) => `${objectId}:${persistentId}:${stageId || ''}`).sort() }))
    isomorphicCorpus = {
      operation: 'common',
      fixture: 'coincident-equal-boxes',
      status: 'pass',
      historyRecords: symmetricResponse.records.length,
      uniqueResults: provenance.records.filter((record) => record.relation !== 'deleted').length,
      ambiguousResults: provenance.ambiguities.length,
      candidateSets,
      arrayOrderFallbackUsed: false,
    }
  } finally {
    symmetricResponse.result?.delete?.()
  }

  if (!chainedSource) throw new Error('Production naming matrix lacks its chained cut source.')
  const nextIndex = chainedSource.record.resultIndex + 1
  const chainedRequest = {
    ...chainedSource.request,
    requestId: 'production-cut-chained-request',
    documentVersion: 20,
    operationId: 'production-cut-chained',
    stageId: 'production:cut:chained-stage',
    resultObjectId: 'production:cut:chained-result',
    resultObjectTag: 1100,
    inputs: [{ inputId: 'object', objectId: chainedSource.request.resultObjectId, role: 'object', stageId: chainedSource.request.stageId, objectTag: chainedSource.request.resultObjectTag, step: chainedSource.request.resultStep, namingEvidence: chainedSource.evidence }],
    stages: [{ stageId: 'production:cut:chained-stage', operation: 'cut', inputIds: ['object'], ordinal: 0 }],
    history: { ...chainedSource.request.history, records: [{ relation: 'modified', source: 'object', kind: chainedSource.record.resultKind || chainedSource.record.kind, sourceIndex: chainedSource.record.resultIndex, resultIndex: nextIndex }] },
  }
  const chainedEvidence = JSON.parse(module.freecadNamingEvidenceJson(JSON.stringify(chainedRequest)))
  validateEvidence('cut-chained', chainedEvidence, 1)
  if (chainedEvidence.stringHasher.entries.length <= chainedSource.evidence.stringHasher.entries.length) throw new Error('Production Worker did not restore and extend the prior StringHasher table.')

  const invalid = JSON.parse(module.freecadNamingEvidenceJson(JSON.stringify({ ...chainedSource.request, requestId: 'production-invalid-history', history: { ...chainedSource.request.history, records: [] } })))
  if (invalid.status !== 'error' || !invalid.error?.includes('requires inputs and native history records')) throw new Error('Production Worker did not fail closed for missing native history.')

  return {
    schemaVersion: 1,
    status: 'pass',
    implementation: 'freecad-linked',
    productionWorkerLinked: true,
    occtVersion: module.occtVersion(),
    freecadVersion: descriptor.freecadVersion,
    sourceCommit: descriptor.sourceCommit,
    callbacks: [...FREECAD_NAMING_CALLBACKS],
    operations: [...FREECAD_NAMING_OPERATIONS],
    cases,
    callbackExecutedCases: cases.filter(({ callbackExecuted }) => callbackExecuted).length,
    ambiguityOnlyCases: cases.filter(({ namingStatus }) => namingStatus === 'ambiguous').length,
    isomorphicCorpus,
    chainedStage: { mappedNames: chainedEvidence.mappedNames.length, stringHasherEntries: chainedEvidence.stringHasher.entries.length },
    invalidHistoryRejected: true,
    systemExact: false,
  }
}
