import { useEffect, useState } from 'react'
import { createRoot } from 'react-dom/client'
import { PropertyEditor } from './App'
import { createProductionFacade } from './facade/productionFacade'
import type { BitBybitWebCadFacade, DocumentSnapshot, ObjectPropertySnapshot } from './facade/types'
import './styles.css'

type WorkerTrace = { constructed: Array<{ name: string; url: string }>; requestTypes: string[]; terminated: string[] }
type PropertyHeatFluxReport = {
  schemaVersion: 1
  status: 'running' | 'pass' | 'failed'
  browserId: 'chrome'
  userAgent: string
  crossOriginIsolated: boolean
  worker?: WorkerTrace
  ui?: { inputType: string; label: string; unit: string; beforeValue: number; afterValue: number; documentVersion: number; dirty: boolean; objectState: string | null; errors: string[] }
  persistence?: { mode: string; sqliteWasm: boolean; opfs: boolean; savedMode: string; loadedValue: number | null; loadedType: string | null; checkpointVersion: number | null; recoveryIntegrity: string; fcstdElement: string | null; fcstdValue: string | null }
  resource?: { byteLength: number; roundTrip: boolean; released: boolean; markerRemoved: boolean }
  release?: { shapeCount: number; kernelReferenceCount: number; workerTerminated: boolean }
  error?: string
}

const fixture = (): DocumentSnapshot => ({
  id: 'chrome-property-heatflux', label: 'Chrome PropertyHeatFlux', version: 1, dirty: false, readOnly: false, units: 'mm',
  tree: [{ id: 'HeatFluxProbe', label: 'Heat flux probe', type: 'feature', state: 'valid' }],
  objects: [{ id: 'HeatFluxProbe', typeId: 'Fem::ConstraintHeatflux', properties: [{ name: 'DFlux', label: 'DFlux', group: 'ConstraintHeatflux', scope: 'data', type: 'App::PropertyHeatFlux', value: 0, unit: 'kg/s^3', recompute: true }] }],
  dependencies: [],
  recompute: { generation: 0, status: 'idle', objectStates: { HeatFluxProbe: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] },
})

const heatFluxProperty = (facade: BitBybitWebCadFacade) => facade.app.document.getObject('HeatFluxProbe')?.properties.find(({ name }) => name === 'DFlux')

function HarnessEditor({ facade, errors }: { facade: BitBybitWebCadFacade; errors: string[] }) {
  const [property, setProperty] = useState<ObjectPropertySnapshot | null>(() => heatFluxProperty(facade) ?? null)
  useEffect(() => facade.subscribe((event) => { if (event.type === 'state.changed') setProperty(heatFluxProperty(facade) ?? null) }), [facade])
  if (!property) return <div data-harness-status="missing-property">DFlux property is missing.</div>
  return <section className="combo-property" data-harness-status="ready"><div className="property-heading"><div><span className="eyebrow">Property view</span><h2>Heat flux probe</h2></div></div><div className="properties-scroll"><div className="property-group"><div className="property-group-title">ConstraintHeatflux</div><PropertyEditor facade={facade} objectId="HeatFluxProbe" property={property} showNotice={(message) => errors.push(message)} /></div></div></section>
}

const waitFor = async (predicate: () => boolean, message: string, timeoutMs = 30_000) => {
  const deadline = performance.now() + timeoutMs
  while (performance.now() < deadline) { if (predicate()) return; await new Promise((resolve) => setTimeout(resolve, 25)) }
  throw new Error(`Timed out waiting for ${message}.`)
}

const trace: WorkerTrace = { constructed: [], requestTypes: [], terminated: [] }
const BrowserWorker = globalThis.Worker
class TrackedWorker extends BrowserWorker {
  private readonly traceName: string
  constructor(scriptURL: string | URL, options?: WorkerOptions) { super(scriptURL, options); this.traceName = options?.name ?? 'unnamed'; trace.constructed.push({ name: this.traceName, url: String(scriptURL) }) }
  override postMessage(message: unknown, transferOrOptions?: StructuredSerializeOptions | Transferable[]) {
    if (message && typeof message === 'object' && 'type' in message && typeof message.type === 'string') trace.requestTypes.push(message.type)
    if (Array.isArray(transferOrOptions)) super.postMessage(message, transferOrOptions)
    else if (transferOrOptions) super.postMessage(message, transferOrOptions)
    else super.postMessage(message)
  }
  override terminate() { trace.terminated.push(this.traceName); super.terminate() }
}
Object.defineProperty(globalThis, 'Worker', { configurable: true, writable: true, value: TrackedWorker })

const initial = 0
const target = 500
const report: PropertyHeatFluxReport = { schemaVersion: 1, status: 'running', browserId: 'chrome', userAgent: navigator.userAgent, crossOriginIsolated: globalThis.crossOriginIsolated === true }
const errors: string[] = []
const facade = createProductionFacade({ initialDocument: fixture(), initialSelectedObjectIds: ['HeatFluxProbe'] })
const root = createRoot(document.getElementById('root')!)
root.render(<HarnessEditor facade={facade} errors={errors} />)

try {
  await waitFor(() => document.querySelector<HTMLInputElement>('input[aria-label="DFlux"]') !== null, 'the production PropertyHeatFlux editor')
  await waitFor(() => facade.project.capabilities().mode === 'sqlite-opfs', 'the SQLite OPFS Worker')
  const input = document.querySelector<HTMLInputElement>('input[aria-label="DFlux"]')!
  const beforeValue = Number(input.value)
  input.focus(); input.value = String(target); input.dispatchEvent(new Event('input', { bubbles: true })); input.blur()
  await waitFor(() => facade.app.document.getActive().version === 2, 'the DFlux property commit')
  const edited = facade.app.document.getActive()
  report.ui = { inputType: input.type, label: input.getAttribute('aria-label') ?? '', unit: input.closest('label')?.querySelector('span')?.textContent ?? '', beforeValue, afterValue: Number(heatFluxProperty(facade)?.value), documentVersion: edited.version, dirty: edited.dirty, objectState: edited.recompute?.objectStates.HeatFluxProbe ?? null, errors }

  const saved = await facade.project.save()
  const loaded = await facade.project.load(edited.id)
  const checkpoint = await facade.project.loadCheckpoint(edited.id, edited.version)
  const recovery = await facade.project.recovery(edited.id)
  const loadedProperty = loaded?.objects.find(({ id }) => id === 'HeatFluxProbe')?.properties.find(({ name }) => name === 'DFlux')
  const archive = facade.project.fcstd.serializeMetadata(edited)
  const archiveProperty = facade.project.fcstd.inspect(archive).objects.find(({ name }) => name === 'HeatFluxProbe')?.properties.find(({ name }) => name === 'DFlux')
  const capabilities = facade.project.capabilities()
  report.persistence = { mode: capabilities.mode, sqliteWasm: capabilities.sqliteWasm, opfs: capabilities.opfs, savedMode: saved.mode, loadedValue: typeof loadedProperty?.value === 'number' ? loadedProperty.value : null, loadedType: loadedProperty?.type ?? null, checkpointVersion: checkpoint?.version ?? null, recoveryIntegrity: recovery.integrity, fcstdElement: archiveProperty?.element ?? null, fcstdValue: archiveProperty?.value ?? null }

  const payload = new TextEncoder().encode('PropertyHeatFlux Chrome resource bytes')
  const stored = await facade.project.resource.put(payload, 'application/octet-stream')
  const restored = await facade.project.resource.get(stored.hash)
  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 = 'property-heatflux-chrome-marker.json'
  const marker = await directory.getFileHandle(markerName, { create: true })
  const writable = await marker.createWritable()
  await writable.write(JSON.stringify({ suite: 'PROP-HEATFLUX-H', value: target }))
  await writable.close(); await directory.removeEntry(markerName)
  let markerRemoved = false
  try { await directory.getFileHandle(markerName) } catch (error) { markerRemoved = error instanceof DOMException && error.name === 'NotFoundError' }
  report.resource = { byteLength: stored.byteLength, roundTrip: restored !== null && new TextDecoder().decode(restored) === new TextDecoder().decode(payload), released, markerRemoved }
} catch (error) {
  report.error = error instanceof Error ? error.stack || error.message : String(error)
} finally {
  root.unmount(); facade.geometry.dispose()
  await facade.project.dispose().catch((error: unknown) => { report.error = `${report.error ? `${report.error}\n` : ''}${error instanceof Error ? error.stack || error.message : String(error)}` })
  Object.defineProperty(globalThis, 'Worker', { configurable: true, writable: true, value: BrowserWorker })
  const geometry = facade.geometry.capabilities()
  report.worker = trace
  report.release = { shapeCount: geometry.shapeCount, kernelReferenceCount: geometry.kernelReferenceCount, workerTerminated: trace.terminated.includes('bitbybit-persistence') }
}

const requiredWorkerRequests = ['initialize', 'save-document', 'load-document', 'load-checkpoint', 'recovery-report', 'put-resource', 'get-resource', 'release-resource', 'dispose']
report.status = !report.error && report.crossOriginIsolated
  && report.worker?.constructed.some(({ name }) => name === 'bitbybit-persistence') === true
  && requiredWorkerRequests.every((type) => report.worker?.requestTypes.includes(type))
  && report.ui?.inputType === 'number' && report.ui.label === 'DFlux' && report.ui.unit === 'kg/s^3' && report.ui.beforeValue === initial && report.ui.afterValue === target && report.ui.documentVersion === 2 && report.ui.dirty && report.ui.objectState === 'touched' && report.ui.errors.length === 0
  && report.persistence?.mode === 'sqlite-opfs' && report.persistence.sqliteWasm && report.persistence.opfs && report.persistence.savedMode === 'sqlite-opfs' && report.persistence.loadedValue === target && report.persistence.loadedType === 'App::PropertyHeatFlux' && report.persistence.checkpointVersion === 2 && report.persistence.recoveryIntegrity === 'ok' && report.persistence.fcstdElement === 'Float' && report.persistence.fcstdValue === String(target)
  && report.resource !== undefined && report.resource.byteLength > 0 && report.resource.roundTrip && report.resource.released && report.resource.markerRemoved
  && report.release?.shapeCount === 0 && report.release.kernelReferenceCount === 0 && report.release.workerTerminated ? 'pass' : 'failed'

document.documentElement.dataset.status = report.status
document.body.textContent = JSON.stringify(report, null, 2)
await fetch('/__chrome-property-heatflux-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
if (report.status !== 'pass') throw new Error(report.error || 'Chrome PropertyHeatFlux harness failed.')

export {}
