import { createMockFacade } from './facade/mockFacade'
import { createAssembly } from './facade/assembly'
import { inspectFcstdArchive, serializeFcstdMetadataArchive } from './facade/fcstd'
import type { AssemblySnapshot } from './facade/assembly'
import type { DocumentSnapshot } from './facade/types'

type AssemblyReport = {
  schemaVersion: 1
  status: 'pass' | 'failed'
  browserId: 'chrome'
  crossOriginIsolated: boolean
  geometry?: { components: number; validShapes: number; releasedShapes: number }
  solver?: { status: string; iterations: number; coincident: string; distance: string; shaftX: number; diagnostics: number; workerStatus: string; angleStatus: string; angleYaw: number }
  tools?: { bomRows: number; collisionCount: number; explodedMoved: boolean; variantX: number; motionFrames: number; motionEndX: number }
  scale?: { components: number; bomRows: number; durationMs: number; budgetMs: number }
  fcstd?: { bytes: number; objects: number; links: number; reopenedObjects: number; roundTrip: boolean; released: boolean }
  persistence?: { mode: string; byteLength: number; hash: string; roundTrip: boolean; released: boolean }
  opfs?: { markerSuite: string; markerComponents: 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 solveInWorker = async (snapshot: AssemblySnapshot) => {
  const worker = new Worker(new URL('./assemblySolverWorker.ts', import.meta.url), { type: 'module', name: 'assembly-solver' })
  try {
    return await new Promise<AssemblySnapshot>((resolve, reject) => {
      const timer = setTimeout(() => reject(new Error('Assembly solver Worker timed out.')), 10_000)
      worker.addEventListener('message', (event: MessageEvent<{ id: number; snapshot?: AssemblySnapshot; error?: string }>) => { clearTimeout(timer); event.data.snapshot ? resolve(event.data.snapshot) : reject(new Error(event.data.error ?? 'Assembly solver Worker failed.')) }, { once: true })
      worker.addEventListener('error', (event) => { clearTimeout(timer); reject(new Error(event.message)) }, { once: true })
      worker.postMessage({ id: 1, snapshot })
    })
  } finally {
    worker.terminate()
  }
}

const fcstdFixture = (): DocumentSnapshot => ({
  id: 'assembly-fcstd', label: 'Assembly FCStd', version: 1, dirty: false, readOnly: false, units: 'mm',
  tree: [{ id: 'Assembly', label: 'Assembly', type: 'folder', state: 'valid' }, { id: 'Base', label: 'Base', type: 'feature', state: 'valid' }, { id: 'Shaft', label: 'Shaft', type: 'feature', state: 'valid' }, { id: 'ShaftLink', label: 'Shaft Link', type: 'feature', state: 'valid' }],
  objects: [
    { id: 'Assembly', typeId: 'App::Part', properties: [] },
    { id: 'Base', typeId: 'Part::Box', properties: [{ name: 'Length', label: 'Length', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 10 }] },
    { id: 'Shaft', typeId: 'Part::Box', properties: [{ name: 'Length', label: 'Length', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 8 }] },
    { id: 'ShaftLink', typeId: 'App::Link', properties: [{ name: 'LinkedObject', label: 'Linked Object', group: 'Link', scope: 'data', type: 'App::PropertyLink', value: 'Shaft' }] },
  ],
  dependencies: [{ sourceId: 'ShaftLink', targetId: 'Shaft', relation: 'link', propertyName: 'LinkedObject' }],
  recompute: { generation: 0, status: 'idle', objectStates: { Assembly: 'up-to-date', Base: 'up-to-date', Shaft: 'up-to-date', ShaftLink: 'up-to-date' }, dirtyObjects: [], order: ['Assembly', 'Base', 'Shaft', 'ShaftLink'], errors: [] },
})

const run = async (): Promise<AssemblyReport> => {
  const facade = createMockFacade()
  const report: AssemblyReport = { schemaVersion: 1, status: 'failed', browserId: 'chrome', crossOriginIsolated: globalThis.crossOriginIsolated === true }
  try {
    if (!report.crossOriginIsolated || !navigator.storage?.getDirectory) throw new Error('Chrome Assembly evidence requires isolated OPFS.')
    await facade.project.list()
    const context = { documentId: 'assembly', documentVersion: 1 }
    const baseShape = await facade.geometry.createBox({ ...context, width: 10, length: 10, height: 2 })
    const shaftShape = await facade.geometry.createBox({ ...context, width: 2, length: 2, height: 8 })
    const assembly = createAssembly('motor', 'Motor assembly')
    assembly.addComponent({ id: 'base', sourceObjectId: baseShape.id, label: 'Base', grounded: true, bounds: { min: { x: -1, y: -1, z: -1 }, max: { x: 1, y: 1, z: 1 } } })
    assembly.addComponent({ id: 'shaft', sourceObjectId: shaftShape.id, label: 'Shaft', grounded: false, placement: { x: 1 }, bounds: { min: { x: -1, y: -1, z: -1 }, max: { x: 1, y: 1, z: 1 } } })
    assembly.addConnector({ id: 'base-origin', componentId: 'base', origin: { x: 0, y: 0, z: 0 }, axis: { x: 1, y: 0, z: 0 } })
    assembly.addConnector({ id: 'shaft-origin', componentId: 'shaft', origin: { x: 0, y: 0, z: 0 }, axis: { x: 1, y: 0, z: 0 } })
    assembly.addJoint({ id: 'coincident', kind: 'coincident', first: 'base-origin', second: 'shaft-origin' })
    assembly.addJoint({ id: 'distance', kind: 'distance', first: 'base-origin', second: 'shaft-origin', value: 5 })
    const initialCollisionCount = assembly.collisions().length
    const workerSolved = await solveInWorker(assembly.snapshot())
    const solved = assembly.solve()
    const exploded = assembly.exploded(2)
    const variant = assembly.variant('inspection', { shaft: { x: 7 } })
    const motion = assembly.motion('shaft', { x: 9, yaw: Math.PI / 2 }, 4)
    const angled = createAssembly('angle', 'Angle fixture')
    angled.addComponent({ id: 'fixed', sourceObjectId: baseShape.id, label: 'Fixed', grounded: true })
    angled.addComponent({ id: 'arm', sourceObjectId: shaftShape.id, label: 'Arm', grounded: false })
    angled.addConnector({ id: 'fixed-axis', componentId: 'fixed', origin: { x: 0, y: 0, z: 0 }, axis: { x: 1, y: 0, z: 0 } })
    angled.addConnector({ id: 'arm-axis', componentId: 'arm', origin: { x: 0, y: 0, z: 0 }, axis: { x: 1, y: 0, z: 0 } })
    angled.addJoint({ id: 'angle', kind: 'angle', first: 'fixed-axis', second: 'arm-axis', value: Math.PI / 2 })
    const angleSolved = angled.solve()

    const scaleStarted = performance.now()
    const large = createAssembly('large', 'Large assembly')
    for (let index = 0; index < 1000; index += 1) large.addComponent({ id: `component-${index}`, sourceObjectId: `part-${index % 10}`, label: `Part ${index % 10}`, grounded: index === 0, placement: { x: index * 2 } })
    const largeBom = large.bom()
    const scaleDurationMs = performance.now() - scaleStarted

    const fcstdBytes = serializeFcstdMetadataArchive(fcstdFixture(), { guiViews: [{ name: 'Assembly', type: 'orthographic', visibility: 'true' }] })
    const inspected = inspectFcstdArchive(fcstdBytes)
    const reopenedFcstd = serializeFcstdMetadataArchive(inspected.proxyDocument, { guiViews: [{ name: 'Assembly', type: 'orthographic', visibility: 'true' }] })
    const reopenedInspection = inspectFcstdArchive(reopenedFcstd)
    const fcstdStored = await facade.project.resource.put(fcstdBytes, 'application/vnd.freecad.fcstd')
    const fcstdLoaded = await facade.project.resource.get(fcstdStored.hash)
    const fcstdRoundTrip = fcstdLoaded !== null && await sha256(fcstdLoaded) === await sha256(fcstdBytes)
    await facade.project.resource.release(fcstdStored.hash)
    const fcstdReleased = await facade.project.resource.get(fcstdStored.hash) === null
    const payload = new TextEncoder().encode(JSON.stringify(solved))
    const stored = await facade.project.resource.put(payload, 'application/vnd.bitbybit.assembly+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 = 'assembly-chrome-marker.json'
    const marker = await directory.getFileHandle(markerName, { create: true })
    const writable = await marker.createWritable()
    await writable.write(JSON.stringify({ suite: 'ASM-TOOLS', components: solved.components.length }))
    await writable.close()
    const markerPayload = JSON.parse(await (await marker.getFile()).text()) as { suite: string; components: number }
    await directory.removeEntry(markerName)
    let markerRemoved = false
    try { await directory.getFileHandle(markerName) } catch (error) { markerRemoved = error instanceof DOMException && error.name === 'NotFoundError' }
    report.geometry = { components: solved.components.length, validShapes: [baseShape, shaftShape].filter((shape) => shape.id.length > 0).length, releasedShapes: 0 }
    report.solver = { status: solved.solver.status, iterations: solved.solver.iterations, coincident: solved.joints.find((joint) => joint.id === 'coincident')?.status || 'missing', distance: solved.joints.find((joint) => joint.id === 'distance')?.status || 'missing', shaftX: solved.components.find((component) => component.id === 'shaft')?.placement.x || 0, diagnostics: solved.diagnostics.length, workerStatus: workerSolved.solver.status, angleStatus: angleSolved.solver.status, angleYaw: angleSolved.components.find((component) => component.id === 'arm')?.placement.yaw ?? Number.NaN }
    report.tools = { bomRows: assembly.bom().length, collisionCount: initialCollisionCount, explodedMoved: exploded.some((component) => component.placement.x !== solved.components.find((entry) => entry.id === component.id)?.placement.x), variantX: variant.placements.find((component) => component.id === 'shaft')?.placement.x || 0, motionFrames: motion.length, motionEndX: motion.at(-1)?.placement.x ?? Number.NaN }
    report.scale = { components: large.snapshot().components.length, bomRows: largeBom.length, durationMs: scaleDurationMs, budgetMs: 1000 }
    const linkProperties = inspected.objects.flatMap((object) => object.properties.filter((property) => property.typeId === 'App::PropertyLink' && property.value === 'Shaft'))
    report.fcstd = { bytes: fcstdBytes.byteLength, objects: inspected.objects.length, links: (inspected.proxyDocument?.dependencies ?? []).filter((dependency) => dependency.relation === 'link').length + linkProperties.length, reopenedObjects: reopenedInspection.objects.length, roundTrip: fcstdRoundTrip, released: fcstdReleased }
    report.persistence = { mode: facade.project.capabilities().mode, byteLength: stored.byteLength, hash: stored.hash, roundTrip, released }
    report.opfs = { markerSuite: markerPayload.suite, markerComponents: markerPayload.components, markerRemoved }
    report.status = report.crossOriginIsolated && report.geometry.validShapes === 2 && report.solver.status === 'solved' && report.solver.iterations === 2 && report.solver.coincident === 'solved' && report.solver.distance === 'solved' && report.solver.shaftX === 5 && report.solver.diagnostics === 0 && report.solver.workerStatus === 'solved' && report.solver.angleStatus === 'solved' && Math.abs(report.solver.angleYaw - Math.PI / 2) < 1e-12 && report.tools.bomRows === 2 && report.tools.collisionCount === 1 && report.tools.explodedMoved && report.tools.variantX === 7 && report.tools.motionFrames === 5 && report.tools.motionEndX === 9 && report.scale.components === 1000 && report.scale.bomRows === 10 && report.scale.durationMs <= report.scale.budgetMs && report.fcstd.bytes > 0 && report.fcstd.objects === 4 && report.fcstd.links === 1 && report.fcstd.reopenedObjects === 4 && report.fcstd.roundTrip && report.fcstd.released && report.persistence.mode === 'sqlite-opfs' && report.persistence.byteLength > 0 && report.persistence.roundTrip && report.persistence.released && report.opfs.markerSuite === 'ASM-TOOLS' && report.opfs.markerComponents === 2 && 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'; report.error = `${report.error ? `${report.error} ` : ''}Shape ownership gate failed.` }
  }
  return report
}

run().then((report) => {
  ;(window as Window & { __bitbybitAssemblyReport?: AssemblyReport }).__bitbybitAssemblyReport = 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 = error instanceof Error ? error.stack || error.message : String(error)
})

export {}
