import { migrateElementMap2Schema, validateElementMap2 } from './elementMap2'
import { assertNativeNamingEvidence, type NativeStageNamingEvidence } from './nativeNamingEvidence'
import { migrateStringHasherSchema, validateElementMap2StringHasherEvidence, validateStringHasherTable } from './stringHasher'
import type { NativeOcctHistoryOperation, NativeOcctHistoryResponse } from './nativeHistoryProvider'

export const FREECAD_PRIVATE_NAMING_ABI_VERSION = 1 as const
export const FREECAD_PRIVATE_NAMING_MAX_REQUEST_BYTES = 16 * 1024 * 1024
export const FREECAD_PRIVATE_NAMING_MAX_RESPONSE_BYTES = 32 * 1024 * 1024

export type FreeCadPrivateNamingAbiDescriptor = {
  schemaVersion: 1
  freecadVersion: string
  sourceCommit: string
  mappedNameRef: true
  stringHasher: true
  elementMap2: true
  operations: NativeOcctHistoryOperation[]
  maxRequestBytes?: number
  maxResponseBytes?: number
}

export type FreeCadPrivateNamingAbiProbe = {
  availability: 'available' | 'unavailable'
  abiVersion: number | null
  descriptor?: FreeCadPrivateNamingAbiDescriptor
  reason?: string
}

/** Embind surface implemented only by a FreeCAD-linked native module. */
export type NativeFreeCadNamingAbiModule = {
  freecadNamingAbiVersion?(): number
  freecadNamingCapabilitiesJson?(): string
  freecadNamingEvidenceJson?(requestJson: string): string
}

export type FreeCadPrivateNamingAbiRequest = {
  schemaVersion: 1
  requestId: string
  documentId: string
  documentVersion: number
  operationId: string
  operation: NativeOcctHistoryOperation
  stageId: string
  resultObjectId: string
  inputs: Array<{ inputId: string; role?: string; stageId?: string; step: string }>
  stages: Array<{ stageId: string; operation?: NativeOcctHistoryOperation; inputIds: string[]; resultStageId?: string; ordinal: number }>
  resultStep?: string
  resultBrep?: string
  resultStepByStage?: Record<string, string>
  history: Pick<NativeOcctHistoryResponse, 'provider' | 'occtVersion' | 'records' | 'hasModified' | 'hasGenerated' | 'hasDeleted' | 'resultStep' | 'resultBrep'>
}

const encoder = new TextEncoder()
const lockedCommit = '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d'
const supportedOperations = new Set<NativeOcctHistoryOperation>(['fuse', 'cut', 'common', 'rotate', 'pad', 'pocket', 'loft', 'pipe', 'revolution', 'groove', 'fillet', 'chamfer', 'hole', 'draft', 'thickness', 'linear-pattern', 'polar-pattern', 'mirrored', 'multi-transform'])

const boundedLimit = (value: unknown, maximum: number) => Number.isSafeInteger(value) && (value as number) > 0 ? Math.min(value as number, maximum) : maximum

export const probeFreeCadPrivateNamingAbi = (module: NativeFreeCadNamingAbiModule): FreeCadPrivateNamingAbiProbe => {
  if (typeof module.freecadNamingAbiVersion !== 'function' || typeof module.freecadNamingCapabilitiesJson !== 'function' || typeof module.freecadNamingEvidenceJson !== 'function') return {
    availability: 'unavailable',
    abiVersion: null,
    reason: 'Native module does not export the versioned FreeCAD naming callbacks.',
  }
  let abiVersion: number
  try { abiVersion = module.freecadNamingAbiVersion() } catch (error) {
    return { availability: 'unavailable', abiVersion: null, reason: `FreeCAD naming ABI version probe failed: ${error instanceof Error ? error.message : String(error)}` }
  }
  if (abiVersion !== FREECAD_PRIVATE_NAMING_ABI_VERSION) return { availability: 'unavailable', abiVersion, reason: `Unsupported FreeCAD naming ABI version ${abiVersion}.` }
  try {
    const raw = module.freecadNamingCapabilitiesJson()
    if (encoder.encode(raw).byteLength > 64 * 1024) throw new RangeError('FreeCAD naming capability payload exceeds 64 KiB.')
    const descriptor = JSON.parse(raw) as FreeCadPrivateNamingAbiDescriptor
    if (descriptor.schemaVersion !== 1 || descriptor.freecadVersion !== '1.1.1' || descriptor.sourceCommit !== lockedCommit || descriptor.mappedNameRef !== true || descriptor.stringHasher !== true || descriptor.elementMap2 !== true) throw new TypeError('FreeCAD naming capabilities are not locked to the required 1.1.1 private ABI.')
    if (!Array.isArray(descriptor.operations) || descriptor.operations.length === 0 || new Set(descriptor.operations).size !== descriptor.operations.length || descriptor.operations.some((operation) => !supportedOperations.has(operation))) throw new TypeError('FreeCAD naming capabilities require a unique non-empty supported operation list.')
    return {
      availability: 'available',
      abiVersion,
      descriptor: {
        ...descriptor,
        operations: [...descriptor.operations],
        maxRequestBytes: boundedLimit(descriptor.maxRequestBytes, FREECAD_PRIVATE_NAMING_MAX_REQUEST_BYTES),
        maxResponseBytes: boundedLimit(descriptor.maxResponseBytes, FREECAD_PRIVATE_NAMING_MAX_RESPONSE_BYTES),
      },
    }
  } catch (error) {
    return { availability: 'unavailable', abiVersion, reason: `FreeCAD naming capability probe failed: ${error instanceof Error ? error.message : String(error)}` }
  }
}

export const createFreeCadPrivateNamingAbiRequest = (input: Omit<FreeCadPrivateNamingAbiRequest, 'schemaVersion'>): FreeCadPrivateNamingAbiRequest => {
  if (!input.requestId.trim() || !input.documentId.trim() || !input.operationId.trim() || !input.stageId.trim() || !input.resultObjectId.trim()) throw new TypeError('FreeCAD naming ABI request IDs must be non-empty.')
  if (!Number.isSafeInteger(input.documentVersion) || input.documentVersion < 0) throw new RangeError('FreeCAD naming ABI documentVersion must be a non-negative integer.')
  return {
    schemaVersion: 1,
    ...input,
    inputs: input.inputs.map((entry) => {
      if (!entry.inputId.trim() || !entry.step.trim()) throw new TypeError('FreeCAD naming ABI inputs require non-empty inputId and STEP text.')
      return { ...entry }
    }),
    stages: input.stages.map((entry) => ({ ...entry, inputIds: [...entry.inputIds] })),
    ...(input.resultStepByStage ? { resultStepByStage: { ...input.resultStepByStage } } : {}),
    history: { ...input.history, records: input.history.records.map((record) => ({ ...record, resultIndexes: record.resultIndexes ? [...record.resultIndexes] : undefined })) },
  }
}

export const captureFreeCadPrivateNamingEvidence = (
  module: NativeFreeCadNamingAbiModule,
  request: FreeCadPrivateNamingAbiRequest,
  probe = probeFreeCadPrivateNamingAbi(module),
): NativeStageNamingEvidence | undefined => {
  if (probe.availability !== 'available' || !probe.descriptor || typeof module.freecadNamingEvidenceJson !== 'function') return undefined
  if (!probe.descriptor.operations.includes(request.operation)) return undefined
  const requestJson = JSON.stringify(request)
  const requestBytes = encoder.encode(requestJson).byteLength
  const requestLimit = boundedLimit(probe.descriptor.maxRequestBytes, FREECAD_PRIVATE_NAMING_MAX_REQUEST_BYTES)
  if (requestBytes > requestLimit) throw new RangeError(`FreeCAD naming ABI request exceeds ${requestLimit} bytes.`)
  const responseJson = module.freecadNamingEvidenceJson(requestJson)
  if (typeof responseJson !== 'string') throw new TypeError('FreeCAD naming ABI must return a JSON string.')
  const responseBytes = encoder.encode(responseJson).byteLength
  const responseLimit = boundedLimit(probe.descriptor.maxResponseBytes, FREECAD_PRIVATE_NAMING_MAX_RESPONSE_BYTES)
  if (responseBytes > responseLimit) throw new RangeError(`FreeCAD naming ABI response exceeds ${responseLimit} bytes.`)
  const evidence = assertNativeNamingEvidence(JSON.parse(responseJson) as NativeStageNamingEvidence)
  if (evidence.stageId !== request.stageId || evidence.resultObjectId !== request.resultObjectId) throw new RangeError('FreeCAD naming ABI response does not match its stage and result object context.')
  if (evidence.status !== 'native-evidence' && evidence.status !== 'ambiguous') throw new TypeError(`FreeCAD naming ABI cannot return non-native status '${evidence.status}'.`)
  if (!evidence.stringHasher || !evidence.elementMap2) throw new TypeError('FreeCAD naming ABI response requires both StringHasher and ElementMap2 evidence.')
  const stringHasher = migrateStringHasherSchema(evidence.stringHasher)
  const stringHasherReport = validateStringHasherTable(stringHasher)
  if (!stringHasherReport.valid) throw new Error(`FreeCAD naming ABI StringHasher is invalid: ${stringHasherReport.issues[0].path}: ${stringHasherReport.issues[0].message}`)
  const elementMap2 = migrateElementMap2Schema(evidence.elementMap2)
  const elementMap2Report = validateElementMap2(elementMap2)
  if (!elementMap2Report.valid) throw new Error(`FreeCAD naming ABI ElementMap2 is invalid: ${elementMap2Report.issues[0].path}: ${elementMap2Report.issues[0].message}`)
  const closureIssues = validateElementMap2StringHasherEvidence(elementMap2, stringHasher)
  if (closureIssues.length > 0) throw new Error(`FreeCAD naming ABI StringHasher closure is invalid: ${closureIssues[0].path}: ${closureIssues[0].message}`)
  return { ...evidence, stringHasher, elementMap2 }
}
