import { unzipSync, zipSync } from 'fflate'
import { XMLBuilder, XMLParser } from 'fast-xml-parser'
import { ATTACHMENT_MAP_MODES, validateAttachmentOffset, validateAttachmentSupport } from './attachment'
import { validateSketchSnapshot } from './sketcher'
import { decodeFreecadPropertyStatus, hasFreecadPropertyStatus, isFreecadPropertyNoPersist, isFreecadPropertyPersistenceSuppressed, isFreecadPropertyTransient, normalizeFreecadPropertyStatusMask } from './propertyStatus'
import type { FreecadPropertyStatusName } from './propertyStatus'
import { migrateElementMap2Schema, parseElementMap2, writeElementMap2 } from './elementMap2'
import type { AnyElementMap2Document, ElementMap2Document } from './elementMap2'
import { migrateStringHasherSchema, parseStringHasherTable, validateElementMap2StringHasherEvidence, validateStringHasherTable, writeStringHasherTable } from './stringHasher'
import type { AnyStringHasherTable, StringHasherTable, StringHasherValidationReport } from './stringHasher'
import type { SketchConstraint, SketchExternalGeometry, SketchExternalMode, SketchGeometry, SketchPointRef, SketchSnapshot } from './sketcher'
import { includedFileMediaType, validateIncludedFileArchiveName, validateIncludedFileValue } from './includedFile'
import type { AttachmentSupportValue, ColorListValue, ColorValue, DocumentSnapshot, DocumentObjectSnapshot, ElementMapSnapshot, IncludedFileValue, LinkSubListValue, ModelTreeItem, ObjectPropertySnapshot, PathCommandValue, PathPropertyValue, PlacementValue, PropertyValue, TopoRefValue, VectorValue } from './types'

export type FcstdArchiveLimits = {
  maxArchiveBytes: number
  maxEntries: number
  maxEntryBytes: number
  maxTotalUncompressedBytes: number
  maxCompressionRatio: number
  maxXmlDepth: number
  maxXmlNodes: number
}

export type FcstdEntryRole = 'document' | 'gui-document' | 'shape' | 'topology-map' | 'thumbnail' | 'script' | 'resource'

export type FcstdEntryMetadata = {
  path: string
  compressedBytes: number
  uncompressedBytes: number
  compressionMethod: number
  role: FcstdEntryRole
}

export type FcstdObjectSupport = 'recognized' | 'proxy' | 'blocked'

export type FcstdPropertySummary = {
  name: string
  typeId: string
  element: string
  value: string
  nativeStatus?: number
  statusNames?: FreecadPropertyStatusName[]
  transientMetadata?: boolean
  subElements?: string[]
  links?: string[]
  linkSubs?: Array<{ objectId: string; subElement: string }>
  enumOptions?: string[]
  resourcePath?: string
  includedFileResource?: FcstdIncludedFileResource
  shapeResource?: { path: string; hasherIndex?: number; elementMap?: string; elementMapEntries?: Array<{ key: string; value: string }>; elementMapResource?: string }
  expression?: string
}

export type FcstdDecodedPropertyValue = { value: unknown; decoded: boolean; error?: string }
export type FcstdPathAccessOptions = { allowFeaturePython?: boolean }
export type FcstdPathEdit = { objectName: string; propertyName?: string; value: PathPropertyValue; allowFeaturePython?: boolean }
export type FcstdNumericPropertyEdit = { objectName: string; propertyName: string; typeId: 'App::PropertyAcceleration' | 'App::PropertyForce' | 'App::PropertyHeatFlux'; value: number; expectedValue?: number }
export type FcstdMeasureAreaEdit = { objectName: string; elements: LinkSubListValue; area: number; expectedElements?: LinkSubListValue; expectedArea?: number }
export type FcstdBoolListEdit = { objectName: string; propertyName: string; value: boolean[]; expectedValue?: boolean[] }
export type FcstdIntegerSetEdit = { objectName: string; propertyName: string; value: number[]; expectedValue?: number[] }
export type FcstdColorEdit = { objectName: string; propertyName: string; value: ColorValue; expectedValue?: ColorValue }
export type FcstdColorListEdit = { objectName: string; propertyName: string; value: ColorListValue; expectedValue?: ColorListValue }
export type FcstdDirectionEdit = { objectName: string; propertyName: string; value: VectorValue; expectedValue?: VectorValue }
export type FcstdFileEdit = { objectName: string; propertyName: string; value: string; expectedValue?: string }
export type FcstdFontEdit = { objectName: string; propertyName: string; value: string; expectedValue?: string }
export type FcstdIncludedFileEdit = { objectName: string; propertyName: string; value: IncludedFileValue; expectedValue?: IncludedFileValue }
export type FcstdIncludedFileResourceEdit = FcstdIncludedFileEdit & { resourceBytes: Uint8Array; expectedResourceBytes?: Uint8Array }

export type FcstdObjectSummary = {
  name: string
  label: string
  typeId: string
  nativeObjectTag?: number
  propertyCount: number
  properties: FcstdPropertySummary[]
  support: FcstdObjectSupport
  extensions: string[]
  sketch?: SketchSnapshot
}

export type FcstdShapeResource = {
  path: string
  format: 'brep'
  mediaType: 'application/x-freecad-brep'
  byteLength: number
  contentHash: string
  status: 'available' | 'empty'
}

export type FcstdShapeResourcePayload = FcstdShapeResource & { bytes: Uint8Array }

export type FcstdStoredShapeResource = FcstdShapeResource & { hash: string }

export type FcstdIncludedFileReference = {
  objectName: string
  propertyName: string
}

export type FcstdIncludedFileResource = {
  path: string
  mediaType: string
  byteLength: number
  contentHash: string
  status: 'available'
}

export type FcstdIncludedFileResourcePayload = FcstdIncludedFileResource & {
  references: FcstdIncludedFileReference[]
  bytes: Uint8Array
}

export type FcstdStoredIncludedFileResource = FcstdIncludedFileResource & {
  references: FcstdIncludedFileReference[]
  hash: string
  value: IncludedFileValue
}

export type FcstdShapeResourceReference = {
  objectName: string
  propertyName: string
  hasherIndex?: number
  elementMap?: string
  elementMapResource?: string
}

export type FcstdElementMapResource = {
  path: string
  format: 'element-map-v1'
  byteLength: number
  contentHash: string
  status: 'available' | 'empty'
  postfixCount?: number
  mapCount?: number
  sections: Array<{ name: 'Edge' | 'Face' | 'Vertex'; nameCount: number }>
  document?: ElementMap2Document
}

export type FcstdStringHasherResource = {
  path: string
  format: 'string-hasher-v1'
  byteLength: number
  contentHash: string
  status: 'available' | 'empty'
  entryCount: number
  validation: StringHasherValidationReport
  document?: StringHasherTable
}

export type FcstdInstantiatedShape<T> = {
  resource: FcstdShapeResource
  references: FcstdShapeResourceReference[]
  shape: T
}

export type FcstdShapeInstantiationOptions<T> = {
  limits?: Partial<FcstdArchiveLimits>
  signal?: AbortSignal
  release?: (shape: T) => Promise<void> | void
}

export type FcstdCompatibilityReport = {
  level: 'metadata-compatible' | 'partial' | 'blocked'
  readOnly: true
  codeExecutionBlocked: true
  recognizedObjects: number
  proxyObjects: number
  blockedObjects: number
  unknownTypeIds: string[]
  warnings: string[]
}

export type FcstdGuiInspection = {
  present: boolean
  rootName: 'Document' | 'GuiDocument' | 'none'
  schemaVersion: string
  viewCount: number
  contentHash: string
  views: Array<{ name: string; type: string; visibility: string }>
  viewProviders: FcstdViewProviderSummary[]
}

export type FcstdMaterialAppearance = {
  ambientColor: string
  diffuseColor: string
  specularColor: string
  emissiveColor: string
  shininess: number
  transparency: number
  image: string
  imagePath: string
  uuid: string
}

export type FcstdViewProviderSummary = {
  objectName: string
  expanded: boolean | null
  treeRank: number | null
  propertyCount: number
  visibility?: boolean
  transparency?: number
  lineColor?: string
  pointColor?: string
  shapeColor?: string
  deviation?: number
  angularDeflection?: number
  displayMode?: number
  shapeAppearanceResource?: string
  shapeAppearance: FcstdMaterialAppearance[]
}

export type FcstdInspection = {
  format: 'FCStd'
  schemaVersion: string
  label: string
  entries: FcstdEntryMetadata[]
  objects: FcstdObjectSummary[]
  guiDocument: FcstdGuiInspection
  shapeResources: FcstdShapeResource[]
  elementMapResources: FcstdElementMapResource[]
  stringHasherResource?: FcstdStringHasherResource
  compatibility: FcstdCompatibilityReport
  proxyDocument: DocumentSnapshot
}

export type FcstdWriteOptions = {
  guiDocumentXml?: string
  guiViews?: Array<{ name: string; type?: string; visibility?: string }>
  opaqueEntries?: Record<string, Uint8Array>
  stringHasherTable?: AnyStringHasherTable
}

export const DEFAULT_FCSTD_LIMITS: FcstdArchiveLimits = {
  maxArchiveBytes: 256 * 1024 * 1024,
  maxEntries: 20_000,
  maxEntryBytes: 128 * 1024 * 1024,
  maxTotalUncompressedBytes: 512 * 1024 * 1024,
  maxCompressionRatio: 200,
  maxXmlDepth: 128,
  maxXmlNodes: 250_000,
}

const recognizedTypeIds = new Set([
  'App::DocumentObjectGroup',
  'App::FeaturePython',
  'Part::Feature',
  'Part::FeaturePython',
  'Part::Box',
  'Part::Cylinder',
  'Part::Sphere',
  'Part::Ellipsoid',
  'Part::Cone',
  'Part::Torus',
  'Part::Prism',
  'Part::Wedge',
  'Part::Helix',
  'Part::Fuse',
  'Part::Cut',
  'Part::Common',
  'Part::Extrusion',
  'Part::Revolution',
  'Part::Loft',
  'Part::Sweep',
  'Part::Fillet',
  'Part::Chamfer',
  'Part::Mirroring',
  'Part::ProjectOnSurface',
  'PartDesign::Body',
  'PartDesign::Feature',
  'PartDesign::Pad',
  'PartDesign::Pocket',
  'PartDesign::Fillet',
  'PartDesign::Chamfer',
  'PartDesign::Revolution',
  'PartDesign::Groove',
  'PartDesign::AdditiveLoft',
  'PartDesign::SubtractiveLoft',
  'PartDesign::AdditivePipe',
  'PartDesign::SubtractivePipe',
  'PartDesign::Draft',
  'PartDesign::Thickness',
  'PartDesign::Mirrored',
  'PartDesign::MultiTransform',
  'PartDesign::LinearPattern',
  'PartDesign::PolarPattern',
  'PartDesign::Hole',
  'PartDesign::Plane',
  'PartDesign::Line',
  'PartDesign::Point',
  'PartDesign::ShapeBinder',
  'PartDesign::SubShapeBinder',
  'Sketcher::SketchObject',
  'Sketcher::SketchObjectSF',
  'Mesh::Import',
  'App::DocumentObjectFileIncluded',
  'App::VRMLObject',
  'Image::ImagePlane',
  'Path::Feature',
  'Robot::RobotObject',
  'Robot::TrajectoryObject',
  'Robot::TrajectoryDressUpObject',
  'Measure::MeasureArea',
  'App::Part',
  'App::FeatureTest',
  'App::FeatureTestException',
  'Assembly::AssemblyLink',
  'Assembly::AssemblyObject',
  'TechDraw::DrawComplexSection',
  'TechDraw::DrawComplexSectionPython',
  'TechDraw::DrawGeomHatch',
  'TechDraw::DrawHatch',
  'TechDraw::DrawSVGTemplate',
  'TechDraw::DrawTileWeld',
  'TechDraw::DrawTileWeldPython',
  'TechDraw::DrawViewImage',
  'TechDraw::DrawViewSection',
  'TechDraw::DrawViewSectionPython',
  'TechDraw::DrawViewAnnotation',
  'TechDraw::DrawViewDraft',
  'TechDraw::DrawViewSpreadsheet',
  'Surface::GeomFillSurface',
  'App::Link',
  'App::LinkGroup',
  'App::LinkGroupPython',
  'App::LinkPython',
])

const nativeTypeIdAliases = new Map([
  ['Part::Extrude', 'Part::Extrusion'],
])

const nativeFcstdTypeId = (typeId: string) => nativeTypeIdAliases.get(typeId) ?? typeId

const hashString = (value: string) => {
  let hash = 2166136261
  for (let index = 0; index < value.length; index += 1) hash = Math.imul(hash ^ value.charCodeAt(index), 16777619)
  return (hash >>> 0).toString(16).padStart(8, '0')
}

const hashBytes = (bytes: Uint8Array) => {
  let hash = 2166136261
  for (const byte of bytes) hash = Math.imul(hash ^ byte, 16777619)
  return (hash >>> 0).toString(16).padStart(8, '0')
}

const xmlEscape = (value: string) => value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
const colorToPacked = (value: unknown, name: string) => {
  if (typeof value !== 'string' || !/^#[0-9a-fA-F]{6}(?:[0-9a-fA-F]{2})?$/.test(value)) throw new TypeError(`FCStd Color property ${name} requires #RRGGBB or #RRGGBBAA.`)
  return Number.parseInt(`${value.slice(1, 7)}${value.length === 9 ? value.slice(7, 9) : 'ff'}`, 16) >>> 0
}
const packedToColor = (value: number) => `#${(value >>> 0).toString(16).padStart(8, '0')}`
const colorChannelRound = (value: number) => value < 0 ? -Math.round(-value) : Math.round(value)
const colorChannelsToPacked = (value: unknown, name: string) => {
  if (!Array.isArray(value) || value.length !== 4 || !value.every((entry) => typeof entry === 'number' && Number.isFinite(entry))) throw new TypeError(`FCStd Color property ${name} requires four finite RGBA channels.`)
  const channels = (value as number[]).map((entry) => {
    const scaled = entry * 255
    if (!Number.isSafeInteger(colorChannelRound(scaled))) throw new RangeError(`FCStd Color property ${name} channel is outside the safely packable range.`)
    return colorChannelRound(scaled) >>> 0
  })
  return ((channels[0] << 24) | (channels[1] << 16) | (channels[2] << 8) | channels[3]) >>> 0
}
const colorListValue = (value: unknown, name: string): ColorListValue => {
  if (!Array.isArray(value) || !value.every((color) => Array.isArray(color) && color.length === 4 && color.every((channel) => typeof channel === 'number' && Number.isFinite(channel)))) throw new TypeError(`FCStd ColorList property ${name} requires a list of four finite RGBA channels.`)
  return value.map((color) => [...color] as ColorValue)
}
const colorListResourceBytes = (value: ColorListValue, name: string) => {
  const bytes = new Uint8Array(4 + value.length * 4)
  const view = new DataView(bytes.buffer)
  view.setUint32(0, value.length, true)
  value.forEach((color, index) => view.setUint32(4 + index * 4, colorChannelsToPacked(color, `${name}[${index}]`), true))
  return bytes
}
const packedToColorChannels = (value: number): [number, number, number, number] => [
  (value >>> 24) / 255,
  ((value >>> 16) & 0xff) / 255,
  ((value >>> 8) & 0xff) / 255,
  (value & 0xff) / 255,
]
const materialAppearanceBytes = (diffuseColor: unknown, transparency: unknown, name: string) => {
  const diffuse = colorToPacked(diffuseColor, name)
  const transparencyValue = transparency === undefined ? 0 : finiteComponent(transparency, `${name}.Transparency`) / 100
  if (transparencyValue < 0 || transparencyValue > 1) throw new RangeError(`FCStd Shape appearance ${name} transparency must be between 0 and 100.`)
  const bytes = new Uint8Array(40)
  const view = new DataView(bytes.buffer)
  let offset = 0
  const uint32 = (value: number) => { view.setUint32(offset, value >>> 0, true); offset += 4 }
  const float32 = (value: number) => { view.setFloat32(offset, value, true); offset += 4 }
  uint32(1)
  uint32(0x333333ff)
  uint32(diffuse)
  uint32(0x000000ff)
  uint32(0x000000ff)
  float32(0.2)
  float32(transparencyValue)
  uint32(0)
  uint32(0)
  uint32(0)
  return bytes
}
const finiteComponent = (value: unknown, name: string) => {
  if (typeof value !== 'number' || !Number.isFinite(value)) throw new TypeError(`FCStd ${name} must be finite.`)
  return value
}
const propertyFileValue = (value: unknown, name: string) => {
  if (typeof value !== 'string') throw new TypeError(`FCStd PropertyFile ${name} requires a string.`)
  if (value.length > 4096) throw new RangeError(`FCStd PropertyFile ${name} reference is too long.`)
  if (value && (value.includes('\0') || value.includes('\\') || value.startsWith('/') || /^[A-Za-z]:/.test(value) || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(value) || value.split('/').some((segment) => segment === '.' || segment === '..' || segment.length === 0))) throw new RangeError(`FCStd PropertyFile ${name} requires a safe project-relative file reference.`)
  return value
}
const propertyFontValue = (value: unknown, name: string) => {
  if (typeof value !== 'string') throw new TypeError(`FCStd PropertyFont ${name} requires a string.`)
  if (value.length > 1024) throw new RangeError(`FCStd PropertyFont ${name} family name is too long.`)
  if (value.includes('\0') || /[\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value)) throw new RangeError(`FCStd PropertyFont ${name} contains unsupported control characters.`)
  return value
}

const guiPropertyXml = (property: ObjectPropertySnapshot) => {
  const nativeType = property.type === 'App::PropertyFloat' && ['Deviation', 'LineWidth', 'PointSize'].includes(property.name) ? 'App::PropertyFloatConstraint' : property.type
  const header = `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(nativeType)}" status="1">`
  if (property.type === 'App::PropertyColor') return `${header}<PropertyColor value="${colorToPacked(property.value, property.name)}"/></Property>`
  if (property.type === 'App::PropertyBool') {
    if (typeof property.value !== 'boolean') throw new TypeError(`FCStd GUI Bool property ${property.name} requires a boolean.`)
    return `${header}<Bool value="${property.value}"/></Property>`
  }
  if (property.type === 'App::PropertyPercent' || property.type === 'App::PropertyInteger') {
    const value = finiteComponent(property.value, property.name)
    if (!Number.isSafeInteger(value) || (property.type === 'App::PropertyPercent' && (value < 0 || value > 100))) throw new RangeError(`FCStd GUI property ${property.name} requires a valid integer.`)
    return `${header}<Integer value="${value}"/></Property>`
  }
  if (property.type === 'App::PropertyFloat' || property.type === 'App::PropertyAngle' || property.type === 'App::PropertyLength' || property.type === 'App::PropertyDistance') return `${header}<Float value="${finiteComponent(property.value, property.name)}"/></Property>`
  if (property.type === 'App::PropertyEnumeration') {
    const index = property.options?.indexOf(String(property.value)) ?? -1
    if (index < 0) throw new TypeError(`FCStd GUI Enumeration property ${property.name} requires a listed option.`)
    return `${header}<Integer value="${index}"/></Property>`
  }
  if (property.type === 'App::PropertyString') return `${header}<String value="${xmlEscape(String(property.value))}"/></Property>`
  return ''
}
const nativeDataPropertyNames = new Map<string, ReadonlySet<string>>([
  ['Path::Feature', new Set(['Path'])],
  ['Robot::TrajectoryDressUpObject', new Set(['Acceleration'])],
  ['Measure::MeasureArea', new Set(['Area', 'Elements'])],
  ['Mesh::Import', new Set(['FileName'])],
  ['App::DocumentObjectFileIncluded', new Set(['File'])],
  ['App::VRMLObject', new Set(['VrmlFile'])],
  ['Image::ImagePlane', new Set(['ImageFile'])],
  ['Robot::RobotObject', new Set(['RobotKinematicFile', 'RobotVrmlFile'])],
  ['Sketcher::SketchObjectSF', new Set(['SketchFlatFile'])],
  ['TechDraw::DrawComplexSection', new Set(['PatIncluded', 'SvgIncluded'])],
  ['TechDraw::DrawComplexSectionPython', new Set(['PatIncluded', 'SvgIncluded'])],
  ['TechDraw::DrawGeomHatch', new Set(['PatIncluded'])],
  ['TechDraw::DrawHatch', new Set(['SvgIncluded'])],
  ['TechDraw::DrawSVGTemplate', new Set(['PageResult'])],
  ['TechDraw::DrawTileWeld', new Set(['SymbolIncluded'])],
  ['TechDraw::DrawTileWeldPython', new Set(['SymbolIncluded'])],
  ['TechDraw::DrawViewImage', new Set(['ImageIncluded'])],
  ['TechDraw::DrawViewSection', new Set(['PatIncluded', 'SvgIncluded'])],
  ['TechDraw::DrawViewSectionPython', new Set(['PatIncluded', 'SvgIncluded'])],
  ['App::FeatureTest', new Set(['BoolList', 'Colour', 'ColourList'])],
  ['App::FeatureTestException', new Set(['BoolList', 'Colour', 'ColourList'])],
  ['App::Part', new Set(['Color'])],
  ['Assembly::AssemblyLink', new Set(['Color'])],
  ['Assembly::AssemblyObject', new Set(['Color'])],
  ['Fem::ConstraintForce', new Set(['Force'])],
  ['Fem::ConstraintRigidBody', new Set(['ForceX', 'ForceY', 'ForceZ'])],
  ['TechDraw::DrawViewAnnotation', new Set(['Font', 'TextColor'])],
  ['TechDraw::DrawViewDraft', new Set(['Color'])],
  ['TechDraw::DrawViewSpreadsheet', new Set(['Font', 'TextColor'])],
  ['Surface::GeomFillSurface', new Set(['ReversedList'])],
  ['App::Link', new Set(['VisibilityList'])],
  ['App::LinkGroup', new Set(['VisibilityList'])],
  ['App::LinkGroupPython', new Set(['VisibilityList'])],
  ['App::LinkPython', new Set(['VisibilityList'])],
  ['Part::Mirroring', new Set(['Normal'])],
  ['Part::ProjectOnSurface', new Set(['Direction'])],
  ['Part::Box', new Set(['Length', 'Width', 'Height'])],
  ['Part::Cylinder', new Set(['Radius', 'Height', 'Angle'])],
  ['Part::Sphere', new Set(['Radius', 'Angle1', 'Angle2', 'Angle3'])],
  ['Part::Ellipsoid', new Set(['Radius1', 'Radius2', 'Radius3', 'Angle1', 'Angle2', 'Angle3'])],
  ['Part::Cone', new Set(['Radius1', 'Radius2', 'Height', 'Angle'])],
  ['Part::Torus', new Set(['Radius1', 'Radius2', 'Angle1', 'Angle2', 'Angle3'])],
  ['Part::Prism', new Set(['Polygon', 'Circumradius', 'Height', 'FirstAngle', 'SecondAngle'])],
  ['Part::Wedge', new Set(['Xmin', 'Ymin', 'Zmin', 'Z2min', 'X2min', 'Xmax', 'Ymax', 'Zmax', 'Z2max', 'X2max'])],
  ['Part::Extrusion', new Set(['Base', 'Dir', 'DirMode', 'DirLink', 'LengthFwd', 'LengthRev', 'Solid', 'Reversed', 'Symmetric', 'TaperAngle', 'TaperAngleRev', 'FaceMakerClass', 'FaceMakerMode', 'InnerWireTaper'])],
  ['Part::Helix', new Set(['Pitch', 'Height', 'Radius', 'Angle', 'LeftHanded', 'Reversed', 'SegmentLength'])],
  ['Part::Revolution', new Set(['Source', 'Base', 'Axis', 'AxisLink', 'Angle', 'Symmetric', 'Solid', 'FaceMakerClass'])],
  ['Part::Loft', new Set(['Sections', 'Solid', 'Ruled', 'Closed', 'MaxDegree', 'Linearize'])],
  ['Part::Sweep', new Set(['Sections', 'Spine', 'Solid', 'Frenet', 'Transition', 'Linearize'])],
  ['PartDesign::Plane', new Set(['MapMode', 'AttachmentOffset'])],
  ['PartDesign::Line', new Set(['MapMode', 'AttachmentOffset'])],
  ['PartDesign::Point', new Set(['MapMode', 'AttachmentOffset'])],
  ['PartDesign::ShapeBinder', new Set(['Support', 'TraceSupport'])],
  ['PartDesign::Pad', new Set(['Profile', 'Length', 'Length2', 'Type', 'Type2', 'SideType', 'UpToFace', 'UpToFace2', 'TaperAngle', 'TaperAngle2', 'Reversed', 'Midplane', 'ReferenceAxis', 'AlongSketchNormal', 'UseCustomVector', 'Direction', 'Offset', 'Offset2'])],
  ['PartDesign::Pocket', new Set(['Profile', 'Length', 'Length2', 'Type', 'Type2', 'SideType', 'UpToFace', 'UpToFace2', 'TaperAngle', 'TaperAngle2', 'Reversed', 'Midplane', 'ReferenceAxis', 'AlongSketchNormal', 'UseCustomVector', 'Direction', 'Offset', 'Offset2'])],
  ['PartDesign::Revolution', new Set(['Profile', 'Angle', 'Angle2', 'Type', 'UpToFace', 'ReferenceAxis', 'Axis', 'Midplane', 'Reversed'])],
  ['PartDesign::Groove', new Set(['Base', 'Profile', 'Angle', 'Angle2', 'Type', 'UpToFace', 'ReferenceAxis', 'Axis', 'Midplane', 'Reversed'])],
  ['PartDesign::AdditiveLoft', new Set(['Profile', 'Sections', 'Ruled', 'Closed'])],
  ['PartDesign::SubtractiveLoft', new Set(['Profile', 'Sections', 'Ruled', 'Closed'])],
  ['PartDesign::AdditivePipe', new Set(['Profile', 'Spine', 'Transition', 'Mode', 'Transformation'])],
  ['PartDesign::SubtractivePipe', new Set(['Profile', 'Spine', 'Transition', 'Mode', 'Transformation'])],
  ['PartDesign::Fillet', new Set(['Base', 'Radius', 'UseAllEdges'])],
  ['PartDesign::Chamfer', new Set(['Base', 'Size', 'Size2', 'Angle', 'ChamferType', 'FlipDirection', 'UseAllEdges'])],
  ['PartDesign::Draft', new Set(['Base', 'Angle', 'Reversed'])],
  ['PartDesign::Thickness', new Set(['Base', 'Value', 'Join', 'Mode', 'Reversed'])],
  ['PartDesign::Mirrored', new Set(['Originals', 'TransformMode'])],
  ['PartDesign::MultiTransform', new Set(['Originals', 'TransformMode', 'Transformations'])],
  ['PartDesign::LinearPattern', new Set(['Originals', 'TransformMode', 'Occurrences', 'Length', 'Offset', 'Direction', 'Mode', 'Reversed', 'Spacings', 'SpacingPattern', 'Direction2', 'Mode2', 'Length2', 'Offset2', 'Occurrences2', 'Reversed2', 'Spacings2', 'SpacingPattern2'])],
  ['PartDesign::PolarPattern', new Set(['Originals', 'TransformMode', 'Occurrences', 'Angle', 'Axis', 'Mode', 'Offset', 'Spacings', 'SpacingPattern', 'Reversed'])],
  ['PartDesign::Hole', new Set(['Threaded', 'ModelThread', 'ThreadType', 'ThreadSize', 'ThreadClass', 'ThreadFit', 'Diameter', 'ThreadDiameter', 'ThreadDirection', 'HoleCutType', 'HoleCutCustomValues', 'HoleCutDiameter', 'HoleCutDepth', 'HoleCutCountersinkAngle', 'DepthType', 'Depth', 'DrillPoint', 'DrillPointAngle', 'DrillForDepth', 'Tapered', 'TaperedAngle', 'ThreadDepthType', 'ThreadDepth', 'UseCustomThreadClearance', 'CustomThreadClearance', 'BaseProfileType'])],
  ['Part::Fuse', new Set(['Base', 'Tool', 'Refine'])],
  ['Part::Cut', new Set(['Base', 'Tool', 'Refine'])],
  ['Part::Common', new Set(['Base', 'Tool', 'Refine'])],
])

// These indexes are a persistent FCStd contract. Keep this in the same order as
// Attacher::AttachEngine::eMapModeStrings in the locked FreeCAD oracle.
const freecadAttachmentMapModes = [
  'Deactivated', 'Translate', 'ObjectXY', 'ObjectXZ', 'ObjectYZ', 'FlatFace', 'TangentPlane', 'NormalToEdge',
  'FrenetNB', 'FrenetTN', 'FrenetTB', 'Concentric', 'SectionOfRevolution', 'ThreePointsPlane', 'ThreePointsNormal', 'Folding',
  'ObjectX', 'ObjectY', 'ObjectZ', 'AxisOfCurvature', 'Directrix1', 'Directrix2', 'Asymptote1', 'Asymptote2', 'Tangent', 'Normal',
  'Binormal', 'TangentU', 'TangentV', 'TwoPointLine', 'IntersectionLine', 'ProximityLine', 'ObjectOrigin', 'Focus1', 'Focus2',
  'OnEdge', 'CenterOfCurvature', 'CenterOfMass', 'IntersectionPoint', 'Vertex', 'ProximityPoint1', 'ProximityPoint2',
  'AxisOfInertia1', 'AxisOfInertia2', 'AxisOfInertia3', 'InertialCS', 'FaceNormal', 'OZX', 'OZY', 'OXY', 'OXZ', 'OYZ', 'OYX',
  'ParallelPlane', 'MidPoint',
] as const

const sketchAttachmentPropertyNames = new Set(['Support', 'MapMode', 'AttachmentOffset'])
const sketchReservedAttachmentPropertyNames = new Set(['AttachmentSupport', 'WebSupportValue'])

const sketchConstraintType = {
  coincident: 1,
  horizontal: 2,
  vertical: 3,
  parallel: 4,
  tangent: 5,
  distance: 6,
  distanceX: 7,
  distanceY: 8,
  angle: 9,
  perpendicular: 10,
  radius: 11,
  equal: 12,
  pointOnObject: 13,
  symmetric: 14,
  internalAlignment: 15,
  snellsLaw: 16,
  block: 17,
  diameter: 18,
  weight: 19,
} as const

const sketchPointPosition = { start: 1, end: 2, center: 3, position: 1 } as const
const sketchGeometryType = {
  point: 'Part::GeomPoint',
  line: 'Part::GeomLineSegment',
  circle: 'Part::GeomCircle',
  arc: 'Part::GeomArcOfCircle',
  ellipse: 'Part::GeomEllipse',
  arcEllipse: 'Part::GeomArcOfEllipse',
  arcHyperbola: 'Part::GeomArcOfHyperbola',
  arcParabola: 'Part::GeomArcOfParabola',
  bspline: 'Part::GeomBSplineCurve',
} as const

const sketchGeometryPayloadXml = (geometry: SketchGeometry) => {
  if (geometry.type === 'point') return `<GeomPoint X="${geometry.position.x}" Y="${geometry.position.y}" Z="0"/>`
  if (geometry.type === 'line') return `<LineSegment StartX="${geometry.start.x}" StartY="${geometry.start.y}" StartZ="0" EndX="${geometry.end.x}" EndY="${geometry.end.y}" EndZ="0"/>`
  if (geometry.type === 'circle') return `<Circle CenterX="${geometry.center.x}" CenterY="${geometry.center.y}" CenterZ="0" NormalX="0" NormalY="0" NormalZ="1" AngleXU="0" Radius="${geometry.radius}"/>`
  if (geometry.type === 'arc') return `<ArcOfCircle CenterX="${geometry.center.x}" CenterY="${geometry.center.y}" CenterZ="0" NormalX="0" NormalY="0" NormalZ="1" AngleXU="0" Radius="${geometry.radius}" StartAngle="${geometry.startAngle}" EndAngle="${geometry.endAngle}"/>`
  if (geometry.type === 'ellipse') return `<Ellipse CenterX="${geometry.center.x}" CenterY="${geometry.center.y}" CenterZ="0" NormalX="0" NormalY="0" NormalZ="1" MajorRadius="${geometry.majorRadius}" MinorRadius="${geometry.minorRadius}" AngleXU="${geometry.rotation}"/>`
  if (geometry.type === 'arcEllipse') return `<ArcOfEllipse CenterX="${geometry.center.x}" CenterY="${geometry.center.y}" CenterZ="0" NormalX="0" NormalY="0" NormalZ="1" MajorRadius="${geometry.majorRadius}" MinorRadius="${geometry.minorRadius}" AngleXU="${geometry.rotation}" StartAngle="${geometry.startAngle}" EndAngle="${geometry.endAngle}"/>`
  if (geometry.type === 'arcHyperbola') return `<ArcOfHyperbola CenterX="${geometry.center.x}" CenterY="${geometry.center.y}" CenterZ="0" NormalX="0" NormalY="0" NormalZ="1" MajorRadius="${geometry.majorRadius}" MinorRadius="${geometry.minorRadius}" AngleXU="${geometry.rotation}" StartAngle="${geometry.startAngle}" EndAngle="${geometry.endAngle}"/>`
  if (geometry.type === 'arcParabola') return `<ArcOfParabola CenterX="${geometry.center.x}" CenterY="${geometry.center.y}" CenterZ="0" NormalX="0" NormalY="0" NormalZ="1" Focal="${geometry.focal}" AngleXU="${geometry.rotation}" StartAngle="${geometry.startAngle}" EndAngle="${geometry.endAngle}"/>`
  const weights = geometry.weights ?? geometry.controlPoints.map(() => 1)
  const knotCount = geometry.controlPoints.length + geometry.degree + 1
  const knots = geometry.knots ?? Array.from({ length: knotCount }, (_, index) => {
    if (index <= geometry.degree) return 0
    if (index >= knotCount - geometry.degree - 1) return 1
    return (index - geometry.degree) / (knotCount - 2 * geometry.degree - 1)
  })
  const uniqueKnots: Array<{ value: number; multiplicity: number }> = []
  for (const knot of knots) {
    const previous = uniqueKnots.at(-1)
    if (previous?.value === knot) previous.multiplicity += 1
    else uniqueKnots.push({ value: knot, multiplicity: 1 })
  }
  const poles = geometry.controlPoints.map((point, index) => `<Pole X="${point.x}" Y="${point.y}" Z="0" Weight="${weights[index]}"/>`).join('')
  const knotXml = uniqueKnots.map((knot) => `<Knot Value="${knot.value}" Mult="${knot.multiplicity}"/>`).join('')
  return `<BSplineCurve PolesCount="${geometry.controlPoints.length}" KnotsCount="${uniqueKnots.length}" Degree="${geometry.degree}" IsPeriodic="${geometry.periodic ? 1 : 0}">${poles}${knotXml}</BSplineCurve>`
}

type SketchInternalAlignmentType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11

type SketchInternalGeometryHelper = {
  key: string
  nativeIndex: number
  nativeId: number
  targetGeometryId: string
  alignmentType: SketchInternalAlignmentType
  internalAlignmentIndex: number
  webInternalGeometryIndex: number
  firstPosition: number
  geometry: Extract<SketchGeometry, { type: 'point' | 'line' | 'circle' }>
  explicitAlignmentId?: string
  syntheticAlignmentId?: string
  weightConstraintId?: string
}

const bsplineExpandedKnots = (geometry: Extract<SketchGeometry, { type: 'bspline' }>) => {
  if (geometry.knots) return geometry.knots
  const knotCount = geometry.controlPoints.length + geometry.degree + 1
  return Array.from({ length: knotCount }, (_, index) => {
    if (index <= geometry.degree) return 0
    if (index >= knotCount - geometry.degree - 1) return 1
    return (index - geometry.degree) / (knotCount - 2 * geometry.degree - 1)
  })
}

const bsplinePointAtParameter = (geometry: Extract<SketchGeometry, { type: 'bspline' }>, parameter: number) => {
  const knots = bsplineExpandedKnots(geometry)
  const degree = geometry.degree
  const lastPole = geometry.controlPoints.length - 1
  const lower = knots[degree]
  const upper = knots[lastPole + 1]
  if (!Number.isFinite(parameter) || parameter < lower || parameter > upper) throw new RangeError(`B-spline ${geometry.id} parameter is outside its knot domain.`)
  let span = lastPole
  if (parameter < upper) {
    span = degree
    while (span < lastPole && parameter >= knots[span + 1]) span += 1
  }
  const weights = geometry.weights ?? geometry.controlPoints.map(() => 1)
  const values = Array.from({ length: degree + 1 }, (_, index) => {
    const poleIndex = span - degree + index
    const weight = weights[poleIndex]
    const pole = geometry.controlPoints[poleIndex]
    return { x: pole.x * weight, y: pole.y * weight, weight }
  })
  for (let level = 1; level <= degree; level += 1) {
    for (let index = degree; index >= level; index -= 1) {
      const knotIndex = span - degree + index
      const denominator = knots[knotIndex + degree - level + 1] - knots[knotIndex]
      const alpha = denominator === 0 ? 0 : (parameter - knots[knotIndex]) / denominator
      values[index] = {
        x: (1 - alpha) * values[index - 1].x + alpha * values[index].x,
        y: (1 - alpha) * values[index - 1].y + alpha * values[index].y,
        weight: (1 - alpha) * values[index - 1].weight + alpha * values[index].weight,
      }
    }
  }
  const result = values[degree]
  if (!Number.isFinite(result.weight) || result.weight <= 0) throw new RangeError(`B-spline ${geometry.id} evaluated to an invalid rational weight.`)
  return { x: result.x / result.weight, y: result.y / result.weight }
}

const sketchInternalGeometryPlan = (sketch: SketchSnapshot): SketchInternalGeometryHelper[] => {
  const geometryById = new Map(sketch.geometry.map((geometry) => [geometry.id, geometry]))
  const constraintIds = new Set(sketch.constraints.map((constraint) => constraint.id))
  const helpers = new Map<string, SketchInternalGeometryHelper>()
  const addHelper = (helper: Omit<SketchInternalGeometryHelper, 'nativeIndex' | 'nativeId'>) => {
    const helperIndex = helpers.size
    const value = { ...helper, nativeIndex: sketch.geometry.length + helperIndex, nativeId: sketch.geometry.length + helperIndex + 1 }
    helpers.set(value.key, value)
    return value
  }
  const ensureControlPointHelper = (geometryId: string, controlPointIndex: number, constraintId: string) => {
    const geometry = geometryById.get(geometryId)
    if (!geometry || geometry.type !== 'bspline') throw new Error(`FCStd native Sketch constraint '${constraintId}' requires B-spline geometry '${geometryId}'.`)
    if (!Number.isSafeInteger(controlPointIndex) || controlPointIndex < 0 || controlPointIndex >= geometry.controlPoints.length) throw new RangeError(`FCStd native Sketch constraint '${constraintId}' control point index is outside '${geometryId}'.`)
    const key = `${geometryId}\0bspline-control-point\0${controlPointIndex}`
    let helper = helpers.get(key)
    if (!helper) {
      const weight = geometry.weights?.[controlPointIndex] ?? 1
      helper = addHelper({
        key,
        targetGeometryId: geometryId,
        alignmentType: 9,
        internalAlignmentIndex: controlPointIndex,
        webInternalGeometryIndex: controlPointIndex,
        firstPosition: 3,
        geometry: { id: `Internal${helpers.size}`, type: 'circle', center: { ...geometry.controlPoints[controlPointIndex] }, radius: weight, construction: true },
      })
    }
    return helper
  }
  for (const constraint of sketch.constraints) {
    if (constraint.type === 'weight') {
      const helper = ensureControlPointHelper(constraint.geometryId, constraint.controlPointIndex, constraint.id)
      if (helper.weightConstraintId) throw new Error(`FCStd native Sketch has duplicate Weight constraints for '${constraint.geometryId}' control point ${constraint.controlPointIndex}.`)
      const radius = finiteComponent(constraint.value, `Sketch constraint ${constraint.id}.value`)
      if (radius <= 0) throw new RangeError(`FCStd native Sketch Weight constraint '${constraint.id}' requires a positive value.`)
      if (helper.geometry.type !== 'circle') throw new Error(`FCStd native Sketch Weight constraint '${constraint.id}' has an invalid helper geometry.`)
      helper.geometry.radius = radius
      helper.weightConstraintId = constraint.id
    }
    if (constraint.type === 'internalAlignment') {
      const target = geometryById.get(constraint.geometryId)
      if (!target) throw new Error(`FCStd native Sketch InternalAlignment '${constraint.id}' references unknown geometry '${constraint.geometryId}'.`)
      let helper: SketchInternalGeometryHelper
      if (constraint.alignmentType === 'bspline-control-point') helper = ensureControlPointHelper(constraint.geometryId, constraint.internalGeometryIndex, constraint.id)
      else if (constraint.alignmentType === 'bspline-knot') {
        if (target.type !== 'bspline') throw new Error(`FCStd native Sketch InternalAlignment '${constraint.id}' requires B-spline geometry '${constraint.geometryId}'.`)
        const uniqueKnots = bsplineExpandedKnots(target).filter((knot, index, knots) => index === 0 || knot !== knots[index - 1])
        if (!Number.isSafeInteger(constraint.internalGeometryIndex) || constraint.internalGeometryIndex < 0 || constraint.internalGeometryIndex >= uniqueKnots.length) throw new RangeError(`FCStd native Sketch InternalAlignment '${constraint.id}' knot index is outside '${constraint.geometryId}'.`)
        const key = `${constraint.geometryId}\0bspline-knot\0${constraint.internalGeometryIndex}`
        helper = helpers.get(key) ?? addHelper({
          key,
          targetGeometryId: constraint.geometryId,
          alignmentType: 10,
          internalAlignmentIndex: constraint.internalGeometryIndex,
          webInternalGeometryIndex: constraint.internalGeometryIndex,
          firstPosition: 1,
          geometry: { id: `Internal${helpers.size}`, type: 'point', position: bsplinePointAtParameter(target, uniqueKnots[constraint.internalGeometryIndex]), construction: true },
        })
      } else if (constraint.alignmentType.startsWith('ellipse-')) {
        if (target.type !== 'ellipse' && target.type !== 'arcEllipse') throw new Error(`FCStd native Sketch InternalAlignment '${constraint.id}' requires ellipse geometry '${constraint.geometryId}'.`)
        const focusIndex = constraint.alignmentType === 'ellipse-focus' ? constraint.internalGeometryIndex : 0
        if ((constraint.alignmentType === 'ellipse-focus' && focusIndex !== 0 && focusIndex !== 1) || (constraint.alignmentType !== 'ellipse-focus' && constraint.internalGeometryIndex !== 0)) throw new RangeError(`FCStd native Sketch InternalAlignment '${constraint.id}' has an invalid ellipse internal index.`)
        const alignmentType = constraint.alignmentType === 'ellipse-major' ? 1 : constraint.alignmentType === 'ellipse-minor' ? 2 : focusIndex === 0 ? 3 : 4
        const key = `${constraint.geometryId}\0${constraint.alignmentType}\0${focusIndex}`
        const majorDirection = { x: Math.cos(target.rotation), y: Math.sin(target.rotation) }
        const minorDirection = { x: -majorDirection.y, y: majorDirection.x }
        const focalDistance = Math.sqrt(target.majorRadius * target.majorRadius - target.minorRadius * target.minorRadius)
        const geometry: SketchInternalGeometryHelper['geometry'] = alignmentType === 1
          ? { id: `Internal${helpers.size}`, type: 'line', start: { x: target.center.x + target.majorRadius * majorDirection.x, y: target.center.y + target.majorRadius * majorDirection.y }, end: { x: target.center.x - target.majorRadius * majorDirection.x, y: target.center.y - target.majorRadius * majorDirection.y }, construction: true }
          : alignmentType === 2
            ? { id: `Internal${helpers.size}`, type: 'line', start: { x: target.center.x + target.minorRadius * minorDirection.x, y: target.center.y + target.minorRadius * minorDirection.y }, end: { x: target.center.x - target.minorRadius * minorDirection.x, y: target.center.y - target.minorRadius * minorDirection.y }, construction: true }
            : { id: `Internal${helpers.size}`, type: 'point', position: { x: target.center.x + (alignmentType === 3 ? 1 : -1) * focalDistance * majorDirection.x, y: target.center.y + (alignmentType === 3 ? 1 : -1) * focalDistance * majorDirection.y }, construction: true }
        helper = helpers.get(key) ?? addHelper({ key, targetGeometryId: constraint.geometryId, alignmentType, internalAlignmentIndex: -1, webInternalGeometryIndex: constraint.internalGeometryIndex, firstPosition: alignmentType <= 2 ? 0 : 1, geometry })
      } else if (constraint.alignmentType.startsWith('hyperbola-')) {
        if (target.type !== 'arcHyperbola') throw new Error(`FCStd native Sketch InternalAlignment '${constraint.id}' requires hyperbola geometry '${constraint.geometryId}'.`)
        if (constraint.internalGeometryIndex !== 0) throw new RangeError(`FCStd native Sketch InternalAlignment '${constraint.id}' has an invalid hyperbola internal index.`)
        const alignmentType = constraint.alignmentType === 'hyperbola-major' ? 5 : constraint.alignmentType === 'hyperbola-minor' ? 6 : 7
        const key = `${constraint.geometryId}\0${constraint.alignmentType}\0${0}`
        const majorDirection = { x: Math.cos(target.rotation), y: Math.sin(target.rotation) }
        const minorDirection = { x: -majorDirection.y, y: majorDirection.x }
        const focalDistance = Math.sqrt(target.majorRadius * target.majorRadius + target.minorRadius * target.minorRadius)
        const geometry: SketchInternalGeometryHelper['geometry'] = alignmentType === 5
          ? { id: `Internal${helpers.size}`, type: 'line', start: { x: target.center.x + target.majorRadius * majorDirection.x, y: target.center.y + target.majorRadius * majorDirection.y }, end: { x: target.center.x - target.majorRadius * majorDirection.x, y: target.center.y - target.majorRadius * majorDirection.y }, construction: true }
          : alignmentType === 6
            ? { id: `Internal${helpers.size}`, type: 'line', start: { x: target.center.x + target.minorRadius * minorDirection.x, y: target.center.y + target.minorRadius * minorDirection.y }, end: { x: target.center.x - target.minorRadius * minorDirection.x, y: target.center.y - target.minorRadius * minorDirection.y }, construction: true }
            : { id: `Internal${helpers.size}`, type: 'point', position: { x: target.center.x + focalDistance * majorDirection.x, y: target.center.y + focalDistance * majorDirection.y }, construction: true }
        helper = helpers.get(key) ?? addHelper({ key, targetGeometryId: constraint.geometryId, alignmentType, internalAlignmentIndex: -1, webInternalGeometryIndex: 0, firstPosition: alignmentType <= 6 ? 0 : 1, geometry })
      } else {
        if (target.type !== 'arcParabola' || (constraint.alignmentType !== 'parabola-focus' && constraint.alignmentType !== 'parabola-focal-axis')) throw new Error(`FCStd native Sketch InternalAlignment '${constraint.id}' requires parabola geometry '${constraint.geometryId}'.`)
        if (constraint.internalGeometryIndex !== 0) throw new RangeError(`FCStd native Sketch InternalAlignment '${constraint.id}' has an invalid parabola internal index.`)
        const alignmentType = constraint.alignmentType === 'parabola-focus' ? 8 : 11
        const key = `${constraint.geometryId}\0${constraint.alignmentType}\0${0}`
        const direction = { x: Math.cos(target.rotation), y: Math.sin(target.rotation) }
        const focus = { x: target.center.x + target.focal * direction.x, y: target.center.y + target.focal * direction.y }
        const geometry: SketchInternalGeometryHelper['geometry'] = alignmentType === 8
          ? { id: `Internal${helpers.size}`, type: 'point', position: focus, construction: true }
          : { id: `Internal${helpers.size}`, type: 'line', start: { ...target.center }, end: focus, construction: true }
        helper = helpers.get(key) ?? addHelper({ key, targetGeometryId: constraint.geometryId, alignmentType, internalAlignmentIndex: -1, webInternalGeometryIndex: 0, firstPosition: alignmentType === 8 ? 1 : 0, geometry })
      }
      if (helper.explicitAlignmentId) throw new Error(`FCStd native Sketch has duplicate InternalAlignment constraints for '${constraint.geometryId}' internal geometry ${constraint.internalGeometryIndex}.`)
      helper.explicitAlignmentId = constraint.id
    }
  }
  let syntheticIndex = 0
  for (const helper of helpers.values()) {
    if (helper.explicitAlignmentId) continue
    let id: string
    do { id = `__WebSyntheticAlignment${syntheticIndex++}` } while (constraintIds.has(id))
    constraintIds.add(id)
    helper.syntheticAlignmentId = id
  }
  return [...helpers.values()]
}

const sketchGeometryListXml = (sketch: SketchSnapshot, internalHelpers: SketchInternalGeometryHelper[]) => {
  const geometries = sketch.geometry.map((geometry, index) => {
    const nativeId = index + 1
    const flags = geometry.construction ? '00000000000000000000000000000010' : '00000000000000000000000000000000'
    return `<Geometry type="${sketchGeometryType[geometry.type]}" id="${nativeId}" migrated="1"><GeoExtensions count="1"><GeoExtension type="Sketcher::SketchGeometryExtension" id="${nativeId}" internalGeometryType="0" geometryModeFlags="${flags}" geometryLayer="0"/></GeoExtensions>${sketchGeometryPayloadXml(geometry)}<Construction value="${geometry.construction ? 1 : 0}"/></Geometry>`
  }).join('')
  const helpers = internalHelpers.map((helper) => `<Geometry type="${sketchGeometryType[helper.geometry.type]}" id="${helper.nativeId}" migrated="1"><GeoExtensions count="1"><GeoExtension type="Sketcher::SketchGeometryExtension" id="${helper.nativeId}" internalGeometryType="${helper.alignmentType}" geometryModeFlags="00000000000000000000000000000010" geometryLayer="0"/></GeoExtensions>${sketchGeometryPayloadXml(helper.geometry)}<Construction value="1"/></Geometry>`).join('')
  return `<Property name="Geometry" type="Part::PropertyGeometryList" status="8192"><GeometryList count="${sketch.geometry.length + internalHelpers.length}">${geometries}${helpers}</GeometryList></Property>`
}

const sketchAxisGeometryXml = (nativeId: -1 | -2) => {
  const payload = nativeId === -1
    ? '<LineSegment StartX="0" StartY="0" StartZ="0" EndX="1" EndY="0" EndZ="0"/>'
    : '<LineSegment StartX="0" StartY="0" StartZ="0" EndX="0" EndY="1" EndZ="0"/>'
  return `<Geometry type="Part::GeomLineSegment" id="${nativeId}" migrated="1"><GeoExtensions count="2"><GeoExtension type="Sketcher::SketchGeometryExtension" id="${nativeId}" internalGeometryType="0" geometryModeFlags="00000000000000000000000000000010" geometryLayer="0"/><GeoExtension type="Sketcher::ExternalGeometryExtension" Ref="" Flags="0"/></GeoExtensions>${payload}<Construction value="1"/></Geometry>`
}

const nativeExternalSubElement = (source: TopoRefValue, sketchId: string, objectIds: ReadonlySet<string>) => {
  if (source.schemaVersion !== 1 || !source.objectId.trim() || !source.persistentId.trim()) throw new TypeError(`FCStd native Sketch ${sketchId} external geometry requires a valid TopoRef.`)
  if (!Number.isSafeInteger(source.topologyVersion) || source.topologyVersion < 0 || !Number.isSafeInteger(source.generation) || source.generation < 0) throw new TypeError(`FCStd native Sketch ${sketchId} external geometry requires non-negative integer topology versions.`)
  if (source.signature !== undefined && typeof source.signature !== 'string') throw new TypeError(`FCStd native Sketch ${sketchId} external geometry signature must be a string.`)
  if (source.candidates !== undefined && (!Array.isArray(source.candidates) || source.candidates.some((candidate) => typeof candidate !== 'string' || !candidate.trim()))) throw new TypeError(`FCStd native Sketch ${sketchId} external geometry candidates must be non-empty strings.`)
  if (!objectIds.has(source.objectId)) throw new Error(`FCStd native Sketch ${sketchId} external geometry references undeclared object '${source.objectId}'.`)
  if (source.objectId === sketchId) throw new Error(`FCStd native Sketch ${sketchId} cannot reference itself as external geometry.`)
  if (source.status !== 'stable') throw new Error(`FCStd native Sketch ${sketchId} external geometry '${source.persistentId}' must have stable topology status.`)
  const prefix = source.kind === 'face' ? 'Face' : source.kind === 'edge' ? 'Edge' : 'Vertex'
  if (!new RegExp(`^${prefix}[1-9]\\d*$`).test(source.persistentId)) throw new Error(`FCStd native Sketch ${sketchId} external geometry '${source.persistentId}' is not a lossless native ${prefix}N reference.`)
  return source.persistentId
}

const topoRefMetadataKey = (source: TopoRefValue) => JSON.stringify({
  schemaVersion: source.schemaVersion,
  objectId: source.objectId,
  kind: source.kind,
  persistentId: source.persistentId,
  topologyVersion: source.topologyVersion,
  generation: source.generation,
  status: source.status,
  signature: source.signature ?? null,
  candidates: source.candidates ?? null,
})

const nativeExternalTypeByMode: Record<SketchExternalMode, number> = { projection: 0, intersection: 1, both: 2 }
const externalModeByNativeType = ['projection', 'intersection', 'both'] as const satisfies readonly SketchExternalMode[]
const nativeExternalFlags = (external: SketchExternalGeometry) =>
  (external.defining ? 1 : 0)
  | (external.frozen ? 2 : 0)
  | (external.detached ? 4 : 0)
  | (external.missing ? 8 : 0)
  | (external.sync ? 16 : 0)

const webExternalStateFromNativeFlags = (flags: number) => ({
  ...(flags & 1 ? { defining: true as const } : {}),
  ...(flags & 2 ? { frozen: true as const } : {}),
  ...(flags & 4 ? { detached: true as const } : {}),
  ...(flags & 8 ? { missing: true as const } : {}),
  ...(flags & 16 ? { sync: true as const } : {}),
})

const sketchExternalPropertiesXml = (sketch: SketchSnapshot, objectIds: ReadonlySet<string>) => {
  type ExternalGroup = { source: TopoRefValue; subElement: string; records: number[]; mode: SketchExternalMode; flags: number }
  const groups: ExternalGroup[] = []
  const groupIndexByReference = new Map<string, number>()
  const externalRecords = sketch.externalGeometry.map((external, index) => {
    const subElement = nativeExternalSubElement(external.source, sketch.id, objectIds)
    const mode = external.mode ?? 'projection'
    const flags = nativeExternalFlags(external)
    if (mode === 'projection' && external.source.kind === 'vertex' && external.projection.type !== 'point') throw new Error(`FCStd native Sketch ${sketch.id} external vertex '${subElement}' requires a point projection.`)
    if (mode === 'projection' && (external.source.kind === 'edge' || external.source.kind === 'face') && external.projection.type === 'point') throw new Error(`FCStd native Sketch ${sketch.id} external ${external.source.kind} '${subElement}' requires a curve projection.`)
    const groupKey = `${external.source.objectId}\0${subElement}`
    let groupIndex = groupIndexByReference.get(groupKey)
    if (groupIndex === undefined) {
      groupIndex = groups.length
      groupIndexByReference.set(groupKey, groupIndex)
      groups.push({ source: external.source, subElement, records: [], mode, flags })
    } else {
      if (topoRefMetadataKey(groups[groupIndex].source) !== topoRefMetadataKey(external.source)) throw new Error(`FCStd native Sketch ${sketch.id} external projections for '${external.source.objectId}.${subElement}' have conflicting TopoRef metadata.`)
      if (groups[groupIndex].mode !== mode || groups[groupIndex].flags !== flags) throw new Error(`FCStd native Sketch ${sketch.id} external projections for '${external.source.objectId}.${subElement}' have conflicting mode or state flags.`)
    }
    groups[groupIndex].records.push(index)
    const nativeId = sketch.geometry.length + index + 1
    const ref = `${external.source.objectId}.${subElement}`
    const geometry = `<Geometry type="${sketchGeometryType[external.projection.type]}" id="${nativeId}" migrated="1"><GeoExtensions count="2"><GeoExtension type="Sketcher::SketchGeometryExtension" id="${nativeId}" internalGeometryType="0" geometryModeFlags="00000000000000000000000000000010" geometryLayer="0"/><GeoExtension type="Sketcher::ExternalGeometryExtension" Ref="${xmlEscape(ref)}" Flags="${flags}" RefIndex="${groupIndex}"/></GeoExtensions>${sketchGeometryPayloadXml(external.projection)}<Construction value="1"/></Geometry>`
    return { external, subElement, groupIndex, geometry }
  })
  for (const group of groups) if (group.mode === 'projection' && group.source.kind !== 'face' && group.records.length !== 1) throw new Error(`FCStd native Sketch ${sketch.id} external ${group.source.kind} '${group.subElement}' must have exactly one projection.`)
  const axes = `${sketchAxisGeometryXml(-1)}${sketchAxisGeometryXml(-2)}`
  const links = groups.map(({ source, subElement }) => `<Link obj="${xmlEscape(source.objectId)}" sub="${xmlEscape(subElement)}"/>`).join('')
  const types = groups.map(({ mode }) => `<I v="${nativeExternalTypeByMode[mode]}"/>`).join('')
  return [
    `<Property name="ExternalGeo" type="Part::PropertyGeometryList" status="67108864"><GeometryList count="${externalRecords.length + 2}">${axes}${externalRecords.map((record) => record.geometry).join('')}</GeometryList></Property>`,
    `<Property name="ExternalGeometry" type="App::PropertyLinkSubList" status="16777216"><LinkSubList count="${groups.length}">${links}</LinkSubList></Property>`,
    `<Property name="ExternalTypes" type="App::PropertyIntegerList" status="67108864"><IntegerList count="${groups.length}">${types}</IntegerList></Property>`,
    propertyXml({ name: 'WebExternalIds', label: 'Web external geometry IDs', group: 'Web', scope: 'data', type: 'App::PropertyStringList', value: externalRecords.map(({ external }) => external.id), hidden: true }, false),
    propertyXml({ name: 'WebExternalProjectionIds', label: 'Web external projection IDs', group: 'Web', scope: 'data', type: 'App::PropertyStringList', value: externalRecords.map(({ external }) => external.projection.id), hidden: true }, false),
    propertyXml({ name: 'WebExternalSources', label: 'Web external TopoRefs', group: 'Web', scope: 'data', type: 'App::PropertyStringList', value: externalRecords.map(({ external }) => encodeURIComponent(JSON.stringify(external.source))), hidden: true }, false),
  ]
}

const nativeAttachmentSupport = (object: DocumentObjectSnapshot, property: ObjectPropertySnapshot, objectIds: ReadonlySet<string>) => {
  if (property.type !== 'App::PropertyLink' && property.type !== 'App::PropertyLinkSub') throw new TypeError(`FCStd native Sketch ${object.id} Support must be an App::PropertyLink or App::PropertyLinkSub.`)
  if (property.expression) throw new Error(`FCStd native Sketch ${object.id} Support expressions are not lossless.`)
  const value = property.value
  if (value === null) return { value, objectId: '', subElement: '', topoRef: undefined }
  let support: AttachmentSupportValue
  if (typeof value === 'string') {
    if (!value.trim()) throw new TypeError(`FCStd native Sketch ${object.id} Support requires a non-empty object id.`)
    support = { objectId: value }
  } else {
    validateAttachmentSupport(value)
    support = value
  }
  if (!objectIds.has(support.objectId)) {
    if (typeof value === 'string') return { value, objectId: '', subElement: '', topoRef: undefined, unresolved: true }
    throw new Error(`FCStd native Sketch ${object.id} Support references undeclared object '${support.objectId}'.`)
  }
  if (support.objectId === object.id) throw new Error(`FCStd native Sketch ${object.id} cannot use itself as Support.`)
  const rawSubElement = support.subElement
  if (rawSubElement === undefined || rawSubElement === null) return { value, objectId: support.objectId, subElement: '', topoRef: undefined }
  if (typeof rawSubElement === 'string') {
    if (!/^(?:Face|Edge|Vertex)[1-9]\d*$/.test(rawSubElement)) throw new Error(`FCStd native Sketch ${object.id} Support '${rawSubElement}' is not a lossless native FaceN/EdgeN/VertexN reference.`)
    return { value, objectId: support.objectId, subElement: rawSubElement, topoRef: undefined }
  }
  const topoRef = rawSubElement as TopoRefValue
  if (topoRef.schemaVersion !== 1 || topoRef.objectId !== support.objectId || !topoRef.persistentId.trim()) throw new TypeError(`FCStd native Sketch ${object.id} Support TopoRef is invalid or targets a different object.`)
  if (!Number.isSafeInteger(topoRef.topologyVersion) || topoRef.topologyVersion < 0 || !Number.isSafeInteger(topoRef.generation) || topoRef.generation < 0) throw new TypeError(`FCStd native Sketch ${object.id} Support TopoRef requires non-negative integer topology versions.`)
  if (topoRef.status !== 'stable') throw new Error(`FCStd native Sketch ${object.id} Support '${topoRef.persistentId}' must have stable topology status.`)
  const prefix = topoRef.kind === 'face' ? 'Face' : topoRef.kind === 'edge' ? 'Edge' : 'Vertex'
  if (!new RegExp(`^${prefix}[1-9]\\d*$`).test(topoRef.persistentId)) throw new Error(`FCStd native Sketch ${object.id} Support '${topoRef.persistentId}' is not a lossless native ${prefix}N reference.`)
  return { value, objectId: support.objectId, subElement: topoRef.persistentId, topoRef }
}

const sketchAttachmentPropertiesXml = (object: DocumentObjectSnapshot, objectIds: ReadonlySet<string>) => {
  if (object.typeId !== 'Sketcher::SketchObject') return []
  const duplicate = object.properties.find((property) => sketchReservedAttachmentPropertyNames.has(property.name))
  if (duplicate) throw new Error(`FCStd native Sketch ${object.id} property ${duplicate.name} is reserved for the native attachment codec.`)
  const supportProperty = object.properties.find((property) => property.name === 'Support')
  const mapModeProperty = object.properties.find((property) => property.name === 'MapMode')
  const offsetProperty = object.properties.find((property) => property.name === 'AttachmentOffset')
  const support = supportProperty ? nativeAttachmentSupport(object, supportProperty, objectIds) : undefined
  let mapMode = 'Deactivated'
  let mapModeXml = ''
  if (mapModeProperty) {
    if (mapModeProperty.type !== 'App::PropertyEnumeration' || mapModeProperty.expression) throw new TypeError(`FCStd native Sketch ${object.id} MapMode must be a non-expression Enumeration.`)
    mapMode = String(mapModeProperty.value)
    if (!(ATTACHMENT_MAP_MODES as readonly string[]).includes(mapMode)) throw new Error(`FCStd native Sketch ${object.id} MapMode '${mapMode}' is not implemented by the Web attachment model.`)
    const index = (freecadAttachmentMapModes as readonly string[]).indexOf(mapMode)
    if (index < 0) throw new Error(`FCStd native Sketch ${object.id} MapMode '${mapMode}' has no native FreeCAD index.`)
    mapModeXml = `<Property name="MapMode" type="App::PropertyEnumeration"><Integer value="${index}"/></Property>`
  }
  if (mapMode !== 'Deactivated' && (!support || !support.objectId)) throw new Error(`FCStd native Sketch ${object.id} MapMode '${mapMode}' requires Support.`)
  if (support?.objectId) {
    if (mapMode === 'FlatFace' && !/^Face[1-9]\d*$/.test(support.subElement)) throw new Error(`FCStd native Sketch ${object.id} FlatFace Support requires a native FaceN sub-element.`)
    if (mapMode === 'NormalToEdge' && !/^Edge[1-9]\d*$/.test(support.subElement)) throw new Error(`FCStd native Sketch ${object.id} NormalToEdge Support requires a native EdgeN sub-element.`)
    if (['ObjectXY', 'ObjectXZ', 'ObjectYZ'].includes(mapMode) && support.subElement) throw new Error(`FCStd native Sketch ${object.id} ${mapMode} Support requires a whole object without a sub-element.`)
  }
  const result: string[] = []
  if (offsetProperty) {
    if (offsetProperty.type !== 'App::PropertyPlacement' || offsetProperty.expression) throw new TypeError(`FCStd native Sketch ${object.id} AttachmentOffset must be a non-expression Placement.`)
    validateAttachmentOffset(offsetProperty.value)
    result.push(propertyXml(offsetProperty, true))
  }
  if (supportProperty && support) {
    const links = support.objectId ? `<Link obj="${xmlEscape(support.objectId)}" sub="${xmlEscape(support.subElement)}"/>` : ''
    if (support.objectId) result.push(`<Property name="AttachmentSupport" type="App::PropertyLinkSubList"><LinkSubList count="1">${links}</LinkSubList></Property>`)
    result.push(propertyXml({ name: 'WebSupportValue', label: 'Web Support value', group: 'Web', scope: 'data', type: 'App::PropertyString', value: encodeURIComponent(JSON.stringify(support.value)), hidden: true }, false))
  }
  if (mapModeXml) result.push(mapModeXml)
  return result
}

const sketchConstraintElement = (geometryIndex: Map<string, number>, geometryById: Map<string, SketchGeometry>, geometryId: string, position = 0, context = 'constraint') => {
  const index = geometryIndex.get(geometryId)
  if (index === undefined) throw new RangeError(`FCStd Sketch ${context} references unknown geometry '${geometryId}'.`)
  return { index, position }
}

const sketchConstraintPoint = (geometryIndex: Map<string, number>, geometryById: Map<string, SketchGeometry>, reference: SketchPointRef, context: string) => {
  const geometry = geometryById.get(reference.geometryId)
  if (!geometry) throw new RangeError(`FCStd Sketch ${context} references unknown geometry '${reference.geometryId}'.`)
  const valid = geometry.type === 'line' ? reference.point === 'start' || reference.point === 'end'
    : geometry.type === 'point' ? reference.point === 'position'
      : geometry.type === 'circle' || geometry.type === 'arc' || geometry.type === 'ellipse' || geometry.type === 'arcEllipse' || geometry.type === 'arcHyperbola' || geometry.type === 'arcParabola' ? reference.point === 'center'
        : false
  if (!valid) throw new RangeError(`FCStd Sketch ${context} point '${reference.point}' is invalid for ${geometry.type} '${geometry.id}'.`)
  return sketchConstraintElement(geometryIndex, geometryById, reference.geometryId, sketchPointPosition[reference.point], context)
}

const sketchConstraintListXml = (sketch: SketchSnapshot, internalHelpers: SketchInternalGeometryHelper[]) => {
  const geometryIndex = new Map<string, number>(sketch.geometry.map((geometry, index) => [geometry.id, index]))
  const geometryById = new Map<string, SketchGeometry>(sketch.geometry.map((geometry) => [geometry.id, geometry]))
  const helperByKey = new Map(internalHelpers.map((helper) => [helper.key, helper]))
  sketch.externalGeometry.forEach((external, index) => {
    geometryIndex.set(external.projection.id, -index - 3)
    geometryById.set(external.projection.id, external.projection)
  })
  const internalAlignmentXml = (helper: SketchInternalGeometryHelper, id: string, driving = true) => {
    const targetIndex = geometryIndex.get(helper.targetGeometryId)
    if (targetIndex === undefined) throw new Error(`FCStd native Sketch internal helper references unknown geometry '${helper.targetGeometryId}'.`)
    return `<Constrain Name="${xmlEscape(id)}" Type="15" InternalAlignmentType="${helper.alignmentType}" InternalAlignmentIndex="${helper.internalAlignmentIndex}" Value="0" LabelDistance="10" LabelPosition="0" IsDriving="${driving ? 1 : 0}" IsInVirtualSpace="0" IsVisible="1" IsActive="1" First="${helper.nativeIndex}" FirstPos="${helper.firstPosition}" Second="${targetIndex}" SecondPos="0" Third="-2000" ThirdPos="0" ElementIds="${helper.nativeIndex} ${targetIndex} -2000" ElementPositions="${helper.firstPosition} 0 0"/>`
  }
  const syntheticConstraints = internalHelpers.filter((helper) => helper.syntheticAlignmentId).map((helper) => internalAlignmentXml(helper, helper.syntheticAlignmentId!))
  const constraints = sketch.constraints.map((constraint) => {
    if (constraint.type === 'snellsLaw' && !('boundaryGeometryId' in constraint)) throw new Error(`FCStd native Sketch SnellsLaw constraint '${constraint.id}' requires two endpoint references and a boundary geometry for lossless export.`)
    if (constraint.driving === false && !('value' in constraint)) throw new Error(`FCStd native Sketch reference constraint '${constraint.id}' must be dimensional.`)
    const elements: Array<{ index: number; position: number }> = []
    if (constraint.type === 'coincident' || constraint.type === 'distance' || constraint.type === 'distanceX' || constraint.type === 'distanceY') {
      elements.push(sketchConstraintPoint(geometryIndex, geometryById, constraint.first, constraint.id), sketchConstraintPoint(geometryIndex, geometryById, constraint.second, constraint.id))
    } else if (constraint.type === 'horizontal' || constraint.type === 'vertical' || constraint.type === 'radius' || constraint.type === 'diameter' || constraint.type === 'angle' || constraint.type === 'block') {
      elements.push(sketchConstraintElement(geometryIndex, geometryById, constraint.geometryId, 0, constraint.id))
    } else if (constraint.type === 'parallel' || constraint.type === 'perpendicular' || constraint.type === 'equal' || constraint.type === 'tangent') {
      elements.push(sketchConstraintElement(geometryIndex, geometryById, constraint.firstGeometryId, 0, constraint.id), sketchConstraintElement(geometryIndex, geometryById, constraint.secondGeometryId, 0, constraint.id))
    } else if (constraint.type === 'pointOnObject') {
      elements.push(sketchConstraintPoint(geometryIndex, geometryById, constraint.point, constraint.id), sketchConstraintElement(geometryIndex, geometryById, constraint.geometryId, 0, constraint.id))
    } else if (constraint.type === 'symmetric') {
      elements.push(sketchConstraintPoint(geometryIndex, geometryById, constraint.first, constraint.id), sketchConstraintPoint(geometryIndex, geometryById, constraint.second, constraint.id), sketchConstraintPoint(geometryIndex, geometryById, constraint.center, constraint.id))
    } else if (constraint.type === 'snellsLaw' && 'boundaryGeometryId' in constraint) {
      if (geometryById.get(constraint.first.geometryId)?.type === 'point' || geometryById.get(constraint.second.geometryId)?.type === 'point' || geometryById.get(constraint.boundaryGeometryId)?.type === 'point') throw new Error(`FCStd native Sketch SnellsLaw constraint '${constraint.id}' requires curve geometries.`)
      elements.push(sketchConstraintPoint(geometryIndex, geometryById, constraint.first, constraint.id), sketchConstraintPoint(geometryIndex, geometryById, constraint.second, constraint.id), sketchConstraintElement(geometryIndex, geometryById, constraint.boundaryGeometryId, 0, constraint.id))
    } else if (constraint.type === 'internalAlignment') {
      const helper = internalHelpers.find((candidate) => candidate.explicitAlignmentId === constraint.id)
      if (!helper) throw new Error(`FCStd native Sketch InternalAlignment '${constraint.id}' has no internal helper geometry.`)
      return internalAlignmentXml(helper, constraint.id, constraint.driving !== false)
    } else if (constraint.type === 'weight') {
      const helper = helperByKey.get(`${constraint.geometryId}\0bspline-control-point\0${constraint.controlPointIndex}`)
      if (!helper) throw new Error(`FCStd native Sketch Weight '${constraint.id}' has no internal helper geometry.`)
      elements.push({ index: helper.nativeIndex, position: 0 })
    }
    while (elements.length < 3) elements.push({ index: -2000, position: 0 })
    const value = 'value' in constraint ? finiteComponent(constraint.value, `Sketch constraint ${constraint.id}.value`) : 0
    const indices = elements.map((element) => element.index).join(' ')
    const positions = elements.map((element) => element.position).join(' ')
    return `<Constrain Name="${xmlEscape(constraint.id)}" Type="${sketchConstraintType[constraint.type]}" Value="${value}" LabelDistance="10" LabelPosition="0" IsDriving="${constraint.driving === false ? 0 : 1}" IsInVirtualSpace="0" IsVisible="1" IsActive="1" First="${elements[0].index}" FirstPos="${elements[0].position}" Second="${elements[1].index}" SecondPos="${elements[1].position}" Third="${elements[2].index}" ThirdPos="${elements[2].position}" ElementIds="${indices}" ElementPositions="${positions}"/>`
  }).join('')
  return `<Property name="Constraints" type="Sketcher::PropertyConstraintList"><ConstraintList count="${sketch.constraints.length + syntheticConstraints.length}">${syntheticConstraints.join('')}${constraints}</ConstraintList></Property>`
}

const sketchNativePropertiesXml = (object: DocumentObjectSnapshot, objectIds: ReadonlySet<string>) => {
  if (!object.sketch) return []
  if (object.typeId !== 'Sketcher::SketchObject') throw new TypeError(`FCStd object ${object.id} has Sketch data but is not a Sketcher::SketchObject.`)
  validateSketchSnapshot(object.sketch)
  if (object.sketch.id !== object.id) throw new Error(`FCStd native Sketch ${object.id} snapshot id must match its object id.`)
  const internalHelpers = sketchInternalGeometryPlan(object.sketch)
  return [
    ...sketchAttachmentPropertiesXml(object, objectIds),
    sketchGeometryListXml(object.sketch, internalHelpers),
    sketchConstraintListXml(object.sketch, internalHelpers),
    ...sketchExternalPropertiesXml(object.sketch, objectIds),
    propertyXml({ name: 'WebGeometryIds', label: 'Web geometry IDs', group: 'Web', scope: 'data', type: 'App::PropertyStringList', value: object.sketch.geometry.map((geometry) => geometry.id), hidden: true }, false),
    propertyXml({ name: 'WebSyntheticConstraintIds', label: 'Web synthetic constraint IDs', group: 'Web', scope: 'data', type: 'App::PropertyStringList', value: internalHelpers.flatMap((helper) => helper.syntheticAlignmentId ? [helper.syntheticAlignmentId] : []), hidden: true }, false),
  ]
}

const isNativeDataProperty = (object: DocumentObjectSnapshot, property: ObjectPropertySnapshot) => {
  const typeId = nativeFcstdTypeId(object.typeId)
  if (property.name === 'Label') return recognizedTypeIds.has(typeId)
  if (property.name === 'Placement' && /^(?:Part|PartDesign|Sketcher)::/.test(typeId)) return true
  return nativeDataPropertyNames.get(typeId)?.has(property.name) ?? false
}

const pathCommandName = (value: unknown, context: string) => {
  if (typeof value !== 'string' || !/^[A-Z][A-Z0-9]*$/i.test(value.trim())) throw new TypeError(`FCStd Path command ${context} requires an alphanumeric name.`)
  return value.trim().toUpperCase()
}

const pathCommands = (value: unknown, context: string): PathCommandValue[] => {
  if (!Array.isArray(value) || value.length === 0) throw new TypeError(`FCStd Path property ${context} requires a non-empty command list.`)
  return value.map((candidate, index) => {
    if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) throw new TypeError(`FCStd Path command ${context}[${index}] is malformed.`)
    const record = candidate as Record<string, unknown>
    const name = pathCommandName(record.name, `${context}[${index}].name`)
    if (!record.parameters || typeof record.parameters !== 'object' || Array.isArray(record.parameters)) throw new TypeError(`FCStd Path command ${context}[${index}] parameters are malformed.`)
    const parameters: Record<string, number> = {}
    for (const [rawKey, rawValue] of Object.entries(record.parameters as Record<string, unknown>)) {
      const key = rawKey.trim().toUpperCase()
      if (!/^[A-Z]$/.test(key) || typeof rawValue !== 'number' || !Number.isFinite(rawValue)) throw new TypeError(`FCStd Path command ${context}[${index}] contains an invalid parameter.`)
      if (Object.prototype.hasOwnProperty.call(parameters, key)) throw new TypeError(`FCStd Path command ${context}[${index}] contains duplicate parameter ${key}.`)
      parameters[key] = Object.is(rawValue, -0) ? 0 : rawValue
    }
    return { name, parameters }
  })
}

const pathNumber = (value: number) => {
  if (!Number.isFinite(value)) throw new TypeError('FCStd Path coordinates must be finite.')
  return value.toFixed(6).replace(/0+$/, '').replace(/\.$/, '') || '0'
}

const pathResourceText = (commands: PathCommandValue[]) => commands.map((command) => {
  const words = Object.entries(command.parameters).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}${pathNumber(value)}`)
  return `${command.name}${words.length > 0 ? ` ${words.join(' ')}` : ''}`
}).join('\n') + '\n'

const pathPropertyValue = (value: unknown, context: string): PathPropertyValue => {
  if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError(`FCStd Path property ${context} requires a structured value.`)
  const record = value as Record<string, unknown>
  if (record.schemaVersion !== 1) throw new TypeError(`FCStd Path property ${context} schemaVersion must be 1.`)
  const commands = pathCommands(record.commands, `${context}.commands`)
  const resourcePath = record.resourcePath === undefined ? undefined : String(record.resourcePath)
  if (resourcePath !== undefined) validateEntryPath(resourcePath)
  const version = record.version === undefined ? 2 : record.version
  if (typeof version !== 'number' || !Number.isSafeInteger(version) || version <= 0) throw new TypeError(`FCStd Path property ${context}.version must be a positive integer.`)
  const center = record.center === undefined ? undefined : record.center
  if (center !== undefined) {
    if (!center || typeof center !== 'object' || Array.isArray(center)) throw new TypeError(`FCStd Path property ${context}.center is malformed.`)
    for (const axis of ['x', 'y', 'z']) if (typeof (center as Record<string, unknown>)[axis] !== 'number' || !Number.isFinite((center as Record<string, unknown>)[axis])) throw new TypeError(`FCStd Path property ${context}.center.${axis} must be finite.`)
  }
  return { schemaVersion: 1, commands, ...(resourcePath === undefined ? {} : { resourcePath }), version, ...(center === undefined ? {} : { center: center as PathPropertyValue['center'] }) }
}

const nativeLinkTopoRefName = (value: TopoRefValue, context: string, expectedObjectId: string) => {
  if (value.schemaVersion !== 1 || value.objectId !== expectedObjectId || !value.persistentId.trim()) throw new TypeError(`${context} TopoRef is invalid or targets a different object.`)
  if (!Number.isSafeInteger(value.topologyVersion) || value.topologyVersion < 0 || !Number.isSafeInteger(value.generation) || value.generation < 0) throw new TypeError(`${context} TopoRef requires non-negative integer topology versions.`)
  if (value.status !== 'stable') throw new Error(`${context} '${value.persistentId}' must have stable topology status.`)
  const prefix = value.kind === 'face' ? 'Face' : value.kind === 'edge' ? 'Edge' : 'Vertex'
  if (!new RegExp(`^${prefix}[1-9]\\d*$`).test(value.persistentId)) throw new Error(`${context} '${value.persistentId}' is not a lossless native ${prefix}N reference.`)
  return value.persistentId
}

const propertyXml = (property: ObjectPropertySnapshot, native: boolean, objectId?: string, topologyElementMap?: ElementMapSnapshot, colorListResource?: string) => {
  if (!native && property.nativeStatus !== undefined && !hasFreecadPropertyStatus(property.nativeStatus, 'PropDynamic')) throw new Error(`FCStd dynamic property ${property.name} requires PropDynamic in its native Property status mask.`)
  const defaultNativeStatus = property.type === 'Part::PropertyPartShape' ? 1 : property.type === 'App::PropertyPlacement' ? 8388608 : undefined
  const nativeStatus = property.nativeStatus === undefined ? defaultNativeStatus : normalizeFreecadPropertyStatusMask(property.nativeStatus)
  const statusAttribute = nativeStatus === undefined ? '' : ` status="${nativeStatus}"`
  const attributes = native ? statusAttribute : ` group="${xmlEscape(property.group ?? '')}" doc="${xmlEscape(property.label ?? property.name)}" attr="0" ro="${property.readOnly ? 1 : 0}" hide="${property.hidden ? 1 : 0}"${statusAttribute}`
  if (isFreecadPropertyTransient(nativeStatus)) return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}></Property>`
  if (property.type === 'Path::PropertyPath') {
    const value = pathPropertyValue(property.value, property.name)
    const resourcePath = value.resourcePath ?? `${objectId ?? property.name}.nc`
    validateEntryPath(resourcePath)
    const center = value.center ?? { x: 0, y: 0, z: 0 }
    return `<Property name="${xmlEscape(property.name)}" type="Path::PropertyPath"${attributes}><Path file="${xmlEscape(resourcePath)}" version="${value.version ?? 2}"><Center x="${pathNumber(center.x)}" y="${pathNumber(center.y)}" z="${pathNumber(center.z)}"/></Path></Property>`
  }
  if (property.type === 'App::PropertyEnumeration') {
    if (!Array.isArray(property.options) || property.options.length === 0 || property.options.some((option) => typeof option !== 'string') || new Set(property.options).size !== property.options.length) throw new TypeError(`FCStd Enumeration property ${property.name} requires unique string options.`)
    const index = property.options.indexOf(String(property.value))
    if (index < 0) throw new TypeError(`FCStd Enumeration property ${property.name} requires a value from its options.`)
    const custom = native ? '' : ' CustomEnum="true"'
    const customList = native ? '' : `<CustomEnumList count="${property.options.length}">${property.options.map((option) => `<Enum value="${xmlEscape(option)}"/>`).join('')}</CustomEnumList>`
    return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><Integer value="${index}"${custom}/>${customList}</Property>`
  }
  if (property.type === 'App::PropertyVector' || property.type === 'App::PropertyDirection') {
    if (!property.value || typeof property.value !== 'object' || Array.isArray(property.value) || !('x' in property.value)) throw new TypeError(`FCStd Vector property ${property.name} requires a vector.`)
    const x = finiteComponent(property.value.x, `${property.name}.x`)
    const y = finiteComponent(property.value.y, `${property.name}.y`)
    const z = finiteComponent(property.value.z, `${property.name}.z`)
    return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><PropertyVector valueX="${x}" valueY="${y}" valueZ="${z}"/></Property>`
  }
  if (property.type === 'App::PropertyPlacement') {
    if (!property.value || typeof property.value !== 'object' || Array.isArray(property.value) || !('position' in property.value) || !('rotation' in property.value)) throw new TypeError(`FCStd Placement property ${property.name} requires a placement.`)
    const px = finiteComponent(property.value.position.x, `${property.name}.position.x`)
    const py = finiteComponent(property.value.position.y, `${property.name}.position.y`)
    const pz = finiteComponent(property.value.position.z, `${property.name}.position.z`)
    const ox = finiteComponent(property.value.rotation.axis.x, `${property.name}.rotation.axis.x`)
    const oy = finiteComponent(property.value.rotation.axis.y, `${property.name}.rotation.axis.y`)
    const oz = finiteComponent(property.value.rotation.axis.z, `${property.name}.rotation.axis.z`)
    const axisLength = Math.hypot(ox, oy, oz)
    if (axisLength === 0) throw new RangeError(`FCStd Placement property ${property.name} rotation axis must be non-zero.`)
    const angle = finiteComponent(property.value.rotation.angle, `${property.name}.rotation.angle`) * Math.PI / 180
    const axis = [ox / axisLength, oy / axisLength, oz / axisLength]
    const sine = Math.sin(angle / 2)
    const quaternion = [axis[0] * sine, axis[1] * sine, axis[2] * sine, Math.cos(angle / 2)]
    return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><PropertyPlacement Px="${px}" Py="${py}" Pz="${pz}" Q0="${quaternion[0]}" Q1="${quaternion[1]}" Q2="${quaternion[2]}" Q3="${quaternion[3]}" A="${angle}" Ox="${axis[0]}" Oy="${axis[1]}" Oz="${axis[2]}"/></Property>`
  }
  if (property.type === 'App::PropertyLink' || property.type === 'App::PropertyLinkHidden') {
    if (property.value !== null && typeof property.value !== 'string') throw new TypeError(`FCStd Link property ${property.name} requires an object name.`)
    return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><Link value="${xmlEscape(property.value ?? '')}"/></Property>`
  }
  if (property.type === 'App::PropertyLinkList') {
    if (!Array.isArray(property.value) || property.value.some((entry) => typeof entry !== 'string')) throw new TypeError(`FCStd LinkList property ${property.name} requires an object-name list.`)
    const links = (property.value as string[]).map((entry) => `<Link value="${xmlEscape(entry)}"/>`).join('')
    return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><LinkList count="${property.value.length}">${links}</LinkList></Property>`
  }
  if (property.type === 'App::PropertyLinkSubList') {
    if (!property.value || typeof property.value !== 'object' || Array.isArray(property.value) || !('entries' in property.value) || property.value.schemaVersion !== 1 || !Array.isArray(property.value.entries)) throw new TypeError(`FCStd LinkSubList property ${property.name} requires a structured value.`)
    const links = property.value.entries.map((entry) => {
      if (!entry || typeof entry.objectId !== 'string' || !entry.objectId.trim()) throw new TypeError(`FCStd LinkSubList property ${property.name} requires non-empty object ids.`)
      const subElement = entry.subElement === null ? '' : typeof entry.subElement === 'string' ? entry.subElement : nativeLinkTopoRefName(entry.subElement, `FCStd LinkSubList property ${property.name}`, entry.objectId)
      if (entry.subElement !== null && !subElement.trim()) throw new TypeError(`FCStd LinkSubList property ${property.name} contains an empty sub-element.`)
      return `<Link obj="${xmlEscape(entry.objectId)}" sub="${xmlEscape(subElement)}"/>`
    }).join('')
    return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><LinkSubList count="${property.value.entries.length}">${links}</LinkSubList></Property>`
  }
  if (property.type === 'Part::PropertyPartShape') {
    if (!property.value || typeof property.value !== 'object' || Array.isArray(property.value) || !('path' in property.value) || typeof property.value.path !== 'string' || !property.value.path.trim()) throw new TypeError(`FCStd Shape property ${property.name} requires a resource path.`)
    validateEntryPath(property.value.path)
    if (!/\.(?:brp|brep)$/i.test(property.value.path)) throw new TypeError(`FCStd Shape property ${property.name} requires a .brp or .brep resource.`)
    const hasherIndex = property.value.hasherIndex
    if (hasherIndex !== undefined && (!Number.isSafeInteger(hasherIndex) || hasherIndex < 0)) throw new RangeError(`FCStd Shape property ${property.name} requires a non-negative HasherIndex.`)
    const hasherIndexAttribute = hasherIndex === undefined ? '' : ` HasherIndex="${hasherIndex}"`
    const elementMap = property.value.elementMap ? ` ElementMap="${xmlEscape(property.value.elementMap)}"` : ' ElementMap=""'
    const generatedMapEntries = objectId && topologyElementMap ? topologyElementMap.entries.filter((entry) => entry.objectId === objectId && (entry.status === 'stable' || entry.status === 'new')).map((entry) => ({ key: entry.candidates?.length === 1 && entry.candidates[0].name ? entry.candidates[0].name : entry.name, value: entry.name })) : undefined
    const mapEntries = property.value.elementMapEntries ?? generatedMapEntries
    if (mapEntries !== undefined && (!Array.isArray(mapEntries) || mapEntries.some((entry) => !entry || typeof entry.key !== 'string' || !entry.key.trim() || typeof entry.value !== 'string'))) throw new TypeError(`FCStd Shape property ${property.name} ElementMap entries require non-empty keys and string values.`)
    const elementMapXml = mapEntries === undefined ? '<ElementMap/>' : `<ElementMap new="1" count="${mapEntries.length}">${mapEntries.map((entry) => `<Element key="${xmlEscape(entry.key)}" value="${xmlEscape(entry.value)}"/>`).join('')}</ElementMap>`
    const mapResource = property.value.elementMapResource
    if (mapResource !== undefined) {
      if (typeof mapResource !== 'string' || !mapResource.trim()) throw new TypeError(`FCStd Shape property ${property.name} requires a non-empty ElementMap2 resource path.`)
      validateEntryPath(mapResource)
      if (!/\.Map\.txt$/i.test(mapResource)) throw new TypeError(`FCStd Shape property ${property.name} requires an ElementMap2 .Map.txt resource.`)
    }
    const elementMap2 = mapResource === undefined ? '' : `<ElementMap2 file="${xmlEscape(mapResource)}"/>`
    return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><Part${hasherIndexAttribute}${elementMap} file="${xmlEscape(property.value.path)}"/>${elementMapXml}${elementMap2}</Property>`
  }
  if (property.type === 'App::PropertyLinkSub') {
    let objectId = ''
    let subElements: string[] = []
    const value = property.value
    if (value !== null && typeof value === 'object' && !Array.isArray(value) && 'objectId' in value && typeof value.objectId === 'string') {
      objectId = value.objectId
      if ('subElements' in value && Array.isArray(value.subElements)) subElements = value.subElements.map((entry) => typeof entry === 'string' ? entry : nativeLinkTopoRefName(entry, `FCStd LinkSub property ${property.name}`, objectId))
      else if ('persistentId' in value && typeof value.persistentId === 'string') subElements = [nativeLinkTopoRefName(value as TopoRefValue, `FCStd LinkSub property ${property.name}`, objectId)]
      else if ('subElement' in value && value.subElement) subElements = [typeof value.subElement === 'string' ? value.subElement : nativeLinkTopoRefName(value.subElement, `FCStd LinkSub property ${property.name}`, objectId)]
    } else if (value !== null) throw new TypeError(`FCStd LinkSub property ${property.name} requires a structured value.`)
    if (subElements.some((entry) => !entry.trim())) throw new TypeError(`FCStd LinkSub property ${property.name} contains an empty sub-element.`)
    const subs = subElements.map((entry) => `<Sub value="${xmlEscape(entry)}"/>`).join('')
    return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><LinkSub value="${xmlEscape(objectId)}" count="${subElements.length}">${subs}</LinkSub></Property>`
  }
  if (property.type === 'App::PropertyFloatList') {
    if (!Array.isArray(property.value) || !property.value.every((entry) => typeof entry === 'number' && Number.isFinite(entry))) throw new TypeError(`FCStd FloatList property ${property.name} requires a finite numeric list.`)
    const values = (property.value as number[]).map((entry) => `<Float value="${entry}"/>`).join('')
    return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><FloatList count="${property.value.length}">${values}</FloatList></Property>`
  }
  if (property.type === 'App::PropertyIntegerList') {
    if (!Array.isArray(property.value) || !property.value.every((entry) => typeof entry === 'number' && Number.isSafeInteger(entry))) throw new TypeError(`FCStd IntegerList property ${property.name} requires a safe-integer list.`)
    const values = (property.value as number[]).map((entry) => `<I v="${entry}"/>`).join('')
    return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><IntegerList count="${property.value.length}">${values}</IntegerList></Property>`
  }
  if (property.type === 'App::PropertyIntegerSet') {
    if (!Array.isArray(property.value) || !property.value.every((entry) => typeof entry === 'number' && Number.isSafeInteger(entry))) throw new TypeError(`FCStd IntegerSet property ${property.name} requires a safe-integer array.`)
    const normalized = [...new Set(property.value as number[])].sort((left, right) => left - right)
    const values = normalized.map((entry) => `<I v="${entry}"/>`).join('')
    return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><IntegerSet count="${normalized.length}">${values}</IntegerSet></Property>`
  }
  if (property.type === 'App::PropertyStringList') {
    if (!Array.isArray(property.value) || !property.value.every((entry) => typeof entry === 'string')) throw new TypeError(`FCStd StringList property ${property.name} requires a string list.`)
    const values = (property.value as string[]).map((entry) => `<String value="${xmlEscape(entry)}"/>`).join('')
    return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><StringList count="${property.value.length}">${values}</StringList></Property>`
  }
  if (property.type === 'App::PropertyBoolList') {
    if (!Array.isArray(property.value) || !property.value.every((entry) => typeof entry === 'boolean')) throw new TypeError(`FCStd BoolList property ${property.name} requires a boolean list.`)
    // FreeCAD serializes dynamic_bitset low-to-high values as a reversed bit string.
    const bitset = [...(property.value as boolean[])].reverse().map((entry) => entry ? '1' : '0').join('')
    return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><BoolList value="${bitset}"/></Property>`
  }
  if (property.type === 'App::PropertyColorList') {
    const value = colorListValue(property.value, property.name)
    if (value.length === 0) return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><ColorList file=""/></Property>`
    if (!colorListResource) throw new Error(`FCStd ColorList property ${property.name} requires an external resource path.`)
    validateEntryPath(colorListResource)
    return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><ColorList file="${xmlEscape(colorListResource)}"/></Property>`
  }
  if (property.type === 'App::PropertyFileIncluded') {
    if (property.value === null) return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><FileIncluded file=""/></Property>`
    const value = validateIncludedFileValue(property.value, `FCStd PropertyFileIncluded ${property.name}`)
    return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><FileIncluded file="${xmlEscape(value.archiveName)}"/></Property>`
  }
  if (property.type === 'App::PropertyColor') return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><PropertyColor value="${colorChannelsToPacked(property.value, property.name)}"/></Property>`
  if (property.type === 'App::PropertyString') {
    if (typeof property.value !== 'string') throw new TypeError(`FCStd String property ${property.name} requires a string.`)
    return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><String value="${xmlEscape(property.value)}"/></Property>`
  }
  if (property.type === 'App::PropertyFont') return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><String value="${xmlEscape(propertyFontValue(property.value, property.name))}"/></Property>`
  if (property.type === 'App::PropertyFile') return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><String value="${xmlEscape(propertyFileValue(property.value, property.name))}"/></Property>`
  if (property.type === 'App::PropertyBool') {
    if (typeof property.value !== 'boolean') throw new TypeError(`FCStd Bool property ${property.name} requires a boolean.`)
    return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><Bool value="${property.value ? 'true' : 'false'}"/></Property>`
  }
  if (property.type === 'App::PropertyLength' || property.type === 'App::PropertyDistance' || property.type === 'App::PropertyAngle' || property.type === 'App::PropertyAcceleration' || property.type === 'App::PropertyArea' || property.type === 'App::PropertyForce' || property.type === 'App::PropertyHeatFlux' || property.type === 'App::PropertyQuantityConstraint' || property.type === 'App::PropertyFloat' || property.type === 'App::PropertyFloatConstraint' || property.type === 'App::PropertyPrecision') {
    if (typeof property.value !== 'number' || !Number.isFinite(property.value)) throw new TypeError(`FCStd numeric property ${property.name} requires a finite number.`)
    return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><Float value="${property.value}"/></Property>`
  }
  if (property.type === 'App::PropertyInteger' || property.type === 'App::PropertyIntegerConstraint' || property.type === 'App::PropertyPercent') {
    if (typeof property.value !== 'number' || !Number.isSafeInteger(property.value)) throw new TypeError(`FCStd integer property ${property.name} requires a safe integer.`)
    if (property.type === 'App::PropertyPercent' && (property.value < 0 || property.value > 100)) throw new RangeError(`FCStd Percent property ${property.name} must be between 0 and 100.`)
    return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><Integer value="${property.value}"/></Property>`
  }
  const value = typeof property.value === 'string' ? property.value : JSON.stringify(property.value)
  const element = ({
    'App::PropertyLink': 'Link',
    'App::PropertyLinkSub': 'LinkSub',
    'App::PropertyLinkList': 'LinkList',
    'App::PropertyVector': 'PropertyVector',
    'App::PropertyDirection': 'PropertyVector',
    'App::PropertyPlacement': 'PropertyPlacement',
    'App::PropertyMultiTransform': 'MultiTransform',
  } as Record<string, string>)[property.type] ?? 'String'
  return `<Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}"${attributes}><${element} value="${xmlEscape(value)}"/></Property>`
}

const expressionEngineXml = (object: DocumentObjectSnapshot) => {
  const expressions = object.properties.filter((property) => property.scope !== 'view' && typeof property.expression === 'string' && property.expression.trim()).map((property) => ({ path: property.name, expression: property.expression as string }))
  if (expressions.length === 0) return ''
  const entries = expressions.map((entry) => `<Expression path="${xmlEscape(entry.path)}" expression="${xmlEscape(entry.expression)}"/>`).join('')
  return `<Property name="ExpressionEngine" type="App::PropertyExpressionEngine"><ExpressionEngine count="${expressions.length}">${entries}</ExpressionEngine></Property>`
}

const shapeResourceReferences = (document: DocumentSnapshot): string[] => document.objects.flatMap((object) => object.properties.flatMap((property) => {
  if (isFreecadPropertyPersistenceSuppressed(property.nativeStatus)) return []
  if (property.type !== 'Part::PropertyPartShape' || !property.value || typeof property.value !== 'object' || Array.isArray(property.value) || !('path' in property.value) || typeof property.value.path !== 'string') return []
  return [property.value.path]
}))

const validateNativeShapeResourceName = (objectName: string, resourcePath: string) => {
  const basename = resourcePath.slice(resourcePath.lastIndexOf('/') + 1)
  if (/\.Shape\.(?:brp|brep)$/i.test(basename) && basename.slice(0, basename.lastIndexOf('.Shape.')) !== objectName) throw new Error(`FCStd Shape resource ${resourcePath} must use ${objectName}.Shape.brp or ${objectName}.Shape.brep for native FreeCAD restore.`)
}

const elementMapResourceReferences = (document: DocumentSnapshot): string[] => document.objects.flatMap((object) => object.properties.flatMap((property) => {
  if (isFreecadPropertyPersistenceSuppressed(property.nativeStatus)) return []
  if (property.type !== 'Part::PropertyPartShape' || !property.value || typeof property.value !== 'object' || Array.isArray(property.value) || !('elementMapResource' in property.value) || typeof property.value.elementMapResource !== 'string' || !property.value.elementMapResource.trim()) return []
  return [property.value.elementMapResource]
}))

const pathResourceReferences = (document: DocumentSnapshot): Array<{ objectId: string; path: string; value: PathPropertyValue }> => document.objects.flatMap((object) => object.properties.flatMap((property) => {
  if (isFreecadPropertyPersistenceSuppressed(property.nativeStatus)) return []
  if (property.type !== 'Path::PropertyPath') return []
  const value = pathPropertyValue(property.value, `${object.id}.${property.name}`)
  const path = value.resourcePath ?? `${object.id}.nc`
  validateEntryPath(path)
  return [{ objectId: object.id, path, value }]
}))

const colorListResourceReferences = (document: DocumentSnapshot): Array<{ objectId: string; propertyName: string; path: string; value: ColorListValue }> => {
  const used = new Set<string>()
  return document.objects.flatMap((object) => object.properties.flatMap((property) => {
    if (property.scope !== 'data' || isFreecadPropertyPersistenceSuppressed(property.nativeStatus) || property.type !== 'App::PropertyColorList') return []
    const value = colorListValue(property.value, `${object.id}.${property.name}`)
    if (value.length === 0) return []
    const base = property.name
    let path = base
    let suffix = 1
    while (used.has(path)) path = `${base}${suffix++}`
    used.add(path)
    return [{ objectId: object.id, propertyName: property.name, path, value }]
  }))
}

const includedFileResourceReferences = (document: DocumentSnapshot): Array<{ objectId: string; propertyName: string; value: IncludedFileValue }> => document.objects.flatMap((object) => object.properties.flatMap((property) => {
  if (property.scope !== 'data' || isFreecadPropertyPersistenceSuppressed(property.nativeStatus) || property.type !== 'App::PropertyFileIncluded' || property.value === null) return []
  return [{ objectId: object.id, propertyName: property.name, value: validateIncludedFileValue(property.value, `${object.id}.${property.name}`) }]
}))

/**
 * Writes the metadata portion of an FCStd archive while keeping unknown files opaque.
 * Shape/BRep payloads and FreeCAD feature-specific serializers remain separate gates.
 */
export const serializeFcstdMetadataArchive = (document: DocumentSnapshot, options: FcstdWriteOptions = {}): Uint8Array => {
  if (!document.id.trim() || !document.label.trim()) throw new TypeError('FCStd metadata writer requires document id and label.')
  const objectIds = document.objects.map((object) => object.id.trim())
  if (objectIds.some((objectId) => !objectId)) throw new TypeError('FCStd metadata writer requires non-empty object ids.')
  if (new Set(objectIds).size !== objectIds.length) throw new TypeError('FCStd metadata writer requires unique object ids.')
  const explicitObjectTags = new Set<number>()
  for (const object of document.objects) {
    if (object.nativeObjectTag === undefined) continue
    if (!Number.isSafeInteger(object.nativeObjectTag) || object.nativeObjectTag <= 0) throw new RangeError(`FCStd object ${object.id} nativeObjectTag must be a positive safe integer.`)
    if (explicitObjectTags.has(object.nativeObjectTag)) throw new RangeError(`FCStd nativeObjectTag ${object.nativeObjectTag} is assigned to more than one object.`)
    explicitObjectTags.add(object.nativeObjectTag)
  }
  let nextObjectTag = 1
  const objectTags = new Map(document.objects.map((object) => {
    if (object.nativeObjectTag !== undefined) return [object.id, object.nativeObjectTag] as const
    while (explicitObjectTags.has(nextObjectTag)) nextObjectTag += 1
    const tag = nextObjectTag
    explicitObjectTags.add(tag)
    nextObjectTag += 1
    return [object.id, tag] as const
  }))
  const objectIdSet = new Set(objectIds)
  const dependencies = new Map(objectIds.map((objectId) => [objectId, [] as string[]]))
  for (const edge of document.dependencies ?? []) {
    if (!objectIdSet.has(edge.sourceId) || !objectIdSet.has(edge.targetId)) throw new Error(`FCStd dependency ${edge.sourceId} -> ${edge.targetId} references an undeclared object.`)
    if (edge.relation !== 'view') dependencies.get(edge.sourceId)?.push(edge.targetId)
  }
  const colorListResources = colorListResourceReferences(document)
  const colorListResourceByProperty = new Map(colorListResources.map((resource) => [`${resource.objectId}\0${resource.propertyName}`, resource.path]))
  const includedFileReferences = includedFileResourceReferences(document)
  const includedFiles = new Map<string, IncludedFileValue>()
  for (const reference of includedFileReferences) {
    const existing = includedFiles.get(reference.value.archiveName)
    if (existing && (existing.resourceHash !== reference.value.resourceHash || existing.byteLength !== reference.value.byteLength || existing.mediaType !== reference.value.mediaType)) throw new Error(`FCStd PropertyFileIncluded archive name ${reference.value.archiveName} refers to conflicting project resources.`)
    includedFiles.set(reference.value.archiveName, reference.value)
  }
  const objectDeclarations = document.objects.map((object) => {
    const targets = dependencies.get(object.id) ?? []
    const dependencyXml = targets.length === 0 ? `<ObjectDeps Name="${xmlEscape(object.id)}" Count="0"/>` : `<ObjectDeps Name="${xmlEscape(object.id)}" Count="${targets.length}">${targets.map((target) => `<Dep Name="${xmlEscape(target)}"/>`).join('')}</ObjectDeps>`
    return `${dependencyXml}<Object name="${xmlEscape(object.id)}" type="${xmlEscape(nativeFcstdTypeId(object.typeId))}" id="${objectTags.get(object.id)}" Touched="1"/>`
  }).join('')
  const objectData = document.objects.map((object) => {
    const sketchGeneratedPropertyNames = object.sketch ? new Set(['Geometry', 'Constraints', 'ExternalGeo', 'ExternalGeometry', 'ExternalTypes', 'WebGeometryIds', 'WebSyntheticConstraintIds', 'WebExternalIds', 'WebExternalProjectionIds', 'WebExternalSources']) : new Set<string>()
    if (object.properties.some((property) => sketchGeneratedPropertyNames.has(property.name))) throw new Error(`FCStd native Sketch ${object.id} properties must come from object.sketch or its attachment codec, not duplicate property snapshots.`)
    const persistedProperties = object.properties.filter((property) => property.scope !== 'view' && property.name !== 'ExpressionEngine' && !isFreecadPropertyNoPersist(property.nativeStatus) && !(object.sketch && sketchAttachmentPropertyNames.has(property.name)))
    const transientProperties = persistedProperties.filter((property) => isFreecadPropertyTransient(property.nativeStatus) && isNativeDataProperty(object, property) && !hasFreecadPropertyStatus(property.nativeStatus, 'PropDynamic'))
    const transientPropertyNames = new Set(transientProperties.map((property) => property.name))
    const properties = persistedProperties.filter((property) => !transientPropertyNames.has(property.name))
    const sketchProperties = sketchNativePropertiesXml(object, objectIdSet)
    const expressionEngine = expressionEngineXml(object)
    const hasShapeProperty = properties.some((property) => property.type === 'Part::PropertyPartShape')
    const shapeMetadata = hasShapeProperty ? '<_Property name="_ElementMapVersion" type="App::PropertyString" status="234881025"/>' : ''
    const transientMetadata = transientProperties.map((property) => `<_Property name="${xmlEscape(property.name)}" type="${xmlEscape(property.type)}" status="${normalizeFreecadPropertyStatusMask(property.nativeStatus as number)}"/>`).join('')
    const propertyCount = properties.length + sketchProperties.length + (expressionEngine ? 1 : 0)
    const transientCount = transientProperties.length + (hasShapeProperty ? 1 : 0)
    return `<Object name="${xmlEscape(object.id)}"><Properties Count="${propertyCount}" TransientCount="${transientCount}">${shapeMetadata}${transientMetadata}${properties.map((property) => propertyXml(property, isNativeDataProperty(object, property), object.id, object.topology?.elementMap, colorListResourceByProperty.get(`${object.id}\0${property.name}`))).join('')}${sketchProperties.join('')}${expressionEngine}</Properties></Object>`
  }).join('')
  const hasShapeResources = shapeResourceReferences(document).length > 0
  const usesStringHasher = document.objects.some((object) => object.properties.some((property) => property.type === 'Part::PropertyPartShape' && property.value && typeof property.value === 'object' && !Array.isArray(property.value) && 'hasherIndex' in property.value && property.value.hasherIndex !== undefined))
  const stringHasherTable = options.stringHasherTable ? migrateStringHasherSchema(options.stringHasherTable) : undefined
  if (stringHasherTable) {
    if (!usesStringHasher) throw new Error('FCStd StringHasher table requires at least one Shape property with HasherIndex.')
    const validation = validateStringHasherTable(stringHasherTable)
    if (!validation.valid) throw new Error(`FCStd StringHasher table is invalid: ${validation.issues[0].path}: ${validation.issues[0].message}`)
  }
  const stringHasherXml = hasShapeResources ? usesStringHasher
    ? '<StringHasher saveall="0" threshold="0" count="0" new="1"/><StringHasher2 file="StringHasher.Table.txt"/>'
    : '<StringHasher saveall="0" threshold="0" count="0"></StringHasher>'
    : ''
  const useHasherXml = hasShapeResources ? '<Property name="UseHasher" type="App::PropertyBool" status="67108865"><Bool value="true"/></Property>' : ''
  const documentXml = `<Document SchemaVersion="4" ProgramVersion="1.1R14556 (Git)" FileVersion="1"${hasShapeResources ? ' StringHasher="1"' : ''}>${stringHasherXml}<Properties Count="${hasShapeResources ? 2 : 1}" TransientCount="0"><Property name="Label" type="App::PropertyString"><String value="${xmlEscape(document.label)}"/></Property>${useHasherXml}</Properties><Objects Count="${document.objects.length}" Dependencies="1">${objectDeclarations}</Objects><ObjectData Count="${document.objects.length}">${objectData}</ObjectData></Document>`
  if (options.guiDocumentXml && options.guiViews) throw new TypeError('FCStd writer accepts either guiDocumentXml or guiViews, not both.')
  const guiViewsXml = options.guiViews?.map((view) => {
    if (!view.name.trim()) throw new TypeError('FCStd GuiDocument view names must be non-empty.')
    const type = view.type ? ` type="${xmlEscape(view.type)}"` : ''
    const visibility = view.visibility ? ` visibility="${xmlEscape(view.visibility)}"` : ''
    return `<View name="${xmlEscape(view.name)}"${type}${visibility}/>`
  }).join('')
  const generatedGuiEntries: Record<string, Uint8Array> = {}
  const nativeViewProviders = document.objects.map((object, objectIndex) => {
    const viewProperties = object.properties.filter((property) => property.scope === 'view')
    const properties = viewProperties.filter((property) => property.name !== 'ShapeColor').map(guiPropertyXml).filter(Boolean)
    const shapeColor = viewProperties.find((property) => property.name === 'ShapeColor')
    if (shapeColor) {
      const resourcePath = `WebShapeAppearance${objectIndex}`
      const transparency = viewProperties.find((property) => property.name === 'Transparency')?.value
      generatedGuiEntries[resourcePath] = materialAppearanceBytes(shapeColor.value, transparency, object.id)
      properties.push(`<Property name="ShapeAppearance" type="App::PropertyMaterialList" status="1"><MaterialList file="${resourcePath}" version="3"/></Property>`)
    }
    return properties.length === 0 ? '' : `<ViewProvider name="${xmlEscape(object.id)}" expanded="0" treeRank="-1"><Properties Count="${properties.length}" TransientCount="0">${properties.join('')}</Properties></ViewProvider>`
  }).filter(Boolean)
  const nativeGuiXml = `<Document SchemaVersion="1"><Expand/><ViewProviderData Count="${nativeViewProviders.length}">${nativeViewProviders.join('')}</ViewProviderData><Camera settings=""/></Document>`
  const guiXml = options.guiDocumentXml ?? (options.guiViews ? `<GuiDocument SchemaVersion="4"><Views>${guiViewsXml ?? ''}</Views></GuiDocument>` : nativeGuiXml)
  if (!/^\s*<(?:GuiDocument|Document)(?:\s|>)/.test(guiXml)) throw new TypeError('FCStd GuiDocument payload root must be GuiDocument or Document.')
  const shapePaths = shapeResourceReferences(document)
  const mapPaths = elementMapResourceReferences(document)
  const pathResources = pathResourceReferences(document)
  const pathPaths = pathResources.map((resource) => resource.path)
  const colorListPaths = colorListResources.map((resource) => resource.path)
  const includedFilePaths = [...includedFiles.keys()]
  if (new Set(pathPaths).size !== pathPaths.length) throw new Error('FCStd Path properties must use unique resource paths.')
  if (new Set(colorListPaths).size !== colorListPaths.length) throw new Error('FCStd ColorList properties must use unique resource paths.')
  if (colorListPaths.some((path) => shapePaths.includes(path) || mapPaths.includes(path) || pathPaths.includes(path) || includedFilePaths.includes(path))) throw new Error('FCStd ColorList resource path collides with another native resource.')
  if (includedFilePaths.some((path) => shapePaths.includes(path) || mapPaths.includes(path) || pathPaths.includes(path))) throw new Error('FCStd PropertyFileIncluded resource path collides with another native resource.')
  const opaqueEntries: Record<string, Uint8Array> = { ...(options.opaqueEntries ?? {}) }
  if (stringHasherTable) {
    if (Object.prototype.hasOwnProperty.call(opaqueEntries, 'StringHasher.Table.txt')) throw new Error('FCStd writer accepts StringHasher.Table.txt either as structured table evidence or as an opaque entry, not both.')
    opaqueEntries['StringHasher.Table.txt'] = new TextEncoder().encode(writeStringHasherTable(stringHasherTable))
  }
  for (const path of shapePaths) {
    validateEntryPath(path)
    if (!Object.prototype.hasOwnProperty.call(opaqueEntries, path)) throw new Error(`FCStd Shape property references missing resource: ${path}`)
  }
  for (const object of document.objects) for (const property of object.properties) if (property.type === 'Part::PropertyPartShape' && property.value && typeof property.value === 'object' && !Array.isArray(property.value) && 'path' in property.value && typeof property.value.path === 'string') validateNativeShapeResourceName(object.id, property.value.path)
  for (const path of mapPaths) {
    validateEntryPath(path)
    if (!/\.Map\.txt$/i.test(path)) throw new Error(`FCStd ElementMap2 resource path is not a .Map.txt entry: ${path}`)
    if (!Object.prototype.hasOwnProperty.call(opaqueEntries, path)) throw new Error(`FCStd Shape property references missing ElementMap2 resource: ${path}`)
  }
  for (const path of pathPaths) {
    validateEntryPath(path)
    if (!/\.nc$/i.test(path)) throw new TypeError(`FCStd Path property resource path is not an .nc entry: ${path}`)
    if (!Object.prototype.hasOwnProperty.call(opaqueEntries, path)) {
      const resource = pathResources.find((candidate) => candidate.path === path)
      if (!resource) throw new Error(`FCStd Path property references missing resource: ${path}`)
      opaqueEntries[path] = new TextEncoder().encode(pathResourceText(resource.value.commands))
    }
  }
  for (const [path, value] of includedFiles) {
    validateEntryPath(path)
    const bytes = opaqueEntries[path]
    if (!bytes) throw new Error(`FCStd PropertyFileIncluded references missing project resource bytes: ${path}`)
    if (bytes.byteLength !== value.byteLength) throw new Error(`FCStd PropertyFileIncluded resource ${path} byte length does not match its project resource metadata.`)
    if (Object.prototype.hasOwnProperty.call(generatedGuiEntries, path)) throw new Error(`FCStd PropertyFileIncluded resource collides with a generated GUI entry: ${path}`)
  }
  for (const resource of colorListResources) {
    validateEntryPath(resource.path)
    if (Object.prototype.hasOwnProperty.call(opaqueEntries, resource.path)) throw new Error(`FCStd ColorList resource collides with an opaque entry: ${resource.path}`)
    if (Object.prototype.hasOwnProperty.call(generatedGuiEntries, resource.path)) throw new Error(`FCStd ColorList resource collides with a generated GUI entry: ${resource.path}`)
  }
  if (usesStringHasher && !Object.prototype.hasOwnProperty.call(opaqueEntries, 'StringHasher.Table.txt')) throw new Error('FCStd HasherIndex requires StringHasher.Table.txt in the archive.')
  for (const path of Object.keys(opaqueEntries)) {
    validateEntryPath(path)
    if (path.toLowerCase() === 'document.xml' || path.toLowerCase() === 'guidocument.xml') throw new Error(`Opaque FCStd entry collides with reserved path: ${path}`)
    if (Object.prototype.hasOwnProperty.call(generatedGuiEntries, path)) throw new Error(`Opaque FCStd entry collides with generated path: ${path}`)
  }
  const entries: Record<string, Uint8Array> = { 'Document.xml': new TextEncoder().encode(documentXml) }
  const emittedOpaque = new Set<string>()
  const emitOpaque = (path: string) => {
    if (emittedOpaque.has(path)) return
    entries[path] = new Uint8Array(opaqueEntries[path])
    emittedOpaque.add(path)
  }
  for (const path of [...(usesStringHasher ? ['StringHasher.Table.txt'] : []), ...shapePaths, ...mapPaths, ...pathPaths, ...includedFilePaths]) emitOpaque(path)
  for (const resource of colorListResources) entries[resource.path] = colorListResourceBytes(resource.value, `${resource.objectId}.${resource.propertyName}`)
  entries['GuiDocument.xml'] = new TextEncoder().encode(guiXml)
  if (!options.guiDocumentXml && !options.guiViews) for (const [path, bytes] of Object.entries(generatedGuiEntries)) entries[path] = bytes
  for (const path of Object.keys(opaqueEntries)) if (!emittedOpaque.has(path)) emitOpaque(path)
  return zipSync(entries, { level: 6 })
}

export const rewriteFcstdMetadataArchive = (archive: Uint8Array, document: DocumentSnapshot, options: FcstdWriteOptions = {}): Uint8Array => {
  if (document.readOnly) {
    if (options.guiDocumentXml !== undefined || options.guiViews !== undefined) throw new Error('Read-only FCStd proxy archives cannot override GuiDocument metadata.')
    if (Object.keys(options.opaqueEntries ?? {}).length > 0) throw new Error('Read-only FCStd proxy archives cannot override archive resources.')
    const proxyDocument = inspectFcstdArchive(archive).proxyDocument
    if (JSON.stringify(document) !== JSON.stringify(proxyDocument)) throw new Error('Read-only FCStd proxy archives cannot be modified during byte-preserving rewrite.')
    return new Uint8Array(archive)
  }
  const entries = inspectZipDirectory(archive, DEFAULT_FCSTD_LIMITS)
  const files = unzipSync(archive)
  const inspection = inspectFcstdArchive(archive)
  const knownColorListResources = new Set(inspection.objects.flatMap((object) => object.properties.filter((property) => property.typeId === 'App::PropertyColorList' && property.resourcePath).map((property) => property.resourcePath!)))
  const knownIncludedFileResources = new Set(inspection.objects.flatMap((object) => object.properties.filter((property) => property.typeId === 'App::PropertyFileIncluded' && property.resourcePath).map((property) => property.resourcePath!)))
  const opaqueEntries: Record<string, Uint8Array> = {}
  for (const entry of entries) {
    const lower = entry.path.toLowerCase()
    if (lower === 'document.xml' || lower === 'guidocument.xml') continue
    if (knownColorListResources.has(entry.path) || knownIncludedFileResources.has(entry.path)) continue
    const bytes = files[entry.path]
    if (!bytes) throw new Error(`FCStd entry ${entry.path} is missing after decompression.`)
    opaqueEntries[entry.path] = new Uint8Array(bytes)
  }
  for (const [path, payload] of Object.entries(options.opaqueEntries ?? {})) opaqueEntries[path] = new Uint8Array(payload)
  const guiDocumentXml = options.guiDocumentXml ?? (files['GuiDocument.xml'] ? new TextDecoder('utf-8', { fatal: true }).decode(files['GuiDocument.xml']) : undefined)
  if (options.stringHasherTable) delete opaqueEntries['StringHasher.Table.txt']
  return serializeFcstdMetadataArchive(document, { guiDocumentXml, opaqueEntries, stringHasherTable: options.stringHasherTable })
}

const blockedTypeId = (typeId: string) => /(?:FeaturePython|PythonFeature|::Python)/i.test(typeId)

const entryRole = (path: string): FcstdEntryRole => {
  const lower = path.toLowerCase()
  if (lower === 'document.xml') return 'document'
  if (lower === 'guidocument.xml') return 'gui-document'
  if (lower === 'thumbnails/thumbnail.png') return 'thumbnail'
  if (lower.endsWith('.brp') || lower.endsWith('.brep')) return 'shape'
  if (lower.endsWith('.map.txt')) return 'topology-map'
  if (lower.endsWith('.py') || lower.endsWith('.fcmacro') || lower.includes('/macro')) return 'script'
  return 'resource'
}

const validateEntryPath = (path: string) => {
  if (!path || path.includes('\0') || path.includes('\\') || path.startsWith('/') || /^[A-Za-z]:/.test(path)) throw new Error(`Unsafe FCStd entry path: ${path || '<empty>'}`)
  const segments = path.split('/')
  if (segments.some((segment) => segment === '..' || segment === '.' || ['__proto__', 'prototype', 'constructor'].includes(segment))) throw new Error(`Unsafe FCStd entry path: ${path}`)
}

const findEndOfCentralDirectory = (bytes: Uint8Array) => {
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
  const minimum = Math.max(0, bytes.byteLength - 65_557)
  for (let offset = bytes.byteLength - 22; offset >= minimum; offset -= 1) {
    if (view.getUint32(offset, true) === 0x06054b50) return offset
  }
  throw new Error('FCStd is not a valid ZIP archive: end-of-central-directory record was not found.')
}

const inspectZipDirectory = (bytes: Uint8Array, limits: FcstdArchiveLimits): FcstdEntryMetadata[] => {
  if (bytes.byteLength > limits.maxArchiveBytes) throw new RangeError(`FCStd archive exceeds ${limits.maxArchiveBytes} bytes.`)
  if (bytes.byteLength < 22) throw new Error('FCStd is not a valid ZIP archive.')
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
  const eocd = findEndOfCentralDirectory(bytes)
  const diskNumber = view.getUint16(eocd + 4, true)
  const centralDirectoryDisk = view.getUint16(eocd + 6, true)
  const entriesOnDisk = view.getUint16(eocd + 8, true)
  const entryCount = view.getUint16(eocd + 10, true)
  const directorySize = view.getUint32(eocd + 12, true)
  const directoryOffset = view.getUint32(eocd + 16, true)
  const commentLength = view.getUint16(eocd + 20, true)
  if (diskNumber !== 0 || centralDirectoryDisk !== 0 || entriesOnDisk !== entryCount) throw new Error('Multi-disk FCStd archives are not supported.')
  if (entryCount === 0xffff || directorySize === 0xffffffff || directoryOffset === 0xffffffff) throw new Error('ZIP64 FCStd archives are outside the supported import boundary.')
  if (entryCount > limits.maxEntries) throw new RangeError(`FCStd archive exceeds ${limits.maxEntries} entries.`)
  if (eocd + 22 + commentLength > bytes.byteLength || directoryOffset + directorySize > eocd) throw new Error('FCStd central directory is truncated or inconsistent.')

  const decoder = new TextDecoder('utf-8', { fatal: true })
  const entries: FcstdEntryMetadata[] = []
  const paths = new Set<string>()
  let cursor = directoryOffset
  let totalUncompressed = 0
  for (let index = 0; index < entryCount; index += 1) {
    if (cursor + 46 > eocd || view.getUint32(cursor, true) !== 0x02014b50) throw new Error('FCStd central directory contains an invalid file header.')
    const flags = view.getUint16(cursor + 8, true)
    const compressionMethod = view.getUint16(cursor + 10, true)
    const compressedBytes = view.getUint32(cursor + 20, true)
    const uncompressedBytes = view.getUint32(cursor + 24, true)
    const nameLength = view.getUint16(cursor + 28, true)
    const extraLength = view.getUint16(cursor + 30, true)
    const entryCommentLength = view.getUint16(cursor + 32, true)
    const diskStart = view.getUint16(cursor + 34, true)
    const next = cursor + 46 + nameLength + extraLength + entryCommentLength
    if (next > eocd) throw new Error('FCStd central-directory entry is truncated.')
    if ((flags & 0x1) !== 0) throw new Error('Encrypted FCStd entries are not supported.')
    if (diskStart !== 0) throw new Error('Multi-disk FCStd entries are not supported.')
    if (compressionMethod !== 0 && compressionMethod !== 8) throw new Error(`Unsupported FCStd ZIP compression method: ${compressionMethod}.`)
    if (compressedBytes === 0xffffffff || uncompressedBytes === 0xffffffff) throw new Error('ZIP64 FCStd entries are outside the supported import boundary.')
    const path = decoder.decode(bytes.subarray(cursor + 46, cursor + 46 + nameLength))
    validateEntryPath(path)
    if (paths.has(path)) throw new Error(`Duplicate FCStd entry path: ${path}`)
    paths.add(path)
    if (uncompressedBytes > limits.maxEntryBytes) throw new RangeError(`FCStd entry ${path} exceeds ${limits.maxEntryBytes} bytes.`)
    const ratio = uncompressedBytes === 0 ? 0 : compressedBytes === 0 ? Number.POSITIVE_INFINITY : uncompressedBytes / compressedBytes
    if (ratio > limits.maxCompressionRatio) throw new RangeError(`FCStd entry ${path} exceeds the maximum compression ratio.`)
    totalUncompressed += uncompressedBytes
    if (totalUncompressed > limits.maxTotalUncompressedBytes) throw new RangeError(`FCStd archive exceeds ${limits.maxTotalUncompressedBytes} uncompressed bytes.`)
    entries.push({ path, compressedBytes, uncompressedBytes, compressionMethod, role: entryRole(path) })
    cursor = next
  }
  if (cursor !== directoryOffset + directorySize) throw new Error('FCStd central-directory size does not match its entries.')
  return entries
}

const asArray = <T>(value: T | T[] | undefined): T[] => value === undefined ? [] : Array.isArray(value) ? value : [value]

const attribute = (node: unknown, name: string): string => {
  if (!node || typeof node !== 'object') return ''
  const record = node as Record<string, unknown>
  const value = record[`@_${name}`] ?? record[name]
  return value === undefined || value === null ? '' : String(value)
}

const propertyValue = (property: Record<string, unknown>): string => {
  for (const value of Object.values(property)) {
    if (!value || typeof value !== 'object') continue
    const candidate = attribute(value, 'value')
    if (candidate) return candidate
  }
  return ''
}

const propertyElement = (property: Record<string, unknown>): string => {
  const entry = Object.entries(property).find(([name, value]) => !name.startsWith('@_') && Boolean(value && typeof value === 'object'))
  return entry?.[0] || ''
}

const finiteAttribute = (node: unknown, name: string, context: string) => {
  const text = attribute(node, name)
  const value = Number(text)
  if (!text || !Number.isFinite(value)) throw new Error(`FCStd ${context} ${name} must be finite.`)
  return value
}

const parsedShapeResource = (element: string, elementValue: unknown, property?: Record<string, unknown>): { path: string; hasherIndex?: number; elementMap?: string; elementMapEntries?: Array<{ key: string; value: string }>; elementMapResource?: string } | undefined => {
  if (element !== 'Part' || !elementValue || typeof elementValue !== 'object') return undefined
  const path = attribute(elementValue, 'file')
  if (!path.trim()) throw new Error('FCStd Part shape resource path is missing.')
  validateEntryPath(path)
  if (!/\.(?:brp|brep)$/i.test(path)) throw new Error(`FCStd Part shape resource path is not BRep: ${path}`)
  const hasherIndexText = attribute(elementValue, 'HasherIndex')
  const hasherIndex = hasherIndexText === '' ? undefined : Number(hasherIndexText)
  if (hasherIndex !== undefined && (!Number.isSafeInteger(hasherIndex) || hasherIndex < 0)) throw new Error(`FCStd Part HasherIndex is invalid: ${hasherIndexText}`)
  const elementMap = attribute(elementValue, 'ElementMap')
  const mapNodes = property?.ElementMap2 === undefined ? [] : asArray(property.ElementMap2 as Record<string, unknown> | Record<string, unknown>[])
  if (mapNodes.length > 1) throw new Error('FCStd Part shape property contains duplicate ElementMap2 resources.')
  const mapResource = mapNodes[0] ? attribute(mapNodes[0], 'file') : ''
  if (mapNodes[0] && (!mapResource.trim() || !/\.Map\.txt$/i.test(mapResource))) throw new Error(`FCStd ElementMap2 resource path is invalid: ${mapResource || '<empty>'}`)
  if (mapResource) validateEntryPath(mapResource)
  const mapEntriesNode = property?.ElementMap === undefined ? [] : asArray(property.ElementMap as Record<string, unknown> | Record<string, unknown>[])
  if (mapEntriesNode.length > 1) throw new Error('FCStd Part shape property contains duplicate ElementMap nodes.')
  const mapEntriesValue = mapEntriesNode[0]
  const mapEntries = mapEntriesValue ? asArray((mapEntriesValue as Record<string, unknown>).Element as Record<string, unknown> | Record<string, unknown>[] | undefined).map((entry) => {
    const key = attribute(entry, 'key')
    if (!key.trim()) throw new Error('FCStd ElementMap entry key must be non-empty.')
    return { key, value: attribute(entry, 'value') }
  }) : []
  if (mapEntriesValue) {
    const countText = attribute(mapEntriesValue, 'count')
    if (!/^\d+$/.test(countText) || Number(countText) !== mapEntries.length) throw new Error('FCStd ElementMap count does not match its child elements.')
  }
  return { path, ...(hasherIndex === undefined ? {} : { hasherIndex }), ...(elementMap ? { elementMap } : {}), ...(mapEntriesValue ? { elementMapEntries: mapEntries } : {}), ...(mapResource ? { elementMapResource: mapResource } : {}) }
}

const structuredPropertyValue = (element: string, elementValue: unknown, property?: Record<string, unknown>): string | undefined => {
  if (element === 'ColorList' && elementValue && typeof elementValue === 'object') {
    const resourcePath = attribute(elementValue, 'file')
    if (!resourcePath) return JSON.stringify([])
    validateEntryPath(resourcePath)
    return JSON.stringify({ resourcePath })
  }
  if (element === 'FileIncluded' && elementValue && typeof elementValue === 'object') {
    const resourcePath = attribute(elementValue, 'file')
    const inlineData = attribute(elementValue, 'data')
    if (inlineData) throw new Error('FCStd force-XML inline FileIncluded payloads are not supported by the archive codec.')
    if (!resourcePath) return JSON.stringify(null)
    validateIncludedFileArchiveName(resourcePath, 'FCStd PropertyFileIncluded')
    return JSON.stringify({ resourcePath })
  }
  if (element === 'PropertyVector') return JSON.stringify({
    x: finiteAttribute(elementValue, 'valueX', element),
    y: finiteAttribute(elementValue, 'valueY', element),
    z: finiteAttribute(elementValue, 'valueZ', element),
  })
  if (element === 'FloatList' && elementValue && typeof elementValue === 'object') {
    if (attribute(elementValue, 'file')) return undefined
    const values = asArray((elementValue as Record<string, unknown>).Float as Record<string, unknown> | Record<string, unknown>[] | undefined).map((entry) => finiteAttribute(entry, 'value', element))
    const countText = attribute(elementValue, 'count')
    if (!/^\d+$/.test(countText) || Number(countText) !== values.length) throw new Error(`FCStd FloatList count does not match its child elements.`)
    return JSON.stringify(values)
  }
  if (element === 'IntegerList' && elementValue && typeof elementValue === 'object') {
    const values = asArray((elementValue as Record<string, unknown>).I as Record<string, unknown> | Record<string, unknown>[] | undefined).map((entry, index) => {
      const text = attribute(entry, 'v') || attribute(entry, 'value')
      const value = Number(text)
      if (!/^-?\d+$/.test(text) || !Number.isSafeInteger(value)) throw new Error(`FCStd IntegerList item ${index} must be a safe integer.`)
      return value
    })
    const countText = attribute(elementValue, 'count')
    if (!/^\d+$/.test(countText) || Number(countText) !== values.length) throw new Error(`FCStd IntegerList count does not match its child elements.`)
    return JSON.stringify(values)
  }
  if (element === 'IntegerSet' && elementValue && typeof elementValue === 'object') {
    const values = asArray((elementValue as Record<string, unknown>).I as Record<string, unknown> | Record<string, unknown>[] | undefined).map((entry, index) => {
      const text = attribute(entry, 'v') || attribute(entry, 'value')
      const value = Number(text)
      if (!/^-?\d+$/.test(text) || !Number.isSafeInteger(value)) throw new Error(`FCStd IntegerSet item ${index} must be a safe integer.`)
      return value
    })
    const countText = attribute(elementValue, 'count')
    if (!/^\d+$/.test(countText) || Number(countText) !== values.length) throw new Error('FCStd IntegerSet count does not match its child elements.')
    if (new Set(values).size !== values.length) throw new Error('FCStd IntegerSet contains duplicate values.')
    return JSON.stringify([...values].sort((left, right) => left - right))
  }
  if (element === 'StringList' && elementValue && typeof elementValue === 'object') {
    const values = asArray((elementValue as Record<string, unknown>).String as Record<string, unknown> | Record<string, unknown>[] | undefined).map((entry) => attribute(entry, 'value'))
    const countText = attribute(elementValue, 'count')
    if (!/^\d+$/.test(countText) || Number(countText) !== values.length) throw new Error(`FCStd StringList count does not match its child elements.`)
    return JSON.stringify(values)
  }
  if (element === 'BoolList' && elementValue && typeof elementValue === 'object') {
    const encoded = attribute(elementValue, 'value')
    if (!/^[01]*$/.test(encoded)) throw new Error('FCStd BoolList value must contain only 0 and 1 bits.')
    return JSON.stringify([...encoded].reverse().map((entry) => entry === '1'))
  }
  if (element === 'GeometryList' && elementValue && typeof elementValue === 'object') {
    const records = asArray((elementValue as Record<string, unknown>).Geometry as Record<string, unknown> | Record<string, unknown>[] | undefined).map((geometry, index) => {
      const type = attribute(geometry, 'type')
      const nativeIdText = attribute(geometry, 'id')
      const nativeId = Number(nativeIdText)
      if (!/^-?\d+$/.test(nativeIdText) || !Number.isSafeInteger(nativeId) || nativeId === 0) throw new Error(`FCStd Sketch Geometry ${index} has an invalid native id.`)
      const constructionNode = geometry.Construction && typeof geometry.Construction === 'object' ? geometry.Construction as Record<string, unknown> : undefined
      const constructionText = constructionNode ? attribute(constructionNode, 'value') : ''
      const construction = constructionText === '1' || constructionText.toLowerCase() === 'true' || attribute(geometry, 'geometryModeFlags').endsWith('10')
      const payload = Object.entries(geometry).find(([name, value]) => !name.startsWith('@_') && name !== 'GeoExtensions' && name !== 'Construction' && value && typeof value === 'object')?.[1]
      const finite = (node: unknown, name: string) => finiteAttribute(node, name, `Sketch Geometry ${index}`)
      let value: SketchGeometry
      if (type === 'Part::GeomPoint') value = { id: String(nativeId), type: 'point', position: { x: finite(payload, 'X'), y: finite(payload, 'Y') } }
      else if (type === 'Part::GeomLineSegment') value = { id: String(nativeId), type: 'line', start: { x: finite(payload, 'StartX'), y: finite(payload, 'StartY') }, end: { x: finite(payload, 'EndX'), y: finite(payload, 'EndY') } }
      else if (type === 'Part::GeomCircle') value = { id: String(nativeId), type: 'circle', center: { x: finite(payload, 'CenterX'), y: finite(payload, 'CenterY') }, radius: finite(payload, 'Radius') }
      else if (type === 'Part::GeomArcOfCircle') value = { id: String(nativeId), type: 'arc', center: { x: finite(payload, 'CenterX'), y: finite(payload, 'CenterY') }, radius: finite(payload, 'Radius'), startAngle: finite(payload, 'StartAngle'), endAngle: finite(payload, 'EndAngle') }
      else if (type === 'Part::GeomEllipse') value = { id: String(nativeId), type: 'ellipse', center: { x: finite(payload, 'CenterX'), y: finite(payload, 'CenterY') }, majorRadius: finite(payload, 'MajorRadius'), minorRadius: finite(payload, 'MinorRadius'), rotation: finite(payload, 'AngleXU') }
      else if (type === 'Part::GeomArcOfEllipse') value = { id: String(nativeId), type: 'arcEllipse', center: { x: finite(payload, 'CenterX'), y: finite(payload, 'CenterY') }, majorRadius: finite(payload, 'MajorRadius'), minorRadius: finite(payload, 'MinorRadius'), rotation: finite(payload, 'AngleXU'), startAngle: finite(payload, 'StartAngle'), endAngle: finite(payload, 'EndAngle') }
      else if (type === 'Part::GeomArcOfHyperbola') value = { id: String(nativeId), type: 'arcHyperbola', center: { x: finite(payload, 'CenterX'), y: finite(payload, 'CenterY') }, majorRadius: finite(payload, 'MajorRadius'), minorRadius: finite(payload, 'MinorRadius'), rotation: finite(payload, 'AngleXU'), startAngle: finite(payload, 'StartAngle'), endAngle: finite(payload, 'EndAngle') }
      else if (type === 'Part::GeomArcOfParabola') value = { id: String(nativeId), type: 'arcParabola', center: { x: finite(payload, 'CenterX'), y: finite(payload, 'CenterY') }, focal: finite(payload, 'Focal'), rotation: finite(payload, 'AngleXU'), startAngle: finite(payload, 'StartAngle'), endAngle: finite(payload, 'EndAngle') }
      else if (type === 'Part::GeomBSplineCurve') {
        const curve = payload && typeof payload === 'object' ? payload as Record<string, unknown> : {}
        const poles = asArray(curve.Pole as Record<string, unknown> | Record<string, unknown>[] | undefined)
        const knots = asArray(curve.Knot as Record<string, unknown> | Record<string, unknown>[] | undefined)
        const degreeText = attribute(curve, 'Degree')
        const degree = Number(degreeText)
        if (!/^\d+$/.test(degreeText) || !Number.isSafeInteger(degree) || degree < 1) throw new Error(`FCStd Sketch Geometry ${index} has an invalid B-spline degree.`)
        const controlPoints = poles.map((pole, poleIndex) => ({ x: finiteAttribute(pole, 'X', `Sketch Geometry ${index} Pole ${poleIndex}`), y: finiteAttribute(pole, 'Y', `Sketch Geometry ${index} Pole ${poleIndex}`) }))
        const weights = poles.map((pole, poleIndex) => finiteAttribute(pole, 'Weight', `Sketch Geometry ${index} Pole ${poleIndex}`))
        const expandedKnots = knots.flatMap((knot, knotIndex) => {
          const multiplicityText = attribute(knot, 'Mult')
          const multiplicity = Number(multiplicityText)
          if (!/^\d+$/.test(multiplicityText) || !Number.isSafeInteger(multiplicity) || multiplicity <= 0) throw new Error(`FCStd Sketch Geometry ${index} Knot ${knotIndex} has an invalid multiplicity.`)
          return Array.from({ length: multiplicity }, () => finiteAttribute(knot, 'Value', `Sketch Geometry ${index} Knot ${knotIndex}`))
        })
        value = { id: String(nativeId), type: 'bspline', degree, controlPoints, weights, knots: expandedKnots, periodic: attribute(curve, 'IsPeriodic') === '1' }
      } else {
        if (!type) throw new Error(`FCStd Sketch Geometry ${index} type is missing.`)
        value = { id: String(nativeId), type: 'unsupported', freecadType: type } as unknown as SketchGeometry
      }
      if (construction) value.construction = true
      const extensions = geometry.GeoExtensions && typeof geometry.GeoExtensions === 'object'
        ? asArray((geometry.GeoExtensions as Record<string, unknown>).GeoExtension as Record<string, unknown> | Record<string, unknown>[] | undefined)
        : []
      const sketchExtension = extensions.find((extension) => attribute(extension, 'type') === 'Sketcher::SketchGeometryExtension')
      if (sketchExtension) {
        const internalTypeText = attribute(sketchExtension, 'internalGeometryType') || '0'
        const internalType = Number(internalTypeText)
        if (!/^\d+$/.test(internalTypeText) || !Number.isSafeInteger(internalType) || internalType < 0 || internalType > 11) throw new Error(`FCStd Sketch Geometry ${index} has an invalid internal geometry type.`)
        if (internalType !== 0) Object.assign(value, { freecadInternalType: internalType })
      }
      const externalExtension = extensions.find((extension) => attribute(extension, 'type') === 'Sketcher::ExternalGeometryExtension')
      if (!externalExtension) return value
      const flagsText = attribute(externalExtension, 'Flags') || '0'
      const flags = Number(flagsText)
      const refIndexText = attribute(externalExtension, 'RefIndex')
      const refIndex = refIndexText === '' ? undefined : Number(refIndexText)
      if (!/^\d+$/.test(flagsText) || !Number.isSafeInteger(flags) || flags < 0) throw new Error(`FCStd Sketch Geometry ${index} has invalid external flags.`)
      if (refIndex !== undefined && (!/^\d+$/.test(refIndexText) || !Number.isSafeInteger(refIndex) || refIndex < 0)) throw new Error(`FCStd Sketch Geometry ${index} has an invalid external reference index.`)
      return Object.assign(value, { freecadExternal: { ref: attribute(externalExtension, 'Ref'), flags, ...(refIndex === undefined ? {} : { refIndex }) } })
    })
    const countText = attribute(elementValue, 'count')
    if (!/^\d+$/.test(countText) || Number(countText) !== records.length) throw new Error('FCStd GeometryList count does not match its Geometry children.')
    return JSON.stringify(records)
  }
  if (element === 'ConstraintList' && elementValue && typeof elementValue === 'object') {
    const records = asArray((elementValue as Record<string, unknown>).Constrain as Record<string, unknown> | Record<string, unknown>[] | undefined).map((constraint, index) => {
      const typeText = attribute(constraint, 'Type')
      const type = Number(typeText)
      if (!/^\d+$/.test(typeText) || !Number.isSafeInteger(type) || type < 0 || type > 19) throw new Error(`FCStd Sketch Constraint ${index} has an invalid type.`)
      const valueText = attribute(constraint, 'Value')
      const value = valueText === '' ? 0 : Number(valueText)
      if (!Number.isFinite(value)) throw new Error(`FCStd Sketch Constraint ${index} has a non-finite value.`)
      const ids = attribute(constraint, 'ElementIds').trim().split(/\s+/).filter(Boolean).map(Number)
      const positions = attribute(constraint, 'ElementPositions').trim().split(/\s+/).filter(Boolean).map(Number)
      if (ids.length !== positions.length || ids.length < 3 || ids.some((id) => !Number.isSafeInteger(id)) || positions.some((position) => !Number.isSafeInteger(position))) throw new Error(`FCStd Sketch Constraint ${index} element references are malformed.`)
      const drivingText = attribute(constraint, 'IsDriving')
      if (drivingText !== '0' && drivingText !== '1') throw new Error(`FCStd Sketch Constraint ${index} IsDriving is invalid.`)
      let internalAlignmentType: number | undefined
      let internalAlignmentIndex: number | undefined
      if (type === 15) {
        const alignmentTypeText = attribute(constraint, 'InternalAlignmentType')
        const alignmentIndexText = attribute(constraint, 'InternalAlignmentIndex')
        internalAlignmentType = Number(alignmentTypeText)
        internalAlignmentIndex = Number(alignmentIndexText)
        if (!/^\d+$/.test(alignmentTypeText) || !Number.isSafeInteger(internalAlignmentType) || internalAlignmentType < 1 || internalAlignmentType > 11 || !/^-?\d+$/.test(alignmentIndexText) || !Number.isSafeInteger(internalAlignmentIndex)) throw new Error(`FCStd Sketch Constraint ${index} has invalid internal-alignment metadata.`)
      }
      return { id: attribute(constraint, 'Name'), type, value, driving: drivingText !== '0', ids, positions, ...(internalAlignmentType === undefined ? {} : { internalAlignmentType, internalAlignmentIndex }) }
    })
    const countText = attribute(elementValue, 'count')
    if (!/^\d+$/.test(countText) || Number(countText) !== records.length) throw new Error('FCStd ConstraintList count does not match its Constrain children.')
    return JSON.stringify(records)
  }
  if (element === 'Path' && elementValue && typeof elementValue === 'object') {
    if (attribute(property, 'type') !== 'Path::PropertyPath') return undefined
    const resourcePath = attribute(elementValue, 'file')
    if (!resourcePath.trim()) throw new Error('FCStd Path property resource path is missing.')
    validateEntryPath(resourcePath)
    if (!/\.nc$/i.test(resourcePath)) throw new Error(`FCStd Path property resource path is not an .nc entry: ${resourcePath}`)
    const versionText = attribute(elementValue, 'version') || '2'
    const version = Number(versionText)
    if (!/^\d+$/.test(versionText) || !Number.isSafeInteger(version) || version <= 0) throw new Error(`FCStd Path property version is invalid: ${versionText}`)
    const centerNode = (elementValue as Record<string, unknown>).Center
    const center = centerNode && typeof centerNode === 'object' ? {
      x: finiteAttribute(centerNode, 'x', 'Path Center'),
      y: finiteAttribute(centerNode, 'y', 'Path Center'),
      z: finiteAttribute(centerNode, 'z', 'Path Center'),
    } : { x: 0, y: 0, z: 0 }
    return JSON.stringify({ schemaVersion: 1, commands: [], resourcePath, version, center })
  }
  const shapeResource = parsedShapeResource(element, elementValue, property)
  if (shapeResource) return JSON.stringify({ ...shapeResource, format: 'brep' })
  if (element === 'LinkSubList' && elementValue && typeof elementValue === 'object') {
    const links = asArray((elementValue as Record<string, unknown>).Link as Record<string, unknown> | Record<string, unknown>[] | undefined).map((entry) => {
      const objectId = attribute(entry, 'obj')
      if (!objectId.trim()) throw new Error('FCStd LinkSubList object id must be non-empty.')
      return { objectId, subElement: attribute(entry, 'sub') }
    })
    const countText = attribute(elementValue, 'count')
    if (!/^\d+$/.test(countText) || Number(countText) !== links.length) throw new Error('FCStd LinkSubList count does not match its child elements.')
    return JSON.stringify({ schemaVersion: 1, entries: links })
  }
  if (element !== 'PropertyPlacement') return undefined
  const position = {
    x: finiteAttribute(elementValue, 'Px', element),
    y: finiteAttribute(elementValue, 'Py', element),
    z: finiteAttribute(elementValue, 'Pz', element),
  }
  let axis: { x: number; y: number; z: number }
  let angleRadians: number
  if (attribute(elementValue, 'A')) {
    axis = { x: finiteAttribute(elementValue, 'Ox', element), y: finiteAttribute(elementValue, 'Oy', element), z: finiteAttribute(elementValue, 'Oz', element) }
    angleRadians = finiteAttribute(elementValue, 'A', element)
  } else {
    const quaternion = ['Q0', 'Q1', 'Q2', 'Q3'].map((name) => finiteAttribute(elementValue, name, element))
    const length = Math.hypot(...quaternion)
    if (length === 0) throw new Error('FCStd PropertyPlacement quaternion must be non-zero.')
    const [x, y, z, rawW] = quaternion.map((value) => value / length)
    const w = Math.max(-1, Math.min(1, rawW))
    angleRadians = 2 * Math.acos(w)
    const sine = Math.sqrt(Math.max(0, 1 - w * w))
    axis = sine < 1e-12 ? { x: 0, y: 0, z: 1 } : { x: x / sine, y: y / sine, z: z / sine }
  }
  const axisLength = Math.hypot(axis.x, axis.y, axis.z)
  if (axisLength === 0) throw new Error('FCStd PropertyPlacement axis must be non-zero.')
  return JSON.stringify({ position, rotation: { axis: { x: axis.x / axisLength, y: axis.y / axisLength, z: axis.z / axisLength }, angle: angleRadians * 180 / Math.PI } })
}

const propertySummaries = (properties: Record<string, unknown>[]): FcstdPropertySummary[] => properties.map((property) => {
  const expression = attribute(property, 'expression')
  const element = propertyElement(property)
  const elementValue = property[element]
  const subElements = element === 'LinkSub' && elementValue && typeof elementValue === 'object'
    ? asArray((elementValue as Record<string, unknown>).Sub as Record<string, unknown> | Record<string, unknown>[] | undefined).map((sub) => attribute(sub, 'value'))
    : undefined
  const links = element === 'LinkList' && elementValue && typeof elementValue === 'object'
    ? asArray((elementValue as Record<string, unknown>).Link as Record<string, unknown> | Record<string, unknown>[] | undefined).map((link) => attribute(link, 'value'))
    : undefined
  const linkSubs = element === 'LinkSubList' && elementValue && typeof elementValue === 'object'
    ? asArray((elementValue as Record<string, unknown>).Link as Record<string, unknown> | Record<string, unknown>[] | undefined).map((link) => ({ objectId: attribute(link, 'obj'), subElement: attribute(link, 'sub') }))
    : undefined
  const customEnumList = property.CustomEnumList && typeof property.CustomEnumList === 'object' ? property.CustomEnumList as Record<string, unknown> : undefined
  const enumOptions = customEnumList ? asArray(customEnumList.Enum as Record<string, unknown> | Record<string, unknown>[] | undefined).map((entry) => attribute(entry, 'value')) : undefined
  if (customEnumList) {
    const countText = attribute(customEnumList, 'count')
    if (!/^\d+$/.test(countText) || Number(countText) !== enumOptions?.length) throw new Error('FCStd CustomEnumList count does not match its Enum children.')
  }
  const shapeResource = parsedShapeResource(element, elementValue, property)
  const resourcePath = (element === 'ColorList' || element === 'FileIncluded') && elementValue && typeof elementValue === 'object' ? attribute(elementValue, 'file') : undefined
  if ((subElements || links || linkSubs) && elementValue && typeof elementValue === 'object') {
    const countText = attribute(elementValue, 'count')
    const count = Number(countText)
    const actual = (subElements ?? links ?? linkSubs as unknown[]).length
    if (!/^\d+$/.test(countText) || !Number.isSafeInteger(count) || count !== actual) throw new Error(`FCStd ${element} count does not match its child elements.`)
  }
  const value = structuredPropertyValue(element, elementValue, property) ?? propertyValue(property)
  const statusText = attribute(property, 'status')
  let nativeStatus: number | undefined
  let statusNames: FreecadPropertyStatusName[] | undefined
  if (statusText) {
    if (!/^\d+$/.test(statusText)) throw new Error(`FCStd Property ${attribute(property, 'name') || '<unnamed>'} status must be an unsigned 32-bit integer.`)
    const decoded = decodeFreecadPropertyStatus(Number(statusText))
    nativeStatus = decoded.mask
    statusNames = decoded.names
  }
  return { name: attribute(property, 'name') || '<unnamed>', typeId: attribute(property, 'type') || element || 'unknown', element, value, ...(nativeStatus === undefined ? {} : { nativeStatus, statusNames }), ...(subElements ? { subElements } : {}), ...(links ? { links } : {}), ...(linkSubs ? { linkSubs } : {}), ...(enumOptions ? { enumOptions } : {}), ...(resourcePath === undefined ? {} : { resourcePath }), ...(shapeResource ? { shapeResource } : {}), ...(expression ? { expression } : {}) }
})

const sketchFromPropertySummaries = (objectId: string, summaries: FcstdPropertySummary[]): SketchSnapshot | undefined => {
  const geometryProperty = summaries.find((property) => property.name === 'Geometry' && property.element === 'GeometryList')
  const constraintProperty = summaries.find((property) => property.name === 'Constraints' && property.element === 'ConstraintList')
  if (!geometryProperty || !constraintProperty) return undefined
  type ParsedNativeSketchGeometry = SketchGeometry & { freecadInternalType?: number }
  let nativeGeometry: ParsedNativeSketchGeometry[]
  let constraints: Array<{ id: string; type: number; value: number; driving: boolean; ids: number[]; positions: number[]; internalAlignmentType?: number; internalAlignmentIndex?: number }>
  try {
    nativeGeometry = JSON.parse(geometryProperty.value) as ParsedNativeSketchGeometry[]
    constraints = JSON.parse(constraintProperty.value) as typeof constraints
  } catch {
    return undefined
  }
  const editableNativeIndexes = nativeGeometry.flatMap((candidate, index) => candidate.freecadInternalType ? [] : [index])
  const webIds = summaries.find((property) => property.name === 'WebGeometryIds' && property.element === 'StringList')
  if (webIds) {
    let ids: unknown
    try { ids = JSON.parse(webIds.value) } catch { throw new Error(`FCStd Sketch ${objectId} WebGeometryIds is not valid JSON.`) }
    if (!Array.isArray(ids) || ids.length !== editableNativeIndexes.length || ids.some((id) => typeof id !== 'string' || !id.trim())) throw new Error(`FCStd Sketch ${objectId} WebGeometryIds count does not match editable GeometryList items.`)
    const idByNativeIndex = new Map(editableNativeIndexes.map((nativeIndex, index) => [nativeIndex, ids[index] as string]))
    nativeGeometry = nativeGeometry.map((candidate, nativeIndex) => idByNativeIndex.has(nativeIndex) ? { ...candidate, id: idByNativeIndex.get(nativeIndex)! } : candidate)
  }
  const geometry = nativeGeometry.filter((candidate) => !candidate.freecadInternalType).map(({ freecadInternalType: _internalType, ...candidate }) => candidate as SketchGeometry)
  type ParsedExternalGeometry = SketchGeometry & { freecadExternal?: { ref: string; flags: number; refIndex?: number } }
  const externalLinkProperty = summaries.find((property) => property.name === 'ExternalGeometry' && property.element === 'LinkSubList')
  const externalGeoProperty = summaries.find((property) => property.name === 'ExternalGeo' && property.element === 'GeometryList')
  const externalTypesProperty = summaries.find((property) => property.name === 'ExternalTypes' && property.element === 'IntegerList')
  let externalGeometry: SketchSnapshot['externalGeometry'] = []
  if (externalLinkProperty || externalGeoProperty || externalTypesProperty) {
    if (!externalLinkProperty || !externalGeoProperty || !externalTypesProperty) return undefined
    const links = externalLinkProperty.linkSubs ?? []
    let nativeExternalGeometry: ParsedExternalGeometry[]
    let externalTypes: number[]
    try {
      nativeExternalGeometry = (JSON.parse(externalGeoProperty.value) as ParsedExternalGeometry[]).filter((candidate) => Boolean(candidate.freecadExternal?.ref))
      externalTypes = JSON.parse(externalTypesProperty.value) as number[]
    } catch {
      return undefined
    }
    const defaultEmptyTypes = links.length === 0 && externalTypes.length === 1 && externalTypes[0] === 0
    if (!Array.isArray(nativeExternalGeometry) || !Array.isArray(externalTypes) || (!defaultEmptyTypes && externalTypes.length !== links.length) || externalTypes.some((type) => !Number.isSafeInteger(type) || type < 0 || type >= externalModeByNativeType.length)) return undefined
    const metadataList = (name: string) => {
      const property = summaries.find((candidate) => candidate.name === name && candidate.element === 'StringList')
      if (!property) return undefined
      let values: unknown
      try { values = JSON.parse(property.value) } catch { throw new Error(`FCStd Sketch ${objectId} ${name} is not valid JSON.`) }
      if (!Array.isArray(values) || values.length !== nativeExternalGeometry.length || values.some((value) => typeof value !== 'string' || !value.trim())) throw new Error(`FCStd Sketch ${objectId} ${name} count does not match ExternalGeo.`)
      return values as string[]
    }
    const externalIds = metadataList('WebExternalIds')
    const projectionIds = metadataList('WebExternalProjectionIds')
    const serializedSources = metadataList('WebExternalSources')
    if ([externalIds, projectionIds, serializedSources].some(Boolean) && (!externalIds || !projectionIds || !serializedSources)) throw new Error(`FCStd Sketch ${objectId} external Web metadata is incomplete.`)
    const nativeKinds = links.map((link) => /^Face[1-9]\d*$/.test(link.subElement) ? 'face' as const : /^Edge[1-9]\d*$/.test(link.subElement) ? 'edge' as const : /^Vertex[1-9]\d*$/.test(link.subElement) ? 'vertex' as const : undefined)
    if (nativeKinds.some((kind) => kind === undefined)) return undefined
    const linkIndexByReference = new Map<string, number>()
    for (const [index, link] of links.entries()) {
      const reference = `${link.objectId}.${link.subElement}`
      if (linkIndexByReference.has(reference)) return undefined
      linkIndexByReference.set(reference, index)
    }
    const groupIndexByExternalReference = new Map<string, number>()
    for (const [index, candidate] of nativeExternalGeometry.entries()) {
      const externalState = candidate.freecadExternal
      if (!externalState || !Number.isSafeInteger(externalState.flags) || externalState.flags < 0 || externalState.flags > 31) throw new Error(`FCStd Sketch ${objectId} ExternalGeo item ${index} has unsupported native flags.`)
      if ((externalState.flags & 16) !== 0 && (externalState.flags & 2) === 0) throw new Error(`FCStd Sketch ${objectId} ExternalGeo item ${index} cannot synchronize unless it is frozen.`)
      if (externalState.refIndex === undefined) continue
      if (externalState.refIndex < 0 || externalState.refIndex >= links.length) throw new Error(`FCStd Sketch ${objectId} ExternalGeo item ${index} has an invalid RefIndex.`)
      const existing = groupIndexByExternalReference.get(externalState.ref)
      if (existing !== undefined && existing !== externalState.refIndex) throw new Error(`FCStd Sketch ${objectId} external reference '${externalState.ref}' has conflicting RefIndex values.`)
      groupIndexByExternalReference.set(externalState.ref, externalState.refIndex)
    }
    const claimedGroupIndexes = new Set(groupIndexByExternalReference.values())
    const projectionGroupIndexes = nativeExternalGeometry.map((candidate, index) => {
      const externalState = candidate.freecadExternal!
      let groupIndex = externalState.refIndex ?? groupIndexByExternalReference.get(externalState.ref)
      if (groupIndex === undefined) {
        const exactIndex = linkIndexByReference.get(externalState.ref)
        groupIndex = exactIndex !== undefined && !claimedGroupIndexes.has(exactIndex)
          ? exactIndex
          : links.findIndex((_link, candidateIndex) => !claimedGroupIndexes.has(candidateIndex))
        if (groupIndex < 0) throw new Error(`FCStd Sketch ${objectId} ExternalGeo item ${index} does not match a native link.`)
        groupIndexByExternalReference.set(externalState.ref, groupIndex)
        claimedGroupIndexes.add(groupIndex)
      }
      return groupIndex
    })
    const projectionCounts = new Map<number, number>()
    for (const groupIndex of projectionGroupIndexes) projectionCounts.set(groupIndex, (projectionCounts.get(groupIndex) ?? 0) + 1)
    if (links.some((_link, index) => !projectionCounts.has(index)) || [...projectionCounts].some(([index, count]) => externalTypes[index] === 0 && nativeKinds[index] !== 'face' && count !== 1)) return undefined
    if (nativeExternalGeometry.some((candidate, index) => externalTypes[projectionGroupIndexes[index]] === 0 && (nativeKinds[projectionGroupIndexes[index]] === 'vertex' ? candidate.type !== 'point' : candidate.type === 'point'))) return undefined
    const flagsByGroup = new Map<number, number>()
    for (const [index, groupIndex] of projectionGroupIndexes.entries()) {
      const flags = nativeExternalGeometry[index].freecadExternal!.flags
      const existing = flagsByGroup.get(groupIndex)
      if (existing !== undefined && existing !== flags) throw new Error(`FCStd Sketch ${objectId} external group ${groupIndex} has conflicting native flags.`)
      flagsByGroup.set(groupIndex, flags)
    }
    const sourceByGroup = new Map<number, string>()
    externalGeometry = nativeExternalGeometry.map((nativeGeometry, index) => {
      const groupIndex = projectionGroupIndexes[index]
      const link = links[groupIndex]
      const externalState = nativeGeometry.freecadExternal
      if (!externalState) throw new Error(`FCStd Sketch ${objectId} ExternalGeo item ${index} is missing native state.`)
      let source: TopoRefValue
      if (serializedSources) {
        try {
          const encoded = serializedSources[index]
          try { source = JSON.parse(encoded) as TopoRefValue }
          catch { source = JSON.parse(decodeURIComponent(encoded)) as TopoRefValue }
        } catch { throw new Error(`FCStd Sketch ${objectId} WebExternalSources item ${index} is not valid JSON.`) }
        const nativeKind = nativeKinds[groupIndex]
        if (source?.schemaVersion !== 1 || source.objectId !== link.objectId || source.persistentId !== link.subElement || source.kind !== nativeKind || source.status !== 'stable' || !Number.isSafeInteger(source.topologyVersion) || source.topologyVersion < 0 || !Number.isSafeInteger(source.generation) || source.generation < 0 || (source.signature !== undefined && typeof source.signature !== 'string') || (source.candidates !== undefined && (!Array.isArray(source.candidates) || source.candidates.some((candidate) => typeof candidate !== 'string' || !candidate.trim())))) throw new Error(`FCStd Sketch ${objectId} WebExternalSources item ${index} does not match native ExternalGeometry.`)
        const serializedSource = topoRefMetadataKey(source)
        const existingSource = sourceByGroup.get(groupIndex)
        if (existingSource !== undefined && existingSource !== serializedSource) throw new Error(`FCStd Sketch ${objectId} Face projection group ${groupIndex} has conflicting Web TopoRef metadata.`)
        sourceByGroup.set(groupIndex, serializedSource)
      } else {
        const kind = nativeKinds[groupIndex]!
        source = { schemaVersion: 1, objectId: link.objectId, kind, persistentId: link.subElement, topologyVersion: 0, generation: 0, status: 'stable' }
      }
      const { freecadExternal: _external, ...projectionValue } = nativeGeometry
      const projection = { ...projectionValue, id: projectionIds?.[index] ?? `ExternalProjection${index}`, construction: true } as SketchGeometry
      const mode = externalModeByNativeType[externalTypes[groupIndex]]
      return {
        id: externalIds?.[index] ?? `External${index}`,
        source,
        projection,
        construction: true as const,
        ...(mode === 'projection' ? {} : { mode }),
        ...webExternalStateFromNativeFlags(externalState.flags),
      }
    })
  }
  const syntheticConstraintProperty = summaries.find((property) => property.name === 'WebSyntheticConstraintIds' && property.element === 'StringList')
  let syntheticConstraintIds = new Set<string>()
  if (syntheticConstraintProperty) {
    let ids: unknown
    try { ids = JSON.parse(syntheticConstraintProperty.value) } catch { throw new Error(`FCStd Sketch ${objectId} WebSyntheticConstraintIds is not valid JSON.`) }
    if (!Array.isArray(ids) || ids.some((id) => typeof id !== 'string' || !id.trim()) || new Set(ids).size !== ids.length) throw new Error(`FCStd Sketch ${objectId} WebSyntheticConstraintIds must contain unique non-empty strings.`)
    syntheticConstraintIds = new Set(ids as string[])
  }
  const nativeGeometryForNativeId = (nativeId: number): ParsedNativeSketchGeometry | undefined => nativeId >= 0 && nativeId < nativeGeometry.length
    ? nativeGeometry[nativeId]
    : nativeId <= -3 ? externalGeometry[-nativeId - 3]?.projection : undefined
  const geometryForNativeId = (nativeId: number): SketchGeometry | undefined => {
    const candidate = nativeGeometryForNativeId(nativeId)
    return candidate && !candidate.freecadInternalType ? candidate : undefined
  }
  type InternalHelperBinding = { geometryId: string; internalGeometryIndex: number; alignmentType: 'ellipse-major' | 'ellipse-minor' | 'ellipse-focus' | 'hyperbola-major' | 'hyperbola-minor' | 'hyperbola-focus' | 'parabola-focus' | 'parabola-focal-axis' | 'bspline-control-point' | 'bspline-knot' }
  const internalHelperBindings = new Map<number, InternalHelperBinding>()
  for (const record of constraints) {
    if (record.type !== 15) continue
    if (record.internalAlignmentType === undefined || record.internalAlignmentIndex === undefined) continue
    const helperNativeId = record.ids[0]
    const targetNativeId = record.ids[1]
    const helper = nativeGeometryForNativeId(helperNativeId)
    const target = geometryForNativeId(targetNativeId)
    if (!helper || helper.freecadInternalType !== record.internalAlignmentType || !target || record.positions[1] !== 0 || record.ids[2] !== -2000 || record.positions[2] !== 0) return undefined
    let binding: InternalHelperBinding | undefined
    if (record.internalAlignmentType === 1 || record.internalAlignmentType === 2) {
      if (helper.type !== 'line' || (target.type !== 'ellipse' && target.type !== 'arcEllipse') || record.positions[0] !== 0 || record.internalAlignmentIndex !== -1) return undefined
      binding = { geometryId: target.id, internalGeometryIndex: 0, alignmentType: record.internalAlignmentType === 1 ? 'ellipse-major' : 'ellipse-minor' }
    } else if (record.internalAlignmentType === 3 || record.internalAlignmentType === 4) {
      if (helper.type !== 'point' || (target.type !== 'ellipse' && target.type !== 'arcEllipse') || record.positions[0] !== 1 || record.internalAlignmentIndex !== -1) return undefined
      binding = { geometryId: target.id, internalGeometryIndex: record.internalAlignmentType === 3 ? 0 : 1, alignmentType: 'ellipse-focus' }
    } else if (record.internalAlignmentType === 5 || record.internalAlignmentType === 6) {
      if (helper.type !== 'line' || target.type !== 'arcHyperbola' || record.positions[0] !== 0 || record.internalAlignmentIndex !== -1) return undefined
      binding = { geometryId: target.id, internalGeometryIndex: 0, alignmentType: record.internalAlignmentType === 5 ? 'hyperbola-major' : 'hyperbola-minor' }
    } else if (record.internalAlignmentType === 7) {
      if (helper.type !== 'point' || target.type !== 'arcHyperbola' || record.positions[0] !== 1 || record.internalAlignmentIndex !== -1) return undefined
      binding = { geometryId: target.id, internalGeometryIndex: 0, alignmentType: 'hyperbola-focus' }
    } else if (record.internalAlignmentType === 8) {
      if (helper.type !== 'point' || target.type !== 'arcParabola' || record.positions[0] !== 1 || record.internalAlignmentIndex !== -1) return undefined
      binding = { geometryId: target.id, internalGeometryIndex: 0, alignmentType: 'parabola-focus' }
    } else if (record.internalAlignmentType === 9) {
      if (helper.type !== 'circle' || target.type !== 'bspline' || record.positions[0] !== 3 || record.internalAlignmentIndex < 0 || record.internalAlignmentIndex >= target.controlPoints.length) return undefined
      binding = { geometryId: target.id, internalGeometryIndex: record.internalAlignmentIndex, alignmentType: 'bspline-control-point' }
    } else if (record.internalAlignmentType === 10) {
      if (helper.type !== 'point' || target.type !== 'bspline' || record.positions[0] !== 1) return undefined
      const uniqueKnots = bsplineExpandedKnots(target).filter((knot, index, knots) => index === 0 || knot !== knots[index - 1])
      if (record.internalAlignmentIndex < 0 || record.internalAlignmentIndex >= uniqueKnots.length) return undefined
      binding = { geometryId: target.id, internalGeometryIndex: record.internalAlignmentIndex, alignmentType: 'bspline-knot' }
    } else if (record.internalAlignmentType === 11) {
      if (helper.type !== 'line' || target.type !== 'arcParabola' || record.positions[0] !== 0 || record.internalAlignmentIndex !== -1) return undefined
      binding = { geometryId: target.id, internalGeometryIndex: 0, alignmentType: 'parabola-focal-axis' }
    } else return undefined
    const existing = internalHelperBindings.get(helperNativeId)
    if (existing && JSON.stringify(existing) !== JSON.stringify(binding)) throw new Error(`FCStd Sketch ${objectId} internal helper ${helperNativeId} has conflicting geometry bindings.`)
    internalHelperBindings.set(helperNativeId, binding)
  }
  if (nativeGeometry.some((candidate, nativeIndex) => candidate.freecadInternalType && !internalHelperBindings.has(nativeIndex))) return undefined
  const point = (record: typeof constraints[number], slot: number): SketchPointRef | undefined => {
    const index = record.ids[slot]
    if (index === -2000) return undefined
    const candidate = geometryForNativeId(index)
    if (!candidate) return undefined
    const position = record.positions[slot]
    if (candidate.type === 'line') return position === 1 ? { geometryId: candidate.id, point: 'start' } : position === 2 ? { geometryId: candidate.id, point: 'end' } : undefined
    if (candidate.type === 'point') return position === 1 ? { geometryId: candidate.id, point: 'position' } : undefined
    if (candidate.type === 'circle' || candidate.type === 'arc' || candidate.type === 'ellipse' || candidate.type === 'arcEllipse' || candidate.type === 'arcHyperbola' || candidate.type === 'arcParabola') return position === 3 ? { geometryId: candidate.id, point: 'center' } : undefined
    return undefined
  }
  const geometryRef = (record: typeof constraints[number], slot: number) => geometryForNativeId(record.ids[slot])?.id
  const mapped: SketchConstraint[] = []
  const consumedSyntheticConstraintIds = new Set<string>()
  for (const [index, record] of constraints.entries()) {
    const id = record.id.trim() || `Constraint${index}`
    if (syntheticConstraintIds.has(id)) {
      if (record.type !== 15 || internalHelperBindings.get(record.ids[0])?.alignmentType !== 'bspline-control-point') throw new Error(`FCStd Sketch ${objectId} synthetic constraint '${id}' is not a B-spline control-point alignment.`)
      if (consumedSyntheticConstraintIds.has(id)) throw new Error(`FCStd Sketch ${objectId} contains duplicate synthetic constraint '${id}'.`)
      consumedSyntheticConstraintIds.add(id)
      continue
    }
    const first = point(record, 0)
    const second = point(record, 1)
    const third = point(record, 2)
    const firstGeometryId = geometryRef(record, 0)
    const secondGeometryId = geometryRef(record, 1)
    const thirdGeometryId = geometryRef(record, 2)
    let constraint: SketchConstraint | undefined
    switch (record.type) {
      case 1: if (first && second) constraint = { id, type: 'coincident', first, second, driving: record.driving }; break
      case 2: if (firstGeometryId) constraint = { id, type: 'horizontal', geometryId: firstGeometryId, driving: record.driving }; break
      case 3: if (firstGeometryId) constraint = { id, type: 'vertical', geometryId: firstGeometryId, driving: record.driving }; break
      case 4: if (firstGeometryId && secondGeometryId) constraint = { id, type: 'parallel', firstGeometryId, secondGeometryId, driving: record.driving }; break
      case 5: if (firstGeometryId && secondGeometryId) constraint = { id, type: 'tangent', firstGeometryId, secondGeometryId, driving: record.driving }; break
      case 6: if (first && second) constraint = { id, type: 'distance', first, second, value: record.value, driving: record.driving }; break
      case 7: if (first && second) constraint = { id, type: 'distanceX', first, second, value: record.value, driving: record.driving }; break
      case 8: if (first && second) constraint = { id, type: 'distanceY', first, second, value: record.value, driving: record.driving }; break
      case 9: if (firstGeometryId) constraint = { id, type: 'angle', geometryId: firstGeometryId, value: record.value, driving: record.driving }; break
      case 10: if (firstGeometryId && secondGeometryId) constraint = { id, type: 'perpendicular', firstGeometryId, secondGeometryId, driving: record.driving }; break
      case 11: if (firstGeometryId) constraint = { id, type: 'radius', geometryId: firstGeometryId, value: record.value, driving: record.driving }; break
      case 12: if (firstGeometryId && secondGeometryId) constraint = { id, type: 'equal', firstGeometryId, secondGeometryId, driving: record.driving }; break
      case 13: if (first && secondGeometryId) constraint = { id, type: 'pointOnObject', point: first, geometryId: secondGeometryId, driving: record.driving }; break
      case 14: if (first && second && third) constraint = { id, type: 'symmetric', first, second, center: third, driving: record.driving }; break
      case 15: {
        const binding = internalHelperBindings.get(record.ids[0])
        if (binding) constraint = { id, type: 'internalAlignment', ...binding, driving: record.driving }
        break
      }
      case 16: if (first && second && thirdGeometryId) constraint = { id, type: 'snellsLaw', first, second, boundaryGeometryId: thirdGeometryId, value: record.value, driving: record.driving }; break
      case 17: if (firstGeometryId) constraint = { id, type: 'block', geometryId: firstGeometryId, driving: record.driving }; break
      case 18: if (firstGeometryId) constraint = { id, type: 'diameter', geometryId: firstGeometryId, value: record.value, driving: record.driving }; break
      case 19: {
        const binding = internalHelperBindings.get(record.ids[0])
        if (binding?.alignmentType === 'bspline-control-point' && Number.isFinite(record.value) && record.value > 0) constraint = { id, type: 'weight', geometryId: binding.geometryId, controlPointIndex: binding.internalGeometryIndex, value: record.value, driving: record.driving }
        break
      }
      default: return undefined
    }
    if (!constraint) return undefined
    mapped.push(constraint)
  }
  if (consumedSyntheticConstraintIds.size !== syntheticConstraintIds.size) throw new Error(`FCStd Sketch ${objectId} WebSyntheticConstraintIds references a missing native constraint.`)
  const sketch: SketchSnapshot = { id: objectId, geometry, externalGeometry, constraints: mapped, solver: { status: 'under-constrained', degreesOfFreedom: 0, residual: 0, iterations: 0, diagnostics: [] } }
  try { validateSketchSnapshot(sketch); return sketch } catch { return undefined }
}

export const decodeFcstdPropertyValue = (property: FcstdPropertySummary): FcstdDecodedPropertyValue => {
  const numericTypes = new Set(['App::PropertyLength', 'App::PropertyDistance', 'App::PropertyAngle', 'App::PropertyAcceleration', 'App::PropertyArea', 'App::PropertyForce', 'App::PropertyHeatFlux', 'App::PropertyQuantityConstraint', 'App::PropertyFloat', 'App::PropertyFloatConstraint', 'App::PropertyPrecision', 'App::PropertyInteger', 'App::PropertyIntegerConstraint', 'App::PropertyPercent'])
  if (numericTypes.has(property.typeId)) {
    const value = Number(property.value)
    return Number.isFinite(value) ? { value, decoded: true } : { value: property.value, decoded: false, error: `Property ${property.name} is not a finite number.` }
  }
  if (property.typeId === 'App::PropertyEnumeration') {
    const index = Number(property.value)
    if (!Number.isSafeInteger(index) || index < 0) return { value: property.value, decoded: false, error: `Property ${property.name} is not a valid enumeration index.` }
    if (property.name === 'MapMode' && !property.enumOptions) return index < freecadAttachmentMapModes.length
      ? { value: freecadAttachmentMapModes[index], decoded: true }
      : { value: index, decoded: false, error: `Property ${property.name} enumeration index is outside the native FreeCAD map modes.` }
    if (!property.enumOptions) return { value: index, decoded: true }
    return index < property.enumOptions.length ? { value: property.enumOptions[index], decoded: true } : { value: index, decoded: false, error: `Property ${property.name} enumeration index is outside its options.` }
  }
  if (property.typeId === 'App::PropertyBool') {
    if (property.value === 'true' || property.value === 'false') return { value: property.value === 'true', decoded: true }
    return { value: property.value, decoded: false, error: `Property ${property.name} is not a boolean.` }
  }
  if (property.typeId === 'App::PropertyColor') {
    if (property.element !== 'PropertyColor' || !/^\d+$/.test(property.value)) return { value: property.value, decoded: false, error: `Property ${property.name} is not a packed RGBA color.` }
    const packed = Number(property.value)
    if (!Number.isSafeInteger(packed) || packed < 0 || packed > 0xffffffff) return { value: property.value, decoded: false, error: `Property ${property.name} packed RGBA value is outside uint32.` }
    return { value: packedToColorChannels(packed), decoded: true }
  }
  if (property.typeId === 'App::PropertyColorList') {
    if (property.element !== 'ColorList') return { value: property.value, decoded: false, error: `Property ${property.name} is not a native ColorList payload.` }
    try {
      const value = JSON.parse(property.value) as unknown
      if (!Array.isArray(value) || !value.every((color) => Array.isArray(color) && color.length === 4 && color.every((channel) => typeof channel === 'number' && Number.isFinite(channel)))) throw new Error('value is not a list of four finite RGBA channels')
      return { value, decoded: true }
    } catch (error) {
      return { value: property.value, decoded: false, error: `Property ${property.name} contains invalid ColorList data: ${error instanceof Error ? error.message : String(error)}` }
    }
  }
  if (property.typeId === 'App::PropertyFile') {
    if (property.element !== 'String') return { value: property.value, decoded: false, error: `Property ${property.name} is not a native String payload.` }
    try {
      return { value: propertyFileValue(property.value, property.name), decoded: true }
    } catch (error) {
      return { value: property.value, decoded: false, error: error instanceof Error ? error.message : String(error) }
    }
  }
  if (property.typeId === 'App::PropertyFont') {
    if (property.element !== 'String') return { value: property.value, decoded: false, error: `Property ${property.name} is not a native String payload.` }
    try {
      return { value: propertyFontValue(property.value, property.name), decoded: true }
    } catch (error) {
      return { value: property.value, decoded: false, error: error instanceof Error ? error.message : String(error) }
    }
  }
  if (property.typeId === 'App::PropertyFileIncluded') {
    if (property.element !== 'FileIncluded') return { value: property.value, decoded: false, error: `Property ${property.name} is not a native FileIncluded payload.` }
    if (!property.resourcePath) return { value: null, decoded: true }
    return { value: property.includedFileResource ?? property.value, decoded: false, error: `Property ${property.name} bytes must be stored through project.fcstd.storeIncludedFiles before the value becomes editable.` }
  }
  if (property.typeId === 'App::PropertyLinkSub' && property.subElements) return {
    value: { schemaVersion: 1, objectId: property.value, subElements: [...property.subElements] },
    decoded: true,
  }
  if (property.typeId === 'App::PropertyLink' || property.typeId === 'App::PropertyLinkHidden') return { value: property.value || null, decoded: true }
  if (property.typeId === 'App::PropertyLinkList' && property.links) return { value: [...property.links], decoded: true }
  if (property.typeId === 'App::PropertyLinkSubList' && property.linkSubs) return { value: { schemaVersion: 1, entries: property.linkSubs.map((entry) => ({ ...entry })) }, decoded: true }
  if (property.typeId === 'Part::PropertyPartShape' && property.shapeResource) return { value: { ...property.shapeResource, format: 'brep' }, decoded: true }
  if (property.typeId === 'Path::PropertyPath') {
    try {
      const value = JSON.parse(property.value) as unknown
      return { value: pathPropertyValue(value, property.name), decoded: true }
    } catch (error) {
      return { value: property.value, decoded: false, error: `Property ${property.name} contains invalid Path data: ${error instanceof Error ? error.message : String(error)}` }
    }
  }
  if (property.typeId === 'App::PropertyDirection' && property.element !== 'PropertyVector') return { value: property.value, decoded: false, error: `Property ${property.name} is not a native PropertyVector payload.` }
  if (property.typeId === 'App::PropertyLinkSub' || property.typeId === 'App::PropertyLinkSubList' || property.typeId === 'App::PropertyLinkList' || property.typeId === 'App::PropertyBoolList' || property.typeId === 'App::PropertyFloatList' || property.typeId === 'App::PropertyIntegerList' || property.typeId === 'App::PropertyIntegerSet' || property.typeId === 'App::PropertyStringList' || property.typeId === 'App::PropertyVector' || property.typeId === 'App::PropertyDirection' || property.typeId === 'App::PropertyPlacement' || property.typeId === 'App::PropertyMultiTransform' || property.typeId === 'Part::PropertyGeometryList' || property.typeId === 'Sketcher::PropertyConstraintList') {
    try { return { value: JSON.parse(property.value), decoded: true } } catch (error) { return { value: property.value, decoded: false, error: `Property ${property.name} contains invalid JSON: ${error instanceof Error ? error.message : String(error)}` } }
  }
  return { value: property.value, decoded: true }
}

export const encodeFcstdPathResource = (value: PathPropertyValue): Uint8Array => new TextEncoder().encode(pathResourceText(pathPropertyValue(value, 'Path').commands))

export const decodeFcstdPathProperty = (bytes: Uint8Array, objectName: string, propertyName = 'Path', options: FcstdPathAccessOptions = {}): PathPropertyValue => {
  if (!objectName.trim()) throw new TypeError('FCStd Path decoder requires an object name.')
  const inspection = inspectFcstdArchive(bytes)
  const object = inspection.objects.find((candidate) => candidate.name === objectName)
  if (!object) throw new Error(`FCStd Path object does not exist: ${objectName}`)
  const featurePython = object.typeId === 'Path::FeaturePython'
  if (object.typeId !== 'Path::Feature' && !(featurePython && options.allowFeaturePython === true)) throw new Error(`FCStd Path editing is limited to native Path::Feature objects unless allowFeaturePython is explicitly enabled; ${objectName} is ${object.typeId}.`)
  const property = object.properties.find((candidate) => candidate.name === propertyName && candidate.typeId === 'Path::PropertyPath')
  if (!property) throw new Error(`FCStd Path property does not exist: ${objectName}.${propertyName}`)
  const decoded = decodeFcstdPropertyValue(property)
  if (!decoded.decoded) throw new Error(decoded.error ?? `FCStd Path property ${objectName}.${propertyName} could not be decoded.`)
  return pathPropertyValue(decoded.value, `${objectName}.${propertyName}`)
}

/**
 * Rewrites only the native .nc payload referenced by Path::PropertyPath. The
 * XML metadata and all unknown FCStd entries remain unchanged, so FreeCAD's
 * native Path loader remains the reopen authority and Python payloads never
 * execute in the browser.
 */
export const rewriteFcstdPathProperty = (bytes: Uint8Array, edit: FcstdPathEdit): Uint8Array => {
  const propertyName = edit.propertyName ?? 'Path'
  const current = decodeFcstdPathProperty(bytes, edit.objectName, propertyName, { allowFeaturePython: edit.allowFeaturePython })
  const next = pathPropertyValue(edit.value, `${edit.objectName}.${propertyName}`)
  if ((next.resourcePath ?? current.resourcePath) !== current.resourcePath) throw new Error('FCStd Path rewrite cannot change the native resource path.')
  if ((next.version ?? current.version) !== current.version) throw new Error('FCStd Path rewrite cannot change the native Path resource version.')
  if (JSON.stringify(next.center ?? current.center) !== JSON.stringify(current.center)) throw new Error('FCStd Path rewrite cannot change the native Path center metadata.')
  const resourcePath = current.resourcePath
  if (!resourcePath) throw new Error('FCStd Path rewrite requires a native resource path.')
  const entries = inspectZipDirectory(bytes, DEFAULT_FCSTD_LIMITS)
  const files = unzipSync(bytes)
  if (!files[resourcePath]) throw new Error(`FCStd Path resource is missing: ${resourcePath}`)
  files[resourcePath] = new Uint8Array(encodeFcstdPathResource({ ...next, resourcePath, version: current.version, center: current.center }))
  const output: Record<string, Uint8Array> = {}
  for (const entry of entries) output[entry.path] = new Uint8Array(files[entry.path])
  return new Uint8Array(zipSync(output, { level: 6 }))
}

/** Rewrites one locked native Float property while preserving every other archive entry. */
export const rewriteFcstdNumericProperty = (bytes: Uint8Array, edit: FcstdNumericPropertyEdit): Uint8Array => {
  if (!edit.objectName.trim() || !edit.propertyName.trim()) throw new TypeError('FCStd numeric property rewrite requires non-empty object and property names.')
  if (!Number.isFinite(edit.value)) throw new TypeError(`FCStd numeric property ${edit.objectName}.${edit.propertyName} requires a finite value.`)
  if (edit.expectedValue !== undefined && !Number.isFinite(edit.expectedValue)) throw new TypeError('FCStd numeric property expectedValue must be finite.')

  const inspection = inspectFcstdArchive(bytes)
  const object = inspection.objects.find((candidate) => candidate.name === edit.objectName)
  if (!object) throw new Error(`FCStd numeric property object does not exist: ${edit.objectName}`)
  const summary = object.properties.find((candidate) => candidate.name === edit.propertyName)
  if (!summary) throw new Error(`FCStd numeric property does not exist: ${edit.objectName}.${edit.propertyName}`)
  if (summary.typeId !== edit.typeId || summary.element !== 'Float') throw new TypeError(`FCStd numeric property ${edit.objectName}.${edit.propertyName} must be ${edit.typeId} with a Float payload.`)
  const decoded = decodeFcstdPropertyValue(summary)
  if (!decoded.decoded || typeof decoded.value !== 'number' || !Number.isFinite(decoded.value)) throw new TypeError(`FCStd numeric property ${edit.objectName}.${edit.propertyName} is not decodable.`)
  if (edit.expectedValue !== undefined && !Object.is(decoded.value, edit.expectedValue)) throw new RangeError(`FCStd numeric property ${edit.objectName}.${edit.propertyName} expected ${edit.expectedValue}, found ${decoded.value}.`)

  const entries = inspectZipDirectory(bytes, DEFAULT_FCSTD_LIMITS)
  const files = unzipSync(bytes)
  const documentPath = entries.find((entry) => entry.path.toLowerCase() === 'document.xml')?.path
  if (!documentPath || !files[documentPath]) throw new Error('FCStd archive does not contain Document.xml.')
  const parsed = parseXmlRoot(files[documentPath], documentPath, 'Document', DEFAULT_FCSTD_LIMITS)
  const objectDeclarations = asArray((((parsed.root.Objects as Record<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const declaration = objectDeclarations.find((candidate) => attribute(candidate, 'name') === edit.objectName)
  if (!declaration) throw new Error(`FCStd numeric property object declaration does not exist: ${edit.objectName}`)
  declaration['@_Touched'] = '1'
  const objectData = asArray((((parsed.root.ObjectData as Record<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const data = objectData.find((candidate) => attribute(candidate, 'name') === edit.objectName)
  const properties = asArray(((((data?.Properties as Record<string, unknown> | undefined)?.Property)) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const property = properties.find((candidate) => attribute(candidate, 'name') === edit.propertyName && attribute(candidate, 'type') === edit.typeId)
  if (!property || !property.Float || typeof property.Float !== 'object' || Array.isArray(property.Float)) throw new TypeError(`FCStd numeric property ${edit.objectName}.${edit.propertyName} has no unique Float payload.`)
  ;(property.Float as Record<string, unknown>)['@_value'] = String(edit.value)
  const documentXml = new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: '@_', format: false, processEntities: false, suppressBooleanAttributes: false, suppressEmptyNode: true }).build({ Document: parsed.root })
  files[documentPath] = new TextEncoder().encode(documentXml)
  const output: Record<string, Uint8Array> = {}
  for (const entry of entries) output[entry.path] = new Uint8Array(files[entry.path])
  const rewritten = new Uint8Array(zipSync(output, { level: 6 }))
  const rewrittenSummary = inspectFcstdArchive(rewritten).objects.find((candidate) => candidate.name === edit.objectName)?.properties.find((candidate) => candidate.name === edit.propertyName)
  const rewrittenValue = rewrittenSummary ? decodeFcstdPropertyValue(rewrittenSummary) : undefined
  if (!rewrittenValue?.decoded || rewrittenValue.value !== edit.value) throw new Error(`FCStd numeric property ${edit.objectName}.${edit.propertyName} did not rewrite exactly.`)
  return rewritten
}

/** Rewrites one native App::PropertyBoolList bitset while preserving every other archive entry. */
export const rewriteFcstdBoolListProperty = (bytes: Uint8Array, edit: FcstdBoolListEdit): Uint8Array => {
  if (!edit.objectName.trim() || !edit.propertyName.trim()) throw new TypeError('FCStd BoolList rewrite requires non-empty object and property names.')
  const validateValue = (value: boolean[], context: string) => {
    if (!Array.isArray(value) || !value.every((entry) => typeof entry === 'boolean')) throw new TypeError(`FCStd BoolList ${context} requires a boolean list.`)
  }
  const equal = (left: boolean[], right: boolean[]) => left.length === right.length && left.every((entry, index) => entry === right[index])
  const encoded = (value: boolean[]) => [...value].reverse().map((entry) => entry ? '1' : '0').join('')
  validateValue(edit.value, 'value')
  if (edit.expectedValue !== undefined) validateValue(edit.expectedValue, 'expectedValue')

  const inspection = inspectFcstdArchive(bytes)
  const object = inspection.objects.find((candidate) => candidate.name === edit.objectName)
  if (!object) throw new Error(`FCStd BoolList object does not exist: ${edit.objectName}`)
  const summary = object.properties.find((candidate) => candidate.name === edit.propertyName)
  if (!summary) throw new Error(`FCStd BoolList property does not exist: ${edit.objectName}.${edit.propertyName}`)
  if (summary.typeId !== 'App::PropertyBoolList' || summary.element !== 'BoolList') throw new TypeError(`FCStd property ${edit.objectName}.${edit.propertyName} must be App::PropertyBoolList with a BoolList payload.`)
  const decoded = decodeFcstdPropertyValue(summary)
  if (!decoded.decoded || !Array.isArray(decoded.value) || !decoded.value.every((entry) => typeof entry === 'boolean')) throw new TypeError(`FCStd BoolList ${edit.objectName}.${edit.propertyName} is not decodable.`)
  if (edit.expectedValue !== undefined && !equal(decoded.value as boolean[], edit.expectedValue)) throw new RangeError(`FCStd BoolList ${edit.objectName}.${edit.propertyName} does not match expectedValue.`)

  const entries = inspectZipDirectory(bytes, DEFAULT_FCSTD_LIMITS)
  const files = unzipSync(bytes)
  const documentPath = entries.find((entry) => entry.path.toLowerCase() === 'document.xml')?.path
  if (!documentPath || !files[documentPath]) throw new Error('FCStd archive does not contain Document.xml.')
  const parsed = parseXmlRoot(files[documentPath], documentPath, 'Document', DEFAULT_FCSTD_LIMITS)
  const objectDeclarations = asArray((((parsed.root.Objects as Record<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const declarations = objectDeclarations.filter((candidate) => attribute(candidate, 'name') === edit.objectName)
  if (declarations.length !== 1) throw new Error(`FCStd BoolList object declaration is not unique: ${edit.objectName}`)
  declarations[0]['@_Touched'] = '1'
  const objectData = asArray((((parsed.root.ObjectData as Record<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const data = objectData.filter((candidate) => attribute(candidate, 'name') === edit.objectName)
  if (data.length !== 1) throw new Error(`FCStd BoolList object data is not unique: ${edit.objectName}`)
  const properties = asArray(((((data[0].Properties as Record<string, unknown> | undefined)?.Property)) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const candidates = properties.filter((candidate) => attribute(candidate, 'name') === edit.propertyName && attribute(candidate, 'type') === 'App::PropertyBoolList')
  if (candidates.length !== 1 || !candidates[0].BoolList || typeof candidates[0].BoolList !== 'object' || Array.isArray(candidates[0].BoolList)) throw new TypeError(`FCStd BoolList ${edit.objectName}.${edit.propertyName} has no unique BoolList payload.`)
  ;(candidates[0].BoolList as Record<string, unknown>)['@_value'] = encoded(edit.value)
  const documentXml = new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: '@_', format: false, processEntities: false, suppressBooleanAttributes: false, suppressEmptyNode: true }).build({ Document: parsed.root })
  files[documentPath] = new TextEncoder().encode(documentXml)
  const output: Record<string, Uint8Array> = {}
  for (const entry of entries) output[entry.path] = new Uint8Array(files[entry.path])
  const rewritten = new Uint8Array(zipSync(output, { level: 6 }))
  const rewrittenSummary = inspectFcstdArchive(rewritten).objects.find((candidate) => candidate.name === edit.objectName)?.properties.find((candidate) => candidate.name === edit.propertyName)
  const rewrittenValue = rewrittenSummary ? decodeFcstdPropertyValue(rewrittenSummary) : undefined
  if (!rewrittenValue?.decoded || !Array.isArray(rewrittenValue.value) || !equal(rewrittenValue.value as boolean[], edit.value)) throw new Error(`FCStd BoolList ${edit.objectName}.${edit.propertyName} did not rewrite exactly.`)
  return rewritten
}

/** Rewrites one native App::PropertyIntegerSet while preserving every other archive entry. */
export const rewriteFcstdIntegerSetProperty = (bytes: Uint8Array, edit: FcstdIntegerSetEdit): Uint8Array => {
  if (!edit.objectName.trim() || !edit.propertyName.trim()) throw new TypeError('FCStd IntegerSet rewrite requires non-empty object and property names.')
  const normalize = (value: number[], context: string) => {
    if (!Array.isArray(value) || !value.every((entry) => typeof entry === 'number' && Number.isSafeInteger(entry))) throw new TypeError(`FCStd IntegerSet ${context} requires a safe-integer array.`)
    return [...new Set(value)].sort((left, right) => left - right)
  }
  const equal = (left: number[], right: number[]) => left.length === right.length && left.every((entry, index) => entry === right[index])
  const nextValue = normalize(edit.value, 'value')
  const expectedValue = edit.expectedValue === undefined ? undefined : normalize(edit.expectedValue, 'expectedValue')

  const inspection = inspectFcstdArchive(bytes)
  const object = inspection.objects.find((candidate) => candidate.name === edit.objectName)
  if (!object) throw new Error(`FCStd IntegerSet object does not exist: ${edit.objectName}`)
  const summary = object.properties.find((candidate) => candidate.name === edit.propertyName)
  if (!summary) throw new Error(`FCStd IntegerSet property does not exist: ${edit.objectName}.${edit.propertyName}`)
  if (summary.typeId !== 'App::PropertyIntegerSet' || summary.element !== 'IntegerSet') throw new TypeError(`FCStd property ${edit.objectName}.${edit.propertyName} must be App::PropertyIntegerSet with an IntegerSet payload.`)
  const decoded = decodeFcstdPropertyValue(summary)
  if (!decoded.decoded || !Array.isArray(decoded.value) || !decoded.value.every((entry) => typeof entry === 'number' && Number.isSafeInteger(entry))) throw new TypeError(`FCStd IntegerSet ${edit.objectName}.${edit.propertyName} is not decodable.`)
  if (expectedValue !== undefined && !equal(decoded.value as number[], expectedValue)) throw new RangeError(`FCStd IntegerSet ${edit.objectName}.${edit.propertyName} does not match expectedValue.`)

  const entries = inspectZipDirectory(bytes, DEFAULT_FCSTD_LIMITS)
  const files = unzipSync(bytes)
  const documentPath = entries.find((entry) => entry.path.toLowerCase() === 'document.xml')?.path
  if (!documentPath || !files[documentPath]) throw new Error('FCStd archive does not contain Document.xml.')
  const parsed = parseXmlRoot(files[documentPath], documentPath, 'Document', DEFAULT_FCSTD_LIMITS)
  const declarations = asArray((((parsed.root.Objects as Record<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | undefined)).filter((candidate) => attribute(candidate, 'name') === edit.objectName)
  if (declarations.length !== 1) throw new Error(`FCStd IntegerSet object declaration is not unique: ${edit.objectName}`)
  declarations[0]['@_Touched'] = '1'
  const objectData = asArray((((parsed.root.ObjectData as Record<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | undefined)).filter((candidate) => attribute(candidate, 'name') === edit.objectName)
  if (objectData.length !== 1) throw new Error(`FCStd IntegerSet object data is not unique: ${edit.objectName}`)
  const properties = asArray(((((objectData[0].Properties as Record<string, unknown> | undefined)?.Property)) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const candidates = properties.filter((candidate) => attribute(candidate, 'name') === edit.propertyName && attribute(candidate, 'type') === 'App::PropertyIntegerSet')
  if (candidates.length !== 1 || !candidates[0].IntegerSet || typeof candidates[0].IntegerSet !== 'object' || Array.isArray(candidates[0].IntegerSet)) throw new TypeError(`FCStd IntegerSet ${edit.objectName}.${edit.propertyName} has no unique IntegerSet payload.`)
  const payload = candidates[0].IntegerSet as Record<string, unknown>
  payload['@_count'] = String(nextValue.length)
  payload.I = nextValue.map((value) => ({ '@_v': String(value) }))
  const documentXml = new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: '@_', format: false, processEntities: false, suppressBooleanAttributes: false, suppressEmptyNode: true }).build({ Document: parsed.root })
  files[documentPath] = new TextEncoder().encode(documentXml)
  const output: Record<string, Uint8Array> = {}
  for (const entry of entries) output[entry.path] = new Uint8Array(files[entry.path])
  const rewritten = new Uint8Array(zipSync(output, { level: 6 }))
  const rewrittenSummary = inspectFcstdArchive(rewritten).objects.find((candidate) => candidate.name === edit.objectName)?.properties.find((candidate) => candidate.name === edit.propertyName)
  const rewrittenValue = rewrittenSummary ? decodeFcstdPropertyValue(rewrittenSummary) : undefined
  if (!rewrittenValue?.decoded || !Array.isArray(rewrittenValue.value) || !equal(rewrittenValue.value as number[], nextValue)) throw new Error(`FCStd IntegerSet ${edit.objectName}.${edit.propertyName} did not rewrite exactly.`)
  return rewritten
}

/** Rewrites one native App::PropertyColor packed RGBA value while preserving every other archive entry. */
export const rewriteFcstdColorProperty = (bytes: Uint8Array, edit: FcstdColorEdit): Uint8Array => {
  if (!edit.objectName.trim() || !edit.propertyName.trim()) throw new TypeError('FCStd PropertyColor rewrite requires non-empty object and property names.')
  const validateValue = (value: ColorValue, context: string) => {
    if (!Array.isArray(value) || value.length !== 4 || !value.every((channel) => typeof channel === 'number' && Number.isFinite(channel))) throw new TypeError(`FCStd PropertyColor ${context} requires four finite RGBA channels.`)
  }
  const equal = (left: ColorValue, right: ColorValue) => left.every((channel, index) => Object.is(channel, right[index]))
  validateValue(edit.value, 'value')
  if (edit.expectedValue !== undefined) validateValue(edit.expectedValue, 'expectedValue')

  const inspection = inspectFcstdArchive(bytes)
  const object = inspection.objects.find((candidate) => candidate.name === edit.objectName)
  if (!object) throw new Error(`FCStd PropertyColor object does not exist: ${edit.objectName}`)
  const summary = object.properties.find((candidate) => candidate.name === edit.propertyName)
  if (!summary) throw new Error(`FCStd PropertyColor property does not exist: ${edit.objectName}.${edit.propertyName}`)
  if (summary.typeId !== 'App::PropertyColor' || summary.element !== 'PropertyColor') throw new TypeError(`FCStd property ${edit.objectName}.${edit.propertyName} must be App::PropertyColor with a PropertyColor payload.`)
  const decoded = decodeFcstdPropertyValue(summary)
  if (!decoded.decoded || !Array.isArray(decoded.value) || decoded.value.length !== 4 || !decoded.value.every((channel) => typeof channel === 'number' && Number.isFinite(channel))) throw new TypeError(`FCStd PropertyColor ${edit.objectName}.${edit.propertyName} is not decodable.`)
  if (edit.expectedValue !== undefined && !equal(decoded.value as ColorValue, edit.expectedValue)) throw new RangeError(`FCStd PropertyColor ${edit.objectName}.${edit.propertyName} does not match expectedValue.`)

  const packed = colorChannelsToPacked(edit.value, `${edit.objectName}.${edit.propertyName}`)
  const nativeValue = packedToColorChannels(packed)
  const entries = inspectZipDirectory(bytes, DEFAULT_FCSTD_LIMITS)
  const files = unzipSync(bytes)
  const documentPath = entries.find((entry) => entry.path.toLowerCase() === 'document.xml')?.path
  if (!documentPath || !files[documentPath]) throw new Error('FCStd archive does not contain Document.xml.')
  const parsed = parseXmlRoot(files[documentPath], documentPath, 'Document', DEFAULT_FCSTD_LIMITS)
  const objectDeclarations = asArray((((parsed.root.Objects as Record<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const declarations = objectDeclarations.filter((candidate) => attribute(candidate, 'name') === edit.objectName)
  if (declarations.length !== 1) throw new Error(`FCStd PropertyColor object declaration is not unique: ${edit.objectName}`)
  declarations[0]['@_Touched'] = '1'
  const objectData = asArray((((parsed.root.ObjectData as Record<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const data = objectData.filter((candidate) => attribute(candidate, 'name') === edit.objectName)
  if (data.length !== 1) throw new Error(`FCStd PropertyColor object data is not unique: ${edit.objectName}`)
  const properties = asArray(((((data[0].Properties as Record<string, unknown> | undefined)?.Property)) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const candidates = properties.filter((candidate) => attribute(candidate, 'name') === edit.propertyName && attribute(candidate, 'type') === 'App::PropertyColor')
  if (candidates.length !== 1 || !candidates[0].PropertyColor || typeof candidates[0].PropertyColor !== 'object' || Array.isArray(candidates[0].PropertyColor)) throw new TypeError(`FCStd PropertyColor ${edit.objectName}.${edit.propertyName} has no unique PropertyColor payload.`)
  ;(candidates[0].PropertyColor as Record<string, unknown>)['@_value'] = String(packed)
  const documentXml = new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: '@_', format: false, processEntities: false, suppressBooleanAttributes: false, suppressEmptyNode: true }).build({ Document: parsed.root })
  files[documentPath] = new TextEncoder().encode(documentXml)
  const output: Record<string, Uint8Array> = {}
  for (const entry of entries) output[entry.path] = new Uint8Array(files[entry.path])
  const rewritten = new Uint8Array(zipSync(output, { level: 6 }))
  const rewrittenSummary = inspectFcstdArchive(rewritten).objects.find((candidate) => candidate.name === edit.objectName)?.properties.find((candidate) => candidate.name === edit.propertyName)
  const rewrittenValue = rewrittenSummary ? decodeFcstdPropertyValue(rewrittenSummary) : undefined
  if (!rewrittenValue?.decoded || !Array.isArray(rewrittenValue.value) || !equal(rewrittenValue.value as ColorValue, nativeValue)) throw new Error(`FCStd PropertyColor ${edit.objectName}.${edit.propertyName} did not rewrite exactly.`)
  return rewritten
}

/** Rewrites one native App::PropertyColorList external resource while preserving every other archive entry. */
export const rewriteFcstdColorListProperty = (bytes: Uint8Array, edit: FcstdColorListEdit): Uint8Array => {
  if (!edit.objectName.trim() || !edit.propertyName.trim()) throw new TypeError('FCStd ColorList rewrite requires non-empty object and property names.')
  const validateValue = (value: ColorListValue, context: string) => {
    if (!Array.isArray(value) || !value.every((color) => Array.isArray(color) && color.length === 4 && color.every((channel) => typeof channel === 'number' && Number.isFinite(channel)))) throw new TypeError(`FCStd ColorList ${context} requires a list of four finite RGBA channels.`)
  }
  const equal = (left: ColorListValue, right: ColorListValue) => left.length === right.length && left.every((color, index) => color.every((channel, channelIndex) => Object.is(channel, right[index]?.[channelIndex])))
  validateValue(edit.value, 'value')
  if (edit.expectedValue !== undefined) validateValue(edit.expectedValue, 'expectedValue')
  const nativeValue = edit.value.map((color, index) => packedToColorChannels(colorChannelsToPacked(color, `${edit.objectName}.${edit.propertyName}[${index}]`)))

  const inspection = inspectFcstdArchive(bytes)
  const object = inspection.objects.find((candidate) => candidate.name === edit.objectName)
  if (!object) throw new Error(`FCStd ColorList object does not exist: ${edit.objectName}`)
  const summary = object.properties.find((candidate) => candidate.name === edit.propertyName)
  if (!summary) throw new Error(`FCStd ColorList property does not exist: ${edit.objectName}.${edit.propertyName}`)
  if (summary.typeId !== 'App::PropertyColorList' || summary.element !== 'ColorList') throw new TypeError(`FCStd property ${edit.objectName}.${edit.propertyName} must be App::PropertyColorList with a ColorList payload.`)
  const decoded = decodeFcstdPropertyValue(summary)
  if (!decoded.decoded || !Array.isArray(decoded.value)) throw new TypeError(`FCStd ColorList ${edit.objectName}.${edit.propertyName} is not decodable.`)
  const current = decoded.value as ColorListValue
  if (edit.expectedValue !== undefined && !equal(current, edit.expectedValue)) throw new RangeError(`FCStd ColorList ${edit.objectName}.${edit.propertyName} does not match expectedValue.`)
  const resourcePath = summary.resourcePath
  if (!resourcePath) throw new Error(`FCStd ColorList ${edit.objectName}.${edit.propertyName} has no external resource to rewrite.`)

  const entries = inspectZipDirectory(bytes, DEFAULT_FCSTD_LIMITS)
  const files = unzipSync(bytes)
  if (!files[resourcePath]) throw new Error(`FCStd ColorList resource is missing: ${resourcePath}`)
  const documentPath = entries.find((entry) => entry.path.toLowerCase() === 'document.xml')?.path
  if (!documentPath || !files[documentPath]) throw new Error('FCStd archive does not contain Document.xml.')
  const parsed = parseXmlRoot(files[documentPath], documentPath, 'Document', DEFAULT_FCSTD_LIMITS)
  const objectDeclarations = asArray((((parsed.root.Objects as Record<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const declarations = objectDeclarations.filter((candidate) => attribute(candidate, 'name') === edit.objectName)
  if (declarations.length !== 1) throw new Error(`FCStd ColorList object declaration is not unique: ${edit.objectName}`)
  declarations[0]['@_Touched'] = '1'
  const objectData = asArray((((parsed.root.ObjectData as Record<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const data = objectData.filter((candidate) => attribute(candidate, 'name') === edit.objectName)
  if (data.length !== 1) throw new Error(`FCStd ColorList object data is not unique: ${edit.objectName}`)
  const properties = asArray(((((data[0].Properties as Record<string, unknown> | undefined)?.Property)) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const candidates = properties.filter((candidate) => attribute(candidate, 'name') === edit.propertyName && attribute(candidate, 'type') === 'App::PropertyColorList')
  if (candidates.length !== 1 || !candidates[0].ColorList || typeof candidates[0].ColorList !== 'object' || Array.isArray(candidates[0].ColorList) || attribute(candidates[0].ColorList as Record<string, unknown>, 'file') !== resourcePath) throw new TypeError(`FCStd ColorList ${edit.objectName}.${edit.propertyName} has no unique matching ColorList payload.`)
  files[resourcePath] = colorListResourceBytes(edit.value, `${edit.objectName}.${edit.propertyName}`)
  files[documentPath] = new TextEncoder().encode(new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: '@_', format: false, processEntities: false, suppressBooleanAttributes: false, suppressEmptyNode: true }).build({ Document: parsed.root }))
  const output: Record<string, Uint8Array> = {}
  for (const entry of entries) output[entry.path] = new Uint8Array(files[entry.path])
  const rewritten = new Uint8Array(zipSync(output, { level: 6 }))
  const rewrittenSummary = inspectFcstdArchive(rewritten).objects.find((candidate) => candidate.name === edit.objectName)?.properties.find((candidate) => candidate.name === edit.propertyName)
  const rewrittenValue = rewrittenSummary ? decodeFcstdPropertyValue(rewrittenSummary) : undefined
  if (!rewrittenValue?.decoded || !Array.isArray(rewrittenValue.value) || !equal(rewrittenValue.value as ColorListValue, nativeValue)) throw new Error(`FCStd ColorList ${edit.objectName}.${edit.propertyName} did not rewrite exactly.`)
  return rewritten
}

/** Rewrites one native App::PropertyDirection PropertyVector payload while preserving every other archive entry. */
export const rewriteFcstdDirectionProperty = (bytes: Uint8Array, edit: FcstdDirectionEdit): Uint8Array => {
  if (!edit.objectName.trim() || !edit.propertyName.trim()) throw new TypeError('FCStd PropertyDirection rewrite requires non-empty object and property names.')
  const validateValue = (value: VectorValue, context: string) => {
    if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError(`FCStd PropertyDirection ${context} requires a vector.`)
    for (const axis of ['x', 'y', 'z'] as const) if (typeof value[axis] !== 'number' || !Number.isFinite(value[axis])) throw new TypeError(`FCStd PropertyDirection ${context}.${axis} must be finite.`)
  }
  const equal = (left: VectorValue, right: VectorValue) => (['x', 'y', 'z'] as const).every((axis) => Object.is(left[axis], right[axis]))
  validateValue(edit.value, 'value')
  if (edit.expectedValue !== undefined) validateValue(edit.expectedValue, 'expectedValue')

  const inspection = inspectFcstdArchive(bytes)
  const object = inspection.objects.find((candidate) => candidate.name === edit.objectName)
  if (!object) throw new Error(`FCStd PropertyDirection object does not exist: ${edit.objectName}`)
  const summary = object.properties.find((candidate) => candidate.name === edit.propertyName)
  if (!summary) throw new Error(`FCStd PropertyDirection property does not exist: ${edit.objectName}.${edit.propertyName}`)
  if (summary.typeId !== 'App::PropertyDirection' || summary.element !== 'PropertyVector') throw new TypeError(`FCStd property ${edit.objectName}.${edit.propertyName} must be App::PropertyDirection with a PropertyVector payload.`)
  const decoded = decodeFcstdPropertyValue(summary)
  if (!decoded.decoded || !decoded.value || typeof decoded.value !== 'object' || Array.isArray(decoded.value)) throw new TypeError(`FCStd PropertyDirection ${edit.objectName}.${edit.propertyName} is not decodable.`)
  validateValue(decoded.value as VectorValue, `${edit.objectName}.${edit.propertyName}`)
  if (edit.expectedValue !== undefined && !equal(decoded.value as VectorValue, edit.expectedValue)) throw new RangeError(`FCStd PropertyDirection ${edit.objectName}.${edit.propertyName} does not match expectedValue.`)

  const entries = inspectZipDirectory(bytes, DEFAULT_FCSTD_LIMITS)
  const files = unzipSync(bytes)
  const documentPath = entries.find((entry) => entry.path.toLowerCase() === 'document.xml')?.path
  if (!documentPath || !files[documentPath]) throw new Error('FCStd archive does not contain Document.xml.')
  const parsed = parseXmlRoot(files[documentPath], documentPath, 'Document', DEFAULT_FCSTD_LIMITS)
  const objectDeclarations = asArray((((parsed.root.Objects as Record<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const declarations = objectDeclarations.filter((candidate) => attribute(candidate, 'name') === edit.objectName)
  if (declarations.length !== 1) throw new Error(`FCStd PropertyDirection object declaration is not unique: ${edit.objectName}`)
  declarations[0]['@_Touched'] = '1'
  const objectData = asArray((((parsed.root.ObjectData as Record<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const data = objectData.filter((candidate) => attribute(candidate, 'name') === edit.objectName)
  if (data.length !== 1) throw new Error(`FCStd PropertyDirection object data is not unique: ${edit.objectName}`)
  const properties = asArray(((((data[0].Properties as Record<string, unknown> | undefined)?.Property)) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const candidates = properties.filter((candidate) => attribute(candidate, 'name') === edit.propertyName && attribute(candidate, 'type') === 'App::PropertyDirection')
  if (candidates.length !== 1 || !candidates[0].PropertyVector || typeof candidates[0].PropertyVector !== 'object' || Array.isArray(candidates[0].PropertyVector)) throw new TypeError(`FCStd PropertyDirection ${edit.objectName}.${edit.propertyName} has no unique PropertyVector payload.`)
  const payload = candidates[0].PropertyVector as Record<string, unknown>
  payload['@_valueX'] = String(edit.value.x)
  payload['@_valueY'] = String(edit.value.y)
  payload['@_valueZ'] = String(edit.value.z)
  const documentXml = new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: '@_', format: false, processEntities: false, suppressBooleanAttributes: false, suppressEmptyNode: true }).build({ Document: parsed.root })
  files[documentPath] = new TextEncoder().encode(documentXml)
  const output: Record<string, Uint8Array> = {}
  for (const entry of entries) output[entry.path] = new Uint8Array(files[entry.path])
  const rewritten = new Uint8Array(zipSync(output, { level: 6 }))
  const rewrittenSummary = inspectFcstdArchive(rewritten).objects.find((candidate) => candidate.name === edit.objectName)?.properties.find((candidate) => candidate.name === edit.propertyName)
  const rewrittenValue = rewrittenSummary ? decodeFcstdPropertyValue(rewrittenSummary) : undefined
  if (!rewrittenValue?.decoded || !rewrittenValue.value || typeof rewrittenValue.value !== 'object' || Array.isArray(rewrittenValue.value) || !equal(rewrittenValue.value as VectorValue, edit.value)) throw new Error(`FCStd PropertyDirection ${edit.objectName}.${edit.propertyName} did not rewrite exactly.`)
  return rewritten
}

/** Rewrites one native App::PropertyFile String payload without exposing host filesystem paths. */
export const rewriteFcstdFileProperty = (bytes: Uint8Array, edit: FcstdFileEdit): Uint8Array => {
  if (!edit.objectName.trim() || !edit.propertyName.trim()) throw new TypeError('FCStd PropertyFile rewrite requires non-empty object and property names.')
  const value = propertyFileValue(edit.value, `${edit.objectName}.${edit.propertyName}`)
  const expectedValue = edit.expectedValue === undefined ? undefined : propertyFileValue(edit.expectedValue, `${edit.objectName}.${edit.propertyName}.expectedValue`)

  const inspection = inspectFcstdArchive(bytes)
  const object = inspection.objects.find((candidate) => candidate.name === edit.objectName)
  if (!object) throw new Error(`FCStd PropertyFile object does not exist: ${edit.objectName}`)
  const summary = object.properties.find((candidate) => candidate.name === edit.propertyName)
  if (!summary) throw new Error(`FCStd PropertyFile property does not exist: ${edit.objectName}.${edit.propertyName}`)
  if (summary.typeId !== 'App::PropertyFile' || summary.element !== 'String') throw new TypeError(`FCStd property ${edit.objectName}.${edit.propertyName} must be App::PropertyFile with a String payload.`)
  const decoded = decodeFcstdPropertyValue(summary)
  if (!decoded.decoded || typeof decoded.value !== 'string') throw new TypeError(`FCStd PropertyFile ${edit.objectName}.${edit.propertyName} is not decodable.`)
  if (expectedValue !== undefined && decoded.value !== expectedValue) throw new RangeError(`FCStd PropertyFile ${edit.objectName}.${edit.propertyName} does not match expectedValue.`)

  const entries = inspectZipDirectory(bytes, DEFAULT_FCSTD_LIMITS)
  const files = unzipSync(bytes)
  const documentPath = entries.find((entry) => entry.path.toLowerCase() === 'document.xml')?.path
  if (!documentPath || !files[documentPath]) throw new Error('FCStd archive does not contain Document.xml.')
  const parsed = parseXmlRoot(files[documentPath], documentPath, 'Document', DEFAULT_FCSTD_LIMITS)
  const objectDeclarations = asArray((((parsed.root.Objects as Record<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const declarations = objectDeclarations.filter((candidate) => attribute(candidate, 'name') === edit.objectName)
  if (declarations.length !== 1) throw new Error(`FCStd PropertyFile object declaration is not unique: ${edit.objectName}`)
  declarations[0]['@_Touched'] = '1'
  const objectData = asArray((((parsed.root.ObjectData as Record<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const data = objectData.filter((candidate) => attribute(candidate, 'name') === edit.objectName)
  if (data.length !== 1) throw new Error(`FCStd PropertyFile object data is not unique: ${edit.objectName}`)
  const properties = asArray(((((data[0].Properties as Record<string, unknown> | undefined)?.Property)) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const candidates = properties.filter((candidate) => attribute(candidate, 'name') === edit.propertyName && attribute(candidate, 'type') === 'App::PropertyFile')
  if (candidates.length !== 1 || !candidates[0].String || typeof candidates[0].String !== 'object' || Array.isArray(candidates[0].String)) throw new TypeError(`FCStd PropertyFile ${edit.objectName}.${edit.propertyName} has no unique String payload.`)
  ;(candidates[0].String as Record<string, unknown>)['@_value'] = value
  files[documentPath] = new TextEncoder().encode(new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: '@_', format: false, processEntities: false, suppressBooleanAttributes: false, suppressEmptyNode: true }).build({ Document: parsed.root }))
  const output: Record<string, Uint8Array> = {}
  for (const entry of entries) output[entry.path] = new Uint8Array(files[entry.path])
  const rewritten = new Uint8Array(zipSync(output, { level: 6 }))
  const rewrittenSummary = inspectFcstdArchive(rewritten).objects.find((candidate) => candidate.name === edit.objectName)?.properties.find((candidate) => candidate.name === edit.propertyName)
  const rewrittenValue = rewrittenSummary ? decodeFcstdPropertyValue(rewrittenSummary) : undefined
  if (!rewrittenValue?.decoded || rewrittenValue.value !== value) throw new Error(`FCStd PropertyFile ${edit.objectName}.${edit.propertyName} did not rewrite exactly.`)
  return rewritten
}

/** Rewrites one native App::PropertyFont String payload while retaining its distinct TypeId. */
export const rewriteFcstdFontProperty = (bytes: Uint8Array, edit: FcstdFontEdit): Uint8Array => {
  if (!edit.objectName.trim() || !edit.propertyName.trim()) throw new TypeError('FCStd PropertyFont rewrite requires non-empty object and property names.')
  const value = propertyFontValue(edit.value, `${edit.objectName}.${edit.propertyName}`)
  const expectedValue = edit.expectedValue === undefined ? undefined : propertyFontValue(edit.expectedValue, `${edit.objectName}.${edit.propertyName}.expectedValue`)

  const inspection = inspectFcstdArchive(bytes)
  const object = inspection.objects.find((candidate) => candidate.name === edit.objectName)
  if (!object) throw new Error(`FCStd PropertyFont object does not exist: ${edit.objectName}`)
  const summary = object.properties.find((candidate) => candidate.name === edit.propertyName)
  if (!summary) throw new Error(`FCStd PropertyFont property does not exist: ${edit.objectName}.${edit.propertyName}`)
  if (summary.typeId !== 'App::PropertyFont' || summary.element !== 'String') throw new TypeError(`FCStd property ${edit.objectName}.${edit.propertyName} must be App::PropertyFont with a String payload.`)
  const decoded = decodeFcstdPropertyValue(summary)
  if (!decoded.decoded || typeof decoded.value !== 'string') throw new TypeError(`FCStd PropertyFont ${edit.objectName}.${edit.propertyName} is not decodable.`)
  if (expectedValue !== undefined && decoded.value !== expectedValue) throw new RangeError(`FCStd PropertyFont ${edit.objectName}.${edit.propertyName} does not match expectedValue.`)

  const entries = inspectZipDirectory(bytes, DEFAULT_FCSTD_LIMITS)
  const files = unzipSync(bytes)
  const documentPath = entries.find((entry) => entry.path.toLowerCase() === 'document.xml')?.path
  if (!documentPath || !files[documentPath]) throw new Error('FCStd archive does not contain Document.xml.')
  const parsed = parseXmlRoot(files[documentPath], documentPath, 'Document', DEFAULT_FCSTD_LIMITS)
  const objectDeclarations = asArray((((parsed.root.Objects as Record<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const declarations = objectDeclarations.filter((candidate) => attribute(candidate, 'name') === edit.objectName)
  if (declarations.length !== 1) throw new Error(`FCStd PropertyFont object declaration is not unique: ${edit.objectName}`)
  declarations[0]['@_Touched'] = '1'
  const objectData = asArray((((parsed.root.ObjectData as Record<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const data = objectData.filter((candidate) => attribute(candidate, 'name') === edit.objectName)
  if (data.length !== 1) throw new Error(`FCStd PropertyFont object data is not unique: ${edit.objectName}`)
  const properties = asArray(((((data[0].Properties as Record<string, unknown> | undefined)?.Property)) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const candidates = properties.filter((candidate) => attribute(candidate, 'name') === edit.propertyName && attribute(candidate, 'type') === 'App::PropertyFont')
  if (candidates.length !== 1 || !candidates[0].String || typeof candidates[0].String !== 'object' || Array.isArray(candidates[0].String)) throw new TypeError(`FCStd PropertyFont ${edit.objectName}.${edit.propertyName} has no unique String payload.`)
  ;(candidates[0].String as Record<string, unknown>)['@_value'] = value
  files[documentPath] = new TextEncoder().encode(new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: '@_', format: false, processEntities: false, suppressBooleanAttributes: false, suppressEmptyNode: true }).build({ Document: parsed.root }))
  const output: Record<string, Uint8Array> = {}
  for (const entry of entries) output[entry.path] = new Uint8Array(files[entry.path])
  const rewritten = new Uint8Array(zipSync(output, { level: 6 }))
  const rewrittenSummary = inspectFcstdArchive(rewritten).objects.find((candidate) => candidate.name === edit.objectName)?.properties.find((candidate) => candidate.name === edit.propertyName)
  const rewrittenValue = rewrittenSummary ? decodeFcstdPropertyValue(rewrittenSummary) : undefined
  if (!rewrittenValue?.decoded || rewrittenValue.value !== value) throw new Error(`FCStd PropertyFont ${edit.objectName}.${edit.propertyName} did not rewrite exactly.`)
  return rewritten
}

/** Rewrites one native App::PropertyFileIncluded entry while preserving its archive identity. */
export const rewriteFcstdIncludedFileProperty = (bytes: Uint8Array, edit: FcstdIncludedFileResourceEdit): Uint8Array => {
  if (!edit.objectName.trim() || !edit.propertyName.trim()) throw new TypeError('FCStd PropertyFileIncluded rewrite requires non-empty object and property names.')
  const value = validateIncludedFileValue(edit.value, `${edit.objectName}.${edit.propertyName}`)
  const expectedValue = edit.expectedValue === undefined ? undefined : validateIncludedFileValue(edit.expectedValue, `${edit.objectName}.${edit.propertyName}.expectedValue`)
  if (!(edit.resourceBytes instanceof Uint8Array)) throw new TypeError(`FCStd PropertyFileIncluded ${edit.objectName}.${edit.propertyName} requires Uint8Array resource bytes.`)
  if (edit.resourceBytes.byteLength !== value.byteLength) throw new RangeError(`FCStd PropertyFileIncluded ${edit.objectName}.${edit.propertyName} resource byte length does not match value.`)
  if (expectedValue !== undefined && !(edit.expectedResourceBytes instanceof Uint8Array)) throw new TypeError(`FCStd PropertyFileIncluded ${edit.objectName}.${edit.propertyName} requires expected resource bytes.`)
  if (expectedValue !== undefined && edit.expectedResourceBytes!.byteLength !== expectedValue.byteLength) throw new RangeError(`FCStd PropertyFileIncluded ${edit.objectName}.${edit.propertyName} expected resource byte length does not match expectedValue.`)

  const inspection = inspectFcstdArchive(bytes)
  const object = inspection.objects.find((candidate) => candidate.name === edit.objectName)
  if (!object) throw new Error(`FCStd PropertyFileIncluded object does not exist: ${edit.objectName}`)
  const summary = object.properties.find((candidate) => candidate.name === edit.propertyName)
  if (!summary) throw new Error(`FCStd PropertyFileIncluded property does not exist: ${edit.objectName}.${edit.propertyName}`)
  if (summary.typeId !== 'App::PropertyFileIncluded' || summary.element !== 'FileIncluded') throw new TypeError(`FCStd property ${edit.objectName}.${edit.propertyName} must be App::PropertyFileIncluded with a FileIncluded payload.`)
  const resourcePath = summary.resourcePath
  const currentResource = summary.includedFileResource
  if (!resourcePath || !currentResource) throw new Error(`FCStd PropertyFileIncluded ${edit.objectName}.${edit.propertyName} has no external resource to rewrite.`)
  if (value.archiveName !== resourcePath) throw new RangeError(`FCStd PropertyFileIncluded ${edit.objectName}.${edit.propertyName} cannot change the native archive name.`)
  if (expectedValue !== undefined && (expectedValue.archiveName !== resourcePath || expectedValue.byteLength !== currentResource.byteLength)) throw new RangeError(`FCStd PropertyFileIncluded ${edit.objectName}.${edit.propertyName} does not match expectedValue.`)

  const entries = inspectZipDirectory(bytes, DEFAULT_FCSTD_LIMITS)
  const files = unzipSync(bytes)
  const currentBytes = files[resourcePath]
  if (!currentBytes) throw new Error(`FCStd PropertyFileIncluded resource is missing: ${resourcePath}`)
  if (expectedValue !== undefined && (currentBytes.byteLength !== edit.expectedResourceBytes!.byteLength || currentBytes.some((byte, index) => byte !== edit.expectedResourceBytes![index]))) throw new RangeError(`FCStd PropertyFileIncluded ${edit.objectName}.${edit.propertyName} resource does not match expectedValue.`)
  const documentPath = entries.find((entry) => entry.path.toLowerCase() === 'document.xml')?.path
  if (!documentPath || !files[documentPath]) throw new Error('FCStd archive does not contain Document.xml.')
  const parsed = parseXmlRoot(files[documentPath], documentPath, 'Document', DEFAULT_FCSTD_LIMITS)
  const objectDeclarations = asArray((((parsed.root.Objects as Record<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const declarations = objectDeclarations.filter((candidate) => attribute(candidate, 'name') === edit.objectName)
  if (declarations.length !== 1) throw new Error(`FCStd PropertyFileIncluded object declaration is not unique: ${edit.objectName}`)
  declarations[0]['@_Touched'] = '1'
  const objectData = asArray((((parsed.root.ObjectData as Record<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const data = objectData.filter((candidate) => attribute(candidate, 'name') === edit.objectName)
  if (data.length !== 1) throw new Error(`FCStd PropertyFileIncluded object data is not unique: ${edit.objectName}`)
  const properties = asArray(((((data[0].Properties as Record<string, unknown> | undefined)?.Property)) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const candidates = properties.filter((candidate) => attribute(candidate, 'name') === edit.propertyName && attribute(candidate, 'type') === 'App::PropertyFileIncluded')
  if (candidates.length !== 1 || !candidates[0].FileIncluded || typeof candidates[0].FileIncluded !== 'object' || Array.isArray(candidates[0].FileIncluded) || attribute(candidates[0].FileIncluded as Record<string, unknown>, 'file') !== resourcePath) throw new TypeError(`FCStd PropertyFileIncluded ${edit.objectName}.${edit.propertyName} has no unique matching FileIncluded payload.`)
  files[resourcePath] = new Uint8Array(edit.resourceBytes)
  files[documentPath] = new TextEncoder().encode(new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: '@_', format: false, processEntities: false, suppressBooleanAttributes: false, suppressEmptyNode: true }).build({ Document: parsed.root }))
  const output: Record<string, Uint8Array> = {}
  for (const entry of entries) output[entry.path] = new Uint8Array(files[entry.path])
  const rewritten = new Uint8Array(zipSync(output, { level: 6 }))
  const rewrittenSummary = inspectFcstdArchive(rewritten).objects.find((candidate) => candidate.name === edit.objectName)?.properties.find((candidate) => candidate.name === edit.propertyName)
  const rewrittenResource = rewrittenSummary?.includedFileResource
  if (rewrittenSummary?.typeId !== 'App::PropertyFileIncluded' || rewrittenSummary.element !== 'FileIncluded' || rewrittenSummary.resourcePath !== resourcePath || rewrittenResource?.path !== resourcePath || rewrittenResource.byteLength !== value.byteLength || rewrittenResource.contentHash !== hashBytes(edit.resourceBytes)) throw new Error(`FCStd PropertyFileIncluded ${edit.objectName}.${edit.propertyName} did not rewrite exactly.`)
  return rewritten
}

/** Rewrites one native MeasureArea input/output pair without touching other archive entries. */
export const rewriteFcstdMeasureArea = (bytes: Uint8Array, edit: FcstdMeasureAreaEdit): Uint8Array => {
  if (!edit.objectName.trim()) throw new TypeError('FCStd MeasureArea rewrite requires a non-empty object name.')
  if (!Number.isFinite(edit.area) || edit.area < 0) throw new TypeError('FCStd MeasureArea rewrite requires a finite non-negative area.')
  if (edit.expectedArea !== undefined && (!Number.isFinite(edit.expectedArea) || edit.expectedArea < 0)) throw new TypeError('FCStd MeasureArea expectedArea must be finite and non-negative.')
  const validateElements = (value: LinkSubListValue, context: string) => {
    if (!value || value.schemaVersion !== 1 || !Array.isArray(value.entries)) throw new TypeError(`FCStd MeasureArea ${context} requires a LinkSubList value.`)
    const identities = value.entries.map((entry, index) => {
      if (!entry || typeof entry.objectId !== 'string' || !entry.objectId.trim()) throw new TypeError(`FCStd MeasureArea ${context} entry ${index} requires an object name.`)
      if (entry.subElement !== null && (typeof entry.subElement !== 'string' || !/^Face[1-9]\d*$/.test(entry.subElement))) throw new TypeError(`FCStd MeasureArea ${context} entry ${index} must reference a native FaceN or the whole object.`)
      return `${entry.objectId}\0${entry.subElement ?? ''}`
    })
    if (new Set(identities).size !== identities.length) throw new RangeError(`FCStd MeasureArea ${context} contains duplicate references.`)
  }
  validateElements(edit.elements, 'elements')
  if (edit.expectedElements) validateElements(edit.expectedElements, 'expectedElements')

  const inspection = inspectFcstdArchive(bytes)
  const object = inspection.objects.find((candidate) => candidate.name === edit.objectName)
  if (!object) throw new Error(`FCStd MeasureArea object does not exist: ${edit.objectName}`)
  if (object.typeId !== 'Measure::MeasureArea') throw new TypeError(`FCStd MeasureArea rewrite requires Measure::MeasureArea, found ${object.typeId}.`)
  const areaSummary = object.properties.find((candidate) => candidate.name === 'Area')
  const elementsSummary = object.properties.find((candidate) => candidate.name === 'Elements')
  if (areaSummary?.typeId !== 'App::PropertyArea' || areaSummary.element !== 'Float') throw new TypeError(`FCStd MeasureArea ${edit.objectName}.Area must be App::PropertyArea with a Float payload.`)
  if (elementsSummary?.typeId !== 'App::PropertyLinkSubList' || elementsSummary.element !== 'LinkSubList') throw new TypeError(`FCStd MeasureArea ${edit.objectName}.Elements must be App::PropertyLinkSubList.`)
  const decodedArea = decodeFcstdPropertyValue(areaSummary)
  const decodedElements = decodeFcstdPropertyValue(elementsSummary)
  if (!decodedArea.decoded || typeof decodedArea.value !== 'number' || !Number.isFinite(decodedArea.value) || !decodedElements.decoded) throw new TypeError(`FCStd MeasureArea ${edit.objectName} properties are not decodable.`)
  if (edit.expectedArea !== undefined && !Object.is(decodedArea.value, edit.expectedArea)) throw new RangeError(`FCStd MeasureArea ${edit.objectName}.Area expected ${edit.expectedArea}, found ${decodedArea.value}.`)
  if (edit.expectedElements && JSON.stringify(decodedElements.value) !== JSON.stringify(edit.expectedElements)) throw new RangeError(`FCStd MeasureArea ${edit.objectName}.Elements does not match expectedElements.`)
  const objectNames = new Set(inspection.objects.map((candidate) => candidate.name))
  for (const entry of edit.elements.entries) if (!objectNames.has(entry.objectId)) throw new RangeError(`FCStd MeasureArea target does not exist: ${entry.objectId}`)

  const entries = inspectZipDirectory(bytes, DEFAULT_FCSTD_LIMITS)
  const files = unzipSync(bytes)
  const documentPath = entries.find((entry) => entry.path.toLowerCase() === 'document.xml')?.path
  if (!documentPath || !files[documentPath]) throw new Error('FCStd archive does not contain Document.xml.')
  const parsed = parseXmlRoot(files[documentPath], documentPath, 'Document', DEFAULT_FCSTD_LIMITS)
  const objectDeclarations = asArray((((parsed.root.Objects as Record<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const declarations = objectDeclarations.filter((candidate) => attribute(candidate, 'name') === edit.objectName)
  if (declarations.length !== 1) throw new Error(`FCStd MeasureArea object declaration is not unique: ${edit.objectName}`)
  declarations[0]['@_Touched'] = '1'
  const objectData = asArray((((parsed.root.ObjectData as Record<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const data = objectData.filter((candidate) => attribute(candidate, 'name') === edit.objectName)
  if (data.length !== 1) throw new Error(`FCStd MeasureArea object data is not unique: ${edit.objectName}`)
  const properties = asArray(((((data[0].Properties as Record<string, unknown> | undefined)?.Property)) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const areaProperties = properties.filter((candidate) => attribute(candidate, 'name') === 'Area' && attribute(candidate, 'type') === 'App::PropertyArea')
  const elementsProperties = properties.filter((candidate) => attribute(candidate, 'name') === 'Elements' && attribute(candidate, 'type') === 'App::PropertyLinkSubList')
  if (areaProperties.length !== 1 || !areaProperties[0].Float || typeof areaProperties[0].Float !== 'object' || Array.isArray(areaProperties[0].Float)) throw new TypeError(`FCStd MeasureArea ${edit.objectName}.Area has no unique Float payload.`)
  if (elementsProperties.length !== 1 || !elementsProperties[0].LinkSubList || typeof elementsProperties[0].LinkSubList !== 'object' || Array.isArray(elementsProperties[0].LinkSubList)) throw new TypeError(`FCStd MeasureArea ${edit.objectName}.Elements has no unique LinkSubList payload.`)
  ;(areaProperties[0].Float as Record<string, unknown>)['@_value'] = String(edit.area)
  elementsProperties[0].LinkSubList = {
    '@_count': String(edit.elements.entries.length),
    ...(edit.elements.entries.length === 0 ? {} : { Link: edit.elements.entries.map((entry) => ({ '@_obj': entry.objectId, '@_sub': entry.subElement ?? '' })) }),
  }
  const documentXml = new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: '@_', format: false, processEntities: false, suppressBooleanAttributes: false, suppressEmptyNode: true }).build({ Document: parsed.root })
  files[documentPath] = new TextEncoder().encode(documentXml)
  const output: Record<string, Uint8Array> = {}
  for (const entry of entries) output[entry.path] = new Uint8Array(files[entry.path])
  const rewritten = new Uint8Array(zipSync(output, { level: 6 }))
  const rewrittenObject = inspectFcstdArchive(rewritten).objects.find((candidate) => candidate.name === edit.objectName)
  const rewrittenArea = rewrittenObject?.properties.find((candidate) => candidate.name === 'Area')
  const rewrittenElements = rewrittenObject?.properties.find((candidate) => candidate.name === 'Elements')
  if (decodeFcstdPropertyValue(rewrittenArea as FcstdPropertySummary).value !== edit.area || JSON.stringify(decodeFcstdPropertyValue(rewrittenElements as FcstdPropertySummary).value) !== JSON.stringify(edit.elements)) throw new Error(`FCStd MeasureArea ${edit.objectName} did not rewrite exactly.`)
  return rewritten
}

const parseXmlRoot = (bytes: Uint8Array, entryName: string, rootName: string | string[], limits: FcstdArchiveLimits) => {
  const xml = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
  if (/<!DOCTYPE|<!ENTITY/i.test(xml)) throw new Error(`FCStd ${entryName} declarations and entities are not allowed.`)
  let nodeCount = 0
  let parsed: Record<string, unknown>
  try {
    parsed = new XMLParser({
      ignoreAttributes: false,
      attributeNamePrefix: '@_',
      parseTagValue: false,
      processEntities: false,
      allowBooleanAttributes: false,
      maxNestedTags: limits.maxXmlDepth,
      updateTag: (tagName) => {
        nodeCount += 1
        if (nodeCount > limits.maxXmlNodes) throw new RangeError(`FCStd ${entryName} exceeds ${limits.maxXmlNodes} XML nodes.`)
        return tagName
      },
    }).parse(xml, true) as Record<string, unknown>
  } catch (error) {
    if (error instanceof RangeError) throw error
    const message = error instanceof Error ? error.message : String(error)
    if (/Maximum nested tags exceeded/.test(message)) throw new RangeError(`FCStd ${entryName} exceeds XML nesting depth ${limits.maxXmlDepth}.`)
    throw new Error(`FCStd ${entryName} contains invalid XML: ${message}`)
  }
  const rootNames = Array.isArray(rootName) ? rootName : [rootName]
  const matchedRootName = rootNames.find((candidate) => Object.prototype.hasOwnProperty.call(parsed, candidate))
  if (!matchedRootName) throw new Error(`FCStd ${entryName} root must be ${rootNames.join(' or ')}.`)
  const rootValue = parsed[matchedRootName]
  const root = (rootValue && typeof rootValue === 'object' ? rootValue : {}) as Record<string, unknown>
  return { xml, root, rootName: matchedRootName }
}

const assertUniqueNodeNames = (nodes: Record<string, unknown>[], kind: string) => {
  const names = nodes.map((node) => attribute(node, 'name'))
  if (names.some((name) => !name)) throw new Error(`FCStd ${kind} names must be non-empty.`)
  const duplicate = names.find((name, index) => names.indexOf(name) !== index)
  if (duplicate) throw new Error(`Duplicate FCStd ${kind} name: ${duplicate}`)
}

const assertContainerCount = (container: unknown, attributeName: string, actual: number, context: string) => {
  if (!container || typeof container !== 'object') return
  const declared = attribute(container, attributeName)
  if (declared && (!/^\d+$/.test(declared) || Number(declared) !== actual)) throw new Error(`FCStd ${context} ${attributeName} does not match its child elements.`)
}

const nativeExpressionMap = (properties: Record<string, unknown>[]): Map<string, string> => {
  const property = properties.find((candidate) => attribute(candidate, 'name') === 'ExpressionEngine')
  if (!property) return new Map()
  if (attribute(property, 'type') !== 'App::PropertyExpressionEngine') throw new Error('FCStd ExpressionEngine property has an invalid type.')
  const engine = property.ExpressionEngine
  if (!engine || typeof engine !== 'object') throw new Error('FCStd ExpressionEngine property is missing its engine payload.')
  const records = asArray((engine as Record<string, unknown>).Expression as Record<string, unknown> | Record<string, unknown>[] | undefined)
  const countText = attribute(engine, 'count')
  if (!/^\d+$/.test(countText) || Number(countText) !== records.length) throw new Error('FCStd ExpressionEngine count does not match its expressions.')
  const result = new Map<string, string>()
  for (const record of records) {
    const path = attribute(record, 'path')
    if (!path) throw new Error('FCStd ExpressionEngine contains an empty expression path.')
    if (result.has(path)) throw new Error(`FCStd ExpressionEngine contains a duplicate path: ${path}`)
    result.set(path, attribute(record, 'expression'))
  }
  return result
}

const parseDocumentXml = (bytes: Uint8Array, limits: FcstdArchiveLimits) => {
  const { root } = parseXmlRoot(bytes, 'Document.xml', 'Document', limits)
  const legacyStringHasherNodes = root.StringHasher === undefined ? [] : asArray(root.StringHasher as Record<string, unknown> | Record<string, unknown>[])
  if (legacyStringHasherNodes.length > 1) throw new Error('FCStd Document contains duplicate StringHasher declarations.')
  if (legacyStringHasherNodes[0]) {
    for (const name of ['saveall', 'threshold', 'count']) {
      const value = attribute(legacyStringHasherNodes[0], name)
      if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value))) throw new Error(`FCStd StringHasher ${name} is invalid.`)
    }
  }
  const stringHasherNodes = root.StringHasher2 === undefined ? [] : asArray(root.StringHasher2 as Record<string, unknown> | Record<string, unknown>[])
  if (stringHasherNodes.length > 1) throw new Error('FCStd Document contains duplicate StringHasher2 resources.')
  const stringHasherPath = stringHasherNodes[0] ? attribute(stringHasherNodes[0], 'file') : ''
  const embeddedStringHasher = Boolean(stringHasherNodes[0] && !stringHasherPath.trim())
  if (embeddedStringHasher) {
    const countText = attribute(stringHasherNodes[0], 'count')
    if (!/^\d+$/.test(countText) || !Number.isSafeInteger(Number(countText))) throw new Error('FCStd embedded StringHasher2 count is invalid.')
  }
  if (stringHasherPath) validateEntryPath(stringHasherPath)
  const objectDeclarations = asArray((((root.Objects as Record<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const objectData = asArray((((root.ObjectData as Record<string, unknown> | undefined)?.Object) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  assertContainerCount(root.Objects, 'Count', objectDeclarations.length, 'Objects')
  assertContainerCount(root.ObjectData, 'Count', objectData.length, 'ObjectData')
  assertUniqueNodeNames(objectDeclarations, 'object declaration')
  assertUniqueNodeNames(objectData, 'object data')
  const dataByName = new Map(objectData.map((data) => [attribute(data, 'name'), data]))
  const documentProperties = asArray((((root.Properties as Record<string, unknown> | undefined)?.Property) as Record<string, unknown> | Record<string, unknown>[] | undefined))
  const labelProperty = documentProperties.find((property) => attribute(property, 'name') === 'Label')
  const nativeObjectTags = new Set<number>()
  const objects = objectDeclarations.map((declaration): FcstdObjectSummary => {
    const name = attribute(declaration, 'name') || '<unnamed>'
    const typeId = attribute(declaration, 'type') || 'App::DocumentObject'
    const objectTagText = attribute(declaration, 'id')
    let nativeObjectTag: number | undefined
    if (objectTagText) {
      if (!/^\d+$/.test(objectTagText) || !Number.isSafeInteger(Number(objectTagText)) || Number(objectTagText) <= 0) throw new Error(`FCStd object ${name} has an invalid native id.`)
      nativeObjectTag = Number(objectTagText)
      if (nativeObjectTags.has(nativeObjectTag)) throw new Error(`FCStd object native id ${nativeObjectTag} is duplicated.`)
      nativeObjectTags.add(nativeObjectTag)
    }
    const data = dataByName.get(name)
    const propertiesContainer = data?.Properties as Record<string, unknown> | undefined
    const properties = asArray(((propertiesContainer?.Property) as Record<string, unknown> | Record<string, unknown>[] | undefined))
    const transientProperties = asArray(((propertiesContainer?._Property) as Record<string, unknown> | Record<string, unknown>[] | undefined))
    assertContainerCount(propertiesContainer, 'Count', properties.length, `${name} Properties`)
    assertContainerCount(propertiesContainer, 'TransientCount', transientProperties.length, `${name} Properties`)
    const expressions = nativeExpressionMap(properties)
    const objectLabelProperty = properties.find((property) => attribute(property, 'name') === 'Label')
    const extensions = [...new Set([...collectXmlNodes(declaration, 'Extension'), ...collectXmlNodes(data, 'Extension')].map((extension) => attribute(extension, 'type') || attribute(extension, 'name')).filter(Boolean))]
    const support: FcstdObjectSupport = blockedTypeId(typeId) ? 'blocked' : recognizedTypeIds.has(typeId) ? 'recognized' : 'proxy'
    const summaries = [
      ...propertySummaries(properties).map((property) => expressions.has(property.name) ? { ...property, expression: expressions.get(property.name) } : property),
      ...propertySummaries(transientProperties).map((property) => ({ ...property, transientMetadata: true as const })),
    ]
    const sketch = typeId === 'Sketcher::SketchObject' ? sketchFromPropertySummaries(name, summaries) : undefined
    return { name, label: objectLabelProperty ? propertyValue(objectLabelProperty) || name : name, typeId, ...(nativeObjectTag === undefined ? {} : { nativeObjectTag }), propertyCount: properties.length, properties: summaries, support, extensions, ...(sketch ? { sketch } : {}) }
  })
  return {
    schemaVersion: attribute(root, 'SchemaVersion') || attribute(root, 'schemaVersion') || 'unknown',
    label: labelProperty ? propertyValue(labelProperty) || 'Unnamed FreeCAD document' : 'Unnamed FreeCAD document',
    objects,
    ...(stringHasherPath ? { stringHasherPath } : {}),
    ...(embeddedStringHasher ? { embeddedStringHasher: true as const } : {}),
    ...(legacyStringHasherNodes.length > 0 ? { declaredStringHasher: true as const } : {}),
  }
}

const parsePathResource = (bytes: Uint8Array, path: string): PathCommandValue[] => {
  let text: string
  try { text = new TextDecoder('utf-8', { fatal: true }).decode(bytes) } catch (error) { throw new Error(`FCStd Path resource is not valid UTF-8: ${path}: ${error instanceof Error ? error.message : String(error)}`) }
  const commands: PathCommandValue[] = []
  for (const [lineIndex, rawLine] of text.split(/\r?\n/).entries()) {
    const withoutComments = rawLine.replace(/\([^)]*\)/g, '').replace(/;.*$/, '').trim()
    if (!withoutComments || withoutComments === '%') continue
    const nameMatch = /^([A-Za-z][A-Za-z0-9]*)\b(.*)$/.exec(withoutComments)
    if (!nameMatch) throw new Error(`FCStd Path resource ${path} line ${lineIndex + 1} has no command name.`)
    const name = pathCommandName(nameMatch[1], `${path}:${lineIndex + 1}`)
    const remainder = nameMatch[2].trim()
    const parameters: Record<string, number> = {}
    let consumed = 0
    const matcher = /([A-Za-z])\s*([+-]?(?:(?:\d+(?:\.\d*)?)|(?:\.\d+))(?:[Ee][+-]?\d+)?)/g
    let match: RegExpExecArray | null
    while ((match = matcher.exec(remainder)) !== null) {
      if (remainder.slice(consumed, match.index).trim()) throw new Error(`FCStd Path resource ${path} line ${lineIndex + 1} contains an invalid parameter token.`)
      const key = match[1].toUpperCase()
      const value = Number(match[2])
      if (!Number.isFinite(value)) throw new Error(`FCStd Path resource ${path} line ${lineIndex + 1} contains a non-finite parameter.`)
      if (Object.prototype.hasOwnProperty.call(parameters, key)) throw new Error(`FCStd Path resource ${path} line ${lineIndex + 1} repeats parameter ${key}.`)
      parameters[key] = Object.is(value, -0) ? 0 : value
      consumed = matcher.lastIndex
    }
    if (remainder.slice(consumed).trim()) throw new Error(`FCStd Path resource ${path} line ${lineIndex + 1} contains trailing text.`)
    commands.push({ name, parameters })
  }
  return pathCommands(commands, path)
}

const parseColorListResource = (bytes: Uint8Array, path: string): ColorListValue => {
  if (bytes.byteLength < 4) throw new Error(`FCStd ColorList resource is truncated: ${path}`)
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
  const count = view.getUint32(0, true)
  if (count > Math.floor((bytes.byteLength - 4) / 4) || bytes.byteLength !== 4 + count * 4) throw new Error(`FCStd ColorList resource count does not match its payload: ${path}`)
  return Array.from({ length: count }, (_, index) => packedToColorChannels(view.getUint32(4 + index * 4, true)))
}

const hydratePathProperties = (document: ReturnType<typeof parseDocumentXml>, files: Record<string, Uint8Array>) => {
  for (const object of document.objects) for (const property of object.properties) {
    if (property.typeId !== 'Path::PropertyPath') continue
    let metadata: Record<string, unknown>
    try { metadata = JSON.parse(property.value) as Record<string, unknown> } catch { throw new Error(`FCStd Path property ${object.name}.${property.name} metadata is invalid JSON.`) }
    const resourcePath = typeof metadata.resourcePath === 'string' ? metadata.resourcePath : ''
    if (!resourcePath || !files[resourcePath]) throw new Error(`FCStd Path property ${object.name}.${property.name} references missing resource: ${resourcePath || '<empty>'}`)
    const commands = parsePathResource(files[resourcePath], resourcePath)
    property.value = JSON.stringify({ ...metadata, commands })
  }
}

const hydrateColorListProperties = (document: ReturnType<typeof parseDocumentXml>, files: Record<string, Uint8Array>) => {
  for (const object of document.objects) for (const property of object.properties) {
    if (property.typeId !== 'App::PropertyColorList' || property.element !== 'ColorList') continue
    const resourcePath = property.resourcePath ?? ''
    if (!resourcePath) {
      property.value = '[]'
      continue
    }
    validateIncludedFileArchiveName(resourcePath, `FCStd PropertyFileIncluded ${object.name}.${property.name}`)
    const bytes = files[resourcePath]
    if (!bytes) throw new Error(`FCStd ColorList property ${object.name}.${property.name} references missing resource: ${resourcePath}`)
    property.value = JSON.stringify(parseColorListResource(bytes, resourcePath))
  }
}

const hydrateIncludedFileProperties = (document: ReturnType<typeof parseDocumentXml>, files: Record<string, Uint8Array>) => {
  for (const object of document.objects) for (const property of object.properties) {
    if (property.typeId !== 'App::PropertyFileIncluded' || property.element !== 'FileIncluded') continue
    const resourcePath = property.resourcePath ?? ''
    if (!resourcePath) {
      property.value = 'null'
      continue
    }
    validateIncludedFileArchiveName(resourcePath, `FCStd PropertyFileIncluded ${object.name}.${property.name}`)
    const bytes = files[resourcePath]
    if (!bytes) throw new Error(`FCStd PropertyFileIncluded ${object.name}.${property.name} references missing resource: ${resourcePath}`)
    property.includedFileResource = { path: resourcePath, mediaType: includedFileMediaType(resourcePath), byteLength: bytes.byteLength, contentHash: hashBytes(bytes), status: 'available' }
    property.value = JSON.stringify(property.includedFileResource)
  }
}

const countXmlNodes = (value: unknown, name: string): number => {
  if (!value || typeof value !== 'object') return 0
  const record = value as Record<string, unknown>
  return Object.entries(record).reduce((count, [key, child]) => count + (key === name ? asArray(child).length : 0) + countXmlNodes(child, name), 0)
}

const collectXmlNodes = (value: unknown, name: string): Record<string, unknown>[] => {
  if (!value || typeof value !== 'object') return []
  return Object.entries(value as Record<string, unknown>).flatMap(([key, child]) => [
    ...(key === name ? asArray(child).filter((entry): entry is Record<string, unknown> => Boolean(entry && typeof entry === 'object')) : []),
    ...collectXmlNodes(child, name),
  ])
}

const binaryMaterialList = (bytes: Uint8Array, path: string, version: string): FcstdMaterialAppearance[] => {
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
  let offset = 0
  const requireBytes = (count: number) => {
    if (!Number.isSafeInteger(count) || count < 0 || offset + count > bytes.byteLength) throw new Error(`FCStd material resource is truncated: ${path}`)
  }
  const uint32 = () => { requireBytes(4); const value = view.getUint32(offset, true); offset += 4; return value }
  const float32 = () => { requireBytes(4); const value = view.getFloat32(offset, true); offset += 4; if (!Number.isFinite(value)) throw new Error(`FCStd material resource contains a non-finite value: ${path}`); return value }
  const count = uint32()
  if (count > Math.floor((bytes.byteLength - 4) / 24)) throw new Error(`FCStd material resource count exceeds its payload: ${path}`)
  const materials = Array.from({ length: count }, (): FcstdMaterialAppearance => ({ ambientColor: packedToColor(uint32()), diffuseColor: packedToColor(uint32()), specularColor: packedToColor(uint32()), emissiveColor: packedToColor(uint32()), shininess: float32(), transparency: float32(), image: '', imagePath: '', uuid: '' }))
  if (version === '3') {
    const decoder = new TextDecoder('utf-8', { fatal: true })
    const readString = () => { const length = uint32(); requireBytes(length); const value = decoder.decode(bytes.subarray(offset, offset + length)); offset += length; return value }
    for (const material of materials) {
      material.image = readString()
      material.imagePath = readString()
      material.uuid = readString()
    }
  }
  if (offset !== bytes.byteLength) throw new Error(`FCStd material resource has unexpected trailing bytes: ${path}`)
  return materials
}

const parseElementMapResource = (bytes: Uint8Array, path: string): FcstdElementMapResource => {
  const status: FcstdElementMapResource['status'] = bytes.byteLength > 0 ? 'available' : 'empty'
  if (bytes.byteLength === 0) return { path, format: 'element-map-v1', byteLength: 0, contentHash: hashBytes(bytes), status, sections: [] }
  let text: string
  try {
    text = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
  } catch (error) {
    throw new Error(`FCStd ElementMap2 resource is not valid UTF-8: ${path}: ${error instanceof Error ? error.message : String(error)}`)
  }
  let document: ElementMap2Document
  try {
    document = migrateElementMap2Schema(parseElementMap2(text))
  } catch (error) {
    const detail = error instanceof Error ? error.message : String(error)
    if (detail.includes('expected BeginElementMap v1')) throw new Error(`FCStd ElementMap2 resource has unsupported header: ${path}`)
    throw new Error(`FCStd ElementMap2 resource is invalid: ${path}: ${detail}`)
  }
  const sections: Array<{ name: 'Edge' | 'Face' | 'Vertex'; nameCount: number }> = []
  for (const map of document.maps) {
    for (const section of map.sections) {
      if (section.name === 'Edge' || section.name === 'Face' || section.name === 'Vertex') sections.push({ name: section.name, nameCount: section.names.length })
    }
  }
  return {
    path,
    format: 'element-map-v1',
    byteLength: bytes.byteLength,
    contentHash: hashBytes(bytes),
    status,
    postfixCount: document.postfixes.length,
    mapCount: document.maps.length,
    sections,
    document,
  }
}

/** Serialize a parsed native ElementMap resource using FreeCAD's v1 grammar. */
export const serializeElementMap2Resource = (document: AnyElementMap2Document): Uint8Array => new TextEncoder().encode(writeElementMap2(document))

const parseStringHasherResource = (bytes: Uint8Array, path: string): FcstdStringHasherResource => {
  if (bytes.byteLength === 0) return {
    path,
    format: 'string-hasher-v1',
    byteLength: 0,
    contentHash: hashBytes(bytes),
    status: 'empty',
    entryCount: 0,
    validation: { valid: true, entryCount: 0, issues: [] },
  }
  let text: string
  try {
    text = new TextDecoder('utf-8', { fatal: true }).decode(bytes)
  } catch (error) {
    throw new Error(`FCStd StringHasher resource is not valid UTF-8: ${path}: ${error instanceof Error ? error.message : String(error)}`)
  }
  let document: StringHasherTable
  try {
    document = migrateStringHasherSchema(parseStringHasherTable(text))
  } catch (error) {
    throw new Error(`FCStd StringHasher resource is invalid: ${path}: ${error instanceof Error ? error.message : String(error)}`)
  }
  const validation = validateStringHasherTable(document)
  if (!validation.valid) throw new Error(`FCStd StringHasher resource is invalid: ${path}: ${validation.issues[0].path}: ${validation.issues[0].message}`)
  return {
    path,
    format: 'string-hasher-v1',
    byteLength: bytes.byteLength,
    contentHash: hashBytes(bytes),
    status: 'available',
    entryCount: document.entries.length,
    validation,
    document,
  }
}

/** Serialize native StringHasher naming evidence using FreeCAD 1.1.1's v1 grammar. */
export const serializeStringHasherTableResource = (table: AnyStringHasherTable): Uint8Array => {
  const validation = validateStringHasherTable(table)
  if (!validation.valid) throw new Error(`StringHasher validation error: ${validation.issues[0].path}: ${validation.issues[0].message}`)
  return new TextEncoder().encode(writeStringHasherTable(table))
}

const optionalViewScalar = (properties: Record<string, unknown>[], name: string) => {
  const property = properties.find((candidate) => attribute(candidate, 'name') === name)
  return property ? propertyValue(property) : undefined
}

const optionalBoolean = (value: string | undefined, name: string) => {
  if (value === undefined) return undefined
  if (value === 'true') return true
  if (value === 'false') return false
  throw new Error(`FCStd GuiDocument ${name} is not a boolean.`)
}

const optionalNumber = (value: string | undefined, name: string, integer = false) => {
  if (value === undefined) return undefined
  const number = Number(value)
  if (!Number.isFinite(number) || (integer && !Number.isSafeInteger(number))) throw new Error(`FCStd GuiDocument ${name} is not a valid number.`)
  return number
}

const optionalPackedColor = (value: string | undefined, name: string) => {
  if (value === undefined) return undefined
  if (!/^\d+$/.test(value)) throw new Error(`FCStd GuiDocument ${name} is not a packed color.`)
  const number = Number(value)
  if (!Number.isSafeInteger(number) || number < 0 || number > 0xffffffff) throw new Error(`FCStd GuiDocument ${name} is outside the packed color range.`)
  return packedToColor(number)
}

const parseGuiDocument = (files: Record<string, Uint8Array>, entries: FcstdEntryMetadata[], limits: FcstdArchiveLimits): FcstdGuiInspection => {
  const entry = entries.find((candidate) => candidate.path.toLowerCase() === 'guidocument.xml')
  if (!entry) return { present: false, rootName: 'none', schemaVersion: 'unknown', viewCount: 0, contentHash: '', views: [], viewProviders: [] }
  const bytes = files[entry.path]
  const { xml, root, rootName } = parseXmlRoot(bytes, 'GuiDocument.xml', ['Document', 'GuiDocument'], limits)
  const views = collectXmlNodes(root, 'View').map((view) => ({ name: attribute(view, 'name'), type: attribute(view, 'type'), visibility: attribute(view, 'visibility') }))
  const providers = collectXmlNodes(root, 'ViewProvider')
  assertUniqueNodeNames(providers, 'GuiDocument ViewProvider')
  const providerContainer = root.ViewProviderData
  if (providerContainer && typeof providerContainer === 'object') {
    const declaredCount = attribute(providerContainer, 'Count')
    if (!/^\d+$/.test(declaredCount) || Number(declaredCount) !== providers.length) throw new Error('FCStd GuiDocument ViewProviderData Count does not match its providers.')
  }
  const viewProviders = providers.map((provider): FcstdViewProviderSummary => {
    const objectName = attribute(provider, 'name')
    const propertiesContainer = provider.Properties as Record<string, unknown> | undefined
    const properties = asArray(((propertiesContainer?.Property) as Record<string, unknown> | Record<string, unknown>[] | undefined))
    if (propertiesContainer) {
      const declaredCount = attribute(propertiesContainer, 'Count')
      if (declaredCount && (!/^\d+$/.test(declaredCount) || Number(declaredCount) !== properties.length)) throw new Error(`FCStd GuiDocument ${objectName} Properties Count does not match its properties.`)
    }
    const shapeAppearanceProperty = properties.find((property) => attribute(property, 'name') === 'ShapeAppearance')
    const materialList = shapeAppearanceProperty?.MaterialList
    const shapeAppearanceResource = materialList && typeof materialList === 'object' ? attribute(materialList, 'file') : ''
    let shapeAppearance: FcstdMaterialAppearance[] = []
    if (shapeAppearanceResource) {
      validateEntryPath(shapeAppearanceResource)
      const materialBytes = files[shapeAppearanceResource]
      if (!materialBytes) throw new Error(`FCStd GuiDocument references missing ShapeAppearance resource: ${shapeAppearanceResource}`)
      shapeAppearance = binaryMaterialList(materialBytes, shapeAppearanceResource, attribute(materialList, 'version'))
    }
    const expandedText = attribute(provider, 'expanded')
    const treeRankText = attribute(provider, 'treeRank')
    const transparency = optionalNumber(optionalViewScalar(properties, 'Transparency'), `${objectName}.Transparency`, true)
    if (transparency !== undefined && (transparency < 0 || transparency > 100)) throw new Error(`FCStd GuiDocument ${objectName}.Transparency is outside 0..100.`)
    return {
      objectName,
      expanded: expandedText === '' ? null : expandedText === '1' ? true : expandedText === '0' ? false : (() => { throw new Error(`FCStd GuiDocument ${objectName}.expanded is invalid.`) })(),
      treeRank: treeRankText === '' ? null : optionalNumber(treeRankText, `${objectName}.treeRank`, true) ?? null,
      propertyCount: properties.length,
      ...(optionalBoolean(optionalViewScalar(properties, 'Visibility'), `${objectName}.Visibility`) === undefined ? {} : { visibility: optionalBoolean(optionalViewScalar(properties, 'Visibility'), `${objectName}.Visibility`) }),
      ...(transparency === undefined ? {} : { transparency }),
      ...(optionalPackedColor(optionalViewScalar(properties, 'LineColor'), `${objectName}.LineColor`) === undefined ? {} : { lineColor: optionalPackedColor(optionalViewScalar(properties, 'LineColor'), `${objectName}.LineColor`) }),
      ...(optionalPackedColor(optionalViewScalar(properties, 'PointColor'), `${objectName}.PointColor`) === undefined ? {} : { pointColor: optionalPackedColor(optionalViewScalar(properties, 'PointColor'), `${objectName}.PointColor`) }),
      ...(shapeAppearance[0]?.diffuseColor ? { shapeColor: shapeAppearance[0].diffuseColor } : {}),
      ...(optionalNumber(optionalViewScalar(properties, 'Deviation'), `${objectName}.Deviation`) === undefined ? {} : { deviation: optionalNumber(optionalViewScalar(properties, 'Deviation'), `${objectName}.Deviation`) }),
      ...(optionalNumber(optionalViewScalar(properties, 'AngularDeflection'), `${objectName}.AngularDeflection`) === undefined ? {} : { angularDeflection: optionalNumber(optionalViewScalar(properties, 'AngularDeflection'), `${objectName}.AngularDeflection`) }),
      ...(optionalNumber(optionalViewScalar(properties, 'DisplayMode'), `${objectName}.DisplayMode`, true) === undefined ? {} : { displayMode: optionalNumber(optionalViewScalar(properties, 'DisplayMode'), `${objectName}.DisplayMode`, true) }),
      ...(shapeAppearanceResource ? { shapeAppearanceResource } : {}),
      shapeAppearance,
    }
  })
  return { present: true, rootName: rootName as 'Document' | 'GuiDocument', schemaVersion: attribute(root, 'SchemaVersion') || attribute(root, 'schemaVersion') || 'unknown', viewCount: views.length + viewProviders.length, contentHash: hashString(xml), views, viewProviders }
}

const createProxyProperty = (name: string, label: string, value: string): ObjectPropertySnapshot => ({ name, label, group: 'FCStd import', scope: 'data', type: 'App::PropertyString', value, readOnly: true })

const createSketchAttachmentProxyProperties = (object: FcstdObjectSummary): ObjectPropertySnapshot[] => {
  if (object.typeId !== 'Sketcher::SketchObject') return []
  const summaries = object.properties
  const nativeSupport = summaries.find((property) => property.name === 'AttachmentSupport' && property.typeId === 'App::PropertyLinkSubList')
  const supportMetadata = summaries.find((property) => property.name === 'WebSupportValue' && property.typeId === 'App::PropertyString')
  const nativeEntries = nativeSupport?.linkSubs ?? []
  let supportValue: PropertyValue | undefined
  if (supportMetadata) {
    try {
      const decoded = JSON.parse(decodeURIComponent(supportMetadata.value)) as unknown
      if (decoded !== null && typeof decoded !== 'string') validateAttachmentSupport(decoded)
      if (typeof decoded === 'string' && !decoded.trim()) throw new Error('empty Support object id')
      const candidate = decoded === null ? null : typeof decoded === 'string' ? { objectId: decoded, subElement: null } : decoded
      const matchesNative = !nativeSupport || candidate === null
        ? nativeEntries.length === 0
        : nativeEntries.length === 1 && nativeEntries[0].objectId === candidate.objectId && (nativeEntries[0].subElement || '') === (typeof candidate === 'string' ? '' : candidate.subElement && typeof candidate.subElement === 'object' ? candidate.subElement.persistentId : candidate.subElement ?? '')
      if (matchesNative) supportValue = decoded as PropertyValue
    } catch {
      supportValue = undefined
    }
  }
  if (supportValue === undefined && nativeSupport && nativeEntries.length <= 1) {
    if (nativeEntries.length === 0) supportValue = null
    else supportValue = { objectId: nativeEntries[0].objectId, ...(nativeEntries[0].subElement ? { subElement: nativeEntries[0].subElement } : {}) }
  }
  const result: ObjectPropertySnapshot[] = []
  if (supportValue !== undefined) result.push({ name: 'Support', label: 'Support', group: 'Attachment', scope: 'data', type: 'App::PropertyLink', value: supportValue, readOnly: true, recompute: true })
  const mapMode = summaries.find((property) => property.name === 'MapMode')
  if (mapMode) {
    const decoded = decodeFcstdPropertyValue(mapMode)
    if (decoded.decoded && typeof decoded.value === 'string' && (ATTACHMENT_MAP_MODES as readonly string[]).includes(decoded.value)) result.push({ name: 'MapMode', label: 'Map mode', group: 'Attachment', scope: 'data', type: 'App::PropertyEnumeration', value: decoded.value, options: [...ATTACHMENT_MAP_MODES], readOnly: true, recompute: true })
  }
  const offset = summaries.find((property) => property.name === 'AttachmentOffset')
  if (offset) {
    const decoded = decodeFcstdPropertyValue(offset)
    if (decoded.decoded && decoded.value && typeof decoded.value === 'object' && !Array.isArray(decoded.value)) {
      try { validateAttachmentOffset(decoded.value); result.push({ name: 'AttachmentOffset', label: 'Attachment offset', group: 'Attachment', scope: 'data', type: 'App::PropertyPlacement', value: decoded.value as PlacementValue, readOnly: true, recompute: true }) } catch { /* keep malformed native metadata read-only but out of the editable surface */ }
    }
  }
  return result
}

const createProxyDocument = (inspection: Omit<FcstdInspection, 'proxyDocument'>): DocumentSnapshot => {
  const documentId = `fcstd-${hashString(`${inspection.label}|${inspection.schemaVersion}|${inspection.objects.map((object) => object.name).join('|')}`)}`
  const viewProperties = new Map(inspection.guiDocument.viewProviders.map((provider) => [provider.objectName, [
    ...(provider.visibility === undefined ? [] : [createProxyProperty('Visibility', 'Visibility', String(provider.visibility))]),
    ...(provider.transparency === undefined ? [] : [createProxyProperty('Transparency', 'Transparency', String(provider.transparency))]),
    ...(provider.lineColor === undefined ? [] : [createProxyProperty('LineColor', 'Line color', provider.lineColor)]),
    ...(provider.pointColor === undefined ? [] : [createProxyProperty('PointColor', 'Point color', provider.pointColor)]),
    ...(provider.shapeColor === undefined ? [] : [createProxyProperty('ShapeColor', 'Shape color', provider.shapeColor)]),
    ...(provider.deviation === undefined ? [] : [createProxyProperty('Deviation', 'Deviation', String(provider.deviation))]),
    ...(provider.angularDeflection === undefined ? [] : [createProxyProperty('AngularDeflection', 'Angular deflection', String(provider.angularDeflection))]),
    ...(provider.displayMode === undefined ? [] : [createProxyProperty('DisplayMode', 'Display mode', String(provider.displayMode))]),
  ]]))
  const tree: ModelTreeItem[] = inspection.objects.map((object) => ({ id: object.name, label: object.label, type: object.typeId.includes('Body') ? 'body' : object.typeId.includes('Sketch') ? 'sketch' : 'feature', state: 'readonly', detail: `${object.typeId} · ${object.support}` }))
  const objects: DocumentObjectSnapshot[] = inspection.objects.map((object) => {
    const pathSummary = object.typeId === 'Path::Feature' ? object.properties.find((property) => property.name === 'Path' && property.typeId === 'Path::PropertyPath') : undefined
    const pathDecoded = pathSummary ? decodeFcstdPropertyValue(pathSummary) : undefined
    return {
      id: object.name,
      typeId: object.typeId,
      ...(object.nativeObjectTag === undefined ? {} : { nativeObjectTag: object.nativeObjectTag }),
      properties: [
      createProxyProperty('Label', 'Label', object.label),
      createProxyProperty('TypeId', 'Type', object.typeId),
      createProxyProperty('ImportSupport', 'Import support', object.support),
      createProxyProperty('PropertyCount', 'Property count', String(object.propertyCount)),
      ...(pathDecoded?.decoded ? [{ name: 'Path', label: 'Path', group: 'Path', scope: 'data' as const, type: 'Path::PropertyPath' as const, value: pathDecoded.value as PathPropertyValue, readOnly: true }] : []),
      ...createSketchAttachmentProxyProperties(object),
      ...object.properties.filter((property) => property.expression).map((property) => createProxyProperty(`Expression:${property.name}`, `Expression ${property.name}`, property.expression ?? '')),
      ...(viewProperties.get(object.name) ?? []),
      ],
      ...(object.sketch ? { sketch: object.sketch } : {}),
    }
  })
  return { id: documentId, label: inspection.label, version: 1, dirty: false, readOnly: true, units: 'mm', tree, objects, dependencies: [], recompute: { generation: 0, status: 'idle', objectStates: Object.fromEntries(objects.map((object) => [object.id, 'up-to-date'])), dirtyObjects: [], order: [], errors: [] } }
}

export const inspectFcstdArchive = (bytes: Uint8Array, limitOverrides: Partial<FcstdArchiveLimits> = {}): FcstdInspection => {
  const limits = { ...DEFAULT_FCSTD_LIMITS, ...limitOverrides }
  for (const [name, value] of Object.entries(limits)) if (!Number.isSafeInteger(value) || value <= 0) throw new RangeError(`FCStd limit ${name} must be a positive safe integer.`)
  const entries = inspectZipDirectory(bytes, limits)
  const documentEntry = entries.find((entry) => entry.path.toLowerCase() === 'document.xml')
  if (!documentEntry) throw new Error('FCStd archive does not contain Document.xml.')
  const files = unzipSync(bytes)
  for (const entry of entries) {
    const content = files[entry.path]
    if (!content || content.byteLength !== entry.uncompressedBytes) throw new Error(`FCStd entry ${entry.path} did not decompress to its declared size.`)
  }
  const document = parseDocumentXml(files[documentEntry.path], limits)
  hydratePathProperties(document, files)
  hydrateColorListProperties(document, files)
  hydrateIncludedFileProperties(document, files)
  const guiDocument = parseGuiDocument(files, entries, limits)
  const shapeResources = entries.filter((entry) => entry.role === 'shape').map((entry): FcstdShapeResource => {
    const payload = files[entry.path]
    return { path: entry.path, format: 'brep', mediaType: 'application/x-freecad-brep', byteLength: payload.byteLength, contentHash: hashBytes(payload), status: payload.byteLength > 0 ? 'available' : 'empty' }
  })
  const elementMapResources = entries.filter((entry) => entry.role === 'topology-map').map((entry) => parseElementMapResource(files[entry.path], entry.path))
  const stringHasherEntry = document.stringHasherPath ? entries.find((entry) => entry.path === document.stringHasherPath) : undefined
  if (document.stringHasherPath && !stringHasherEntry) throw new Error(`FCStd Document references missing StringHasher2 resource: ${document.stringHasherPath}`)
  const stringHasherResource = stringHasherEntry ? parseStringHasherResource(files[stringHasherEntry.path], stringHasherEntry.path) : undefined
  if (stringHasherResource?.document) for (const resource of elementMapResources) {
    if (!resource.document) continue
    const issues = validateElementMap2StringHasherEvidence(resource.document, stringHasherResource.document)
    if (issues.length > 0) throw new Error(`FCStd ElementMap2 resource has unresolved StringHasher evidence: ${resource.path}: ${issues[0].path}: ${issues[0].message}`)
  }
  const shapeResourcePaths = new Set(shapeResources.map((resource) => resource.path))
  const elementMapResourcePaths = new Set(elementMapResources.map((resource) => resource.path))
  for (const object of document.objects) for (const property of object.properties) {
    if (property.shapeResource && !shapeResourcePaths.has(property.shapeResource.path)) throw new Error(`FCStd Shape property ${object.name}.${property.name} references missing resource: ${property.shapeResource.path}`)
    if (property.shapeResource) validateNativeShapeResourceName(object.name, property.shapeResource.path)
    if (property.shapeResource?.elementMapResource && !elementMapResourcePaths.has(property.shapeResource.elementMapResource)) throw new Error(`FCStd Shape property ${object.name}.${property.name} references missing ElementMap2 resource: ${property.shapeResource.elementMapResource}`)
    if (property.shapeResource?.hasherIndex !== undefined && !stringHasherResource && !document.embeddedStringHasher && !document.declaredStringHasher) throw new Error(`FCStd Shape property ${object.name}.${property.name} HasherIndex requires StringHasher.`)
  }
  const warnings: string[] = []
  const scriptEntries = entries.filter((entry) => entry.role === 'script')
  if (scriptEntries.length > 0) warnings.push(`${scriptEntries.length} script or macro resource(s) were isolated and will not execute.`)
  const blockedObjects = document.objects.filter((object) => object.support === 'blocked')
  if (blockedObjects.length > 0) warnings.push(`${blockedObjects.length} Python-backed object(s) require a non-executing proxy.`)
  const proxyObjects = document.objects.filter((object) => object.support === 'proxy')
  if (proxyObjects.length > 0) warnings.push(`${proxyObjects.length} unrecognized object type(s) require a read-only proxy.`)
  const level: FcstdCompatibilityReport['level'] = blockedObjects.length > 0 ? 'blocked' : proxyObjects.length > 0 || scriptEntries.length > 0 ? 'partial' : 'metadata-compatible'
  const inspection: Omit<FcstdInspection, 'proxyDocument'> = {
    format: 'FCStd',
    schemaVersion: document.schemaVersion,
    label: document.label,
    entries,
    objects: document.objects,
    guiDocument,
    shapeResources,
    elementMapResources,
    ...(stringHasherResource ? { stringHasherResource } : {}),
    compatibility: {
      level,
      readOnly: true,
      codeExecutionBlocked: true,
      recognizedObjects: document.objects.filter((object) => object.support === 'recognized').length,
      proxyObjects: proxyObjects.length,
      blockedObjects: blockedObjects.length,
      unknownTypeIds: [...new Set(proxyObjects.map((object) => object.typeId))].sort(),
      warnings,
    },
  }
  return { ...inspection, proxyDocument: createProxyDocument(inspection) }
}

export const extractFcstdShapeResources = (bytes: Uint8Array, limitOverrides: Partial<FcstdArchiveLimits> = {}): FcstdShapeResourcePayload[] => {
  const inspection = inspectFcstdArchive(bytes, limitOverrides)
  const files = unzipSync(bytes)
  return inspection.shapeResources.map((resource) => ({ ...resource, bytes: new Uint8Array(files[resource.path]) }))
}

export const extractFcstdIncludedFileResources = (bytes: Uint8Array, limitOverrides: Partial<FcstdArchiveLimits> = {}): FcstdIncludedFileResourcePayload[] => {
  const inspection = inspectFcstdArchive(bytes, limitOverrides)
  const files = unzipSync(bytes)
  const resources = new Map<string, FcstdIncludedFileResourcePayload>()
  for (const object of inspection.objects) for (const property of object.properties) {
    const resource = property.includedFileResource
    if (property.typeId !== 'App::PropertyFileIncluded' || !resource) continue
    const reference = { objectName: object.name, propertyName: property.name }
    const existing = resources.get(resource.path)
    if (existing) {
      existing.references.push(reference)
      continue
    }
    const payload = files[resource.path]
    if (!payload || payload.byteLength !== resource.byteLength) throw new Error(`FCStd PropertyFileIncluded resource could not be extracted: ${resource.path}`)
    resources.set(resource.path, { ...resource, references: [reference], bytes: new Uint8Array(payload) })
  }
  return [...resources.values()]
}

export const storeFcstdIncludedFileResources = async (
  bytes: Uint8Array,
  put: (payload: Uint8Array, mediaType: string) => Promise<{ hash: string }>,
  limitOverrides: Partial<FcstdArchiveLimits> = {},
): Promise<FcstdStoredIncludedFileResource[]> => Promise.all(extractFcstdIncludedFileResources(bytes, limitOverrides).map(async (resource) => {
  const stored = await put(resource.bytes, resource.mediaType)
  if (!stored || typeof stored.hash !== 'string' || !/^[a-f0-9]{64}$/i.test(stored.hash)) throw new Error(`FCStd PropertyFileIncluded resource ${resource.path} returned an invalid SHA-256 project resource hash.`)
  const value = validateIncludedFileValue({ schemaVersion: 1, archiveName: resource.path, resourceHash: stored.hash, byteLength: resource.byteLength, mediaType: resource.mediaType }, resource.path)
  return { path: resource.path, mediaType: resource.mediaType, byteLength: resource.byteLength, contentHash: resource.contentHash, status: resource.status, references: resource.references.map((reference) => ({ ...reference })), hash: value.resourceHash, value }
}))

const throwIfShapeInstantiationAborted = (signal?: AbortSignal) => {
  if (signal?.aborted) throw new DOMException('FCStd Shape instantiation was cancelled.', 'AbortError')
}

export const instantiateFcstdShapeResource = async <T>(
  bytes: Uint8Array,
  resourcePath: string,
  importShape: (input: { format: 'brep'; text: string }) => Promise<T>,
  options: FcstdShapeInstantiationOptions<T> = {},
): Promise<FcstdInstantiatedShape<T>> => {
  throwIfShapeInstantiationAborted(options.signal)
  if (typeof resourcePath !== 'string' || !resourcePath.trim()) throw new TypeError('FCStd Shape resource path must be non-empty.')
  if (typeof importShape !== 'function') throw new TypeError('FCStd Shape importer must be a function.')
  const inspection = inspectFcstdArchive(bytes, options.limits)
  const resource = inspection.shapeResources.find((candidate) => candidate.path === resourcePath)
  if (!resource) throw new Error(`FCStd Shape resource does not exist: ${resourcePath}`)
  const references = inspection.objects.flatMap((object) => object.properties.flatMap((property): FcstdShapeResourceReference[] => property.shapeResource?.path === resourcePath
    ? [{ objectName: object.name, propertyName: property.name, ...(property.shapeResource.hasherIndex === undefined ? {} : { hasherIndex: property.shapeResource.hasherIndex }), ...(property.shapeResource.elementMap === undefined ? {} : { elementMap: property.shapeResource.elementMap }), ...(property.shapeResource.elementMapResource === undefined ? {} : { elementMapResource: property.shapeResource.elementMapResource }) }]
    : []))
  if (references.length === 0) throw new Error(`FCStd Shape resource is not referenced by Part::PropertyPartShape: ${resourcePath}`)
  if (resource.status === 'empty') throw new Error(`FCStd Shape resource is empty: ${resourcePath}`)
  const payload = unzipSync(bytes)[resource.path]
  if (!payload || payload.byteLength !== resource.byteLength) throw new Error(`FCStd Shape resource could not be extracted: ${resourcePath}`)
  let text: string
  try {
    text = new TextDecoder('utf-8', { fatal: true }).decode(payload)
  } catch (error) {
    throw new TypeError(`FCStd Shape resource is not valid UTF-8 BRep text: ${resourcePath}`, { cause: error })
  }
  if (!text.trim()) throw new Error(`FCStd Shape resource contains no BRep text: ${resourcePath}`)
  throwIfShapeInstantiationAborted(options.signal)
  const shape = await importShape({ format: 'brep', text })
  if (options.signal?.aborted) {
    await options.release?.(shape)
    throwIfShapeInstantiationAborted(options.signal)
  }
  return { resource: { ...resource }, references, shape }
}

export const storeFcstdShapeResources = async (
  bytes: Uint8Array,
  put: (payload: Uint8Array, mediaType: string) => Promise<{ hash: string }>,
  limitOverrides: Partial<FcstdArchiveLimits> = {},
): Promise<FcstdStoredShapeResource[]> => {
  const resources = extractFcstdShapeResources(bytes, limitOverrides).filter((resource) => resource.status === 'available')
  return Promise.all(resources.map(async (resource) => {
    const stored = await put(resource.bytes, resource.mediaType)
    if (!stored || typeof stored.hash !== 'string' || !stored.hash.trim()) throw new Error(`FCStd shape resource ${resource.path} returned an invalid resource hash.`)
    return { path: resource.path, format: resource.format, mediaType: resource.mediaType, byteLength: resource.byteLength, contentHash: resource.contentHash, status: resource.status, hash: stored.hash }
  }))
}
