import { cloneSketch, solveSketch, type SketchSnapshot, type SketchSolveOptions, type SketchSolveResult } from './sketcher'

export const SKETCH_SOLVER_PROTOCOL_VERSION = 1 as const

export type SketchSolverCompatibility = 'experimental' | 'freecad-1.1.1'

export type SketchSolverCapabilities = {
  providerId: string
  providerVersion: string
  engine: 'typescript-basic' | 'planegcs-wasm'
  availability: 'available' | 'unavailable'
  compatibility: SketchSolverCompatibility
  supportedGeometry: string[]
  supportedConstraints: string[]
  reason?: string
}

export type SketchSolverRequest = {
  protocolVersion: typeof SKETCH_SOLVER_PROTOCOL_VERSION
  requestId: string
  documentId: string
  documentVersion: number
  generation: number
  snapshot: SketchSnapshot
  options?: SketchSolveOptions
}

export type SketchSolverResponse = {
  protocolVersion: typeof SKETCH_SOLVER_PROTOCOL_VERSION
  requestId: string
  documentId: string
  documentVersion: number
  generation: number
  provider: SketchSolverCapabilities
  result: SketchSolveResult
}

export interface SketchSolverProvider {
  capabilities(): SketchSolverCapabilities
  solve(request: SketchSolverRequest, signal: AbortSignal): Promise<SketchSolverResponse>
}

export class SketchSolverUnavailableError extends Error {
  readonly code = 'SKETCH_SOLVER_UNAVAILABLE'

  constructor(readonly capabilities: SketchSolverCapabilities) {
    super(capabilities.reason || `Sketch solver provider '${capabilities.providerId}' is unavailable.`)
    this.name = 'SketchSolverUnavailableError'
  }
}

export const assertSketchSolverRequest = (request: SketchSolverRequest) => {
  if (request.protocolVersion !== SKETCH_SOLVER_PROTOCOL_VERSION) throw new RangeError(`Unsupported sketch solver protocol version: ${request.protocolVersion}.`)
  if (!request.requestId || !request.documentId) throw new TypeError('Sketch solver request requires requestId and documentId.')
  if (!Number.isSafeInteger(request.documentVersion) || request.documentVersion < 0) throw new RangeError('Sketch solver documentVersion must be a non-negative integer.')
  if (!Number.isSafeInteger(request.generation) || request.generation < 1) throw new RangeError('Sketch solver generation must be a positive integer.')
}

export const assertSketchSolverResponse = (request: SketchSolverRequest, response: SketchSolverResponse) => {
  if (response.protocolVersion !== request.protocolVersion || response.requestId !== request.requestId || response.documentId !== request.documentId || response.documentVersion !== request.documentVersion || response.generation !== request.generation) throw new Error('Sketch solver response does not match its request context.')
  return response
}

const abortError = () => new DOMException('Sketch solve cancelled.', 'AbortError')

export class BasicSketchSolverProvider implements SketchSolverProvider {
  capabilities(): SketchSolverCapabilities {
    return {
      providerId: 'web-cad.basic-sketch-solver',
      providerVersion: '1.0.0',
      engine: 'typescript-basic',
      availability: 'available',
      compatibility: 'experimental',
      supportedGeometry: ['point', 'line', 'circle', 'arc'],
      supportedConstraints: ['coincident', 'horizontal', 'vertical', 'parallel', 'perpendicular', 'tangent', 'distance', 'distanceX', 'distanceY', 'angle', 'radius', 'equal', 'pointOnObject', 'symmetric', 'block', 'diameter'],
    }
  }

  async solve(request: SketchSolverRequest, signal: AbortSignal): Promise<SketchSolverResponse> {
    assertSketchSolverRequest(request)
    if (signal.aborted) throw abortError()
    const result = solveSketch(cloneSketch(request.snapshot), request.options)
    if (signal.aborted) throw abortError()
    return assertSketchSolverResponse(request, {
      protocolVersion: request.protocolVersion,
      requestId: request.requestId,
      documentId: request.documentId,
      documentVersion: request.documentVersion,
      generation: request.generation,
      provider: this.capabilities(),
      result,
    })
  }
}

export class UnavailablePlanegcsProvider implements SketchSolverProvider {
  constructor(private readonly reason = 'No FreeCAD 1.1.1 planegcs WASM artifact is installed.') {}

  capabilities(): SketchSolverCapabilities {
    return {
      providerId: 'freecad.planegcs-wasm',
      providerVersion: 'unavailable',
      engine: 'planegcs-wasm',
      availability: 'unavailable',
      compatibility: 'experimental',
      supportedGeometry: [],
      supportedConstraints: [],
      reason: this.reason,
    }
  }

  solve(): Promise<SketchSolverResponse> {
    return Promise.reject(new SketchSolverUnavailableError(this.capabilities()))
  }
}

export type SketchSolverExecution = {
  status: 'completed' | 'cancelled' | 'stale'
  generation: number
  response?: SketchSolverResponse
}

export class SketchSolverCoordinator {
  private active: { generation: number; controller: AbortController } | null = null
  private generation = 0

  constructor(private readonly currentDocumentVersion: (documentId: string) => number | null) {}

  cancel() { this.active?.controller.abort() }

  async solve(provider: SketchSolverProvider, input: Omit<SketchSolverRequest, 'protocolVersion' | 'generation'>): Promise<SketchSolverExecution> {
    this.active?.controller.abort()
    const generation = ++this.generation
    const controller = new AbortController()
    this.active = { generation, controller }
    const request: SketchSolverRequest = { ...input, protocolVersion: SKETCH_SOLVER_PROTOCOL_VERSION, generation, snapshot: cloneSketch(input.snapshot) }
    try {
      const response = await provider.solve(request, controller.signal)
      if (controller.signal.aborted || this.active?.generation !== generation) return { status: 'cancelled', generation }
      if (this.currentDocumentVersion(input.documentId) !== input.documentVersion) return { status: 'stale', generation }
      return { status: 'completed', generation, response }
    } catch (error) {
      if (controller.signal.aborted || (error instanceof Error && error.name === 'AbortError')) return { status: 'cancelled', generation }
      throw error
    } finally {
      if (this.active?.generation === generation) this.active = null
    }
  }
}

export type SketchSolverReplayCase = {
  id: string
  request: Omit<SketchSolverRequest, 'protocolVersion' | 'generation'>
  expected: {
    status: SketchSolveResult['status']
    degreesOfFreedom: number
    maxResidual: number
  }
}

export type SketchSolverReplayResult = { id: string; passed: boolean; differences: string[] }

export const runSketchSolverReplay = async (provider: SketchSolverProvider, cases: SketchSolverReplayCase[]): Promise<SketchSolverReplayResult[]> => {
  const coordinator = new SketchSolverCoordinator(() => 1)
  const results: SketchSolverReplayResult[] = []
  for (const fixture of cases) {
    const execution = await coordinator.solve(provider, fixture.request)
    const actual = execution.response?.result
    const differences: string[] = []
    if (!actual) differences.push(`execution status was ${execution.status}`)
    else {
      if (actual.status !== fixture.expected.status) differences.push(`status: expected ${fixture.expected.status}, received ${actual.status}`)
      if (actual.degreesOfFreedom !== fixture.expected.degreesOfFreedom) differences.push(`degreesOfFreedom: expected ${fixture.expected.degreesOfFreedom}, received ${actual.degreesOfFreedom}`)
      if (actual.residual > fixture.expected.maxResidual) differences.push(`residual: expected <= ${fixture.expected.maxResidual}, received ${actual.residual}`)
    }
    results.push({ id: fixture.id, passed: differences.length === 0, differences })
  }
  return results
}
