import { PLANEGCS_WASM_CAPABILITIES } from './planegcsAdapter'
import { assertSketchSolverRequest, assertSketchSolverResponse, type SketchSolverCapabilities, type SketchSolverProvider, type SketchSolverRequest, type SketchSolverResponse } from './sketchSolverProtocol'

type WorkerLike = Pick<Worker, 'postMessage' | 'terminate'> & {
  addEventListener(type: 'message', listener: (event: MessageEvent) => void): void
  addEventListener(type: 'error', listener: (event: ErrorEvent) => void): void
  removeEventListener(type: 'message', listener: (event: MessageEvent) => void): void
  removeEventListener(type: 'error', listener: (event: ErrorEvent) => void): void
}

type WorkerResponse = { type: 'ready'; capabilities: SketchSolverCapabilities } | { type: 'response'; response: SketchSolverResponse } | { type: 'error'; requestId?: string; error: string }

export type PlanegcsWorkerOptions = {
  moduleUrl?: string
  initializationTimeoutMs?: number
  workerFactory?: () => WorkerLike
}

const unavailableCapabilities = (reason: string): SketchSolverCapabilities => ({ ...PLANEGCS_WASM_CAPABILITIES, availability: 'unavailable', reason })
const abortError = () => new DOMException('FreeCAD planegcs Worker request cancelled.', 'AbortError')
const defaultWorkerFactory = () => new Worker(new URL('./planegcsWorkerEntry.ts', import.meta.url), { type: 'module', name: 'freecad-planegcs' })

export class PlanegcsWorkerProvider implements SketchSolverProvider {
  private worker: WorkerLike
  private readonly workerFactory: () => WorkerLike
  private readonly options: Required<Pick<PlanegcsWorkerOptions, 'moduleUrl' | 'initializationTimeoutMs'>>
  private readonly pending = new Map<string, { resolve: (response: SketchSolverResponse) => void; reject: (error: Error) => void }>()
  private initialization: Promise<SketchSolverCapabilities> | null = null
  private initializationReject: ((error: Error) => void) | null = null
  private disposed = false
  private current = unavailableCapabilities('FreeCAD planegcs Worker has not been initialized.')

  private readonly onMessage = (event: MessageEvent<WorkerResponse>) => {
    if (event.data.type === 'ready') { this.current = event.data.capabilities; return }
    if (event.data.type === 'error') {
      const pending = event.data.requestId ? this.pending.get(event.data.requestId) : undefined
      if (pending && event.data.requestId) { this.pending.delete(event.data.requestId); pending.reject(new Error(event.data.error)) }
      if (!event.data.requestId) this.initializationReject?.(new Error(event.data.error))
      return
    }
    const pending = this.pending.get(event.data.response.requestId)
    if (!pending) return
    this.pending.delete(event.data.response.requestId)
    pending.resolve(event.data.response)
  }

  private readonly onError = (event: ErrorEvent) => this.replaceWorker(new Error(event.message || 'FreeCAD planegcs Worker failed.'))

  constructor(options: PlanegcsWorkerOptions = {}) {
    this.workerFactory = options.workerFactory || defaultWorkerFactory
    this.options = { moduleUrl: options.moduleUrl || '/native/planegcs/freecad-planegcs.js', initializationTimeoutMs: options.initializationTimeoutMs ?? 120_000 }
    this.worker = this.workerFactory()
    this.attachWorker(this.worker)
  }

  private attachWorker(worker: WorkerLike) {
    worker.addEventListener('message', this.onMessage)
    worker.addEventListener('error', this.onError)
  }

  private detachWorker(worker: WorkerLike) {
    worker.removeEventListener('message', this.onMessage)
    worker.removeEventListener('error', this.onError)
  }

  private replaceWorker(error: Error) {
    const previous = this.worker
    this.detachWorker(previous)
    previous.terminate()
    this.initializationReject?.(error)
    this.initializationReject = null
    this.initialization = null
    for (const pending of this.pending.values()) pending.reject(error)
    this.pending.clear()
    this.current = unavailableCapabilities(error.message)
    if (!this.disposed) {
      this.worker = this.workerFactory()
      this.attachWorker(this.worker)
    }
  }

  capabilities(): SketchSolverCapabilities { return { ...this.current, supportedGeometry: [...this.current.supportedGeometry], supportedConstraints: [...this.current.supportedConstraints] } }

  initialize(): Promise<SketchSolverCapabilities> {
    if (this.disposed) return Promise.reject(new Error('FreeCAD planegcs Worker provider is disposed.'))
    if (this.current.availability === 'available') return Promise.resolve(this.capabilities())
    if (this.initialization) return this.initialization
    this.initialization = new Promise<SketchSolverCapabilities>((resolve, reject) => {
      let settled = false
      const timer = setTimeout(() => fail(new Error('FreeCAD planegcs Worker initialization timed out.')), this.options.initializationTimeoutMs)
      const fail = (error: Error) => { if (settled) return; settled = true; clearTimeout(timer); this.worker.removeEventListener('message', waitForReady); this.initialization = null; this.initializationReject = null; this.current = unavailableCapabilities(error.message); reject(error) }
      this.initializationReject = fail
      const waitForReady = (event: MessageEvent<WorkerResponse>) => { if (event.data.type === 'error') return fail(new Error(event.data.error)); if (event.data.type !== 'ready') return; settled = true; clearTimeout(timer); this.worker.removeEventListener('message', waitForReady); this.initializationReject = null; resolve(this.capabilities()) }
      this.worker.addEventListener('message', waitForReady)
      this.worker.postMessage({ type: 'initialize', moduleUrl: this.options.moduleUrl })
    })
    return this.initialization
  }

  async solve(request: SketchSolverRequest, signal: AbortSignal): Promise<SketchSolverResponse> {
    assertSketchSolverRequest(request)
    if (signal.aborted) throw abortError()
    const cancel = () => this.replaceWorker(abortError())
    signal.addEventListener('abort', cancel, { once: true })
    try {
      await this.initialize()
      if (signal.aborted) throw abortError()
      const response = new Promise<SketchSolverResponse>((resolve, reject) => {
        this.pending.set(request.requestId, { resolve, reject })
        this.worker.postMessage({ type: 'solve', request })
      })
      return assertSketchSolverResponse(request, await response)
    } finally {
      signal.removeEventListener('abort', cancel)
    }
  }

  dispose() {
    if (this.disposed) return
    this.disposed = true
    this.worker.postMessage({ type: 'dispose' })
    this.replaceWorker(new Error('FreeCAD planegcs Worker provider disposed.'))
  }
}
