import { cloneElementMap2, migrateElementMap2Schema, type AnyElementMap2Document, type ElementMap2MappedNameReference } from './elementMap2'
import { cloneStringHasherTable, migrateStringHasherSchema, validateStringHasherTable, type AnyStringHasherTable } from './stringHasher'

export type NativeMappedNameRelation = 'preserved' | 'modified' | 'generated' | 'deleted' | 'ambiguous'

/** Runtime evidence emitted by a native builder for one result subshape. */
export type NativeMappedNameRef = {
  kind: 'face' | 'edge' | 'vertex'
  resultIndex: number
  resultPersistentId: string
  reference: ElementMap2MappedNameReference
  relation?: NativeMappedNameRelation
  sourceRefs?: Array<{
    objectId: string
    persistentId: string
    stageId?: string
  }>
  candidates?: Array<{
    objectId: string
    persistentId: string
    stageId?: string
  }>
}

export type NativeNamingEvidenceStatus = 'native-evidence' | 'opaque-preserved' | 'final-shape-only' | 'ambiguous' | 'missing'

export type NativeStageNamingEvidence = {
  schemaVersion: 1
  stageId: string
  resultObjectId: string
  status: NativeNamingEvidenceStatus
  mappedNames?: NativeMappedNameRef[]
  stringHasher?: AnyStringHasherTable
  elementMap2?: AnyElementMap2Document
  reason?: string
}

export type NativeNamingEvidenceIssue = {
  code: 'SCHEMA_VERSION' | 'STAGE_ID' | 'RESULT_OBJECT_ID' | 'STATUS' | 'MAPPED_NAME' | 'RESULT_INDEX' | 'STRING_HASHER' | 'STRING_ID' | 'SOURCE_REF' | 'AMBIGUOUS'
  path: string
  message: string
}

export type NativeNamingEvidenceReport = {
  valid: boolean
  status: NativeNamingEvidenceStatus
  mappedNameCount: number
  stringHasherEntryCount: number
  issues: NativeNamingEvidenceIssue[]
}

const statuses = new Set<NativeNamingEvidenceStatus>(['native-evidence', 'opaque-preserved', 'final-shape-only', 'ambiguous', 'missing'])
const relationSet = new Set<NativeMappedNameRelation>(['preserved', 'modified', 'generated', 'deleted', 'ambiguous'])
const push = (issues: NativeNamingEvidenceIssue[], code: NativeNamingEvidenceIssue['code'], path: string, message: string) => issues.push({ code, path, message })

export const validateNativeNamingEvidence = (evidence: NativeStageNamingEvidence): NativeNamingEvidenceReport => {
  const issues: NativeNamingEvidenceIssue[] = []
  if (evidence.schemaVersion !== 1) push(issues, 'SCHEMA_VERSION', 'schemaVersion', `unsupported runtime naming evidence schema ${evidence.schemaVersion}`)
  if (typeof evidence.stageId !== 'string' || !evidence.stageId.trim()) push(issues, 'STAGE_ID', 'stageId', 'stageId must be a non-empty string')
  if (typeof evidence.resultObjectId !== 'string' || !evidence.resultObjectId.trim()) push(issues, 'RESULT_OBJECT_ID', 'resultObjectId', 'resultObjectId must be a non-empty string')
  if (!statuses.has(evidence.status)) push(issues, 'STATUS', 'status', `unsupported naming evidence status ${String(evidence.status)}`)
  if (evidence.mappedNames !== undefined && !Array.isArray(evidence.mappedNames)) push(issues, 'MAPPED_NAME', 'mappedNames', 'mappedNames must be an array')
  const mappedNames = Array.isArray(evidence.mappedNames) ? evidence.mappedNames : []
  const resultIndexes = new Set<string>()
  let stringHasher: ReturnType<typeof migrateStringHasherSchema> | undefined
  if (evidence.stringHasher) {
    try {
      stringHasher = migrateStringHasherSchema(evidence.stringHasher)
    } catch (error) {
      push(issues, 'STRING_HASHER', 'stringHasher', error instanceof Error ? error.message : String(error))
    }
  }
  const stringIds = new Set(stringHasher?.entries.map((entry) => entry.id) ?? [])
  if (stringHasher) {
    const validation = validateStringHasherTable(stringHasher)
    for (const issue of validation.issues) push(issues, 'STRING_HASHER', issue.path, issue.message)
  }
  for (const [index, mapped] of mappedNames.entries()) {
    const path = `mappedNames[${index}]`
    if (!mapped || typeof mapped !== 'object') {
      push(issues, 'MAPPED_NAME', path, 'mapped name entry must be an object')
      continue
    }
    if (!['face', 'edge', 'vertex'].includes(mapped.kind)) push(issues, 'MAPPED_NAME', `${path}.kind`, `unsupported mapped name kind ${String(mapped.kind)}`)
    if (!Number.isSafeInteger(mapped.resultIndex) || mapped.resultIndex < 0) push(issues, 'RESULT_INDEX', `${path}.resultIndex`, 'resultIndex must be a non-negative safe integer')
    const key = `${mapped.kind}:${mapped.resultIndex}`
    if (resultIndexes.has(key)) push(issues, 'MAPPED_NAME', `${path}.resultIndex`, `duplicate result subshape index ${key}`)
    resultIndexes.add(key)
    if (typeof mapped.resultPersistentId !== 'string' || !mapped.resultPersistentId.trim()) push(issues, 'MAPPED_NAME', `${path}.resultPersistentId`, 'resultPersistentId must be a non-empty string')
    const reference = mapped.reference
    if (!reference || typeof reference !== 'object') {
      push(issues, 'MAPPED_NAME', `${path}.reference`, 'mapped name reference must be an object')
    } else if (typeof reference.name !== 'string' || !reference.name || /[.\s]/.test(reference.name)) {
      push(issues, 'MAPPED_NAME', `${path}.reference.name`, 'mapped name must be a non-empty string and contain no dots or whitespace')
    }
    for (const [sidIndex, sid] of (reference && typeof reference === 'object' && Array.isArray(reference.stringIds) ? reference.stringIds : []).entries()) {
      if (!Number.isSafeInteger(sid) || sid < 0) push(issues, 'STRING_ID', `${path}.reference.stringIds[${sidIndex}]`, `invalid StringID ${sid}`)
      else if (stringHasher && !stringIds.has(sid)) push(issues, 'STRING_ID', `${path}.reference.stringIds[${sidIndex}]`, `missing StringHasher ID ${sid}`)
    }
    if (reference && typeof reference === 'object' && reference.prefixStringId !== undefined && stringHasher && !stringIds.has(reference.prefixStringId)) push(issues, 'STRING_ID', `${path}.reference.prefixStringId`, `missing StringHasher prefix ID ${reference.prefixStringId}`)
    if (mapped.relation !== undefined && !relationSet.has(mapped.relation)) push(issues, 'MAPPED_NAME', `${path}.relation`, `unsupported relation ${String(mapped.relation)}`)
    for (const [sourceIndex, source] of (Array.isArray(mapped.sourceRefs) ? mapped.sourceRefs : []).entries()) {
      if (!source || typeof source !== 'object' || typeof source.objectId !== 'string' || !source.objectId.trim() || typeof source.persistentId !== 'string' || !source.persistentId.trim()) push(issues, 'SOURCE_REF', `${path}.sourceRefs[${sourceIndex}]`, 'source objectId and persistentId are required')
    }
    for (const [candidateIndex, candidate] of (Array.isArray(mapped.candidates) ? mapped.candidates : []).entries()) {
      if (!candidate || typeof candidate !== 'object' || typeof candidate.objectId !== 'string' || !candidate.objectId.trim() || typeof candidate.persistentId !== 'string' || !candidate.persistentId.trim()) push(issues, 'SOURCE_REF', `${path}.candidates[${candidateIndex}]`, 'candidate objectId and persistentId are required')
    }
    if ((mapped.relation === 'ambiguous' || (Array.isArray(mapped.candidates) && mapped.candidates.length > 0)) && (!Array.isArray(mapped.candidates) || mapped.candidates.length < 2)) push(issues, 'AMBIGUOUS', path, 'ambiguous mapped names must retain at least two candidates')
  }
  if (evidence.status === 'native-evidence' && mappedNames.length === 0) push(issues, 'MAPPED_NAME', 'mappedNames', 'native-evidence status requires mapped name references')
  if (evidence.status === 'final-shape-only' && mappedNames.length > 0) push(issues, 'STATUS', 'mappedNames', 'final-shape-only stages cannot carry generated mapped-name evidence')
  if (evidence.status === 'ambiguous' && mappedNames.every((mapped) => (mapped.candidates?.length ?? 0) < 2)) push(issues, 'AMBIGUOUS', 'mappedNames', 'ambiguous status requires persisted candidate sets')
  return { valid: issues.length === 0, status: evidence.status, mappedNameCount: mappedNames.length, stringHasherEntryCount: stringHasher?.entries.length ?? 0, issues }
}

export const assertNativeNamingEvidence = <T extends NativeStageNamingEvidence>(evidence: T): T => {
  const report = validateNativeNamingEvidence(evidence)
  if (!report.valid) throw new Error(`Invalid native naming evidence: ${report.issues[0].path}: ${report.issues[0].message}`)
  return evidence
}

export const cloneNativeNamingEvidence = (evidence: NativeStageNamingEvidence): NativeStageNamingEvidence => ({
  ...evidence,
  mappedNames: evidence.mappedNames?.map((mapped) => ({
    ...mapped,
    reference: {
      ...mapped.reference,
      indexedName: mapped.reference.indexedName ? { ...mapped.reference.indexedName } : undefined,
      stringIds: mapped.reference.stringIds ? [...mapped.reference.stringIds] : undefined,
    },
    sourceRefs: mapped.sourceRefs?.map((source) => ({ ...source })),
    candidates: mapped.candidates?.map((candidate) => ({ ...candidate })),
  })),
  stringHasher: evidence.stringHasher ? cloneStringHasherTable(evidence.stringHasher) : undefined,
  elementMap2: evidence.elementMap2 ? cloneElementMap2(migrateElementMap2Schema(evidence.elementMap2)) : undefined,
})

export const createFinalShapeOnlyNamingEvidence = (stageId: string, resultObjectId: string, reason = 'Native provider returned final Shape without MappedNameRef/StringHasher evidence.'): NativeStageNamingEvidence => ({
  schemaVersion: 1,
  stageId,
  resultObjectId,
  status: 'final-shape-only',
  reason,
})

export const createNativeStageNamingEvidence = (input: Omit<NativeStageNamingEvidence, 'schemaVersion'>): NativeStageNamingEvidence => assertNativeNamingEvidence({ schemaVersion: 1, ...input })

export const hasNativeMappedNameEvidence = (evidence: NativeStageNamingEvidence | undefined): boolean => Boolean(evidence && (evidence.status === 'native-evidence' || evidence.status === 'opaque-preserved') && (evidence.mappedNames?.length ?? 0) > 0)
