import type { CamApi, CamKinematicOptions, CamPoint, CamPostprocessor } from './cam'
import {
  OPEN_CAM_LIB_ARTIFACT_SHA256,
  OPEN_CAM_LIB_SOURCE_REVISION,
  generateCamoticsSourceStageGcode,
  runOpenCamLibDropCutter,
  type OpenCamLibDropCutterInput,
  type OpenCamLibDropCutterResult,
} from './camNativeSimulation'

/**
 * CAM stage order is deliberately explicit. LinuxCNC WASM is the only stage
 * allowed to parse, plan, or execute the resulting G-code.
 */
export const CAM_PIPELINE_ORDER = ['CAD', 'OCL', 'CAMotics', 'GCODE', 'LinuxCNC WASM'] as const
export type CamPipelineStage = typeof CAM_PIPELINE_ORDER[number]

export const CAMOTICS_SOURCE_REVISION = 'e84665f2fa9d1151f03282ac7e01320bc65e015b'
export const CAMOTICS_SOURCE_PATH = 'CAMotics'
export const CAMOTICS_SOURCE_URL = 'https://github.com/CauldronDevelopmentLLC/CAMotics'

export type CamoticsStageEvidence = {
  backend: 'camotics-source'
  sourcePath: string
  sourceRevision: string
  sourceUrl: string
  generator: 'camotics-source-stage'
  simulator: 'camotics-camsim'
  axisScope: '3-axis-source-capability'
  workspaceNativeCli: 'verified-sidecar'
  nativeCliPath: 'CAMotics/camsim'
  browserNativeExecutable: false
  parserAuthority: 'linuxcnc-wasm'
  camoticsParsesCanonicalGcode: false
}

export type LinuxcncWasmSubmission = {
  backend: 'linuxcnc-wasm'
  status: 'accepted' | 'rejected' | 'dry-run' | 'unavailable'
  programSha256?: string
  message?: string
  trace?: {
    lines?: number
    blocks?: number
    trajectorySegments?: number
    parserAuthority?: string
    machineCase?: string
  }
}

export type LinuxcncWasmAdapter = {
  submit(program: string): Promise<LinuxcncWasmSubmission>
}

export type LinuxcncIframeAdapterOptions = {
  url: string
  run?: boolean
  timeoutMs?: number
  name?: string
}

export type OpenCamLibPipelineRunner = (input: OpenCamLibDropCutterInput) => Promise<OpenCamLibDropCutterResult>
export type CamPipelineOptions = {
  postprocessor?: CamPostprocessor
  kinematics?: CamKinematicOptions
  openCamLib?: {
    triangles?: OpenCamLibDropCutterInput['triangles']
    sampling?: number
    runner?: OpenCamLibPipelineRunner
  }
}

export type OpenCamLibStageEvidence = {
  backend: 'upstream-opencamlib-wasm'
  sourceRevision: string
  artifactSha256: string
  algorithm: 'path-drop-cutter'
  surfaceModel: 'cad-operation-plane' | 'cad-triangle-mesh'
  triangleCount: number
  inputPoints: number
  outputPoints: number
  sampling: number
}

export type CamPipelineResult = {
  stage: CamPipelineStage
  order: readonly CamPipelineStage[]
  operationId: string
  postprocessor: CamPostprocessor
  gcode: string
  camoticsMotionGcode: string
  openCamLib: OpenCamLibStageEvidence
  camotics: CamoticsStageEvidence
  linuxcnc: LinuxcncWasmSubmission | null
}

export const camoticsSourceEvidence = (): CamoticsStageEvidence => ({
  backend: 'camotics-source',
  sourcePath: CAMOTICS_SOURCE_PATH,
  sourceRevision: CAMOTICS_SOURCE_REVISION,
  sourceUrl: CAMOTICS_SOURCE_URL,
  generator: 'camotics-source-stage',
  simulator: 'camotics-camsim',
  axisScope: '3-axis-source-capability',
  workspaceNativeCli: 'verified-sidecar',
  nativeCliPath: 'CAMotics/camsim',
  browserNativeExecutable: false,
  parserAuthority: 'linuxcnc-wasm',
  camoticsParsesCanonicalGcode: false,
})

/**
 * Builds the machine program but does not submit it. This function is the
 * only public route from CAD/OCL/CAMotics evidence to the LinuxCNC adapter.
 */
const operationPlane = (min: CamPoint, max: CamPoint, z: number): OpenCamLibDropCutterInput['triangles'] => [
  [[min[0], min[1], z], [max[0], min[1], z], [max[0], max[1], z]],
  [[min[0], min[1], z], [max[0], max[1], z], [min[0], max[1], z]],
]

export const prepareCamPipeline = async (
  cam: CamApi,
  operationId: string,
  options: CamPipelineOptions = {},
): Promise<CamPipelineResult> => {
  const postprocessor = options.postprocessor ?? 'linuxcnc'
  if (postprocessor !== 'linuxcnc') throw new RangeError('The LinuxCNC WASM CAM pipeline requires the linuxcnc postprocessor.')

  const operation = cam.snapshot().operations.find((candidate) => candidate.id === operationId)
  if (!operation) throw new RangeError(`CAM pipeline operation does not exist: ${operationId}`)
  if (operation.path.length < 2) throw new RangeError(`CAM pipeline operation has no generated path: ${operationId}`)

  const snapshot = cam.snapshot()
  const cutter = snapshot.tools.find((candidate) => candidate.id === operation.toolId)
  if (!cutter) throw new RangeError(`CAM pipeline tool does not exist: ${operation.toolId}`)
  const minimumZ = Math.min(...operation.path.map((point) => point[2]))
  const triangles = options.openCamLib?.triangles ?? operationPlane(snapshot.stock.min, snapshot.stock.max, minimumZ)
  const sampling = options.openCamLib?.sampling ?? Math.max(0.01, cutter.diameter / 2)
  const runOpenCamLib = options.openCamLib?.runner ?? runOpenCamLibDropCutter
  const ocl = await runOpenCamLib({
    triangles,
    path: operation.path,
    cutter: { shape: cutter.shape === 'ballend' ? 'ballend' : 'endmill', diameter: cutter.diameter, length: cutter.length },
    sampling,
    minimumZ,
  })
  if (ocl.backend !== 'upstream-opencamlib-wasm' || ocl.sourceRevision !== OPEN_CAM_LIB_SOURCE_REVISION || ocl.artifactSha256 !== OPEN_CAM_LIB_ARTIFACT_SHA256) {
    throw new Error('CAM pipeline rejected OpenCAMLib output without the pinned upstream WASM identity.')
  }
  if (ocl.points.length < 2) throw new Error('CAM pipeline rejected an empty OpenCAMLib cutter-location path.')
  const resolvedOperation = cam.replaceGeneratedPath(operationId, ocl.points)

  // CAMotics source stage emits canonical motion words. The full post output
  // retains tool/controller/modal blocks, while LinuxCNC remains the parser.
  const camoticsMotion = generateCamoticsSourceStageGcode(resolvedOperation.path.map((point, index) => ({ type: index === 0 ? 'rapid' as const : 'cut' as const, point, feed: resolvedOperation.feed })), { feed: resolvedOperation.feed })
  const gcode = options.kinematics
    ? cam.exportMultiAxisGcode(operationId, postprocessor, options.kinematics)
    : cam.exportGcode(postprocessor)
  if (!gcode.trim()) throw new Error('CAM pipeline generated an empty G-code program.')

  return {
    stage: 'GCODE',
    order: CAM_PIPELINE_ORDER,
    operationId,
    postprocessor,
    gcode,
    camoticsMotionGcode: camoticsMotion.gcode,
    openCamLib: {
      backend: ocl.backend,
      sourceRevision: ocl.sourceRevision,
      artifactSha256: ocl.artifactSha256,
      algorithm: ocl.algorithm,
      surfaceModel: options.openCamLib?.triangles ? 'cad-triangle-mesh' : 'cad-operation-plane',
      triangleCount: ocl.triangleCount,
      inputPoints: operation.path.length,
      outputPoints: ocl.points.length,
      sampling,
    },
    camotics: camoticsSourceEvidence(),
    linuxcnc: null,
  }
}

export const submitCamPipelineToLinuxcnc = async (
  prepared: CamPipelineResult,
  adapter: LinuxcncWasmAdapter,
): Promise<CamPipelineResult> => {
  if (prepared.stage !== 'GCODE') throw new Error(`LinuxCNC WASM submission requires G-code stage, got ${prepared.stage}.`)
  if (!prepared.gcode.trim()) throw new Error('LinuxCNC WASM submission requires a non-empty G-code program.')
  const linuxcnc = await adapter.submit(prepared.gcode)
  if (linuxcnc.backend !== 'linuxcnc-wasm') throw new Error('CAM pipeline rejected a non-LinuxCNC execution backend.')
  return { ...prepared, stage: 'LinuxCNC WASM', linuxcnc }
}

/**
 * Browser bridge to the real LinuxCNC WASM page. The iframe owns the worker,
 * RS274 parser, task planner, kinematics and execution; this adapter only
 * transports the already generated program and observes the result.
 */
export const createLinuxcncIframeAdapter = (options: LinuxcncIframeAdapterOptions): LinuxcncWasmAdapter => {
  if (typeof window === 'undefined' || typeof document === 'undefined') throw new Error('LinuxCNC iframe adapter requires a browser document.')
  const timeoutMs = options.timeoutMs ?? 120_000
  const frame = document.createElement('iframe')
  frame.src = options.url
  frame.title = 'LinuxCNC WASM machine bridge'
  frame.hidden = true
  const ready = new Promise<void>((resolve) => frame.addEventListener('load', () => resolve(), { once: true }))
  document.body.append(frame)
  return {
    async submit(program) {
      await ready
      if (!frame.contentWindow) throw new Error('LinuxCNC WASM iframe did not create a content window.')
      const requestId = `bitbybit-${Date.now()}-${Math.random().toString(36).slice(2)}`
      return new Promise<LinuxcncWasmSubmission>((resolve, reject) => {
        const timer = window.setTimeout(() => {
          window.removeEventListener('message', onMessage)
          reject(new Error('LinuxCNC WASM bridge timed out waiting for parser/controller acceptance.'))
        }, timeoutMs)
        const onMessage = (event: MessageEvent) => {
          const data = event.data
          if (event.source !== frame.contentWindow || data?.type !== 'bitbybit-linuxcnc-result' || data.requestId !== requestId) return
          window.clearTimeout(timer)
          window.removeEventListener('message', onMessage)
          const machineCase = frame.contentDocument?.querySelector<HTMLElement>('[data-case].active')?.dataset.case
          resolve({
            backend: 'linuxcnc-wasm',
            status: data.status,
            programSha256: data.programSha256,
            message: data.message,
            trace: { lines: data.lines, blocks: data.blocks, trajectorySegments: data.trajectorySegments, parserAuthority: data.parserAuthority, machineCase },
          })
        }
        window.addEventListener('message', onMessage)
        const targetWindow = frame.contentWindow
        if (!targetWindow) {
          window.clearTimeout(timer)
          window.removeEventListener('message', onMessage)
          reject(new Error('LinuxCNC WASM iframe lost its content window before submission.'))
          return
        }
        targetWindow.postMessage({ type: 'bitbybit-linuxcnc-submit', requestId, name: options.name ?? 'bitbybit-cam.ngc', program, run: options.run !== false }, new URL(options.url, window.location.href).origin)
      })
    },
  }
}
