import { TLRecord, RecordId, TLStore, IndexKey } from "@tldraw/tldraw"
import * as Automerge from "@automerge/automerge"

// Track invalid index warnings to avoid console spam - log summary instead of per-shape
let _invalidIndexCount = 0
let _invalidIndexLogTimer: ReturnType<typeof setTimeout> | null = null
function logInvalidIndexThrottled(index: string, shapeId: string) {
  _invalidIndexCount++
  // Log a summary every 2 seconds instead of per-shape
  if (!_invalidIndexLogTimer) {
    _invalidIndexLogTimer = setTimeout(() => {
      if (_invalidIndexCount > 0) {
        console.warn(`Invalid index reset to 'a1' for ${_invalidIndexCount} shape(s) (e.g. "${index}" on ${shapeId})`)
        _invalidIndexCount = 0
      }
      _invalidIndexLogTimer = null
    }, 2000)
  }
}

// Helper function to validate if a string is a valid tldraw IndexKey
// tldraw uses fractional indexing based on https://observablehq.com/@dgreensp/implementing-fractional-indexing
// The first letter encodes integer part length: a=1 digit, b=2 digits, c=3 digits, etc.
// Examples: "a0"-"a9", "b10"-"b99", "c100"-"c999", with optional fraction "a1V4rr"
// Invalid: "b1" (b expects 2 digits but has 1), simple sequential numbers
function isValidIndexKey(index: string): boolean {
  if (!index || typeof index !== 'string' || index.length === 0) {
    return false
  }

  // Must start with a letter
  if (!/^[a-zA-Z]/.test(index)) {
    return false
  }

  const prefix = index[0]
  const rest = index.slice(1)

  // For lowercase prefixes, validate digit count matches the prefix
  if (prefix >= 'a' && prefix <= 'z') {
    // Calculate expected minimum digit count: a=1, b=2, c=3, etc.
    const expectedDigits = prefix.charCodeAt(0) - 'a'.charCodeAt(0) + 1

    // Extract the integer part (leading digits)
    const integerMatch = rest.match(/^(\d+)/)
    if (!integerMatch) {
      // No digits at all - invalid
      return false
    }

    const integerPart = integerMatch[1]

    // Check if integer part has correct number of digits for the prefix
    if (integerPart.length < expectedDigits) {
      // Invalid: "b1" has b (expects 2 digits) but only has 1 digit
      return false
    }
  }

  // Check overall format: letter followed by alphanumeric
  if (/^[a-zA-Z][a-zA-Z0-9]+$/.test(index)) {
    return true
  }

  return false
}

export function applyAutomergePatchesToTLStore(
  patches: Automerge.Patch[],
  store: TLStore,
  automergeDoc?: any // Optional Automerge document to read full records from
) {
  const toRemove: TLRecord["id"][] = []
  const updatedObjects: { [id: string]: TLRecord } = {}

  patches.forEach((patch) => {
    if (!isStorePatch(patch)) return

    const id = pathToId(patch.path)
    
    // Skip records with empty or invalid IDs
    if (!id || id === '') {
      return
    }
    
    // CRITICAL: Skip custom record types that aren't TLDraw records
    // These should only exist in Automerge, not in TLDraw store
    // Components like ObsidianVaultBrowser read directly from Automerge
    if (typeof id === 'string' && id.startsWith('obsidian_vault:')) {
      return // Skip - not a TLDraw record, don't process
    }
    
    const existingRecord = getRecordFromStore(store, id)
    
    // CRITICAL: For shapes, get coordinates and parentId from store's current state BEFORE any patch processing
    // This ensures we preserve coordinates and parent relationships even if patches don't include them
    // This is especially important when patches come back after store.put operations
    let storeCoordinates: { x?: number; y?: number } = {}
    let storeParentId: string | undefined = undefined
    if (existingRecord && existingRecord.typeName === 'shape') {
      const storeX = (existingRecord as any).x
      const storeY = (existingRecord as any).y
      if (typeof storeX === 'number' && !isNaN(storeX) && storeX !== null && storeX !== undefined) {
        storeCoordinates.x = storeX
      }
      if (typeof storeY === 'number' && !isNaN(storeY) && storeY !== null && storeY !== undefined) {
        storeCoordinates.y = storeY
      }
      // CRITICAL: Preserve parentId from store (might be a frame or group!)
      const existingParentId = (existingRecord as any).parentId
      if (existingParentId && typeof existingParentId === 'string') {
        storeParentId = existingParentId
      }
    }
    
    // CRITICAL: If record doesn't exist in store yet, try to get it from Automerge document
    // This prevents coordinates from defaulting to 0,0 when patches create new records
    let automergeRecord: any = null
    let automergeParentId: string | undefined = undefined
    if (!existingRecord && automergeDoc && automergeDoc.store && automergeDoc.store[id]) {
      try {
        automergeRecord = automergeDoc.store[id]
        // Extract coordinates and parentId from Automerge record if it's a shape
        if (automergeRecord && automergeRecord.typeName === 'shape') {
          const docX = automergeRecord.x
          const docY = automergeRecord.y
          if (typeof docX === 'number' && !isNaN(docX) && docX !== null && docX !== undefined) {
            storeCoordinates.x = docX
          }
          if (typeof docY === 'number' && !isNaN(docY) && docY !== null && docY !== undefined) {
            storeCoordinates.y = docY
          }
          // CRITICAL: Preserve parentId from Automerge document (might be a frame!)
          if (automergeRecord.parentId && typeof automergeRecord.parentId === 'string') {
            automergeParentId = automergeRecord.parentId
          }
        }
      } catch (e) {
        // If we can't read from Automerge doc, continue without it
        console.warn(`Could not read record ${id} from Automerge document:`, e)
      }
    }
    
    // Infer typeName from ID pattern if record doesn't exist
    let defaultTypeName = 'shape'
    let defaultRecord: any = {
      id,
      typeName: 'shape',
      type: 'geo', // Default shape type
      x: 0,
      y: 0,
      rotation: 0,
      isLocked: false,
      opacity: 1,
      meta: {},
      props: {}
    }
    
    // Check if ID pattern indicates a record type
    // Note: obsidian_vault records are skipped above, so we don't need to handle them here
    if (typeof id === 'string') {
      if (id.startsWith('shape:')) {
        defaultTypeName = 'shape'
        // Keep default shape record structure
      } else if (id.startsWith('page:')) {
        defaultTypeName = 'page'
        defaultRecord = {
          id,
          typeName: 'page',
          name: '',
          index: 'a0' as any,
          meta: {}
        }
      } else if (id.startsWith('camera:')) {
        defaultTypeName = 'camera'
        defaultRecord = {
          id,
          typeName: 'camera',
          x: 0,
          y: 0,
          z: 1,
          meta: {}
        }
      } else if (id.startsWith('instance:')) {
        defaultTypeName = 'instance'
        defaultRecord = {
          id,
          typeName: 'instance',
          currentPageId: 'page:page' as any,
          meta: {}
        }
      } else if (id.startsWith('pointer:')) {
        defaultTypeName = 'pointer'
        defaultRecord = {
          id,
          typeName: 'pointer',
          x: 0,
          y: 0,
          lastActivityTimestamp: 0,
          meta: {}
        }
      } else if (id.startsWith('document:')) {
        defaultTypeName = 'document'
        defaultRecord = {
          id,
          typeName: 'document',
          gridSize: 10,
          name: '',
          meta: {}
        }
      }
    }
    
    // CRITICAL: When creating a new record, prefer using the full record from Automerge document
    // This ensures we get all properties including coordinates, not just defaults
    let record: any
    if (updatedObjects[id]) {
      record = updatedObjects[id]
    } else if (existingRecord) {
      record = JSON.parse(JSON.stringify(existingRecord))
    } else if (automergeRecord) {
      // Use the full record from Automerge document - this has all properties including coordinates
      record = JSON.parse(JSON.stringify(automergeRecord))
    } else {
      // Fallback to default record only if we can't get it from anywhere else
      record = defaultRecord
    }
    
    // CRITICAL: For shapes, ensure x and y are always present (even if record came from updatedObjects)
    // This prevents coordinates from being lost when records are created from patches
    if (record.typeName === 'shape') {
      if (typeof record.x !== 'number' || record.x === null || isNaN(record.x)) {
        record = { ...record, x: defaultRecord.x || 0 }
      }
      if (typeof record.y !== 'number' || record.y === null || isNaN(record.y)) {
        record = { ...record, y: defaultRecord.y || 0 }
      }
    }
    
    // CRITICAL: Ensure typeName matches ID pattern (fixes misclassification)
    // Note: obsidian_vault records are skipped above, so we don't need to handle them here
    if (typeof id === 'string') {
      let correctTypeName = record.typeName
      if (id.startsWith('shape:') && record.typeName !== 'shape') {
        correctTypeName = 'shape'
      } else if (id.startsWith('page:') && record.typeName !== 'page') {
        correctTypeName = 'page'
      } else if (id.startsWith('camera:') && record.typeName !== 'camera') {
        correctTypeName = 'camera'
      } else if (id.startsWith('instance:') && record.typeName !== 'instance') {
        correctTypeName = 'instance'
      } else if (id.startsWith('pointer:') && record.typeName !== 'pointer') {
        correctTypeName = 'pointer'
      } else if (id.startsWith('document:') && record.typeName !== 'document') {
        correctTypeName = 'document'
      }
      
      // Create new object with correct typeName if it changed
      if (correctTypeName !== record.typeName) {
        record = { ...record, typeName: correctTypeName } as TLRecord
      }
    }

    // CRITICAL: Store original x and y before patch application to preserve them
    // Priority: Use coordinates from store's current state (most reliable), then from record, then undefined
    // This ensures we preserve coordinates even when patches come back after store.put operations
    const recordX = (record.typeName === 'shape' && typeof record.x === 'number' && !isNaN(record.x)) ? record.x : undefined
    const recordY = (record.typeName === 'shape' && typeof record.y === 'number' && !isNaN(record.y)) ? record.y : undefined
    const originalX = storeCoordinates.x !== undefined ? storeCoordinates.x : recordX
    const originalY = storeCoordinates.y !== undefined ? storeCoordinates.y : recordY
    const hadOriginalCoordinates = originalX !== undefined && originalY !== undefined
    
    // CRITICAL: Store original richText and arrow text before patch application to preserve them
    // This ensures richText and arrow text aren't lost when patches only update other properties
    let originalRichText: any = undefined
    let originalArrowText: any = undefined
    if (record.typeName === 'shape') {
      // Get richText from store's current state (most reliable)
      if (existingRecord && (existingRecord as any).props && (existingRecord as any).props.richText) {
        originalRichText = (existingRecord as any).props.richText
      } else if ((record as any).props && (record as any).props.richText) {
        originalRichText = (record as any).props.richText
      }
      
      // Get arrow text from store's current state (most reliable)
      if ((record as any).type === 'arrow') {
        if (existingRecord && (existingRecord as any).props && (existingRecord as any).props.text !== undefined) {
          originalArrowText = (existingRecord as any).props.text
        } else if ((record as any).props && (record as any).props.text !== undefined) {
          originalArrowText = (record as any).props.text
        }
      }
    }

    switch (patch.action) {
      case "insert": {
        updatedObjects[id] = applyInsertToObject(patch, record)
        break
      }
      case "put":
        updatedObjects[id] = applyPutToObject(patch, record)
        break
      case "del": {
        const id = pathToId(patch.path)
        toRemove.push(id as TLRecord["id"])
        break
      }
      case "splice": {
        updatedObjects[id] = applySpliceToObject(patch, record)
        break
      }
      case "inc": {
        updatedObjects[id] = applyIncToObject(patch, record)
        break
      }
      case "mark":
      case "unmark":
      case "conflict": {
        // These actions are not currently supported for TLDraw
        break
      }
      default: {
      }
    }
    
    // CRITICAL: After patch application, ensure x and y coordinates are preserved for shapes
    // This prevents coordinates from being reset to 0,0 when patches don't include them
    if (updatedObjects[id] && updatedObjects[id].typeName === 'shape') {
      const patchedRecord = updatedObjects[id]
      const patchedX = (patchedRecord as any).x
      const patchedY = (patchedRecord as any).y
      const patchedHasValidX = typeof patchedX === 'number' && !isNaN(patchedX) && patchedX !== null && patchedX !== undefined
      const patchedHasValidY = typeof patchedY === 'number' && !isNaN(patchedY) && patchedY !== null && patchedY !== undefined
      
      // CRITICAL: If we had original coordinates, preserve them unless patch explicitly set different valid coordinates
      // This prevents coordinates from collapsing to 0,0 after bulk upload
      if (hadOriginalCoordinates) {
        // Only use patched coordinates if they're explicitly set and different from original
        // Otherwise, preserve the original coordinates
        if (patchedHasValidX && patchedX !== originalX) {
          // Patch explicitly set a different X coordinate - use it
          updatedObjects[id] = { ...patchedRecord, x: patchedX }
        } else {
          // Preserve original X coordinate
          updatedObjects[id] = { ...patchedRecord, x: originalX }
        }
        
        if (patchedHasValidY && patchedY !== originalY) {
          // Patch explicitly set a different Y coordinate - use it
          updatedObjects[id] = { ...updatedObjects[id], y: patchedY } as TLRecord
        } else {
          // Preserve original Y coordinate
          updatedObjects[id] = { ...updatedObjects[id], y: originalY } as TLRecord
        }
      } else {
        // No original coordinates - use patched values or defaults
        if (!patchedHasValidX) {
          updatedObjects[id] = { ...patchedRecord, x: defaultRecord.x || 0 }
        }
        if (!patchedHasValidY) {
          updatedObjects[id] = { ...updatedObjects[id], y: defaultRecord.y || 0 } as TLRecord
        }
      }
      
      // CRITICAL: Preserve richText and arrow text after patch application
      // This prevents richText and arrow text from being lost when patches only update other properties
      const currentRecord = updatedObjects[id]
      
      // Preserve richText for geo/note/text shapes
      if (originalRichText !== undefined && (currentRecord as any).type !== 'arrow') {
        const patchedProps = (currentRecord as any).props || {}
        const patchedRichText = patchedProps.richText
        // If patch didn't include richText, preserve the original
        if (patchedRichText === undefined || patchedRichText === null) {
          updatedObjects[id] = {
            ...currentRecord,
            props: {
              ...patchedProps,
              richText: originalRichText
            }
          } as TLRecord
        }
      }
      
      // Preserve arrow text for arrow shapes
      if (originalArrowText !== undefined && (currentRecord as any).type === 'arrow') {
        const patchedProps = (currentRecord as any).props || {}
        const patchedText = patchedProps.text
        // If patch didn't include text, preserve the original
        if (patchedText === undefined || patchedText === null) {
          updatedObjects[id] = {
            ...currentRecord,
            props: {
              ...patchedProps,
              text: originalArrowText
            }
          } as TLRecord
        }
      }

      // CRITICAL: Preserve parentId from store or Automerge document
      // This prevents shapes from losing their frame/group parent relationships
      // which causes them to reset to (0,0) on the page instead of maintaining their position in the frame
      // Priority: store parentId (most reliable), then Automerge parentId, then patch value
      const preservedParentId = storeParentId || automergeParentId
      if (preservedParentId !== undefined) {
        const patchedParentId = (currentRecord as any).parentId
        // If patch didn't include parentId, or it's missing/default, use the preserved parentId
        if (!patchedParentId || (patchedParentId === 'page:page' && preservedParentId !== 'page:page')) {
          updatedObjects[id] = {
            ...currentRecord,
            parentId: preservedParentId
          } as TLRecord
        }
      }
    }
    
    // CRITICAL: Re-check typeName after patch application to ensure it's still correct
    // Note: obsidian_vault records are skipped above, so we don't need to handle them here
  })
  
  // Sanitize records before putting them in the store
  const toPut: TLRecord[] = []
  const failedRecords: any[] = []
  
  Object.values(updatedObjects).forEach(record => {
    // Skip records with empty or invalid IDs
    if (!record || !record.id || record.id === '') {
      return
    }
    
    // CRITICAL: Skip custom record types that aren't TLDraw records
    // These should only exist in Automerge, not in TLDraw store
    if (typeof record.id === 'string' && record.id.startsWith('obsidian_vault:')) {
      return // Skip - not a TLDraw record
    }
    
    // Filter out SharedPiano shapes since they're no longer supported
    if (record.typeName === 'shape' && (record as any).type === 'SharedPiano') {
      return // Skip - SharedPiano is deprecated
    }
    
    try {
      const sanitized = sanitizeRecord(record)
      toPut.push(sanitized)
    } catch (error) {
      // If it's a missing typeName/id error, skip it
      if (error instanceof Error && 
          (error.message.includes('missing required typeName') || 
           error.message.includes('missing required id'))) {
        // Skip records with missing required fields
        return
      }
      failedRecords.push(record)
    }
  })

  // put / remove the records in the store
  // Log patch application for debugging

  if (failedRecords.length > 0) {
    console.warn(`Failed to sanitize ${failedRecords.length} record(s)`)
  }
  
    // CRITICAL: Final safety check - ensure no geo shapes have w/h/geo at top level
    // Also ensure text shapes don't have props.text (should use props.richText instead)
    const finalSanitized = toPut.map(record => {
      if (record.typeName === 'shape' && record.type === 'geo') {
        // Store values before removing from top level
        const wValue = 'w' in record ? (record as any).w : undefined
        const hValue = 'h' in record ? (record as any).h : undefined
        const geoValue = 'geo' in record ? (record as any).geo : undefined
        
        // Create cleaned record without w/h/geo at top level
        const cleaned: any = {}
        for (const key in record) {
          if (key !== 'w' && key !== 'h' && key !== 'geo') {
            cleaned[key] = (record as any)[key]
          }
        }
        
        // Ensure props exists and move values there if needed
        if (!cleaned.props) cleaned.props = {}
        if (wValue !== undefined && (!('w' in cleaned.props) || cleaned.props.w === undefined)) {
          cleaned.props.w = wValue
        }
        if (hValue !== undefined && (!('h' in cleaned.props) || cleaned.props.h === undefined)) {
          cleaned.props.h = hValue
        }
        
        // CRITICAL: props.geo is REQUIRED for geo shapes - TLDraw validation will fail without it
        // Use geoValue if available, otherwise default to 'rectangle'
        if (geoValue !== undefined) {
          cleaned.props.geo = geoValue
        } else if (!cleaned.props.geo || cleaned.props.geo === undefined || cleaned.props.geo === null) {
          // Default to rectangle if geo is missing
          cleaned.props.geo = 'rectangle'
        }
        
        // CRITICAL: props.dash is REQUIRED for geo shapes - TLDraw validation will fail without it
        // Ensure it's always set, defaulting to 'draw' if missing
        if (!cleaned.props.dash || cleaned.props.dash === undefined || cleaned.props.dash === null) {
          cleaned.props.dash = 'draw'
        }
        
        return cleaned as TLRecord
      }
    
    // CRITICAL: Remove props.text from text shapes (TLDraw schema doesn't allow it)
    if (record.typeName === 'shape' && record.type === 'text' && (record as any).props && 'text' in (record as any).props) {
      const cleaned = { ...record }
      if (cleaned.props && 'text' in cleaned.props) {
        delete (cleaned.props as any).text
      }
      return cleaned as TLRecord
    }
    
    return record
  })
  
  store.mergeRemoteChanges(() => {
    if (toRemove.length) store.remove(toRemove)
    if (finalSanitized.length) store.put(finalSanitized)
  })
}

// Helper function to clean NaN values from richText content
// This prevents SVG export errors when TLDraw tries to render text with invalid coordinates
function cleanRichTextNaN(richText: any): any {
  if (!richText || typeof richText !== 'object') {
    return richText
  }
  
  // Deep clone to avoid mutating the original
  const cleaned = JSON.parse(JSON.stringify(richText))
  
  // Recursively clean content array
  if (Array.isArray(cleaned.content)) {
    cleaned.content = cleaned.content.map((item: any) => {
      if (typeof item === 'object' && item !== null) {
        // Remove any NaN values from the item
        const cleanedItem: any = {}
        for (const key in item) {
          const value = item[key]
          // Skip NaN values - they cause SVG export errors
          if (typeof value === 'number' && isNaN(value)) {
            // Skip NaN values
            continue
          }
          // Recursively clean nested objects
          if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
            cleanedItem[key] = cleanRichTextNaN(value)
          } else if (Array.isArray(value)) {
            cleanedItem[key] = value.map((v: any) => 
              typeof v === 'object' && v !== null ? cleanRichTextNaN(v) : v
            )
          } else {
            cleanedItem[key] = value
          }
        }
        return cleanedItem
      }
      return item
    })
  }
  
  return cleaned
}

// Minimal sanitization - only fix critical issues that break TLDraw
// EXPORTED: Use this same sanitization for patch-based loading (same as dev mode)
export function sanitizeRecord(record: any): TLRecord {
  const sanitized = { ...record }
  
  // CRITICAL FIXES ONLY - preserve all other properties
  
  // Only fix critical structural issues
  if (!sanitized.id || sanitized.id === '') {
    throw new Error("Record missing required id field")
  }
  
  if (!sanitized.typeName || sanitized.typeName === '') {
    throw new Error("Record missing required typeName field")
  }
  
  // For shapes, only ensure basic required fields exist
  if (sanitized.typeName === 'shape') {
    // CRITICAL: Remove instance-only properties from shapes (these cause validation errors)
    // These properties should only exist on instance records, not shape records
    const instanceOnlyProperties = ['insets', 'brush', 'zoomBrush', 'scribbles', 'duplicateProps']
    instanceOnlyProperties.forEach(prop => {
      if (prop in sanitized) {
        delete (sanitized as any)[prop]
      }
    })
    
    // Ensure required shape fields exist
    // CRITICAL: Only set defaults if coordinates are truly missing or invalid
    // DO NOT overwrite valid coordinates (including 0, which is a valid position)
    // Only set to 0 if the value is undefined, null, or NaN
    if (sanitized.x === undefined || sanitized.x === null || (typeof sanitized.x === 'number' && isNaN(sanitized.x))) {
      sanitized.x = 0
    }
    if (sanitized.y === undefined || sanitized.y === null || (typeof sanitized.y === 'number' && isNaN(sanitized.y))) {
      sanitized.y = 0
    }
    if (typeof sanitized.rotation !== 'number') sanitized.rotation = 0
    if (typeof sanitized.isLocked !== 'boolean') sanitized.isLocked = false
    if (typeof sanitized.opacity !== 'number') sanitized.opacity = 1
    // CRITICAL: Preserve all existing meta properties - only create empty object if meta doesn't exist
    if (!sanitized.meta || typeof sanitized.meta !== 'object') {
      sanitized.meta = {}
    } else {
      // Ensure meta is a mutable copy to preserve all properties (including text for rectangles)
      sanitized.meta = { ...sanitized.meta }
    }
    // CRITICAL: IndexKey must follow tldraw's fractional indexing format
    // Valid format: starts with 'a' followed by digits, optionally followed by alphanumeric jitter
    // Examples: "a1", "a2", "a10", "a1V", "a24sT", "a1V4rr" (fractional between a1 and a2)
    // Invalid: "c1", "b1", "z999" (old format - not valid fractional indices)
    if (!isValidIndexKey(sanitized.index)) {
      logInvalidIndexThrottled(sanitized.index, sanitized.id)
      sanitized.index = 'a1' as IndexKey
    }
    if (!sanitized.parentId) sanitized.parentId = 'page:page'
    if (!sanitized.props || typeof sanitized.props !== 'object') sanitized.props = {}
    
    // CRITICAL: Ensure props is a deep mutable copy to preserve all nested properties
    // This is essential for custom shapes like ObsNote and for preserving richText in geo shapes
    // Use JSON parse/stringify to create a deep copy of nested objects (like richText.content)
    try {
      sanitized.props = JSON.parse(JSON.stringify(sanitized.props))
    } catch (e) {
      // If JSON serialization fails (e.g., due to functions or circular references),
      // create a shallow copy and recursively clean it
      // Deep copy failed, using shallow copy
      const propsCopy: any = {}
      for (const key in sanitized.props) {
        try {
          const value = sanitized.props[key]
          // Skip functions
          if (typeof value === 'function') {
            continue
          }
          // Try to serialize individual values
          try {
            propsCopy[key] = JSON.parse(JSON.stringify(value))
          } catch (valueError) {
            // If individual value can't be serialized, use it as-is if it's a primitive
            if (value === null || value === undefined || typeof value !== 'object') {
              propsCopy[key] = value
            }
            // Otherwise skip it
          }
        } catch (keyError) {
          // Skip properties that can't be accessed
          continue
        }
      }
      sanitized.props = propsCopy
    }
    
    // CRITICAL: Map old shape type names to new ones (migration support)
    // This handles renamed shape types from old data
    if (sanitized.type === 'Transcribe') {
      sanitized.type = 'Transcription'
    }

    // CRITICAL: Normalize case for custom shape types (lowercase → PascalCase)
    // The schema expects PascalCase (e.g., "ChatBox" not "chatBox")
    const customShapeTypeMap: Record<string, string> = {
      'chatBox': 'ChatBox',
      'videoChat': 'VideoChat',
      'embed': 'Embed',
      'markdown': 'Markdown',
      'mycrozineTemplate': 'MycrozineTemplate',
      'slide': 'Slide',
      'prompt': 'Prompt',
      'transcription': 'Transcription',
      'obsNote': 'ObsNote',
      'fathomNote': 'FathomNote',
      'holon': 'Holon',
      'obsidianBrowser': 'ObsidianBrowser',
      'fathomMeetingsBrowser': 'FathomMeetingsBrowser',
      'meetingIntelligenceBrowser': 'MeetingIntelligenceBrowser',
      'imageGen': 'ImageGen',
      'videoGen': 'VideoGen',
      'multmux': 'Multmux',
    }

    // Normalize the shape type if it's a custom type with incorrect case
    if (sanitized.type && typeof sanitized.type === 'string' && customShapeTypeMap[sanitized.type]) {
      sanitized.type = customShapeTypeMap[sanitized.type]
    }

    // CRITICAL: Sanitize Multmux shapes AFTER case normalization - ensure all required props exist
    // Old shapes may have wsUrl (removed) or undefined values
    if (sanitized.type === 'Multmux') {
      // Remove deprecated wsUrl prop
      if ('wsUrl' in sanitized.props) {
        delete sanitized.props.wsUrl
      }
      // CRITICAL: Create a clean props object with all required values
      // This ensures no undefined values slip through validation
      // Every value MUST be explicitly defined - undefined values cause ValidationError
      const w = (typeof sanitized.props.w === 'number' && !isNaN(sanitized.props.w)) ? sanitized.props.w : 800
      const h = (typeof sanitized.props.h === 'number' && !isNaN(sanitized.props.h)) ? sanitized.props.h : 600
      const sessionId = (typeof sanitized.props.sessionId === 'string') ? sanitized.props.sessionId : ''
      const sessionName = (typeof sanitized.props.sessionName === 'string') ? sanitized.props.sessionName : ''
      const token = (typeof sanitized.props.token === 'string') ? sanitized.props.token : ''
      // Fix old port (3000 -> 3002) during sanitization
      let serverUrl = (typeof sanitized.props.serverUrl === 'string') ? sanitized.props.serverUrl : 'http://localhost:3002'
      if (serverUrl === 'http://localhost:3000') {
        serverUrl = 'http://localhost:3002'
      }
      const pinnedToView = (sanitized.props.pinnedToView === true) ? true : false
      // Filter out any undefined or non-string elements from tags array
      let tags: string[] = ['terminal', 'multmux']
      if (Array.isArray(sanitized.props.tags)) {
        const filteredTags = sanitized.props.tags.filter((t: any) => typeof t === 'string' && t !== '')
        if (filteredTags.length > 0) {
          tags = filteredTags
        }
      }

      // Build clean props object - all values are guaranteed to be defined
      const cleanProps = {
        w: w,
        h: h,
        sessionId: sessionId,
        sessionName: sessionName,
        token: token,
        serverUrl: serverUrl,
        pinnedToView: pinnedToView,
        tags: tags,
      }

      // CRITICAL: Verify no undefined values before assigning
      // This is a safety check - if any value is undefined, something went wrong above
      for (const [key, value] of Object.entries(cleanProps)) {
        if (value === undefined) {
          console.error(`❌ CRITICAL: Multmux prop ${key} is undefined after sanitization! This should never happen.`)
          // Fix it with a default value based on key
          switch (key) {
            case 'w': (cleanProps as any).w = 800; break
            case 'h': (cleanProps as any).h = 600; break
            case 'sessionId': (cleanProps as any).sessionId = ''; break
            case 'sessionName': (cleanProps as any).sessionName = ''; break
            case 'token': (cleanProps as any).token = ''; break
            case 'serverUrl': (cleanProps as any).serverUrl = 'http://localhost:3002'; break
            case 'pinnedToView': (cleanProps as any).pinnedToView = false; break
            case 'tags': (cleanProps as any).tags = ['terminal', 'multmux']; break
          }
        }
      }

      sanitized.props = cleanProps
    }

    // CRITICAL: Sanitize Map shapes - ensure all required props have defaults
    // Old shapes may be missing pinnedToView, isMinimized, or other newer properties
    if (sanitized.type === 'Map') {
      // Ensure boolean props have proper defaults (old data may have undefined)
      if (typeof sanitized.props.pinnedToView !== 'boolean') {
        sanitized.props.pinnedToView = false
      }
      if (typeof sanitized.props.isMinimized !== 'boolean') {
        sanitized.props.isMinimized = false
      }
      if (typeof sanitized.props.showSidebar !== 'boolean') {
        sanitized.props.showSidebar = true
      }
      if (typeof sanitized.props.interactive !== 'boolean') {
        sanitized.props.interactive = true
      }
      if (typeof sanitized.props.showGPS !== 'boolean') {
        sanitized.props.showGPS = false
      }
      if (typeof sanitized.props.showSearch !== 'boolean') {
        sanitized.props.showSearch = false
      }
      if (typeof sanitized.props.showDirections !== 'boolean') {
        sanitized.props.showDirections = false
      }
      if (typeof sanitized.props.sharingLocation !== 'boolean') {
        sanitized.props.sharingLocation = false
      }
      // Ensure array props exist
      if (!Array.isArray(sanitized.props.annotations)) {
        sanitized.props.annotations = []
      }
      if (!Array.isArray(sanitized.props.waypoints)) {
        sanitized.props.waypoints = []
      }
      if (!Array.isArray(sanitized.props.collaborators)) {
        sanitized.props.collaborators = []
      }
      if (!Array.isArray(sanitized.props.gpsUsers)) {
        sanitized.props.gpsUsers = []
      }
      if (!Array.isArray(sanitized.props.tags)) {
        sanitized.props.tags = ['map']
      }
      // Ensure string props exist
      if (typeof sanitized.props.styleKey !== 'string') {
        sanitized.props.styleKey = 'voyager'
      }
      if (typeof sanitized.props.title !== 'string') {
        sanitized.props.title = 'Collaborative Map'
      }
      if (typeof sanitized.props.description !== 'string') {
        sanitized.props.description = ''
      }
      // Ensure viewport exists with defaults
      if (!sanitized.props.viewport || typeof sanitized.props.viewport !== 'object') {
        sanitized.props.viewport = {
          center: { lat: 40.7128, lng: -74.006 },
          zoom: 12,
          bearing: 0,
          pitch: 0,
        }
      }
      // Ensure numeric props
      if (typeof sanitized.props.w !== 'number' || isNaN(sanitized.props.w)) {
        sanitized.props.w = 800
      }
      if (typeof sanitized.props.h !== 'number' || isNaN(sanitized.props.h)) {
        sanitized.props.h = 550
      }
    }

    // CRITICAL: Infer type from properties BEFORE defaulting to 'geo'
    // This ensures arrows and other shapes are properly recognized
    if (!sanitized.type || typeof sanitized.type !== 'string') {
      // Check for arrow-specific properties first
      if (sanitized.props?.start !== undefined || 
          sanitized.props?.end !== undefined || 
          sanitized.props?.arrowheadStart !== undefined || 
          sanitized.props?.arrowheadEnd !== undefined ||
          sanitized.props?.kind === 'line' ||
          sanitized.props?.kind === 'curved' ||
          sanitized.props?.kind === 'straight') {
        sanitized.type = 'arrow'
      }
      // Check for line-specific properties
      else if (sanitized.props?.points !== undefined) {
        sanitized.type = 'line'
      }
      // Check for geo-specific properties (w/h/geo)
      else if (sanitized.props?.geo !== undefined || 
               ('w' in sanitized && 'h' in sanitized) ||
               ('w' in sanitized.props && 'h' in sanitized.props)) {
        sanitized.type = 'geo'
      }
      // Check for note-specific properties
      else if (sanitized.props?.growY !== undefined || 
               sanitized.props?.verticalAlign !== undefined) {
        sanitized.type = 'note'
      }
      // Check for text-specific properties
      else if (sanitized.props?.textAlign !== undefined || 
               sanitized.props?.autoSize !== undefined) {
        sanitized.type = 'text'
      }
      // Check for draw-specific properties
      else if (sanitized.props?.segments !== undefined) {
        sanitized.type = 'draw'
      }
      // Default to geo only if no other indicators found
      else {
        sanitized.type = 'geo'
      }
    }
    
    // CRITICAL: For geo shapes, move w/h/geo from top level to props (required by TLDraw schema)
    if (sanitized.type === 'geo' || ('w' in sanitized && 'h' in sanitized && sanitized.type !== 'arrow')) {
      // If type is missing but has w/h, assume it's a geo shape (but only if not already identified as arrow)
      if (!sanitized.type || sanitized.type === 'geo') {
        sanitized.type = 'geo'
      }
      
      // Ensure props exists
      if (!sanitized.props) sanitized.props = {}
      
      // Store values before removing from top level
      const wValue = 'w' in sanitized ? (sanitized as any).w : undefined
      const hValue = 'h' in sanitized ? (sanitized as any).h : undefined
      const geoValue = 'geo' in sanitized ? (sanitized as any).geo : undefined
      
      // Move w from top level to props (if present at top level)
      if (wValue !== undefined) {
        if (!('w' in sanitized.props) || sanitized.props.w === undefined) {
          sanitized.props.w = wValue
        }
        delete (sanitized as any).w
      }
      
      // Move h from top level to props (if present at top level)
      if (hValue !== undefined) {
        if (!('h' in sanitized.props) || sanitized.props.h === undefined) {
          sanitized.props.h = hValue
        }
        delete (sanitized as any).h
      }
      
      // Move geo from top level to props (if present at top level)
      if (geoValue !== undefined) {
        if (!('geo' in sanitized.props) || sanitized.props.geo === undefined) {
          sanitized.props.geo = geoValue
        }
        delete (sanitized as any).geo
      }
      
      // CRITICAL: props.geo is REQUIRED for geo shapes - TLDraw validation will fail without it
      // Ensure it's always set, defaulting to 'rectangle' if missing
      if (!sanitized.props.geo || sanitized.props.geo === undefined || sanitized.props.geo === null) {
        sanitized.props.geo = 'rectangle'
      }
      
      // CRITICAL: props.dash is REQUIRED for geo shapes - TLDraw validation will fail without it
      // Ensure it's always set, defaulting to 'draw' if missing
      if (!sanitized.props.dash || sanitized.props.dash === undefined || sanitized.props.dash === null) {
        sanitized.props.dash = 'draw'
      }
      
    }
    
    // Only fix type if completely missing
    if (!sanitized.type || typeof sanitized.type !== 'string') {
      // Simple type inference - only if absolutely necessary
      if (sanitized.props?.geo) {
        sanitized.type = 'geo'
      } else {
        sanitized.type = 'geo' // Safe default
      }
    }
    
    // CRITICAL: Fix crop structure for image/video shapes if it exists
    if (sanitized.type === 'image' || sanitized.type === 'video') {
      if (sanitized.props.crop !== null && sanitized.props.crop !== undefined) {
        if (!sanitized.props.crop.topLeft || !sanitized.props.crop.bottomRight) {
          if (sanitized.props.crop.x !== undefined && sanitized.props.crop.y !== undefined) {
            // Convert old format to new format
            sanitized.props.crop = {
              topLeft: { x: sanitized.props.crop.x || 0, y: sanitized.props.crop.y || 0 },
              bottomRight: { 
                x: (sanitized.props.crop.x || 0) + (sanitized.props.crop.w || 1), 
                y: (sanitized.props.crop.y || 0) + (sanitized.props.crop.h || 1) 
              }
            }
          } else {
            sanitized.props.crop = {
              topLeft: { x: 0, y: 0 },
              bottomRight: { x: 1, y: 1 }
            }
          }
        }
      }
    }
    
    // CRITICAL: Fix line shapes - ensure valid points structure (required by schema)
    if (sanitized.type === 'line') {
      // Remove invalid w/h from props (they cause validation errors)
      if ('w' in sanitized.props) delete sanitized.props.w
      if ('h' in sanitized.props) delete sanitized.props.h

      // Line shapes REQUIRE points property with at least 2 points
      if (!sanitized.props.points || typeof sanitized.props.points !== 'object' || Array.isArray(sanitized.props.points)) {
        sanitized.props.points = {
          'a1': { id: 'a1', index: 'a1' as any, x: 0, y: 0 },
          'a2': { id: 'a2', index: 'a2' as any, x: 100, y: 0 }
        }
      } else {
        // Ensure the points object has at least 2 valid points
        const pointKeys = Object.keys(sanitized.props.points)
        if (pointKeys.length < 2) {
          sanitized.props.points = {
            'a1': { id: 'a1', index: 'a1' as any, x: 0, y: 0 },
            'a2': { id: 'a2', index: 'a2' as any, x: 100, y: 0 }
          }
        }
      }
    }

    // CRITICAL: Fix draw shapes - ensure valid segments structure (required by schema)
    // Draw shapes with empty segments cause "No nearest point found" errors
    if (sanitized.type === 'draw') {
      // Remove invalid w/h from props (they cause validation errors)
      if ('w' in sanitized.props) delete sanitized.props.w
      if ('h' in sanitized.props) delete sanitized.props.h

      // Draw shapes REQUIRE segments property with at least one segment containing points
      if (!sanitized.props.segments || !Array.isArray(sanitized.props.segments) || sanitized.props.segments.length === 0) {
        // Create a minimal valid segment with at least 2 points
        sanitized.props.segments = [{
          type: 'free',
          points: [
            { x: 0, y: 0, z: 0.5 },
            { x: 10, y: 0, z: 0.5 }
          ]
        }]
      } else {
        // Ensure each segment has valid points
        sanitized.props.segments = sanitized.props.segments.map((segment: any) => {
          if (!segment.points || !Array.isArray(segment.points) || segment.points.length < 2) {
            return {
              type: segment.type || 'free',
              points: [
                { x: 0, y: 0, z: 0.5 },
                { x: 10, y: 0, z: 0.5 }
              ]
            }
          }
          return segment
        })
      }

      // Ensure required draw shape properties exist
      if (typeof sanitized.props.isClosed !== 'boolean') sanitized.props.isClosed = false
      if (typeof sanitized.props.isComplete !== 'boolean') sanitized.props.isComplete = true
      if (typeof sanitized.props.isPen !== 'boolean') sanitized.props.isPen = false
    }
    
    // CRITICAL: Fix group shapes - remove invalid w/h from props
    if (sanitized.type === 'group') {
      if ('w' in sanitized.props) delete sanitized.props.w
      if ('h' in sanitized.props) delete sanitized.props.h
    }
    
    // CRITICAL: Fix note shapes - ensure richText structure if it exists
    if (sanitized.type === 'note') {
      if (sanitized.props.richText) {
        if (Array.isArray(sanitized.props.richText)) {
          sanitized.props.richText = { content: sanitized.props.richText, type: 'doc' }
        } else if (typeof sanitized.props.richText === 'object' && sanitized.props.richText !== null) {
          if (!sanitized.props.richText.type) sanitized.props.richText = { ...sanitized.props.richText, type: 'doc' }
          if (!sanitized.props.richText.content) sanitized.props.richText = { ...sanitized.props.richText, content: [] }
        }
      }
      // CRITICAL: Clean NaN values from richText content to prevent SVG export errors
      if (sanitized.props.richText) {
        sanitized.props.richText = cleanRichTextNaN(sanitized.props.richText)
      }
    }
    
    // CRITICAL: Convert props.text to props.richText for geo shapes (tldraw schema change)
    // tldraw no longer accepts props.text on geo shapes - must use richText
    // This migration handles shapes that were saved before the schema change
    if (sanitized.type === 'geo' && 'text' in sanitized.props && typeof sanitized.props.text === 'string') {
      const textContent = sanitized.props.text

      // Convert text string to richText format for tldraw
      sanitized.props.richText = {
        type: 'doc',
        content: textContent ? [{
          type: 'paragraph',
          content: [{
            type: 'text',
            text: textContent
          }]
        }] : []
      }

      // CRITICAL: Preserve original text in meta.text for backward compatibility
      // This is used by search (src/utils/searchUtils.ts) and other legacy code
      if (!sanitized.meta) sanitized.meta = {}
      sanitized.meta.text = textContent

      // Remove invalid props.text
      delete sanitized.props.text
    }

    // CRITICAL: Fix richText structure for geo shapes (preserve content)
    if (sanitized.type === 'geo' && sanitized.props.richText) {
      if (Array.isArray(sanitized.props.richText)) {
        sanitized.props.richText = { content: sanitized.props.richText, type: 'doc' }
      } else if (typeof sanitized.props.richText === 'object' && sanitized.props.richText !== null) {
        if (!sanitized.props.richText.type) sanitized.props.richText = { ...sanitized.props.richText, type: 'doc' }
        if (!sanitized.props.richText.content) sanitized.props.richText = { ...sanitized.props.richText, content: [] }
      }
      // CRITICAL: Clean NaN values from richText content to prevent SVG export errors
      sanitized.props.richText = cleanRichTextNaN(sanitized.props.richText)
    }
    
    // CRITICAL: Fix arrow shapes - ensure valid start/end structure (required by schema)
    // Arrows with invalid start/end cause "No nearest point found" errors
    if (sanitized.type === 'arrow') {
      // Ensure start property exists and has valid structure
      if (!sanitized.props.start || typeof sanitized.props.start !== 'object') {
        sanitized.props.start = { x: 0, y: 0 }
      } else {
        // Ensure start has x and y properties (could be bound to a shape or free)
        const start = sanitized.props.start as any
        if (start.type === 'binding') {
          // Binding type must have boundShapeId, normalizedAnchor, and other properties
          if (!start.boundShapeId) {
            // Invalid binding - convert to point
            sanitized.props.start = { x: start.x ?? 0, y: start.y ?? 0 }
          }
        } else if (start.type === 'point' || start.type === undefined) {
          // Point type must have x and y
          if (typeof start.x !== 'number' || typeof start.y !== 'number') {
            sanitized.props.start = { x: 0, y: 0 }
          }
        }
      }

      // Ensure end property exists and has valid structure
      if (!sanitized.props.end || typeof sanitized.props.end !== 'object') {
        sanitized.props.end = { x: 100, y: 0 }
      } else {
        // Ensure end has x and y properties (could be bound to a shape or free)
        const end = sanitized.props.end as any
        if (end.type === 'binding') {
          // Binding type must have boundShapeId
          if (!end.boundShapeId) {
            // Invalid binding - convert to point
            sanitized.props.end = { x: end.x ?? 100, y: end.y ?? 0 }
          }
        } else if (end.type === 'point' || end.type === undefined) {
          // Point type must have x and y
          if (typeof end.x !== 'number' || typeof end.y !== 'number') {
            sanitized.props.end = { x: 100, y: 0 }
          }
        }
      }

      // Ensure bend is a valid number
      if (typeof sanitized.props.bend !== 'number' || isNaN(sanitized.props.bend)) {
        sanitized.props.bend = 0
      }

      // Ensure arrowhead properties exist
      if (!sanitized.props.arrowheadStart) sanitized.props.arrowheadStart = 'none'
      if (!sanitized.props.arrowheadEnd) sanitized.props.arrowheadEnd = 'arrow'

      // Ensure text property exists and is a string
      if (sanitized.props.text === undefined || sanitized.props.text === null) {
        sanitized.props.text = ''
      } else if (typeof sanitized.props.text !== 'string') {
        // If text is not a string (e.g., RichText object), convert it to string
        try {
          if (typeof sanitized.props.text === 'object' && sanitized.props.text !== null) {
            // Try to extract text from RichText object
            const textObj = sanitized.props.text as any
            if (Array.isArray(textObj.content)) {
              // Extract text from RichText content
              const extractText = (content: any[]): string => {
                return content.map((item: any) => {
                  if (item.type === 'text' && item.text) {
                    return item.text
                  } else if (item.content && Array.isArray(item.content)) {
                    return extractText(item.content)
                  }
                  return ''
                }).join('')
              }
              sanitized.props.text = extractText(textObj.content)
            } else if (textObj.text && typeof textObj.text === 'string') {
              sanitized.props.text = textObj.text
            } else {
              sanitized.props.text = String(sanitized.props.text)
            }
          } else {
            sanitized.props.text = String(sanitized.props.text)
          }
        } catch (e) {
          console.warn(`⚠️ AutomergeToTLStore: Error converting arrow text to string for ${sanitized.id}:`, e)
          sanitized.props.text = String(sanitized.props.text)
        }
      }
      // Note: We preserve text even if it's an empty string - that's a valid value
    }
    
    // CRITICAL: Fix richText structure for text shapes - REQUIRED field
    if (sanitized.type === 'text') {
      // CRITICAL: Convert props.text to props.richText for text shapes (fixes sync issue)
      // Text shapes may arrive from other clients with props.text instead of props.richText
      // We must convert BEFORE initializing richText to empty, otherwise content is lost
      if ('text' in sanitized.props && typeof sanitized.props.text === 'string' && sanitized.props.text.trim()) {
        const textContent = sanitized.props.text
        // Only use text content if richText is missing or empty
        const hasRichTextContent = sanitized.props.richText &&
          typeof sanitized.props.richText === 'object' &&
          sanitized.props.richText.content &&
          Array.isArray(sanitized.props.richText.content) &&
          sanitized.props.richText.content.length > 0

        if (!hasRichTextContent) {
          // Convert text string to richText format for tldraw
          sanitized.props.richText = {
            type: 'doc',
            content: [{
              type: 'paragraph',
              content: [{
                type: 'text',
                text: textContent
              }]
            }]
          }
          // Converted props.text to richText for text shape
        }
        // Preserve original text in meta for backward compatibility
        if (!sanitized.meta) sanitized.meta = {}
        sanitized.meta.text = textContent
      }

      // Text shapes MUST have props.richText as an object - initialize if missing
      if (!sanitized.props.richText || typeof sanitized.props.richText !== 'object' || sanitized.props.richText === null) {
        sanitized.props.richText = { content: [], type: 'doc' }
      } else if (Array.isArray(sanitized.props.richText)) {
        sanitized.props.richText = { content: sanitized.props.richText, type: 'doc' }
      } else if (typeof sanitized.props.richText === 'object' && sanitized.props.richText !== null) {
        if (!sanitized.props.richText.type) sanitized.props.richText = { ...sanitized.props.richText, type: 'doc' }
        if (!sanitized.props.richText.content) sanitized.props.richText = { ...sanitized.props.richText, content: [] }
      }
      // CRITICAL: Clean NaN values from richText content to prevent SVG export errors
      sanitized.props.richText = cleanRichTextNaN(sanitized.props.richText)
      
      // CRITICAL: Ensure required text shape properties exist (TLDraw validation requires these)
      // color is REQUIRED and must be one of the valid color values
      const validColors = ['black', 'grey', 'light-violet', 'violet', 'blue', 'light-blue', 'yellow', 'orange', 'green', 'light-green', 'light-red', 'red', 'white']
      if (!sanitized.props.color || typeof sanitized.props.color !== 'string' || !validColors.includes(sanitized.props.color)) {
        sanitized.props.color = 'black'
      }
      // Ensure other required properties have defaults
      if (typeof sanitized.props.w !== 'number') sanitized.props.w = 300
      if (!sanitized.props.size || typeof sanitized.props.size !== 'string') sanitized.props.size = 'm'
      if (!sanitized.props.font || typeof sanitized.props.font !== 'string') sanitized.props.font = 'draw'
      if (!sanitized.props.textAlign || typeof sanitized.props.textAlign !== 'string') sanitized.props.textAlign = 'start'
      if (typeof sanitized.props.autoSize !== 'boolean') sanitized.props.autoSize = false
      if (typeof sanitized.props.scale !== 'number') sanitized.props.scale = 1
      
      // Remove invalid properties for text shapes (these cause validation errors)
      // Remove properties that are only valid for custom shapes, not standard TLDraw text shapes
      // CRITICAL: 'text' property is NOT allowed - text shapes must use props.richText instead
      const invalidTextProps = ['h', 'geo', 'text', 'isEditing', 'editingContent', 'isTranscribing', 'isPaused', 'fixedHeight', 'pinnedToView', 'isModified', 'originalContent', 'editingName', 'editingDescription', 'isConnected', 'holonId', 'noteId', 'title', 'content', 'tags', 'showPreview', 'backgroundColor', 'textColor']
      invalidTextProps.forEach(prop => {
        if (prop in sanitized.props) {
          delete sanitized.props[prop]
        }
      })
    }
    
    // CRITICAL: Additional safety check - Remove invalid 'text' property from text shapes
    // Text shapes should only use props.richText, not props.text
    // This is a redundant check to ensure text property is always removed
    if (sanitized.type === 'text' && 'text' in sanitized.props) {
      delete sanitized.props.text
    }
    
    // CRITICAL: Only convert unknown shapes with richText to text if they're truly unknown
    // DO NOT convert geo/note shapes - they can legitimately have richText
    if (sanitized.props?.richText && sanitized.type !== 'text' && sanitized.type !== 'geo' && sanitized.type !== 'note') {
      // This is an unknown shape type with richText - convert to text shape
      // But preserve all existing properties first
      const existingProps = { ...sanitized.props }
      sanitized.type = 'text'
      sanitized.props = existingProps
      
      // Fix richText structure if needed
      if (Array.isArray(sanitized.props.richText)) {
        sanitized.props.richText = { content: sanitized.props.richText, type: 'doc' }
      } else if (typeof sanitized.props.richText === 'object' && sanitized.props.richText !== null) {
        if (!sanitized.props.richText.type) sanitized.props.richText = { ...sanitized.props.richText, type: 'doc' }
        if (!sanitized.props.richText.content) sanitized.props.richText = { ...sanitized.props.richText, content: [] }
      }
      // CRITICAL: Clean NaN values from richText content to prevent SVG export errors
      sanitized.props.richText = cleanRichTextNaN(sanitized.props.richText)
      
      // CRITICAL: Ensure required text shape properties exist (TLDraw validation requires these)
      // color is REQUIRED and must be one of the valid color values
      const validColors = ['black', 'grey', 'light-violet', 'violet', 'blue', 'light-blue', 'yellow', 'orange', 'green', 'light-green', 'light-red', 'red', 'white']
      if (!sanitized.props.color || typeof sanitized.props.color !== 'string' || !validColors.includes(sanitized.props.color)) {
        sanitized.props.color = 'black'
      }
      // Ensure other required properties have defaults
      if (typeof sanitized.props.w !== 'number') sanitized.props.w = 300
      if (!sanitized.props.size || typeof sanitized.props.size !== 'string') sanitized.props.size = 'm'
      if (!sanitized.props.font || typeof sanitized.props.font !== 'string') sanitized.props.font = 'draw'
      if (!sanitized.props.textAlign || typeof sanitized.props.textAlign !== 'string') sanitized.props.textAlign = 'start'
      if (typeof sanitized.props.autoSize !== 'boolean') sanitized.props.autoSize = false
      if (typeof sanitized.props.scale !== 'number') sanitized.props.scale = 1
      
      // Remove invalid properties for text shapes (these cause validation errors)
      // Remove properties that are only valid for custom shapes, not standard TLDraw text shapes
      const invalidTextProps = ['h', 'geo', 'isEditing', 'editingContent', 'isTranscribing', 'isPaused', 'fixedHeight', 'pinnedToView', 'isModified', 'originalContent', 'editingName', 'editingDescription', 'isConnected', 'holonId', 'noteId', 'title', 'content', 'tags', 'showPreview', 'backgroundColor', 'textColor']
      invalidTextProps.forEach(prop => {
        if (prop in sanitized.props) {
          delete sanitized.props[prop]
        }
      })
    }
  } else if (sanitized.typeName === 'instance') {
    // CRITICAL: Handle instance records - ensure required fields exist
    if (!sanitized.meta || typeof sanitized.meta !== 'object') {
      sanitized.meta = {}
    } else {
      sanitized.meta = { ...sanitized.meta }
    }
    // Only fix critical instance fields that cause validation errors
    if ('brush' in sanitized && (sanitized.brush === null || sanitized.brush === undefined)) {
      (sanitized as any).brush = { x: 0, y: 0, w: 0, h: 0 }
    }
    if ('zoomBrush' in sanitized && (sanitized.zoomBrush === null || sanitized.zoomBrush === undefined)) {
      (sanitized as any).zoomBrush = { x: 0, y: 0, w: 0, h: 0 }
    }
    if ('insets' in sanitized && (sanitized.insets === undefined || !Array.isArray(sanitized.insets))) {
      (sanitized as any).insets = [false, false, false, false]
    }
    if ('scribbles' in sanitized && (sanitized.scribbles === undefined || !Array.isArray(sanitized.scribbles))) {
      (sanitized as any).scribbles = []
    }
    // CRITICAL: duplicateProps is REQUIRED for instance records - TLDraw validation will fail without it
    if (!('duplicateProps' in sanitized) || sanitized.duplicateProps === undefined || typeof sanitized.duplicateProps !== 'object') {
      (sanitized as any).duplicateProps = { 
        shapeIds: [],
        offset: { x: 0, y: 0 }
      }
    }
  } else if (sanitized.typeName === 'document') {
    // CRITICAL: Preserve all existing meta properties
    if (!sanitized.meta || typeof sanitized.meta !== 'object') {
      sanitized.meta = {}
    } else {
      sanitized.meta = { ...sanitized.meta }
    }
  } else if (sanitized.typeName === 'page') {
    // CRITICAL: Preserve all existing meta properties
    if (!sanitized.meta || typeof sanitized.meta !== 'object') {
      sanitized.meta = {}
    } else {
      sanitized.meta = { ...sanitized.meta }
    }
  }
  
  // CRITICAL: Final safety check - ensure text shapes never have invalid 'text' property
  // This is a last-resort check before returning to catch any edge cases
  if (sanitized.typeName === 'shape' && sanitized.type === 'text' && sanitized.props && 'text' in sanitized.props) {
    delete sanitized.props.text
  }
  
  return sanitized
}

const isStorePatch = (patch: Automerge.Patch): boolean => {
  return patch.path[0] === "store" && patch.path.length > 1
}

// Helper function to safely get a record from the store
const getRecordFromStore = (store: TLStore, id: string): TLRecord | null => {
  try {
    return store.get(id as any) as TLRecord | null
  } catch {
    return null
  }
}

// path: ["store", "camera:page:page", "x"] => "camera:page:page"
const pathToId = (path: Automerge.Prop[]): RecordId<any> => {
  return path[1] as RecordId<any>
}

const applyInsertToObject = (patch: Automerge.InsertPatch, object: any): TLRecord => {
  const { path, values } = patch
  let current = object
  const insertionPoint = path[path.length - 1] as number
  const pathEnd = path[path.length - 2] as string
  const parts = path.slice(2, -2)
  
  // Create missing properties as we navigate
  for (const part of parts) {
    if (current[part] === undefined || current[part] === null) {
      // Create missing property - use array for numeric indices
      if (typeof part === 'number' || (typeof part === 'string' && !isNaN(Number(part)))) {
        current[part] = []
      } else {
        current[part] = {}
      }
    }
    current = current[part]
  }
  
  // Ensure pathEnd exists and is an array
  if (current[pathEnd] === undefined || current[pathEnd] === null) {
    current[pathEnd] = []
  }
  
  // splice is a mutator... yay.
  const clone = Array.isArray(current[pathEnd]) ? current[pathEnd].slice(0) : []
  clone.splice(insertionPoint, 0, ...values)
  current[pathEnd] = clone
  return object
}

const applyPutToObject = (patch: Automerge.PutPatch, object: any): TLRecord => {
  const { path, value } = patch
  let current = object
  // special case
  if (path.length === 2) {
    // this would be creating the object, but we have done
    return object
  }

  const parts = path.slice(2, -2)
  const property = path[path.length - 1] as string
  const target = path[path.length - 2] as string

  if (path.length === 3) {
    return { ...object, [property]: value }
  }

  // default case - create missing properties as we navigate
  for (const part of parts) {
    if (current[part] === undefined || current[part] === null) {
      // Create missing property - use object for named properties, array for numeric indices
      if (typeof part === 'number' || (typeof part === 'string' && !isNaN(Number(part)))) {
        current[part] = []
      } else {
        current[part] = {}
      }
    }
    current = current[part]
  }
  
  // Ensure target exists
  if (current[target] === undefined || current[target] === null) {
    current[target] = {}
  }
  
  current[target] = { ...current[target], [property]: value }
  return object
}

const applySpliceToObject = (patch: Automerge.SpliceTextPatch, object: any): TLRecord => {
  const { path, value } = patch
  let current = object
  const insertionPoint = path[path.length - 1] as number
  const pathEnd = path[path.length - 2] as string
  const parts = path.slice(2, -2)
  
  // Create missing properties as we navigate
  for (const part of parts) {
    if (current[part] === undefined || current[part] === null) {
      // Create missing property - use array for numeric indices or when splicing
      if (typeof part === 'number' || (typeof part === 'string' && !isNaN(Number(part)))) {
        current[part] = []
      } else {
        current[part] = {}
      }
    }
    current = current[part]
  }
  
  // Ensure pathEnd exists and is an array for splicing
  if (current[pathEnd] === undefined || current[pathEnd] === null) {
    current[pathEnd] = []
  }
  
  // TODO: we're not supporting actual splices yet because TLDraw won't generate them natively
  if (insertionPoint !== 0) {
    throw new Error("Splices are not supported yet")
  }
  current[pathEnd] = value // .splice(insertionPoint, 0, value)
  return object
}

const applyIncToObject = (patch: Automerge.IncPatch, object: any): TLRecord => {
  const { path, value } = patch
  let current = object
  const parts = path.slice(2, -1)
  const pathEnd = path[path.length - 1] as string
  for (const part of parts) {
    if (current[part] === undefined) {
      throw new Error("NO WAY")
    }
    current = current[part]
  }
  current[pathEnd] = (current[pathEnd] || 0) + value
  return object
}
