import type { ShapeHandle } from './types'
import type { CamPoint } from './cam'
import { BitbybitGeometryRuntime } from './geometryRuntime'
import { CAMOTICS_WASM_ARTIFACT_SHA256 } from './camoticsWasm'

export const OPEN_CAM_LIB_SOURCE_REVISION = '95b036fe28ce6d77c97b98e5fbc337904ae49560'
export const OPEN_CAM_LIB_ARTIFACT_URL = '/vendor/opencamlib/ocl.js'
export const OPEN_CAM_LIB_ARTIFACT_SHA256 = '30ca7dd307a131f9df66288d530c3fbacf155f375f83a2d733c5c00a85a712e0'
export const OPEN_CAM_LIB_ARTIFACT_SRI = 'sha256-MMp90wehMfnfZiiNUww/us8VXzdfg6LXM8XACoWnEuA='
export const CAMOTICS_SOURCE_REVISION = 'e84665f2fa9d1151f03282ac7e01320bc65e015b'
export const CAMOTICS_CBANG_REVISION = '9b6672a0e2b800a909d799ffa3ab46774cf171b8'
export const CAMOTICS_NATIVE_ARTIFACT_EVIDENCE = 'config/camotics-native-artifact.json'

export type NativeCamCapabilities = {
  openCamLib: {
    status: 'available'
    backend: 'upstream-opencamlib-wasm'
    sourceRevision: string
    artifactSha256: string
    algorithms: ['path-drop-cutter', 'adaptive-path-drop-cutter', 'waterline', 'adaptive-waterline']
  }
  solidRemoval: {
    status: 'available'
    backend: 'bitbybit-occt-wasm'
    model: 'sampled-flat-end-brep'
    nativeSolid: true
    freeCadNativeEquivalent: false
    sweepModes: readonly ['sampled-flat-end', 'continuous-segment']
  }
  camotics: {
    status: 'native-host-available'
    backend: 'camotics-native-host-qt5-tpl'
    sourceRevision: string
    cbangRevision: string
    sourcePaths: string[]
    workspaceNativeExecutable: true
    nativeGuiPath: 'CAMotics/camotics'
    nativeTplPath: 'CAMotics/tplang'
    nativeArtifactEvidence: string
    browserExecutable: false
    browserWasmKernelExecutable: true
    nativeCliPath: 'CAMotics/camsim'
    wasmKernel: {
      backend: 'camotics-upstream-sweep-wasm'
      artifactSha256: string
      scope: readonly ['conic-sweep', 'spheroid-sweep', 'sweep-bounding-boxes']
      fullCamoticsProgram: false
      gcodeParserIncluded: false
    }
    generationAuthority: 'camotics-source-stage'
    parserAuthority: 'linuxcnc-wasm'
    camoticsParsesCanonicalGcode: false
    reason: string
  }
}

export const nativeCamCapabilities = (): NativeCamCapabilities => ({
  openCamLib: {
    status: 'available',
    backend: 'upstream-opencamlib-wasm',
    sourceRevision: OPEN_CAM_LIB_SOURCE_REVISION,
    artifactSha256: OPEN_CAM_LIB_ARTIFACT_SHA256,
    algorithms: ['path-drop-cutter', 'adaptive-path-drop-cutter', 'waterline', 'adaptive-waterline'],
  },
  solidRemoval: {
    status: 'available',
    backend: 'bitbybit-occt-wasm',
    model: 'sampled-flat-end-brep',
    nativeSolid: true,
    freeCadNativeEquivalent: false,
    sweepModes: ['sampled-flat-end', 'continuous-segment'],
  },
  camotics: {
    status: 'native-host-available',
    backend: 'camotics-native-host-qt5-tpl',
    sourceRevision: CAMOTICS_SOURCE_REVISION,
    cbangRevision: CAMOTICS_CBANG_REVISION,
    sourcePaths: ['CAMotics/src/gcode/machine/GCodeMachine.cpp', 'CAMotics/src/tplang/GCodeModule.cpp', 'CAMotics/src/camotics/sim/ToolSweep.cpp'],
    workspaceNativeExecutable: true,
    nativeGuiPath: 'CAMotics/camotics',
    nativeTplPath: 'CAMotics/tplang',
    nativeArtifactEvidence: CAMOTICS_NATIVE_ARTIFACT_EVIDENCE,
    browserExecutable: false,
    browserWasmKernelExecutable: true,
    nativeCliPath: 'CAMotics/camsim',
    wasmKernel: {
      backend: 'camotics-upstream-sweep-wasm',
      artifactSha256: CAMOTICS_WASM_ARTIFACT_SHA256,
      scope: ['conic-sweep', 'spheroid-sweep', 'sweep-bounding-boxes'],
      fullCamoticsProgram: false,
      gcodeParserIncluded: false,
    },
    generationAuthority: 'camotics-source-stage',
    parserAuthority: 'linuxcnc-wasm',
    camoticsParsesCanonicalGcode: false,
    reason: 'CAMotics 1.3 native Qt5 GUI and TPL compiler are workspace-verified host sidecars with pinned C! 1.7.2 and V8 ABI settings. The browser executes a standalone upstream Conic/Spheroid sweep WASM subset, not the native ELF, Qt GUI, TPL compiler, or full program; browser G-code generation preserves source-stage motion semantics and sends canonical G-code to LinuxCNC WASM.',
  },
})

export type CamoticsSourceMove = {
  type: 'rapid' | 'cut'
  point: CamPoint
  feed?: number
}
export type CamoticsSourceStageResult = {
  backend: 'camotics-source-stage-contract'
  sourceRevision: string
  sourcePaths: string[]
  nativeExecutable: false
  gcode: string
  lineCount: number
  parserAuthority: 'linuxcnc-wasm'
  camoticsParsesCanonicalGcode: false
}

const gcodeNumber = (value: number) => {
  if (!Number.isFinite(value)) throw new RangeError('CAMotics source-stage coordinates must be finite.')
  const formatted = value.toFixed(3).replace(/0+$/, '')
  return formatted === '-0.' ? '0.' : formatted
}

/**
 * Source-stage motion contract derived from CAMotics GCodeMachine start,
 * changeTool, setFeed, move and end. It deliberately does not parse its
 * output; LinuxCNC WASM is the only downstream parser/controller authority.
 */
export const generateCamoticsSourceStageGcode = (moves: CamoticsSourceMove[], options: { toolNumber?: number; feed?: number } = {}): CamoticsSourceStageResult => {
  if (moves.length === 0) throw new RangeError('CAMotics source-stage motion list must not be empty.')
  const toolNumber = options.toolNumber ?? 1
  if (!Number.isSafeInteger(toolNumber) || toolNumber <= 0) throw new RangeError('CAMotics source-stage toolNumber must be a positive safe integer.')
  if (options.feed !== undefined) positive(options.feed, 'CAMotics source-stage feed')
  let previousFeed: number | undefined
  let previousPoint: CamPoint | undefined
  const lines = ['G21G90', `M6 T${toolNumber}`]
  for (const [index, move] of moves.entries()) {
    finitePoint(move.point, `moves[${index}].point`)
    if (move.feed !== undefined) positive(move.feed, `moves[${index}].feed`)
    const feed = move.feed ?? (move.type === 'cut' ? options.feed : undefined)
    if (feed !== undefined && feed !== previousFeed) lines.push(`F${gcodeNumber(feed)}`)
    const axes = (['X', 'Y', 'Z'] as const).flatMap((axis, coordinate) => previousPoint === undefined || gcodeNumber(move.point[coordinate]) !== gcodeNumber(previousPoint[coordinate]) ? [`${axis}${gcodeNumber(move.point[coordinate])}`] : [])
    if (axes.length > 0) lines.push(`${move.type === 'rapid' ? 'G0' : 'G1'} ${axes.join(' ')}`)
    previousFeed = feed ?? previousFeed
    previousPoint = [...move.point]
  }
  lines.push('M2')
  return {
    backend: 'camotics-source-stage-contract',
    sourceRevision: CAMOTICS_SOURCE_REVISION,
    sourcePaths: ['CAMotics/src/gcode/machine/GCodeMachine.cpp'],
    nativeExecutable: false,
    gcode: `${lines.join('\n')}\n`,
    lineCount: lines.length,
    parserAuthority: 'linuxcnc-wasm',
    camoticsParsesCanonicalGcode: false,
  }
}

type OclOwned = { delete(): void }
type OclPoint = OclOwned & { x: number; y: number; z: number }
type OclVector<T> = OclOwned & { size(): number; get(index: number): T }
type OclModule = {
  Point: new (x: number, y: number, z: number) => OclPoint
  Triangle: new (a: OclPoint, b: OclPoint, c: OclPoint) => OclOwned
  STLSurf: new () => OclOwned & { addTriangle(triangle: OclOwned): void; size(): number }
  Line: new (a: OclPoint, b: OclPoint) => OclOwned
  Path: new () => OclOwned & { appendLine(line: OclOwned): void }
  CylCutter: new (diameter: number, length: number) => OclOwned
  BallCutter: new (diameter: number, length: number) => OclOwned
  PathDropCutter: new () => OclOwned & {
    setSTL(surface: OclOwned): void
    setCutter(cutter: OclOwned): void
    setPath(path: OclOwned): void
    setZ(z: number): void
    setSampling(sampling: number): void
    run(): void
    getPoints(): OclVector<OclPoint>
  }
}

declare global {
  interface Window {
    ocl?: () => Promise<OclModule>
  }
}

let oclModulePromise: Promise<OclModule> | null = null

const loadOpenCamLibScript = () => new Promise<void>((resolve, reject) => {
  if (typeof window.ocl === 'function') { resolve(); return }
  const existing = document.querySelector<HTMLScriptElement>('script[data-opencamlib-wasm]')
  if (existing) {
    existing.addEventListener('load', () => resolve(), { once: true })
    existing.addEventListener('error', () => reject(new Error('OpenCAMLib WASM script failed to load.')), { once: true })
    return
  }
  const script = document.createElement('script')
  script.src = OPEN_CAM_LIB_ARTIFACT_URL
  script.integrity = OPEN_CAM_LIB_ARTIFACT_SRI
  script.crossOrigin = 'anonymous'
  script.dataset.opencamlibWasm = 'true'
  script.addEventListener('load', () => resolve(), { once: true })
  script.addEventListener('error', () => reject(new Error('OpenCAMLib WASM script failed integrity or network validation.')), { once: true })
  document.head.append(script)
})

export const loadOpenCamLib = async (): Promise<OclModule> => {
  if (typeof window === 'undefined' || typeof document === 'undefined' || typeof WebAssembly === 'undefined') throw new Error('OpenCAMLib WASM requires a browser WebAssembly runtime.')
  if (!oclModulePromise) {
    oclModulePromise = (async () => {
      await loadOpenCamLibScript()
      if (typeof window.ocl !== 'function') throw new Error('OpenCAMLib WASM factory was not registered by the verified artifact.')
      return window.ocl()
    })().catch((error) => {
      oclModulePromise = null
      throw error
    })
  }
  return oclModulePromise
}

const finitePoint = (value: CamPoint, label: string) => {
  if (value.length !== 3 || value.some((coordinate) => !Number.isFinite(coordinate))) throw new RangeError(`${label} must contain three finite coordinates.`)
}

const positive = (value: number, label: string) => {
  if (!Number.isFinite(value) || value <= 0) throw new RangeError(`${label} must be a finite number greater than zero.`)
}

export type OpenCamLibTriangle = [CamPoint, CamPoint, CamPoint]
export type OpenCamLibDropCutterInput = {
  triangles: OpenCamLibTriangle[]
  path: CamPoint[]
  cutter: { shape: 'endmill' | 'ballend'; diameter: number; length: number }
  sampling: number
  minimumZ?: number
}
export type OpenCamLibDropCutterResult = {
  backend: 'upstream-opencamlib-wasm'
  sourceRevision: string
  artifactSha256: string
  algorithm: 'path-drop-cutter'
  triangleCount: number
  points: CamPoint[]
}

export const runOpenCamLibDropCutter = async (input: OpenCamLibDropCutterInput): Promise<OpenCamLibDropCutterResult> => {
  if (input.triangles.length === 0) throw new RangeError('OpenCAMLib requires at least one surface triangle.')
  if (input.path.length < 2) throw new RangeError('OpenCAMLib path requires at least two points.')
  input.triangles.forEach((triangle, triangleIndex) => triangle.forEach((candidate, pointIndex) => finitePoint(candidate, `triangles[${triangleIndex}][${pointIndex}]`)))
  input.path.forEach((candidate, index) => finitePoint(candidate, `path[${index}]`))
  positive(input.cutter.diameter, 'cutter.diameter')
  positive(input.cutter.length, 'cutter.length')
  positive(input.sampling, 'sampling')
  if (input.minimumZ !== undefined && !Number.isFinite(input.minimumZ)) throw new RangeError('minimumZ must be finite.')

  const module = await loadOpenCamLib()
  const surface = new module.STLSurf()
  const path = new module.Path()
  const cutter = input.cutter.shape === 'ballend'
    ? new module.BallCutter(input.cutter.diameter, input.cutter.length)
    : new module.CylCutter(input.cutter.diameter, input.cutter.length)
  const operation = new module.PathDropCutter()
  const transient: OclOwned[] = []
  let clPoints: OclVector<OclPoint> | null = null
  try {
    for (const triangle of input.triangles) {
      const points = triangle.map((candidate) => new module.Point(...candidate)) as [OclPoint, OclPoint, OclPoint]
      const nativeTriangle = new module.Triangle(...points)
      surface.addTriangle(nativeTriangle)
      nativeTriangle.delete()
      points.forEach((candidate) => candidate.delete())
    }
    for (let index = 1; index < input.path.length; index += 1) {
      const from = new module.Point(...input.path[index - 1])
      const to = new module.Point(...input.path[index])
      const line = new module.Line(from, to)
      path.appendLine(line)
      transient.push(line, from, to)
    }
    operation.setSTL(surface)
    operation.setCutter(cutter)
    operation.setPath(path)
    operation.setZ(input.minimumZ ?? Math.min(...input.path.map((candidate) => candidate[2])))
    operation.setSampling(input.sampling)
    operation.run()
    clPoints = operation.getPoints()
    const points: CamPoint[] = []
    for (let index = 0; index < clPoints.size(); index += 1) {
      const nativePoint = clPoints.get(index)
      points.push([nativePoint.x, nativePoint.y, nativePoint.z])
      nativePoint.delete()
    }
    if (points.length === 0 || points.some((candidate) => candidate.some((coordinate) => !Number.isFinite(coordinate)))) throw new Error('OpenCAMLib returned an invalid or empty cutter-location path.')
    return {
      backend: 'upstream-opencamlib-wasm',
      sourceRevision: OPEN_CAM_LIB_SOURCE_REVISION,
      artifactSha256: OPEN_CAM_LIB_ARTIFACT_SHA256,
      algorithm: 'path-drop-cutter',
      triangleCount: surface.size(),
      points,
    }
  } finally {
    clPoints?.delete()
    operation.delete()
    cutter.delete()
    transient.reverse().forEach((candidate) => candidate.delete())
    path.delete()
    surface.delete()
  }
}

export type OcctSolidRemovalInput = {
  id: string
  stock: { min: CamPoint; max: CamPoint }
  path: CamPoint[]
  cutterDiameter: number
  maxChord: number
  meshPrecision?: number
  sweepMode?: 'sampled-flat-end' | 'continuous-segment'
}
export type OcctSolidRemovalResult = {
  backend: 'bitbybit-occt-wasm'
  model: 'sampled-flat-end-brep' | 'continuous-segment-sweep-brep'
  sweepMode: 'sampled-flat-end' | 'continuous-segment'
  nativeSolid: true
  freeCadNativeEquivalent: false
  sampleCount: number
  stockVolume: number
  removedVolume: number
  remainingVolume: number
  structuralValid: boolean
  solids: number
  meshVertices: number
  meshTriangles: number
  remainingShape: ShapeHandle
}

export const sampleFlatEndPath = (path: CamPoint[], maxChord: number): CamPoint[] => {
  if (path.length === 0) throw new RangeError('Material-removal path must not be empty.')
  path.forEach((candidate, index) => finitePoint(candidate, `path[${index}]`))
  positive(maxChord, 'maxChord')
  const samples: CamPoint[] = [[...path[0]]]
  for (let index = 1; index < path.length; index += 1) {
    const from = path[index - 1]
    const to = path[index]
    const segmentLength = Math.hypot(to[0] - from[0], to[1] - from[1], to[2] - from[2])
    const steps = Math.max(1, Math.ceil(segmentLength / maxChord))
    for (let step = 1; step <= steps; step += 1) {
      const amount = step / steps
      samples.push([
        from[0] + (to[0] - from[0]) * amount,
        from[1] + (to[1] - from[1]) * amount,
        from[2] + (to[2] - from[2]) * amount,
      ])
    }
  }
  return samples
}

export const simulateOcctSolidRemoval = async (runtime: BitbybitGeometryRuntime, input: OcctSolidRemovalInput): Promise<OcctSolidRemovalResult> => {
  if (!input.id.trim()) throw new RangeError('Material-removal id must not be empty.')
  finitePoint(input.stock.min, 'stock.min')
  finitePoint(input.stock.max, 'stock.max')
  input.stock.max.forEach((coordinate, axis) => {
    if (coordinate <= input.stock.min[axis]) throw new RangeError(`stock.max[${axis}] must be greater than stock.min[${axis}].`)
  })
  positive(input.cutterDiameter, 'cutterDiameter')
  positive(input.maxChord, 'maxChord')
  positive(input.meshPrecision ?? 0.1, 'meshPrecision')
  const sweepMode = input.sweepMode ?? 'sampled-flat-end'
  const samples = sampleFlatEndPath(input.path, input.maxChord).filter((candidate) => candidate[2] < input.stock.max[2])
  if (samples.length === 0) throw new RangeError('Material-removal path does not intersect the stock height.')
  await runtime.initialize()
  if (runtime.capabilities().status !== 'ready') throw new Error(`Bitbybit OCCT is unavailable: ${runtime.capabilities().reason ?? 'unknown reason'}`)

  const documentId = `cam-solid-removal:${input.id}`
  const width = input.stock.max[0] - input.stock.min[0]
  const length = input.stock.max[1] - input.stock.min[1]
  const height = input.stock.max[2] - input.stock.min[2]
  const center: CamPoint = [input.stock.min[0] + width / 2, input.stock.min[1] + length / 2, input.stock.min[2] + height / 2]
  // Bitbybit's BoxDto names the Y span `height` and Z span `length`; map the
  // CAM stock's XYZ bounds explicitly so the native BRep has the requested box.
  const stock = await runtime.createBox({ documentId, documentVersion: 1, width, length: height, height: length, center, originOnCenter: true })
  const tools: ShapeHandle[] = []
  let mergedTool: ShapeHandle | null = null
  let remaining: ShapeHandle | null = null
  try {
    const stockMass = await runtime.massProperties(stock)
    const radius = input.cutterDiameter / 2
    const first = samples[0]
    const last = samples[samples.length - 1]
    const straightX = Math.abs(last[1] - first[1]) <= 1e-9 && Math.abs(last[2] - first[2]) <= 1e-9
    const straightY = Math.abs(last[0] - first[0]) <= 1e-9 && Math.abs(last[2] - first[2]) <= 1e-9
    const bottom = Math.max(input.stock.min[2], Math.min(first[2], last[2]))
    const cutterHeight = input.stock.max[2] - bottom
    // Use one connected segment envelope for a horizontal axis-aligned pass.
    // The sampled-cylinder fallback remains for arbitrary paths; the envelope
    // avoids invalid seams from independently fusing every chord sample.
    if (sweepMode === 'continuous-segment') {
      const endpoints = samples.length === 1 ? samples : samples.slice(0, -1)
      for (const [index, from] of endpoints.entries()) {
        const to = samples.length === 1 ? from : samples[index + 1]
        const dx = to[0] - from[0]
        const dy = to[1] - from[1]
        const dz = to[2] - from[2]
        const length = Math.hypot(dx, dy, dz)
        if (length <= 1e-12) continue
        tools.push(await runtime.createCylinder({
          documentId,
          documentVersion: 1,
          radius,
          height: length,
          center: from,
          direction: [dx / length, dy / length, dz / length],
          originOnCenter: false,
        }))
        // Endpoint spheres keep adjacent swept chords connected at sharp
        // corners while retaining a native OCCT BRep for arbitrary curves.
        tools.push(await runtime.createSphere({ documentId, documentVersion: 1, radius, center: from }))
      }
      if (samples.length > 0) tools.push(await runtime.createSphere({ documentId, documentVersion: 1, radius, center: samples.at(-1)! }))
    } else if (cutterHeight > 0 && (straightX || straightY) && samples.length > 1) {
      const spanX = Math.max(0, Math.abs(last[0] - first[0]))
      const spanY = Math.max(0, Math.abs(last[1] - first[1]))
      if (straightX && spanX > 0) {
        tools.push(await runtime.createBox({ documentId, documentVersion: 1, width: spanX, length: cutterHeight, height: input.cutterDiameter, center: [(first[0] + last[0]) / 2, first[1], bottom + cutterHeight / 2], originOnCenter: true }))
      } else if (straightY && spanY > 0) {
        tools.push(await runtime.createBox({ documentId, documentVersion: 1, width: input.cutterDiameter, length: cutterHeight, height: spanY, center: [first[0], (first[1] + last[1]) / 2, bottom + cutterHeight / 2], originOnCenter: true }))
      }
    } else {
      for (const sample of samples) {
        const sampleBottom = Math.max(input.stock.min[2], sample[2])
        const sampleHeight = input.stock.max[2] - sampleBottom
        if (sampleHeight <= 0) continue
        tools.push(await runtime.createCylinder({
          documentId,
          documentVersion: 1,
          radius,
          height: sampleHeight,
          center: [sample[0], sample[1], sampleBottom],
          direction: [0, 0, 1],
          originOnCenter: false,
        }))
      }
    }
    if (tools.length === 0) throw new RangeError('Material-removal sampling produced no cutter solids.')
    mergedTool = tools.length === 1
      ? tools[0]
      : await runtime.union({ documentId, documentVersion: 2, shapes: tools, keepEdges: false })
    remaining = await runtime.cut({ documentId, documentVersion: 3, base: stock, tools: [mergedTool], keepEdges: false })
    const [remainingMass, quality, mesh] = await Promise.all([
      runtime.massProperties(remaining),
      runtime.qualityReport(remaining),
      runtime.mesh(remaining, input.meshPrecision ?? 0.1),
    ])
    const removedVolume = stockMass.volume - remainingMass.volume
    if (!quality.structuralValid || quality.solids < 1 || removedVolume <= 0 || removedVolume > stockMass.volume) throw new Error(`OCCT solid material removal returned an invalid BRep or volume delta: ${JSON.stringify({ stockVolume: stockMass.volume, remainingVolume: remainingMass.volume, removedVolume, quality })}`)
    return {
      backend: 'bitbybit-occt-wasm',
      model: sweepMode === 'continuous-segment' ? 'continuous-segment-sweep-brep' : 'sampled-flat-end-brep',
      sweepMode,
      nativeSolid: true,
      freeCadNativeEquivalent: false,
      sampleCount: samples.length,
      stockVolume: stockMass.volume,
      removedVolume,
      remainingVolume: remainingMass.volume,
      structuralValid: quality.structuralValid,
      solids: quality.solids,
      meshVertices: mesh.positions.length / 3,
      meshTriangles: mesh.indices.length / 3,
      remainingShape: remaining,
    }
  } catch (error) {
    if (remaining) await runtime.release(remaining)
    throw error
  } finally {
    if (mergedTool && !tools.some((tool) => tool.id === mergedTool?.id)) await runtime.release(mergedTool)
    await Promise.all(tools.map((tool) => runtime.release(tool)))
    await runtime.release(stock)
  }
}
