import type { AttachmentSupportValue, DocumentSnapshot, LinkSubListValue, LinkSubValue, ObjectTopologySnapshot, ResolveTopologyReferenceInput, SubshapeRef, TopoRefValue, TopologyAdjacency } from './types'
import { cloneElementMapSnapshot } from './elementMap'
import { parseElementMapName } from './elementMap'
import { matchSubshapesWithAdjacency, type SubshapeMatch, type SubshapeSignature } from './topologyNaming'

export type PersistedTopoRef = TopoRefValue

export type TopoRefResolution = {
  status: 'resolved' | 'ambiguous' | 'deleted'
  ref?: SubshapeRef
  candidates?: SubshapeRef[]
}

export type TopologyMigration = {
  records: PersistedTopoRef[]
  matches: SubshapeMatch[]
  counts: Record<NonNullable<SubshapeRef['status']>, number>
}

export type TopologyReferenceMigrationIssue = {
  ownerObjectId: string
  sourceObjectId: string
  referenceName: string
  kind: SubshapeRef['kind']
  status: 'ambiguous' | 'deleted'
  persistentId: string
  candidates?: string[]
}

export type DocumentTopologyReferenceMigration = {
  changedOwnerIds: string[]
  issues: TopologyReferenceMigrationIssue[]
}

const topologyTransientField = /(?:face|edge|vertex|subshape)Index/i

export const validateTopologySnapshotForPersistence = (snapshot: ObjectTopologySnapshot): void => {
  const ids = new Set(snapshot.entries.map((entry) => entry.ref.persistentId))
  if (ids.size !== snapshot.entries.length || [...ids].some((id) => topologyTransientField.test(id))) throw new RangeError('Topology snapshot contains transient or duplicate persistent IDs.')
  if (snapshot.adjacency) {
    for (const [mapName, map] of Object.entries(snapshot.adjacency)) {
      for (const [source, targets] of Object.entries(map)) {
        if (!ids.has(source) || targets.some((target) => !ids.has(target)) || targets.some((target) => topologyTransientField.test(target))) throw new RangeError(`Topology adjacency map ${mapName} contains a transient or unknown persistent ID (source=${source}, targets=${targets.join(',')}).`)
      }
    }
  }
  if (snapshot.elementMap) {
    if (snapshot.elementMap.schemaVersion !== 1) throw new RangeError('Topology ElementMap schemaVersion is not supported.')
    const names = new Set<string>()
    for (const entry of snapshot.elementMap.entries) {
      const parsedName = parseElementMapName(entry.name)
      if (!parsedName || parsedName.kind !== entry.kind || names.has(entry.name)) throw new RangeError('Topology ElementMap contains an invalid, kind-mismatched, or duplicate FreeCAD element name.')
      names.add(entry.name)
      if (!entry.objectId.trim() || !entry.persistentId.trim()) throw new RangeError('Topology ElementMap entries require non-empty object and persistent IDs.')
      if (!ids.has(entry.persistentId) && entry.status !== 'deleted') throw new RangeError('Topology ElementMap contains an unknown live persistent ID.')
      if (topologyTransientField.test(entry.persistentId)) throw new RangeError('Topology ElementMap contains a transient persistent ID.')
      for (const candidate of entry.candidates ?? []) {
        const candidateName = candidate.name === undefined ? null : parseElementMapName(candidate.name)
        if (!candidate.objectId.trim() || !candidate.persistentId.trim() || (candidate.name !== undefined && (!candidateName || candidateName.kind !== entry.kind)) || topologyTransientField.test(candidate.persistentId)) throw new RangeError('Topology ElementMap contains a transient or malformed candidate persistent ID.')
      }
    }
  }
}

export const cloneObjectTopologySnapshot = (snapshot: ObjectTopologySnapshot): ObjectTopologySnapshot => {
  validateTopologySnapshotForPersistence(snapshot)
  return {
    ...snapshot,
    entries: snapshot.entries.map((entry) => ({
      ref: { ...entry.ref, candidates: entry.ref.candidates ? [...entry.ref.candidates] : undefined },
      signature: { ...entry.signature, centroid: [...entry.signature.centroid], bounds: { min: [...entry.signature.bounds.min], max: [...entry.signature.bounds.max] }, normal: [...entry.signature.normal] },
    })),
    migration: {
      ...snapshot.migration,
      matches: snapshot.migration.matches.map((match) => ({ ...match, current: { ...match.current, candidates: match.current.candidates ? [...match.current.candidates] : undefined } })),
    },
    history: {
      ...snapshot.history,
      counts: { ...snapshot.history.counts },
      relations: snapshot.history.relations.map((relation) => relation.candidates ? { ...relation, candidates: relation.candidates.map((candidate) => ({ ...candidate })) } : { ...relation }),
    },
    ...(snapshot.elementMap ? { elementMap: cloneElementMapSnapshot(snapshot.elementMap) } : {}),
    adjacency: snapshot.adjacency ? cloneTopologyAdjacency(snapshot.adjacency) : undefined,
  }
}

const cloneTopologyAdjacency = (adjacency: TopologyAdjacency): TopologyAdjacency => ({
  faceNeighbors: Object.fromEntries(Object.entries(adjacency.faceNeighbors).map(([key, values]) => [key, [...values]])),
  faceEdges: Object.fromEntries(Object.entries(adjacency.faceEdges).map(([key, values]) => [key, [...values]])),
  edgeFaces: Object.fromEntries(Object.entries(adjacency.edgeFaces).map(([key, values]) => [key, [...values]])),
  edgeVertices: Object.fromEntries(Object.entries(adjacency.edgeVertices).map(([key, values]) => [key, [...values]])),
  vertexEdges: Object.fromEntries(Object.entries(adjacency.vertexEdges).map(([key, values]) => [key, [...values]])),
})

const transientIndexKeys = new Set(['faceIndex', 'edgeIndex', 'vertexIndex', 'subshapeIndex'])
const statuses = new Set<NonNullable<SubshapeRef['status']>>(['stable', 'ambiguous', 'new', 'deleted'])
const kinds = new Set<SubshapeRef['kind']>(['face', 'edge', 'vertex'])

const finiteNonNegativeInteger = (value: unknown) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0

export const createPersistedTopoRef = (objectId: string, ref: SubshapeRef, generation: number): PersistedTopoRef => {
  if (!objectId.trim()) throw new RangeError('TopoRef objectId is required.')
  if (!finiteNonNegativeInteger(generation)) throw new RangeError('TopoRef generation must be a non-negative integer.')
  if (!ref.persistentId.trim()) throw new RangeError('TopoRef persistentId is required.')
  if (!finiteNonNegativeInteger(ref.topologyVersion)) throw new RangeError('TopoRef topologyVersion must be a non-negative integer.')
  return {
    schemaVersion: 1,
    objectId,
    kind: ref.kind,
    persistentId: ref.persistentId,
    topologyVersion: ref.topologyVersion,
    generation,
    status: ref.status ?? 'stable',
    signature: ref.signature,
    candidates: ref.candidates ? [...new Set(ref.candidates)] : undefined,
  }
}

export const serializeTopoRef = (record: PersistedTopoRef) => JSON.stringify(createPersistedTopoRef(record.objectId, {
  shapeId: record.objectId,
  kind: record.kind,
  persistentId: record.persistentId,
  topologyVersion: record.topologyVersion,
  status: record.status,
  signature: record.signature,
  candidates: record.candidates,
}, record.generation))

export const parseTopoRef = (json: string): PersistedTopoRef => {
  let value: unknown
  try { value = JSON.parse(json) } catch { throw new SyntaxError('TopoRef JSON is invalid.') }
  if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError('TopoRef must be a JSON object.')
  const source = value as Record<string, unknown>
  for (const key of transientIndexKeys) if (key in source) throw new RangeError(`TopoRef must not persist transient ${key}.`)
  if (source.schemaVersion !== 1) throw new RangeError('TopoRef schemaVersion is not supported.')
  if (typeof source.objectId !== 'string' || typeof source.persistentId !== 'string') throw new TypeError('TopoRef objectId and persistentId must be strings.')
  if (!kinds.has(source.kind as SubshapeRef['kind'])) throw new RangeError('TopoRef kind is invalid.')
  if (!statuses.has(source.status as NonNullable<SubshapeRef['status']>)) throw new RangeError('TopoRef status is invalid.')
  if (!finiteNonNegativeInteger(source.topologyVersion) || !finiteNonNegativeInteger(source.generation)) throw new RangeError('TopoRef version fields must be non-negative integers.')
  if (source.signature !== undefined && typeof source.signature !== 'string') throw new TypeError('TopoRef signature must be a string.')
  if (source.candidates !== undefined && (!Array.isArray(source.candidates) || !source.candidates.every((candidate) => typeof candidate === 'string'))) throw new TypeError('TopoRef candidates must be strings.')
  return createPersistedTopoRef(source.objectId, {
    shapeId: source.objectId,
    kind: source.kind as SubshapeRef['kind'],
    persistentId: source.persistentId,
    topologyVersion: source.topologyVersion as number,
    status: source.status as NonNullable<SubshapeRef['status']>,
    signature: source.signature as string | undefined,
    candidates: source.candidates as string[] | undefined,
  }, source.generation as number)
}

export const resolveTopoRef = (record: PersistedTopoRef, current: SubshapeRef[]): TopoRefResolution => {
  const sameKind = current.filter((ref) => ref.kind === record.kind)
  const byId = sameKind.filter((ref) => ref.persistentId === record.persistentId)
  if (byId.length === 1 && byId[0].status !== 'ambiguous') return { status: 'resolved', ref: byId[0] }
  const bySignature = record.signature ? sameKind.filter((ref) => ref.signature === record.signature) : []
  const candidates = byId.length > 0 ? byId : bySignature
  if (candidates.length === 1 && candidates[0].status !== 'ambiguous') return { status: 'resolved', ref: candidates[0] }
  if (candidates.length > 0) return { status: 'ambiguous', candidates }
  return { status: 'deleted' }
}

export const migrateTopoRefs = (
  objectId: string,
  previous: Array<{ ref: SubshapeRef; signature: SubshapeSignature }>,
  current: Array<{ ref: SubshapeRef; signature: SubshapeSignature }>,
  generation: number,
  previousAdjacency?: TopologyAdjacency,
  currentAdjacency?: TopologyAdjacency,
): TopologyMigration => {
  const matches = matchSubshapesWithAdjacency(previous, current, previousAdjacency, currentAdjacency).map(({ adjacencyScore: _adjacencyScore, ...match }) => match)
  const counts: TopologyMigration['counts'] = { stable: 0, ambiguous: 0, new: 0, deleted: 0 }
  const records = matches.map((match) => {
    counts[match.status] += 1
    return createPersistedTopoRef(objectId, match.current, generation)
  })
  return { records, matches, counts }
}

const migrateReferenceThroughSnapshot = (record: PersistedTopoRef, snapshot: ObjectTopologySnapshot): PersistedTopoRef => {
  if (record.objectId.trim() === '' || record.generation >= snapshot.generation) return { ...record, candidates: record.candidates ? [...record.candidates] : undefined }
  if (record.status === 'deleted') return { ...record, topologyVersion: snapshot.documentVersion, generation: snapshot.generation, candidates: undefined }
  const stable = snapshot.migration.matches.find((match) => match.status === 'stable' && match.previousId === record.persistentId)
  if (stable) return createPersistedTopoRef(record.objectId, stable.current, snapshot.generation)
  const ambiguous = snapshot.migration.matches.filter((match) => match.status === 'ambiguous' && match.current.candidates?.includes(record.persistentId))
  if (ambiguous.length > 0) return {
    ...record,
    topologyVersion: snapshot.documentVersion,
    generation: snapshot.generation,
    status: 'ambiguous',
    candidates: [...new Set(ambiguous.map((match) => match.current.persistentId))],
  }
  const deleted = snapshot.migration.matches.some((match) => match.status === 'deleted' && match.previousId === record.persistentId)
  if (deleted) return { ...record, topologyVersion: snapshot.documentVersion, generation: snapshot.generation, status: 'deleted', candidates: undefined }
  const resolution = resolveTopoRef(record, snapshot.entries.map((entry) => entry.ref))
  if (resolution.status === 'resolved' && resolution.ref) return createPersistedTopoRef(record.objectId, resolution.ref, snapshot.generation)
  if (resolution.status === 'ambiguous') return { ...record, topologyVersion: snapshot.documentVersion, generation: snapshot.generation, status: 'ambiguous', candidates: resolution.candidates?.map((candidate) => candidate.persistentId) }
  return { ...record, topologyVersion: snapshot.documentVersion, generation: snapshot.generation, status: 'deleted', candidates: undefined }
}

export const migrateDocumentTopologyReferences = (
  document: DocumentSnapshot,
  sourceObjectIds: Iterable<string>,
): DocumentTopologyReferenceMigration => {
  const sources = new Map([...new Set(sourceObjectIds)].map((objectId) => {
    const source = document.objects.find((object) => object.id === objectId)
    return source?.topology ? [objectId, source.topology] as const : null
  }).filter((entry): entry is readonly [string, ObjectTopologySnapshot] => Boolean(entry)))
  const changedOwnerIds = new Set<string>()
  const issues: TopologyReferenceMigrationIssue[] = []
  const migrate = (ownerObjectId: string, referenceName: string, record: PersistedTopoRef) => {
    const source = sources.get(record.objectId)
    if (!source) return record
    const migrated = migrateReferenceThroughSnapshot(record, source)
    if (migrated.generation !== record.generation || migrated.status !== record.status || migrated.persistentId !== record.persistentId) changedOwnerIds.add(ownerObjectId)
    if (migrated.status === 'ambiguous' || migrated.status === 'deleted') issues.push({ ownerObjectId, sourceObjectId: record.objectId, referenceName, kind: record.kind, status: migrated.status, persistentId: migrated.persistentId, candidates: migrated.candidates ? [...migrated.candidates] : undefined })
    return migrated
  }
  for (const object of document.objects) {
    for (const property of object.properties) {
      const supportValue = property.value && typeof property.value === 'object' && !Array.isArray(property.value) ? property.value as Record<string, unknown> : null
      const supportSubElement = supportValue?.subElement
      if (property.name === 'Support' && typeof supportValue?.objectId === 'string' && supportSubElement && typeof supportSubElement === 'object' && !Array.isArray(supportSubElement) && 'schemaVersion' in supportSubElement) {
        property.value = { ...supportValue, subElement: migrate(object.id, property.name, supportSubElement as PersistedTopoRef) } as AttachmentSupportValue
        continue
      }
      if (property.type === 'App::PropertyLinkSubList' && property.value && typeof property.value === 'object' && !Array.isArray(property.value) && 'entries' in property.value && Array.isArray(property.value.entries)) {
        property.value = { ...property.value, entries: property.value.entries.map((entry, index) => ({ ...entry, subElement: entry.subElement && typeof entry.subElement === 'object' ? migrate(object.id, `${property.name}[${index}]`, entry.subElement) : entry.subElement })) } as LinkSubListValue
        continue
      }
      if (property.type !== 'App::PropertyLinkSub' || !property.value || typeof property.value !== 'object' || Array.isArray(property.value) || !('schemaVersion' in property.value)) continue
      if ('subElements' in property.value) {
        property.value = {
          ...property.value,
          subElements: property.value.subElements.map((entry, index) => typeof entry === 'string' ? entry : migrate(object.id, `${property.name}[${index}]`, entry)),
        } as LinkSubValue
      } else if ('persistentId' in property.value) property.value = migrate(object.id, property.name, property.value)
    }
    for (const external of object.sketch?.externalGeometry ?? []) external.source = migrate(object.id, `ExternalGeometry:${external.id}`, external.source)
  }
  return { changedOwnerIds: [...changedOwnerIds], issues }
}

export const resolveDocumentTopologyReference = (
  document: DocumentSnapshot,
  input: ResolveTopologyReferenceInput,
): PersistedTopoRef => {
  const owner = document.objects.find((object) => object.id === input.ownerObjectId)
  if (!owner) throw new Error(`Topology reference owner does not exist: ${input.ownerObjectId}`)
  let current: PersistedTopoRef | null = null
  let replace: ((record: PersistedTopoRef) => void) | null = null
  if (input.referenceName.startsWith('ExternalGeometry:')) {
    const externalId = input.referenceName.slice('ExternalGeometry:'.length)
    const external = owner.sketch?.externalGeometry.find((candidate) => candidate.id === externalId)
    if (external) { current = external.source; replace = (record) => { external.source = record } }
  } else {
    const property = owner.properties.find((candidate) => candidate.name === input.referenceName && (candidate.type === 'App::PropertyLinkSub' || candidate.type === 'App::PropertyLinkSubList'))
    if (property?.value && typeof property.value === 'object' && !Array.isArray(property.value) && 'schemaVersion' in property.value) {
      if ('entries' in property.value) {
        const collection = property.value
        const index = collection.entries.findIndex((entry) => entry.subElement && typeof entry.subElement === 'object' && (!input.currentPersistentId || entry.subElement.persistentId === input.currentPersistentId))
        const entry = index >= 0 ? collection.entries[index] : undefined
        if (entry?.subElement && typeof entry.subElement === 'object') {
          current = entry.subElement
          replace = (record) => { property.value = { ...collection, entries: collection.entries.map((candidate, candidateIndex) => candidateIndex === index ? { ...candidate, subElement: record } : candidate) } }
        }
      } else if ('subElements' in property.value) {
        const collection = property.value
        const index = collection.subElements.findIndex((entry) => typeof entry !== 'string' && (!input.currentPersistentId || entry.persistentId === input.currentPersistentId))
        const entry = index >= 0 ? collection.subElements[index] : undefined
        if (entry && typeof entry !== 'string') {
          current = entry
          replace = (record) => { property.value = { ...collection, subElements: collection.subElements.map((candidate, candidateIndex) => candidateIndex === index ? record : candidate) } }
        }
      } else {
        current = property.value as PersistedTopoRef
        replace = (record) => { property.value = record }
      }
    }
    const support = owner.properties.find((candidate) => {
      const value = candidate.value && typeof candidate.value === 'object' && !Array.isArray(candidate.value) ? candidate.value as Record<string, unknown> : null
      const subElement = value?.subElement
      return candidate.name === input.referenceName && candidate.name === 'Support' && typeof value?.objectId === 'string' && subElement && typeof subElement === 'object' && !Array.isArray(subElement) && 'schemaVersion' in subElement
    })
    if (support && support.value && typeof support.value === 'object' && !Array.isArray(support.value)) {
      const supportValue = support.value as Record<string, unknown>
      current = supportValue.subElement as PersistedTopoRef
      replace = (record) => { support.value = { ...supportValue, subElement: record } as AttachmentSupportValue }
    }
  }
  if (!current || !replace) throw new Error(`Topology reference does not exist: ${input.ownerObjectId}.${input.referenceName}`)
  const source = document.objects.find((object) => object.id === current?.objectId)
  if (!source?.topology) throw new Error(`Source object has no current topology snapshot: ${current.objectId}`)
  const candidate = source.topology.entries.find((entry) => entry.ref.persistentId === input.candidatePersistentId && entry.ref.kind === current?.kind)
  if (!candidate) throw new RangeError(`Topology candidate is not available for ${input.referenceName}: ${input.candidatePersistentId}`)
  const resolved = createPersistedTopoRef(current.objectId, { ...candidate.ref, status: 'stable', candidates: undefined }, source.topology.generation)
  replace(resolved)
  return resolved
}
