const finite = (value) => typeof value === 'number' && Number.isFinite(value)
const toleranceFor = (scenario, path) => path.includes('boundingBox') ? scenario.tolerance.linear : scenario.tolerance.scalar
const domainFor = (path) => path.includes('boundingBox') ? 'shape' : ['solids', 'faces', 'edges', 'vertices'].some((name) => path.endsWith(`.${name}`)) ? 'topology' : ['volume', 'area'].some((name) => path.endsWith(`.${name}`)) ? 'shape' : path.includes('objects') || path.includes('tree') ? 'objectTree' : path.includes('properties') ? 'properties' : 'identity'

export function compareSemanticResult(scenario, actual, { source = 'FreeCAD', target = 'Web' } = {}) {
  const differences = []
  const comparedDomains = new Set()
  const compare = (expected, received, path) => {
    const domain = domainFor(path)
    comparedDomains.add(domain)
    if (typeof expected === 'number') {
      if (!finite(received) || Math.abs(expected - received) > toleranceFor(scenario, path)) differences.push({ kind: 'numeric', classification: 'unknown', domain, path, expected, actual: received, tolerance: toleranceFor(scenario, path) })
      return
    }
    if (Array.isArray(expected)) {
      if (!Array.isArray(received) || received.length !== expected.length) differences.push({ kind: 'structure', classification: 'unknown', domain, path, expected: `array(${expected.length})`, actual: Array.isArray(received) ? `array(${received.length})` : typeof received })
      else expected.forEach((value, index) => compare(value, received[index], `${path}[${index}]`))
      return
    }
    if (expected && typeof expected === 'object') {
      if (!received || typeof received !== 'object' || Array.isArray(received)) { differences.push({ kind: 'structure', classification: 'unknown', domain, path, expected: 'object', actual: typeof received }); return }
      for (const [key, value] of Object.entries(expected)) compare(value, received[key], `${path}.${key}`)
      return
    }
    if (received !== expected) differences.push({ kind: 'value', classification: 'unknown', domain, path, expected, actual: received })
  }
  compare(scenario.expected, actual, scenario.id)
  const allowedRuntimeFields = new Set(['schemaVersion', 'fixtureId', 'freecadVersion', 'freecadVersion', 'runtimeStatus'])
  for (const key of Object.keys(actual || {})) if (!Object.prototype.hasOwnProperty.call(scenario.expected || {}, key) && !allowedRuntimeFields.has(key)) differences.push({ kind: 'field', classification: 'unknown', domain: domainFor(key), path: `${scenario.id}.${key}`, expected: 'absent', actual: actual[key] })
  return {
    schemaVersion: 1,
    fixtureId: scenario.id,
    source,
    target,
    status: differences.length === 0 ? 'pass' : 'fail-unknown-difference',
    domains: Object.fromEntries(['identity', 'shape', 'topology', 'objectTree', 'properties'].map((domain) => [domain, comparedDomains.has(domain) ? 'compared' : 'no-fixture-field'])),
    differences,
  }
}
