import type { ElementMapEntry, ElementMapSnapshot, SubshapeRef, TopologyHistoryResult, TopologySnapshotEntry } from './types'

const elementKinds: SubshapeRef['kind'][] = ['vertex', 'edge', 'face']
const prefixes: Record<SubshapeRef['kind'], string> = { vertex: 'Vertex', edge: 'Edge', face: 'Face' }
const elementNamePattern = /^(Face|Edge|Vertex)([1-9][0-9]*)$/

export const formatElementMapName = (kind: SubshapeRef['kind'], index: number): string => {
  if (!elementKinds.includes(kind)) throw new RangeError(`Unsupported ElementMap kind: ${kind}`)
  if (!Number.isSafeInteger(index) || index < 1) throw new RangeError('ElementMap indexes are one-based positive integers.')
  return `${prefixes[kind]}${index}`
}

export const parseElementMapName = (name: string): { kind: SubshapeRef['kind']; index: number } | null => {
  if (typeof name !== 'string') return null
  const match = elementNamePattern.exec(name.trim())
  if (!match) return null
  const kind = match[1].toLowerCase() as SubshapeRef['kind']
  const index = Number(match[2])
  return Number.isSafeInteger(index) && index > 0 ? { kind, index } : null
}

const cloneCandidate = (candidate: NonNullable<ElementMapEntry['candidates']>[number]) => ({ ...candidate })
const cloneEntry = (entry: ElementMapEntry): ElementMapEntry => ({ ...entry, ...(entry.candidates ? { candidates: entry.candidates.map(cloneCandidate) } : {}) })

const sourceKey = (objectId: string, persistentId: string) => `${objectId}\u0000${persistentId}`
type HistorySourceCandidate = { objectId: string; persistentId: string; sourceStageId?: string }

/**
 * Builds the Web-side equivalent of FreeCAD's ElementMap names. Names are
 * inherited only from an unambiguous source relation; all other cases remain
 * explicit so a downstream LinkSub repair can present candidates instead of
 * silently binding a repeated face/edge.
 */
export const createElementMapSnapshot = (
  previous: ElementMapSnapshot | undefined,
  outputObjectId: string,
  output: TopologySnapshotEntry[],
  history: TopologyHistoryResult,
  sourceMaps: ReadonlyMap<string, ElementMapSnapshot | undefined> = new Map(),
  nativeNamingEvidence?: ElementMapSnapshot['nativeNamingEvidence'],
): ElementMapSnapshot => {
  const priorByKey = new Map<string, ElementMapEntry>()
  for (const entry of previous?.entries ?? []) priorByKey.set(sourceKey(entry.objectId, entry.persistentId), entry)
  for (const [objectId, map] of sourceMaps) for (const entry of map?.entries ?? []) priorByKey.set(sourceKey(objectId, entry.persistentId), entry)

  const expandHistorySource = (candidate: HistorySourceCandidate, visited = new Set<string>()): HistorySourceCandidate[] => {
    if (!candidate.sourceStageId) return [candidate]
    const key = `${candidate.sourceStageId}\u0000${sourceKey(candidate.objectId, candidate.persistentId)}`
    if (visited.has(key)) return [candidate]
    const nextVisited = new Set(visited).add(key)
    const predecessors = history.relations.filter((relation) => relation.resultStageId === candidate.sourceStageId && relation.resultPersistentId === candidate.persistentId && relation.relation !== 'deleted')
    if (predecessors.length === 0) return [candidate]
    const expanded = predecessors.flatMap((relation): HistorySourceCandidate[] => {
      if (relation.relation === 'ambiguous') return (relation.candidates ?? []).map((source) => ({ objectId: source.sourceObjectId, persistentId: source.persistentId, sourceStageId: relation.sourceStageId }))
      if (!relation.sourceObjectId || !relation.sourcePersistentId) return []
      return [{ objectId: relation.sourceObjectId, persistentId: relation.sourcePersistentId, sourceStageId: relation.sourceStageId }]
    }).flatMap((source) => expandHistorySource(source, nextVisited))
    return expanded.length > 0 ? expanded : [candidate]
  }

  const usedNames = new Set<string>()
  const nextIndex = new Map<SubshapeRef['kind'], number>(elementKinds.map((kind) => [kind, 1]))
  const allocate = (kind: SubshapeRef['kind']) => {
    let index = nextIndex.get(kind) ?? 1
    let name = formatElementMapName(kind, index)
    while (usedNames.has(name)) { index += 1; name = formatElementMapName(kind, index) }
    nextIndex.set(kind, index + 1)
    usedNames.add(name)
    return name
  }

  const outputEntries: ElementMapEntry[] = []
  for (const entry of output) {
    const relations = history.relations.filter((relation) => relation.resultPersistentId === entry.ref.persistentId && (relation.relation === 'preserved' || relation.relation === 'modified' || relation.relation === 'generated' || relation.relation === 'ambiguous'))
    const sourceCandidates = relations.flatMap((relation): HistorySourceCandidate[] => {
      if (relation.relation === 'ambiguous') return (relation.candidates ?? []).map((candidate) => ({ objectId: candidate.sourceObjectId, persistentId: candidate.persistentId, sourceStageId: relation.sourceStageId }))
      if (!relation.sourceObjectId || !relation.sourcePersistentId) return []
      return [{ objectId: relation.sourceObjectId, persistentId: relation.sourcePersistentId, sourceStageId: relation.sourceStageId }]
    }).flatMap((candidate) => expandHistorySource(candidate))
      .map(({ objectId, persistentId }) => ({ objectId, persistentId }))
      .filter((candidate, index, all) => all.findIndex((other) => other.objectId === candidate.objectId && other.persistentId === candidate.persistentId) === index)
    const sourceNames = sourceCandidates.map((candidate) => {
      const name = priorByKey.get(sourceKey(candidate.objectId, candidate.persistentId))?.name
      return name ? { ...candidate, name } : null
    }).filter((candidate): candidate is { objectId: string; persistentId: string; name: string } => Boolean(candidate))
    const uniqueNames = [...new Set(sourceNames.map((candidate) => candidate.name as string))]
    const uniqueSources = new Set(sourceCandidates.map((candidate) => sourceKey(candidate.objectId, candidate.persistentId)))
    const hasAmbiguousSource = relations.some((relation) => relation.relation === 'ambiguous') || uniqueSources.size > 1
    const generatedWithoutSource = relations.some((relation) => relation.relation === 'generated') && uniqueSources.size === 0
    const status = entry.ref.status === 'ambiguous' || hasAmbiguousSource ? 'ambiguous' : generatedWithoutSource || entry.ref.status === 'new' ? 'new' : 'stable'
    const canInheritName = !hasAmbiguousSource && sourceCandidates.length === 1 && sourceNames.length === 1 && uniqueNames.length === 1
    const name = canInheritName && !usedNames.has(uniqueNames[0]) ? (usedNames.add(uniqueNames[0]), uniqueNames[0]) : allocate(entry.ref.kind)
    outputEntries.push({
      name,
      objectId: outputObjectId,
      kind: entry.ref.kind,
      persistentId: entry.ref.persistentId,
      status,
      ...(sourceNames.length > 0 || (status === 'ambiguous' && sourceCandidates.length > 0) ? { candidates: sourceCandidates.map((candidate) => ({ ...candidate, name: priorByKey.get(sourceKey(candidate.objectId, candidate.persistentId))?.name })) } : {}),
    })
  }

  const deletedEntries: ElementMapEntry[] = []
  for (const relation of history.relations.filter((candidate) => candidate.relation === 'deleted' && candidate.sourceObjectId && candidate.sourcePersistentId)) {
    const key = sourceKey(relation.sourceObjectId!, relation.sourcePersistentId!)
    const source = priorByKey.get(key)
    if (!source || usedNames.has(source.name)) continue
    usedNames.add(source.name)
    deletedEntries.push({ ...cloneEntry(source), status: 'deleted', objectId: relation.sourceObjectId!, persistentId: relation.sourcePersistentId! })
  }
  return {
    schemaVersion: 1,
    entries: [...outputEntries, ...deletedEntries],
    ...(nativeNamingEvidence && nativeNamingEvidence.length > 0 ? { nativeNamingEvidence: nativeNamingEvidence.map((evidence) => ({
      ...evidence,
      mappedNames: evidence.mappedNames?.map((mapped) => ({
        ...mapped,
        reference: { ...mapped.reference, stringIds: mapped.reference.stringIds ? [...mapped.reference.stringIds] : undefined },
        sourceRefs: mapped.sourceRefs?.map((source) => ({ ...source })),
        candidates: mapped.candidates?.map((candidate) => ({ ...candidate })),
      })),
      stringHasher: evidence.stringHasher ? { schemaVersion: evidence.stringHasher.schemaVersion, nativeVersion: evidence.stringHasher.nativeVersion, entries: evidence.stringHasher.entries.map((entry) => ({ ...entry, relatedIds: [...entry.relatedIds] })) } : undefined,
    })) } : {}),
  }
}

export const cloneElementMapSnapshot = (snapshot: ElementMapSnapshot): ElementMapSnapshot => ({
  schemaVersion: 1,
  entries: snapshot.entries.map(cloneEntry),
  ...(snapshot.nativeNamingEvidence ? { nativeNamingEvidence: snapshot.nativeNamingEvidence.map((evidence) => ({
    ...evidence,
    mappedNames: evidence.mappedNames?.map((mapped) => ({ ...mapped, reference: { ...mapped.reference, stringIds: mapped.reference.stringIds ? [...mapped.reference.stringIds] : undefined }, sourceRefs: mapped.sourceRefs?.map((source) => ({ ...source })), candidates: mapped.candidates?.map((candidate) => ({ ...candidate })) })),
    stringHasher: evidence.stringHasher ? { schemaVersion: evidence.stringHasher.schemaVersion, nativeVersion: evidence.stringHasher.nativeVersion, entries: evidence.stringHasher.entries.map((entry) => ({ ...entry, relatedIds: [...entry.relatedIds] })) } : undefined,
  })) } : {}),
})
