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

type WorkerTrace = {
  constructed: Array<{ name: string; url: string }>
  requestTypes: string[]
  terminated: string[]
}

type PropertyAreaReport = {
  schemaVersion: 1
  status: 'running' | 'pass' | 'failed'
  browserId: 'chrome'
  userAgent: string
  crossOriginIsolated: boolean
  worker?: WorkerTrace
  ui?: { areaReadOnly: boolean; beforeText: string; afterText: string; beforeArea: number; afterArea: number; beforeElementCount: number; afterElementCount: number; triggerLabel: string; documentVersion: number; dirty: boolean; objectState: string | null; errors: string[] }
  persistence?: { mode: string; sqliteWasm: boolean; opfs: boolean; savedMode: string; loadedArea: number | null; loadedAreaType: string | null; loadedElementCount: number | null; loadedElementsType: string | null; checkpointVersion: number | null; recoveryIntegrity: string; fcstdAreaElement: string | null; fcstdAreaValue: string | null; fcstdElementsElement: string | null; fcstdElementCount: number | null }
  resource?: { byteLength: number; roundTrip: boolean; released: boolean; markerRemoved: boolean }
  release?: { shapeCount: number; kernelReferenceCount: number; workerTerminated: boolean }
  error?: string
}

const outputStatus = encodeFreecadPropertyStatus(['PropReadOnly', 'PropOutput'])
const faceEntry = (persistentId: string, area: number, index: number) => ({
  ref: { shapeId: 'AreaBox-shape', kind: 'face' as const, persistentId, topologyVersion: 1, status: 'stable' as const },
  signature: { kind: 'face' as const, canonical: persistentId, hash: persistentId, centroid: [index, 0, 0] as [number, number, number], bounds: { min: [index, 0, 0] as [number, number, number], max: [index + 1, 1, 0] as [number, number, number] }, area, normal: [0, 0, 1] as [number, number, number] },
})

const fixture = (): DocumentSnapshot => ({
  id: 'chrome-property-area',
  label: 'Chrome PropertyArea',
  version: 1,
  dirty: false,
  readOnly: false,
  units: 'mm',
  tree: [
    { id: 'AreaBox', label: 'Area box', type: 'feature', state: 'valid' },
    { id: 'AreaProbe', label: 'Area probe', type: 'feature', state: 'valid' },
  ],
  objects: [
    {
      id: 'AreaBox',
      typeId: 'Part::Box',
      properties: [
        { name: 'Length', label: 'Length', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 10, unit: 'mm', recompute: true },
        { name: 'Width', label: 'Width', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 5, unit: 'mm', recompute: true },
        { name: 'Height', label: 'Height', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 2, unit: 'mm', recompute: true },
      ],
      topology: {
        shapeId: 'AreaBox-shape',
        documentVersion: 1,
        generation: 1,
        entries: [faceEntry('box-face-1', 10, 0), faceEntry('box-face-2', 10, 1)],
        migration: { previousGeneration: null, matches: [] },
        history: { operationId: 'AreaBox:1', provider: 'occt-native', relations: [], counts: { preserved: 0, modified: 0, generated: 0, deleted: 0, ambiguous: 0 } },
      },
    },
    {
      id: 'AreaProbe',
      typeId: 'Measure::MeasureArea',
      properties: [
        { name: 'Area', label: 'Area', group: 'Measurement', scope: 'data', type: 'App::PropertyArea', value: 20, unit: 'mm^2', nativeStatus: outputStatus, readOnly: true, recompute: false },
        { name: 'Elements', label: 'Elements', group: 'Measurement', scope: 'data', type: 'App::PropertyLinkSubList', value: { schemaVersion: 1, entries: [{ objectId: 'AreaBox', subElement: 'Face1' }, { objectId: 'AreaBox', subElement: 'Face2' }] }, recompute: true },
      ],
    },
  ],
  dependencies: [{ sourceId: 'AreaProbe', targetId: 'AreaBox', relation: 'topo-ref', propertyName: 'Elements', reference: 'Face1' }],
  recompute: { generation: 0, status: 'idle', objectStates: { AreaBox: 'up-to-date', AreaProbe: 'up-to-date' }, dirtyObjects: [], order: [], errors: [] },
})

const areaProperties = (facade: BitBybitWebCadFacade) => facade.app.document.getObject('AreaProbe')?.properties.filter((property) => property.name === 'Area' || property.name === 'Elements') ?? []
const property = (facade: BitBybitWebCadFacade, name: 'Area' | 'Elements') => facade.app.document.getObject('AreaProbe')?.properties.find((candidate) => candidate.name === name)
const elementCount = (value: unknown) => value && typeof value === 'object' && !Array.isArray(value) && 'entries' in value && Array.isArray(value.entries) ? value.entries.length : null

function HarnessEditor({ facade, errors }: { facade: BitBybitWebCadFacade; errors: string[] }) {
  const [properties, setProperties] = useState<ObjectPropertySnapshot[]>(() => areaProperties(facade))
  useEffect(() => facade.subscribe((event) => {
    if (event.type === 'state.changed') setProperties(areaProperties(facade))
  }), [facade])
  if (properties.length !== 2) return <div data-harness-status="missing-property">MeasureArea properties are missing.</div>
  return <section className="combo-property" data-harness-status="ready"><div className="property-heading"><div><span className="eyebrow">Property view</span><h2>Area probe</h2></div></div><div className="properties-scroll"><div className="property-group"><div className="property-group-title">Measurement</div>{properties.map((entry) => <div data-property={entry.name} key={entry.name}><PropertyEditor facade={facade} objectId="AreaProbe" property={entry} showNotice={(message) => errors.push(message)} /></div>)}</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 report: PropertyAreaReport = { schemaVersion: 1, status: 'running', browserId: 'chrome', userAgent: navigator.userAgent, crossOriginIsolated: globalThis.crossOriginIsolated === true }
const errors: string[] = []
const facade = createProductionFacade({ initialDocument: fixture(), initialSelectedObjectIds: ['AreaProbe'] })
const root = createRoot(document.getElementById('root')!)
root.render(<HarnessEditor facade={facade} errors={errors} />)

try {
  await waitFor(() => document.querySelector<HTMLButtonElement>('button[aria-label="Clear subshape links"]') !== null, 'the production MeasureArea Elements editor')
  await waitFor(() => facade.project.capabilities().mode === 'sqlite-opfs', 'the SQLite OPFS Worker')
  const clearButton = document.querySelector<HTMLButtonElement>('button[aria-label="Clear subshape links"]')!
  const beforeText = document.querySelector<HTMLElement>('[data-property="Area"] .property-readonly')?.textContent ?? ''
  const beforeArea = Number(property(facade, 'Area')?.value)
  const beforeElementCount = elementCount(property(facade, 'Elements')?.value)
  const areaReadOnly = document.querySelector<HTMLInputElement>('input[aria-label="Area"]') === null
  clearButton.click()
  await waitFor(() => facade.app.document.getActive().version === 2, 'the UI Elements commit')
  await waitFor(() => document.querySelector<HTMLElement>('[data-property="Area"] .property-readonly')?.textContent === '0 mm^2', 'the derived Area UI update')
  const edited = facade.app.document.getActive()
  const editedArea = property(facade, 'Area')
  const editedElements = property(facade, 'Elements')
  report.ui = {
    areaReadOnly,
    beforeText,
    afterText: document.querySelector<HTMLElement>('[data-property="Area"] .property-readonly')?.textContent ?? '',
    beforeArea,
    afterArea: Number(editedArea?.value),
    beforeElementCount: beforeElementCount ?? -1,
    afterElementCount: elementCount(editedElements?.value) ?? -1,
    triggerLabel: clearButton.getAttribute('aria-label') ?? '',
    documentVersion: edited.version,
    dirty: edited.dirty,
    objectState: edited.recompute?.objectStates.AreaProbe ?? 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 loadedObject = loaded?.objects.find((object) => object.id === 'AreaProbe')
  const loadedArea = loadedObject?.properties.find((candidate) => candidate.name === 'Area')
  const loadedElements = loadedObject?.properties.find((candidate) => candidate.name === 'Elements')
  const archive = facade.project.fcstd.serializeMetadata(edited)
  const archiveObject = facade.project.fcstd.inspect(archive).objects.find((object) => object.name === 'AreaProbe')
  const archiveArea = archiveObject?.properties.find((candidate) => candidate.name === 'Area')
  const archiveElements = archiveObject?.properties.find((candidate) => candidate.name === 'Elements')
  const capabilities = facade.project.capabilities()
  report.persistence = {
    mode: capabilities.mode,
    sqliteWasm: capabilities.sqliteWasm,
    opfs: capabilities.opfs,
    savedMode: saved.mode,
    loadedArea: typeof loadedArea?.value === 'number' ? loadedArea.value : null,
    loadedAreaType: loadedArea?.type ?? null,
    loadedElementCount: elementCount(loadedElements?.value),
    loadedElementsType: loadedElements?.type ?? null,
    checkpointVersion: checkpoint?.version ?? null,
    recoveryIntegrity: recovery.integrity,
    fcstdAreaElement: archiveArea?.element ?? null,
    fcstdAreaValue: archiveArea?.value ?? null,
    fcstdElementsElement: archiveElements?.element ?? null,
    fcstdElementCount: archiveElements?.linkSubs?.length ?? null,
  }

  const payload = new TextEncoder().encode('PropertyArea Chrome resource')
  const stored = await facade.project.resource.put(payload, 'application/vnd.freecad.property-area+json')
  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-area-chrome-marker.json'
  const marker = await directory.getFileHandle(markerName, { create: true })
  const writable = await marker.createWritable()
  await writable.write(JSON.stringify({ suite: 'PROP-AREA-H', area: 0, elements: 0 }))
  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?.areaReadOnly === true && report.ui.beforeText === '20 mm^2' && report.ui.afterText === '0 mm^2' && report.ui.beforeArea === 20 && report.ui.afterArea === 0 && report.ui.beforeElementCount === 2 && report.ui.afterElementCount === 0 && report.ui.triggerLabel === 'Clear subshape links' && 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.loadedArea === 0 && report.persistence.loadedAreaType === 'App::PropertyArea' && report.persistence.loadedElementCount === 0 && report.persistence.loadedElementsType === 'App::PropertyLinkSubList' && report.persistence.checkpointVersion === 2 && report.persistence.recoveryIntegrity === 'ok' && report.persistence.fcstdAreaElement === 'Float' && Number(report.persistence.fcstdAreaValue) === 0 && report.persistence.fcstdElementsElement === 'LinkSubList' && report.persistence.fcstdElementCount === 0
  && 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-area-report', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(report) })
if (report.status !== 'pass') throw new Error(report.error || 'Chrome PropertyArea harness failed.')

export {}
