export type ExternalDocumentStatus = 'open' | 'closed' | 'missing'

export type ExternalSourceObjectEvidence = {
  objectId: string
  length: number
  width: number
  height: number
  volume: number
  surfaceArea: number
  shapeId: string
}

export type ExternalSourceEvidence = {
  documentId: string
  path: string
  revision: number
  objects: {
    SourceBox: ExternalSourceObjectEvidence
    SecondBox: ExternalSourceObjectEvidence
  }
}

export type ExternalLinkTarget = {
  documentId: string
  objectId: string
  subElements?: string[]
}

type ExternalRegistryState = {
  status: ExternalDocumentStatus
  source: ExternalSourceEvidence
}

type ExternalRegistryArchive = {
  schemaVersion: 1
  current: ExternalRegistryState
  undo: ExternalRegistryState[]
  redo: ExternalRegistryState[]
}

export type ExternalPartDesignLinkSnapshot = {
  schemaVersion: 1
  source: { documentId: string; path: string; revision: number; status: ExternalDocumentStatus }
  properties: {
    XLink: ExternalLinkTarget | null
    XLinkSub: ExternalLinkTarget | null
    XLinkList: ExternalLinkTarget[]
    XLinkSubList: ExternalLinkTarget[]
  }
  propertyStatus: Record<'XLink' | 'XLinkSub' | 'XLinkList' | 'XLinkSubList', ['21']>
  dimensions: {
    xLinkLength: number | null
    xLinkSubWidth: number | null
    xLinkListHeights: number[]
    xLinkSubListAreas: number[]
  }
  binder: {
    support: ExternalLinkTarget[]
    area: number
    shapeId: string
    state: ['Up-to-date']
    status: 'Valid'
    cacheState: 'linked' | 'cached'
  }
  history: { undo: number; redo: number }
}

const finitePositive = (value: unknown) => typeof value === 'number' && Number.isFinite(value) && value > 0
const clone = <T>(value: T): T => structuredClone(value)

const validateObject = (value: ExternalSourceObjectEvidence, name: string) => {
  if (!value || value.objectId !== name || !finitePositive(value.length) || !finitePositive(value.width) || !finitePositive(value.height) || !finitePositive(value.volume) || !finitePositive(value.surfaceArea) || typeof value.shapeId !== 'string' || !value.shapeId) throw new TypeError(`External source object ${name} is invalid.`)
  const expectedVolume = value.length * value.width * value.height
  const expectedArea = 2 * (value.length * value.width + value.length * value.height + value.width * value.height)
  if (Math.abs(value.volume - expectedVolume) > 1e-7 || Math.abs(value.surfaceArea - expectedArea) > 1e-7) throw new RangeError(`External source object ${name} geometry evidence does not match its dimensions.`)
}

const validateSource = (value: ExternalSourceEvidence) => {
  if (!value || value.documentId !== 'XLinkSource' || value.path !== '/projects/XLinkSource.FCStd' || !Number.isSafeInteger(value.revision) || value.revision < 0) throw new TypeError('External source document identity is invalid.')
  validateObject(value.objects?.SourceBox, 'SourceBox')
  validateObject(value.objects?.SecondBox, 'SecondBox')
}

const validateState = (value: ExternalRegistryState) => {
  if (!value || !['open', 'closed', 'missing'].includes(value.status)) throw new TypeError('External source status is invalid.')
  validateSource(value.source)
}

const objectTarget = (objectId: string): ExternalLinkTarget => ({ documentId: 'XLinkSource', objectId })
const subTarget = (objectId: string, subElements: string[]): ExternalLinkTarget => ({ documentId: 'XLinkSource', objectId, subElements: [...subElements] })

export class ExternalPartDesignLinkRegistry {
  private current: ExternalRegistryState
  private readonly undoStack: ExternalRegistryState[]
  private readonly redoStack: ExternalRegistryState[]

  constructor(source: ExternalSourceEvidence, archive?: Pick<ExternalRegistryArchive, 'current' | 'undo' | 'redo'>) {
    validateSource(source)
    this.current = archive ? clone(archive.current) : { status: 'open', source: clone(source) }
    this.undoStack = archive ? clone(archive.undo) : []
    this.redoStack = archive ? clone(archive.redo) : []
    validateState(this.current)
    this.undoStack.forEach(validateState)
    this.redoStack.forEach(validateState)
  }

  closeSource() {
    if (this.current.status !== 'open') throw new Error('Only an open external source can be closed.')
    this.current = { ...this.current, status: 'closed' }
  }

  markSourceMissing() {
    if (this.current.status === 'open') throw new Error('An open external source cannot become missing before it is closed.')
    this.current = { ...this.current, status: 'missing' }
  }

  relinkSource(source: ExternalSourceEvidence) {
    validateSource(source)
    if (source.documentId !== this.current.source.documentId || source.path !== this.current.source.path) throw new Error('External source relink requires the original document id and path.')
    this.current = { status: 'open', source: clone(source) }
  }

  editSource(source: ExternalSourceEvidence) {
    validateSource(source)
    if (this.current.status !== 'open') throw new Error('External source edits require an open and resolved document.')
    if (source.documentId !== this.current.source.documentId || source.path !== this.current.source.path || source.revision <= this.current.source.revision) throw new Error('External source edits require the same document and a newer revision.')
    this.undoStack.push(clone(this.current))
    this.redoStack.length = 0
    this.current = { status: 'open', source: clone(source) }
  }

  undo() {
    const previous = this.undoStack.pop()
    if (!previous) throw new Error('External link history has nothing to undo.')
    this.redoStack.push(clone(this.current))
    this.current = previous
  }

  redo() {
    const next = this.redoStack.pop()
    if (!next) throw new Error('External link history has nothing to redo.')
    this.undoStack.push(clone(this.current))
    this.current = next
  }

  snapshot(): ExternalPartDesignLinkSnapshot {
    const resolved = this.current.status === 'open'
    const source = this.current.source.objects.SourceBox
    const second = this.current.source.objects.SecondBox
    const XLink = objectTarget('SourceBox')
    const XLinkSub = subTarget('SourceBox', ['Face1'])
    const XLinkList = [objectTarget('SourceBox'), objectTarget('SecondBox')]
    const XLinkSubList = [subTarget('SourceBox', ['Face1']), subTarget('SecondBox', ['Face1', 'Face2'])]
    return {
      schemaVersion: 1,
      source: { documentId: this.current.source.documentId, path: this.current.source.path, revision: this.current.source.revision, status: this.current.status },
      properties: { XLink: resolved ? XLink : null, XLinkSub: resolved ? XLinkSub : null, XLinkList: resolved ? XLinkList : [], XLinkSubList: resolved ? XLinkSubList : [] },
      propertyStatus: { XLink: ['21'], XLinkSub: ['21'], XLinkList: ['21'], XLinkSubList: ['21'] },
      dimensions: {
        xLinkLength: resolved ? source.length : null,
        xLinkSubWidth: resolved ? source.width : null,
        xLinkListHeights: resolved ? [source.height, second.height] : [],
        xLinkSubListAreas: resolved ? [source.width * source.height, second.width * second.height, second.width * second.height] : [],
      },
      binder: { support: resolved ? [XLinkSub] : [], area: source.width * source.height, shapeId: source.shapeId, state: ['Up-to-date'], status: 'Valid', cacheState: resolved ? 'linked' : 'cached' },
      history: { undo: this.undoStack.length, redo: this.redoStack.length },
    }
  }

  serialize() {
    const archive: ExternalRegistryArchive = { schemaVersion: 1, current: clone(this.current), undo: clone(this.undoStack), redo: clone(this.redoStack) }
    return JSON.stringify(archive)
  }

  static deserialize(text: string) {
    const archive = JSON.parse(text) as ExternalRegistryArchive
    if (archive?.schemaVersion !== 1 || !Array.isArray(archive.undo) || !Array.isArray(archive.redo)) throw new TypeError('External link registry archive is invalid.')
    validateState(archive.current)
    return new ExternalPartDesignLinkRegistry(archive.current.source, archive)
  }
}
