import { createMockFacade } from './facade/mockFacade'
import { createCamJob, type CamDressupKind, type CamOperationKind } from './facade/cam'
import { menuDefinitions, workbenchDefinitions } from './freecadManifest'

type CamReport = {
  schemaVersion: 1
  status: 'pass' | 'failed'
  browserId: 'chrome'
  crossOriginIsolated: boolean
  geometry?: { sourceShape: string; valid: boolean; volume: number }
  job?: {
    stockMode: string
    tools: number
    controllers: number
    operations: number
    operationKinds: string[]
    operationStatuses: string[]
    pathPoints: number
    dressups: number
    setup: { safeHeightOffset: number; clearanceHeightOffset: number; coolantMode: string }
    sanity: string
    sanityIssues: number
    simulation: string
    collisions: number
    collisionFixture: number
    simulationDiagnostics: number
    postprocessors: string[]
    post: string
    postHashes: Record<string, string>
    unsafePostRejected: boolean
    gcodeBytes: number
    gcodeHash: string
    deterministic: boolean
  }
  extended?: { commandSurface: number; operationKinds: number; dressupKinds: number; toolAssetRoundTrip: boolean; jobAssetRoundTrip: boolean; metadataRoundTrip: boolean; metadataComments: number; metadataProperties: number; metadataCompounds: number; undoRedo: boolean; startPointChanged: boolean; materialRemoval: boolean; removalTimelinePoints: number; fixtureCollision: boolean; multiAxis: boolean }
  persistence?: { mode: string; byteLength: number; hash: string; roundTrip: boolean; released: boolean }
  opfs?: { markerSuite: string; operations: number; markerRemoved: boolean }
  afterRelease?: { shapeCount: number; kernelReferenceCount: number }
  error?: string
}

const sha256 = async (bytes: Uint8Array) => [...new Uint8Array(await crypto.subtle.digest('SHA-256', Uint8Array.from(bytes).buffer))].map((value) => value.toString(16).padStart(2, '0')).join('')

const run = async (): Promise<CamReport> => {
  const facade = createMockFacade()
  const report: CamReport = { schemaVersion: 1, status: 'failed', browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true }
  try {
    if (!report.crossOriginIsolated || !navigator.storage?.getDirectory) throw new Error('Chrome CAM evidence requires isolated OPFS.')
    await facade.project.list()
    const shape = await facade.geometry.createBox({ documentId: 'cam', documentVersion: 1, width: 5, length: 2, height: 3 })
    const quality = await facade.geometry.qualityReport(shape)
    const mass = await facade.geometry.massProperties(shape)

    const cam = createCamJob('job', 'Box machining job', { min: [0, 0, 0], max: [5, 2, 3], mode: 'from-base-bound-box' })
    cam.updateSetupSheet({ safeHeightOffset: 1, clearanceHeightOffset: 2, coolantMode: 'flood' })
    cam.addTool({ id: 'T1', name: '1mm end mill', shape: 'endmill', diameter: 1, length: 20, cuttingEdgeHeight: 8, shankDiameter: 1 })
    cam.addToolController({ id: 'TC1', label: 'End Mill Controller', toolId: 'T1', toolNumber: 1, spindleSpeed: 16000, spindleDirection: 'forward', horizontalFeed: 240, verticalFeed: 80, horizontalRapid: 1800, verticalRapid: 900 })
    cam.addTool({ id: 'T2', name: '0.5mm drill', shape: 'drill', diameter: 0.5, length: 15, cuttingEdgeHeight: 6, shankDiameter: 0.5 })
    cam.addToolController({ id: 'TC2', label: 'Drill Controller', toolId: 'T2', toolNumber: 2, spindleSpeed: 12000, spindleDirection: 'forward', horizontalFeed: 120, verticalFeed: 60, horizontalRapid: 1800, verticalRapid: 900 })

    cam.addOperation({ id: 'profile', label: 'Outside Profile', kind: 'profile', toolId: 'T1', controllerId: 'TC1', depth: 1, feed: 240, verticalFeed: 80, stepDown: 1, stepOver: 50, coolantMode: 'flood' })
    cam.generatePath('profile')
    cam.applyDressup('profile', { kind: 'lead-in-out', parameters: { length: 0.5 } })
    cam.generatePath('profile')
    cam.addOperation({ id: 'pocket', label: 'Top Pocket', kind: 'pocket', toolId: 'T1', controllerId: 'TC1', depth: 1, feed: 220, verticalFeed: 80, stepDown: 1, stepOver: 50, direction: 'climb', coolantMode: 'flood' })
    cam.generatePath('pocket')
    cam.addOperation({ id: 'drilling', label: 'Mounting Holes', kind: 'drilling', toolId: 'T2', controllerId: 'TC2', depth: 1.5, feed: 60, verticalFeed: 60, stepDown: 1.5, locations: [[1, 1, 3], [4, 1, 3]], coolantMode: 'mist' })
    cam.generatePath('drilling')
    cam.addComment('Chrome CAM metadata evidence')
    cam.setPropertyBagValue('Material', 'Al6061')
    cam.setPropertyBagValue('BatchSize', 1)
    cam.setPropertyBagValue('DryRun', false)
    cam.createCompound(['profile', 'pocket'], 'roughing', 'Roughing Operations')

    const sanity = cam.sanityCheck()
    const simulation = cam.simulate()
    const postHashes: Record<string, string> = {}
    for (const postprocessor of cam.postprocessors()) postHashes[postprocessor] = await sha256(new TextEncoder().encode(cam.exportGcode(postprocessor)))
    const gcodeText = cam.exportGcode('linuxcnc')
    const secondGcode = cam.exportGcode('linuxcnc')
    const gcode = new TextEncoder().encode(gcodeText)

    const collision = createCamJob('collision', 'Collision')
    collision.addTool({ id: 'T1', name: 'Tool', diameter: 2, length: 10 })
    collision.addOperation({ id: 'draft', kind: 'contour', toolId: 'T1', depth: 1, feed: 10 })
    const collisionFixture = collision.simulate().collisions.length
    let unsafePostRejected = false
    try { cam.exportGcode('shell' as never) } catch { unsafePostRejected = true }

    const assetText = cam.exportToolBit('T1')
    const assetCam = createCamJob('asset')
    const importedTool = assetCam.importToolBit(assetText, 'T99')
    const toolAssetRoundTrip = importedTool.id === 'T99' && importedTool.name === '1mm end mill' && assetText === cam.exportToolBit('T1')
    const extendedCam = createCamJob('extended', 'Extended command surface', { min: [0, 0, 0], max: [10, 10, 5] })
    extendedCam.addTool({ id: 'T1', name: '2mm end mill', diameter: 2, length: 25 })
    const operationKinds: CamOperationKind[] = ['profile', 'pocket', 'contour', 'mill-face', 'helix', 'adaptive', 'slot', 'drilling', 'tapping', 'engrave', 'deburr', 'v-carve', 'pocket-3d', 'surface', 'waterline', 'thread-milling', 'probe', 'area', 'area-workplane', 'custom', 'shape', 'path-shape-tool-controller']
    for (const [index, kind] of operationKinds.entries()) {
      extendedCam.addOperation({ id: `extended-${index}`, kind, toolId: 'T1', depth: 1, feed: 100 })
      extendedCam.generatePath(`extended-${index}`)
    }
    const beforeStart = extendedCam.snapshot().operations[0].path[0]
    const afterStart = extendedCam.setStartPoint('extended-0', 1).path[0]
    const startPointChanged = beforeStart.some((value, index) => value !== afterStart[index])
    const dressupKinds: CamDressupKind[] = ['array', 'axis-map', 'boundary', 'dogbone', 'drag-knife', 'lead-in-out', 'ramp-entry', 'holding-tags', 'z-correct']
    for (const [index, kind] of dressupKinds.entries()) {
      const operationId = `extended-${index + 1}`
      extendedCam.applyDressup(operationId, { kind, parameters: kind === 'array' ? { count: 2, offsetX: 0, offsetY: 0 } : kind === 'dogbone' ? { radius: 0.5 } : kind === 'drag-knife' ? { offset: 0.5 } : {} })
      extendedCam.generatePath(operationId)
    }
    const fixtureCam = createCamJob('fixture', 'Fixture collision', { min: [0, 0, 0], max: [10, 10, 5] })
    fixtureCam.addTool({ id: 'T1', name: 'Fixture probe tool', diameter: 2, length: 20, cuttingEdgeHeight: 1, shankDiameter: 2, holderDiameter: 4, holderLength: 20 })
    fixtureCam.addOperation({ id: 'profile', kind: 'profile', toolId: 'T1', depth: 1, feed: 100 })
    fixtureCam.generatePath('profile')
    fixtureCam.updateCollisionFixtures([{ id: 'clamp', min: [-1, -1, 5], max: [1, 1, 7] }])
    const fixtureSimulation = fixtureCam.simulate()
    const jobAsset = cam.exportJob()
    const reopenedCam = createCamJob('job', 'Box machining job')
    reopenedCam.importJob(jobAsset)
    const jobAssetRoundTrip = reopenedCam.exportJob() === jobAsset
    const metadataRoundTrip = reopenedCam.snapshot().metadata.comments.length === 1 && reopenedCam.snapshot().metadata.propertyBag.Material === 'Al6061' && reopenedCam.snapshot().metadata.propertyBag.BatchSize === 1 && reopenedCam.snapshot().metadata.propertyBag.DryRun === false && reopenedCam.snapshot().metadata.compounds[0]?.id === 'roughing'
    const metadataBeforeRemoval = reopenedCam.snapshot().metadata
    reopenedCam.removeOperation('pocket')
    const metadataDependencyCleanup = reopenedCam.snapshot().metadata.compounds.length === 0
    reopenedCam.undo()
    const metadataUndoRestored = reopenedCam.snapshot().metadata.compounds.length === metadataBeforeRemoval.compounds.length
    reopenedCam.toggleOperation('profile', false)
    const undoRedo = metadataDependencyCleanup && metadataUndoRestored && reopenedCam.canUndo() && reopenedCam.undo().operations.find((operation) => operation.id === 'profile')?.active === true && reopenedCam.canRedo() && reopenedCam.redo().operations.find((operation) => operation.id === 'profile')?.active === false
    const multiAxisResult = cam.kinematics('profile', { configuration: '5-axis', rotary: { axis: 'C', startDegrees: -30, endDegrees: 30 }, tilt: { axis: 'B', startDegrees: 0, endDegrees: 45 } })
    const multiAxisGcode = cam.exportMultiAxisGcode('profile', 'linuxcnc', { configuration: '5-axis', rotary: { axis: 'C', startDegrees: -30, endDegrees: 30 }, tilt: { axis: 'B', startDegrees: 0, endDegrees: 45 } })
    const multiAxis = multiAxisResult.status === 'pass' && multiAxisResult.poses.length === cam.snapshot().operations.find((operation) => operation.id === 'profile')?.path.length && multiAxisResult.poses.at(-1)?.c === 30 && multiAxisResult.poses.at(-1)?.b === 45 && multiAxisGcode.includes('B45.000 C30.000') && multiAxisGcode.includes('M428') && multiAxisGcode.includes('G93') && multiAxisGcode.includes('M429')
    report.extended = { commandSurface: workbenchDefinitions.CAM.groups.flatMap((group) => group.commands).length, operationKinds: new Set(extendedCam.snapshot().operations.map((operation) => operation.kind)).size, dressupKinds: new Set(extendedCam.snapshot().operations.flatMap((operation) => operation.dressups.map((dressup) => dressup.kind))).size, toolAssetRoundTrip, jobAssetRoundTrip, metadataRoundTrip: metadataRoundTrip && metadataDependencyCleanup && metadataUndoRestored, metadataComments: cam.snapshot().metadata.comments.length, metadataProperties: Object.keys(cam.snapshot().metadata.propertyBag).length, metadataCompounds: cam.snapshot().metadata.compounds.length, undoRedo, startPointChanged, materialRemoval: simulation.materialRemoval.enabled && simulation.materialRemoval.removedVolume > 0 && simulation.materialRemoval.remainingVolume < simulation.materialRemoval.stockVolume, removalTimelinePoints: simulation.materialRemoval.timeline.length, fixtureCollision: fixtureSimulation.status === 'collision' && fixtureSimulation.diagnostics.some((diagnostic) => diagnostic.type === 'fixture'), multiAxis }
    if (menuDefinitions.CAM.length !== report.extended.commandSurface) throw new Error('CAM menu and toolbar command surfaces diverged.')

    const snapshot = cam.snapshot()
    const payload = new TextEncoder().encode(JSON.stringify(snapshot))
    const stored = await facade.project.resource.put(payload, 'application/vnd.bitbybit.cam-job+json')
    const loaded = await facade.project.resource.get(stored.hash)
    const roundTrip = loaded !== null && await sha256(loaded) === await sha256(payload)
    await facade.project.resource.release(stored.hash)
    const released = await facade.project.resource.get(stored.hash) === null

    const directory = await navigator.storage.getDirectory()
    const markerName = 'cam-chrome-marker.json'
    const marker = await directory.getFileHandle(markerName, { create: true })
    const writable = await marker.createWritable()
    await writable.write(JSON.stringify({ suite: 'CAM-ALL', operations: snapshot.operations.length }))
    await writable.close()
    const markerPayload = JSON.parse(await (await marker.getFile()).text()) as { suite: string; operations: number }
    await directory.removeEntry(markerName)
    let markerRemoved = false
    try { await directory.getFileHandle(markerName) } catch (error) { markerRemoved = error instanceof DOMException && error.name === 'NotFoundError' }

    report.geometry = { sourceShape: shape.id, valid: quality.structuralValid, volume: mass.volume }
    report.job = {
      stockMode: snapshot.stock.mode || '',
      tools: snapshot.tools.length,
      controllers: snapshot.toolControllers.length,
      operations: snapshot.operations.length,
      operationKinds: snapshot.operations.map((operation) => operation.kind),
      operationStatuses: snapshot.operations.map((operation) => operation.status),
      pathPoints: snapshot.operations.reduce((total, operation) => total + operation.path.length, 0),
      dressups: snapshot.operations.reduce((total, operation) => total + operation.dressups.length, 0),
      setup: { safeHeightOffset: snapshot.setupSheet.safeHeightOffset, clearanceHeightOffset: snapshot.setupSheet.clearanceHeightOffset, coolantMode: snapshot.setupSheet.coolantMode },
      sanity: sanity.status,
      sanityIssues: sanity.issues.length,
      simulation: simulation.status,
      collisions: simulation.collisions.length,
      collisionFixture,
      simulationDiagnostics: simulation.diagnostics.length,
      postprocessors: cam.postprocessors(),
      post: 'linuxcnc',
      postHashes,
      unsafePostRejected,
      gcodeBytes: gcode.byteLength,
      gcodeHash: await sha256(gcode),
      deterministic: gcodeText === secondGcode,
    }
    report.persistence = { mode: facade.project.capabilities().mode, byteLength: stored.byteLength, hash: stored.hash, roundTrip, released }
    report.opfs = { markerSuite: markerPayload.suite, operations: markerPayload.operations, markerRemoved }
    const expectedKinds = JSON.stringify(['profile', 'pocket', 'drilling'])
    report.status = report.geometry.valid && report.job.stockMode === 'from-base-bound-box' && report.job.tools === 2 && report.job.controllers === 2 && report.job.operations === 3 && JSON.stringify(report.job.operationKinds) === expectedKinds && report.job.operationStatuses.every((status) => status === 'generated') && report.job.pathPoints >= 20 && report.job.dressups === 1 && report.job.setup.safeHeightOffset === 1 && report.job.setup.clearanceHeightOffset === 2 && report.job.setup.coolantMode === 'flood' && report.job.sanity === 'pass' && report.job.sanityIssues === 0 && report.job.simulation === 'pass' && report.job.collisions === 0 && report.job.collisionFixture === 1 && report.job.simulationDiagnostics === 0 && report.job.postprocessors.length === 7 && Object.keys(report.job.postHashes).length === 7 && report.job.post === 'linuxcnc' && report.job.unsafePostRejected && report.job.gcodeBytes > 0 && report.job.deterministic && report.extended.commandSurface === 60 && report.extended.operationKinds === 22 && report.extended.dressupKinds === 9 && report.extended.toolAssetRoundTrip && report.extended.jobAssetRoundTrip && report.extended.metadataRoundTrip && report.extended.metadataComments === 1 && report.extended.metadataProperties === 3 && report.extended.metadataCompounds === 1 && report.extended.undoRedo && report.extended.startPointChanged && report.extended.materialRemoval && report.extended.removalTimelinePoints === report.job.pathPoints && report.extended.fixtureCollision && report.extended.multiAxis && report.persistence.mode === 'sqlite-opfs' && report.persistence.roundTrip && report.persistence.released && report.opfs.markerSuite === 'CAM-ALL' && report.opfs.operations === 3 && report.opfs.markerRemoved ? 'pass' : 'failed'
  } catch (error) {
    report.error = error instanceof Error ? error.stack || error.message : String(error)
  } finally {
    facade.geometry.dispose()
    await new Promise((resolve) => setTimeout(resolve, 0))
    const capabilities = facade.geometry.capabilities()
    report.afterRelease = { shapeCount: capabilities.shapeCount, kernelReferenceCount: capabilities.kernelReferenceCount }
    if (capabilities.shapeCount !== 0 || capabilities.kernelReferenceCount !== 0) report.status = 'failed'
  }
  return report
}

run().then((report) => {
  ;(window as Window & { __bitbybitCamReport?: CamReport }).__bitbybitCamReport = report
  document.documentElement.dataset.status = report.status
  document.querySelector('#result')!.textContent = JSON.stringify(report, null, 2)
}).catch((error) => {
  document.documentElement.dataset.status = 'failed'
  document.querySelector('#result')!.textContent = String(error)
})

export {}
