import type { NativeTopologyHistoryRecord, SubshapeRef, TopologyAdjacency, TopologyHistoryRelation as StoredTopologyHistoryRelation, TopologyHistoryResult as StoredTopologyHistoryResult } from './types'
import { assertNativeNamingEvidence, type NativeStageNamingEvidence } from './nativeNamingEvidence'
import { matchSubshapes, matchSubshapesWithAdjacency, type SubshapeSignature } from './topologyNaming'

export type TopologyHistoryEntry = { ref: SubshapeRef; signature: SubshapeSignature }
export type TopologyHistoryInput = { objectId: string; entries: TopologyHistoryEntry[]; sourceStageId?: string; adjacency?: TopologyAdjacency }

export type NativeTopologyHistoryStageCapture = {
  stageId: string
  operationId?: string
  ordinal: number
  inputs: TopologyHistoryInput[]
  output: TopologyHistoryEntry[]
  outputAdjacency?: TopologyAdjacency
  records: NativeTopologyHistoryRecord[]
  resultObjectId?: string
  namingEvidence?: NativeStageNamingEvidence
}

export type NativeTopologyHistoryLineageSource = {
  objectId: string
  history: TopologyHistoryResult
}

export type TopologyHistoryRelation = StoredTopologyHistoryRelation
export type TopologyHistoryResult = StoredTopologyHistoryResult

const sourceKey = (objectId: string, persistentId: string) => `${encodeURIComponent(objectId)}::${persistentId}`
const emptyCounts = (): TopologyHistoryResult['counts'] => ({ preserved: 0, modified: 0, generated: 0, deleted: 0, ambiguous: 0 })
const entriesOfKind = (entries: TopologyHistoryEntry[], kind: SubshapeRef['kind']) => entries.filter((entry) => entry.ref.kind === kind)

type NativeCaptureOptions = {
  unresolved: 'throw' | 'ambiguous'
  resultStageId?: string
  outputAdjacency?: TopologyAdjacency
}

const relationKey = (relation: TopologyHistoryRelation) => [
  relation.sourceStageId ?? '',
  relation.resultStageId ?? '',
  relation.sourceObjectId ?? '',
  relation.sourcePersistentId ?? '',
  relation.relation,
  relation.resultPersistentId ?? '',
  ...(relation.resultCandidates ?? []),
].join('|')

const captureNativeTopologyHistoryInternal = (
  operationId: string,
  inputs: TopologyHistoryInput[],
  output: TopologyHistoryEntry[],
  records: NativeTopologyHistoryRecord[],
  options: NativeCaptureOptions,
): TopologyHistoryResult => {
  if (!operationId.trim()) throw new RangeError('Topology history operationId is required.')
  const inputById = new Map(inputs.map((input) => [input.objectId, input]))
  if (inputById.size !== inputs.length) throw new RangeError('Native topology history input object IDs must be unique.')
  const counts = emptyCounts()
  const relations: TopologyHistoryRelation[] = []
  const seen = new Set<string>()

  for (const record of records) {
    const input = inputById.get(record.sourceObjectId)
    if (!input) throw new RangeError(`Native topology history references unknown input object ${record.sourceObjectId}.`)
    const sources = entriesOfKind(input.entries, record.sourceKind)
    if (!Number.isSafeInteger(record.sourceIndex) || record.sourceIndex < 0 || record.sourceIndex >= sources.length) throw new RangeError(`Native topology history source index is out of range for ${record.sourceObjectId} ${record.sourceKind}.`)
    const source = sources[record.sourceIndex]
    const sourceStageId = record.sourceStageId ?? input.sourceStageId
    const resultStageId = record.resultStageId ?? options.resultStageId
    if (record.relation === 'deleted') {
      if (record.resultIndexes?.length) throw new RangeError('Deleted native topology history records cannot reference result indexes.')
      const key = [sourceStageId ?? '', resultStageId ?? '', record.sourceObjectId, source.ref.persistentId, 'deleted'].join('|')
      if (!seen.has(key)) {
        seen.add(key)
        relations.push({ relation: 'deleted', sourceObjectId: record.sourceObjectId, sourcePersistentId: source.ref.persistentId, sourceStageId, resultStageId, score: 1 })
        counts.deleted += 1
      }
      continue
    }
    const resultKind = record.resultKind ?? record.sourceKind
    const results = entriesOfKind(output, resultKind)
    if (!record.resultIndexes?.length) throw new RangeError(`${record.relation} native topology history records require result indexes.`)
    for (const resultIndex of record.resultIndexes) {
      if (!Number.isSafeInteger(resultIndex) || resultIndex < 0 || resultIndex >= results.length) throw new RangeError(`Native topology history result index is out of range for ${resultKind}.`)
      const result = results[resultIndex]
      const key = [sourceStageId ?? '', resultStageId ?? '', record.sourceObjectId, source.ref.persistentId, record.relation, result.ref.persistentId].join('|')
      if (seen.has(key)) continue
      seen.add(key)
      relations.push({ relation: record.relation, sourceObjectId: record.sourceObjectId, sourcePersistentId: source.ref.persistentId, resultStageId, sourceStageId, resultPersistentId: result.ref.persistentId, score: 1 })
      counts[record.relation] += 1
    }
  }

  for (const input of inputs) {
    for (const source of input.entries) {
      const explained = relations.some((relation) => relation.sourceObjectId === input.objectId && relation.sourcePersistentId === source.ref.persistentId)
      if (explained) continue
      const candidates = output.filter((result) => result.ref.kind === source.ref.kind && result.signature.hash === source.signature.hash)
      if (candidates.length !== 1 && input.adjacency && options.outputAdjacency) {
        const adjacencyMatches = matchSubshapesWithAdjacency(input.entries, output, input.adjacency, options.outputAdjacency)
        const resolved = adjacencyMatches.filter((match) => match.status === 'stable' && match.previousId === source.ref.persistentId)
        if (resolved.length === 1) {
          const resolvedEntry = output.find((entry) => entry.ref.persistentId === resolved[0].current.persistentId)
          if (resolvedEntry) candidates.splice(0, candidates.length, resolvedEntry)
        }
      }
      if (candidates.length !== 1) {
        if (options.unresolved === 'throw') throw new Error(`Native topology history does not explain ${input.objectId}/${source.ref.persistentId}.`)
        const ambiguous: TopologyHistoryRelation = {
          relation: 'ambiguous',
          sourceObjectId: input.objectId,
          sourcePersistentId: source.ref.persistentId,
          sourceStageId: input.sourceStageId,
          resultStageId: options.resultStageId,
          resultCandidates: candidates.map((candidate) => candidate.ref.persistentId),
          score: 0,
        }
        const key = relationKey(ambiguous)
        if (!seen.has(key)) {
          seen.add(key)
          relations.push(ambiguous)
          counts.ambiguous += 1
        }
        continue
      }
      const result = candidates[0]
      const key = [input.sourceStageId ?? '', options.resultStageId ?? '', input.objectId, source.ref.persistentId, 'preserved', result.ref.persistentId].join('|')
      if (!seen.has(key)) {
        seen.add(key)
        relations.push({ relation: 'preserved', sourceObjectId: input.objectId, sourcePersistentId: source.ref.persistentId, resultPersistentId: result.ref.persistentId, sourceStageId: input.sourceStageId, resultStageId: options.resultStageId, score: 1 })
        counts.preserved += 1
      }
    }
  }
  return { operationId, provider: 'occt-native', relations, counts }
}

export const captureNativeTopologyHistory = (
  operationId: string,
  inputs: TopologyHistoryInput[],
  output: TopologyHistoryEntry[],
  records: NativeTopologyHistoryRecord[],
): TopologyHistoryResult => captureNativeTopologyHistoryInternal(operationId, inputs, output, records, { unresolved: 'throw' })

/**
 * Captures native history against each OCCT result stage before merging it.
 * Result indexes are therefore interpreted in the stage that produced them,
 * instead of being incorrectly applied to the final feature topology.
 */
export const captureNativeTopologyHistoryStages = (
  operationId: string,
  stages: NativeTopologyHistoryStageCapture[],
): TopologyHistoryResult => {
  if (!operationId.trim()) throw new RangeError('Topology history operationId is required.')
  if (stages.length === 0) throw new RangeError('Native topology history requires at least one stage.')
  const ordered = [...stages].sort((left, right) => left.ordinal - right.ordinal)
  const stageById = new Map<string, NativeTopologyHistoryStageCapture>()
  const ordinalSet = new Set<number>()
  for (const stage of ordered) {
    if (!stage.stageId.trim()) throw new RangeError('Native topology history stageId is required.')
    if (stageById.has(stage.stageId)) throw new RangeError(`Duplicate native topology history stage ${stage.stageId}.`)
    if (!Number.isSafeInteger(stage.ordinal) || stage.ordinal < 0 || ordinalSet.has(stage.ordinal)) throw new RangeError(`Native topology history stage ${stage.stageId} has an invalid or duplicate ordinal.`)
    if (!stage.operationId?.trim() && !operationId.trim()) throw new RangeError(`Native topology history stage ${stage.stageId} requires an operationId.`)
    if (stage.namingEvidence) {
      assertNativeNamingEvidence(stage.namingEvidence)
      if (stage.namingEvidence.stageId !== stage.stageId) throw new RangeError(`Native naming evidence stage ${stage.namingEvidence.stageId} does not match ${stage.stageId}.`)
      if (stage.resultObjectId && stage.namingEvidence.resultObjectId !== stage.resultObjectId) throw new RangeError(`Native naming evidence result ${stage.namingEvidence.resultObjectId} does not match ${stage.resultObjectId}.`)
    }
    stageById.set(stage.stageId, stage)
    ordinalSet.add(stage.ordinal)
  }

  const relations: TopologyHistoryRelation[] = []
  const seen = new Set<string>()
  for (const stage of ordered) {
    for (const input of stage.inputs) {
      if (!input.sourceStageId) continue
      const sourceStage = stageById.get(input.sourceStageId)
      if (!sourceStage) throw new RangeError(`Native topology history stage ${stage.stageId} references unknown source stage ${input.sourceStageId}.`)
      if (sourceStage.ordinal >= stage.ordinal) throw new RangeError(`Native topology history stage ${stage.stageId} must only consume an earlier stage.`)
      if (sourceStage.resultObjectId && sourceStage.resultObjectId !== input.objectId) throw new RangeError(`Native topology history stage ${stage.stageId} input ${input.objectId} does not match source stage ${input.sourceStageId}.`)
    }
    const normalizedRecords = stage.records.map((record) => {
      const input = stage.inputs.find((candidate) => candidate.objectId === record.sourceObjectId)
      const sourceStageId = record.sourceStageId ?? input?.sourceStageId
      const resultStageId = record.resultStageId ?? stage.stageId
      if (sourceStageId && !stageById.has(sourceStageId)) throw new RangeError(`Native topology history record references unknown source stage ${sourceStageId}.`)
      if (resultStageId !== stage.stageId) throw new RangeError(`Native topology history record for ${stage.stageId} targets result stage ${resultStageId}.`)
      return { ...record, sourceStageId, resultStageId }
    })
    const captured = captureNativeTopologyHistoryInternal(
      stage.operationId ?? `${operationId}:${stage.stageId}`,
      stage.inputs,
      stage.output,
      normalizedRecords,
      { unresolved: 'ambiguous', resultStageId: stage.stageId, outputAdjacency: stage.outputAdjacency },
    )
    for (const relation of captured.relations) {
      const key = relationKey(relation)
      if (seen.has(key)) continue
      seen.add(key)
      relations.push(relation)
    }
  }
  const counts = emptyCounts()
  for (const relation of relations) counts[relation.relation] += 1
  const result: TopologyHistoryResult = {
    operationId,
    provider: 'occt-native',
    relations,
    counts,
    stages: ordered.map((stage) => ({
      stageId: stage.stageId,
      operationId: stage.operationId ?? `${operationId}:${stage.stageId}`,
      inputObjectIds: stage.inputs.map((input) => input.objectId),
      resultObjectId: stage.resultObjectId,
      ordinal: stage.ordinal,
    })),
  }
  const namingEvidence = ordered.flatMap((stage) => stage.namingEvidence ? [stage.namingEvidence] : [])
  if (namingEvidence.length > 0) result.namingEvidence = namingEvidence.map((evidence) => ({ ...evidence, mappedNames: evidence.mappedNames?.map((mapped) => ({ ...mapped, reference: { ...mapped.reference }, sourceRefs: mapped.sourceRefs?.map((source) => ({ ...source })), candidates: mapped.candidates?.map((candidate) => ({ ...candidate })) })) }))
  return result
}

/**
 * Carries native stage lineage through feature boundaries. A downstream
 * relation is linked to the last native stage that produced its source
 * object, while every original per-stage relation remains available.
 */
export const composeNativeTopologyHistoryLineage = (
  operationId: string,
  current: TopologyHistoryResult,
  sources: NativeTopologyHistoryLineageSource[],
): TopologyHistoryResult => {
  if (!operationId.trim()) throw new RangeError('Topology history operationId is required.')
  if (current.provider !== 'occt-native') return current
  const sourceByObjectId = new Map<string, TopologyHistoryResult>()
  for (const source of sources) {
    if (!source.objectId.trim() || sourceByObjectId.has(source.objectId)) throw new RangeError('Native topology lineage source object IDs must be unique and non-empty.')
    if (source.history.provider === 'occt-native') sourceByObjectId.set(source.objectId, source.history)
  }

  const stageById = new Map<string, NonNullable<TopologyHistoryResult['stages']>[number]>()
  const orderedStages: NonNullable<TopologyHistoryResult['stages']> = []
  const appendStages = (history: TopologyHistoryResult) => {
    for (const stage of [...(history.stages ?? [])].sort((left, right) => left.ordinal - right.ordinal)) {
      const previous = stageById.get(stage.stageId)
      if (previous) {
        if (previous.operationId !== stage.operationId || previous.resultObjectId !== stage.resultObjectId || previous.inputObjectIds.join('\0') !== stage.inputObjectIds.join('\0')) throw new RangeError(`Native topology lineage stage ${stage.stageId} has conflicting definitions.`)
        continue
      }
      const cloned = { ...stage, inputObjectIds: [...stage.inputObjectIds], ordinal: orderedStages.length }
      stageById.set(stage.stageId, cloned)
      orderedStages.push(cloned)
    }
  }
  for (const source of sources) {
    const history = sourceByObjectId.get(source.objectId)
    if (history) appendStages(history)
  }
  appendStages(current)

  const lastSourceStage = new Map<string, string>()
  for (const source of sources) {
    const history = sourceByObjectId.get(source.objectId)
    const last = history?.stages?.length ? [...history.stages].sort((left, right) => right.ordinal - left.ordinal)[0] : undefined
    if (last) lastSourceStage.set(source.objectId, last.stageId)
  }
  const relations: TopologyHistoryRelation[] = []
  const seen = new Set<string>()
  const appendRelation = (relation: TopologyHistoryRelation) => {
    const key = relationKey(relation)
    if (seen.has(key)) return
    seen.add(key)
    relations.push({ ...relation, resultCandidates: relation.resultCandidates ? [...relation.resultCandidates] : undefined, candidates: relation.candidates?.map((candidate) => ({ ...candidate })) })
  }
  for (const source of sources) {
    const history = sourceByObjectId.get(source.objectId)
    if (history) for (const relation of history.relations) appendRelation(relation)
  }
  for (const relation of current.relations) appendRelation({
    ...relation,
    sourceStageId: relation.sourceStageId ?? (relation.sourceObjectId ? lastSourceStage.get(relation.sourceObjectId) : undefined),
  })
  const counts = emptyCounts()
  for (const relation of relations) counts[relation.relation] += 1
  const namingEvidence = sources.flatMap((source) => sourceByObjectId.get(source.objectId)?.namingEvidence ?? []).concat(current.namingEvidence ?? [])
  return { operationId, provider: 'occt-native', relations, counts, stages: orderedStages, ...(namingEvidence.length > 0 ? { namingEvidence } : {}) }
}

export const captureSignatureTopologyHistory = (
  operationId: string,
  inputs: Array<{ objectId: string; entries: TopologyHistoryEntry[] }>,
  output: TopologyHistoryEntry[],
): TopologyHistoryResult => {
  if (!operationId.trim()) throw new RangeError('Topology history operationId is required.')
  const sources = new Map<string, { objectId: string; persistentId: string; signature: SubshapeSignature }>()
  const previous = inputs.flatMap(({ objectId, entries }) => entries.map((entry) => {
    const key = sourceKey(objectId, entry.ref.persistentId)
    sources.set(key, { objectId, persistentId: entry.ref.persistentId, signature: entry.signature })
    return { ref: { ...entry.ref, persistentId: key }, signature: entry.signature }
  }))
  const matches = matchSubshapes(previous, output)
  const counts = emptyCounts()
  const relations = matches.map((match): TopologyHistoryRelation => {
    if (match.status === 'new') return { relation: 'generated', resultPersistentId: match.current.persistentId, score: match.score }
    if (match.status === 'deleted') {
      const source = match.previousId ? sources.get(match.previousId) : undefined
      return { relation: 'deleted', sourceObjectId: source?.objectId, sourcePersistentId: source?.persistentId, score: 0 }
    }
    if (match.status === 'ambiguous') {
      const candidates = (match.current.candidates ?? []).map((id) => sources.get(id)).filter((candidate): candidate is NonNullable<typeof candidate> => Boolean(candidate)).map((candidate) => ({ sourceObjectId: candidate.objectId, persistentId: candidate.persistentId }))
      return { relation: 'ambiguous', resultPersistentId: match.current.persistentId, candidates, score: match.score }
    }
    const source = match.previousId ? sources.get(match.previousId) : undefined
    const outputEntry = output.find((entry) => entry.ref.persistentId === match.current.persistentId || entry.ref.signature === match.current.signature)
    const relation = source && outputEntry && source.signature.hash === outputEntry.signature.hash ? 'preserved' : 'modified'
    return { relation, sourceObjectId: source?.objectId, sourcePersistentId: source?.persistentId, resultPersistentId: match.current.persistentId, score: match.score }
  })
  for (const relation of relations) counts[relation.relation] += 1
  return { operationId, provider: 'signature-fallback', relations, counts }
}
