import { useMemo, useEffect, useState, useCallback, useRef } from "react"
import { TLStoreSnapshot, InstancePresenceRecordType, getIndexAbove, IndexKey } from "@tldraw/tldraw"
import { CloudflareNetworkAdapter, ConnectionState } from "./CloudflareAdapter"
import { useAutomergeStoreV2, useAutomergePresence } from "./useAutomergeStoreV2"
import { TLStoreWithStatus } from "@tldraw/tldraw"
import { Repo, parseAutomergeUrl, stringifyAutomergeUrl, AutomergeUrl, DocumentId } from "@automerge/automerge-repo"
import { DocHandle } from "@automerge/automerge-repo"
import { IndexedDBStorageAdapter } from "@automerge/automerge-repo-storage-indexeddb"
import { getDocumentId, saveDocumentId } from "./documentIdMapping"

/**
 * Validate if an index is a valid tldraw fractional index
 * Valid indices: "a0", "a1", "a1V", "a24sT", "a1V4rr", "Zz", etc.
 * Invalid indices: "b1", "c2", or any simple letter+number that isn't a valid fractional index
 *
 * tldraw uses fractional indexing where indices are strings that can be compared lexicographically
 * The format allows inserting new items between any two existing items without renumbering.
 * Based on: https://observablehq.com/@dgreensp/implementing-fractional-indexing
 */
function isValidTldrawIndex(index: string): boolean {
  if (!index || typeof index !== 'string' || index.length === 0) return false

  // tldraw uses fractional indexing where:
  // - First character is a lowercase letter indicating integer part length (a=1, b=2, c=3, etc.)
  // - Followed by alphanumeric characters for the value and optional jitter
  // Examples: "a0", "a1", "b10", "b99", "c100", "a1V4rr", "b10Lz"
  //
  // Also uppercase letters for negative indices (Z=1, Y=2, etc.)

  // Valid fractional index: lowercase letter followed by alphanumeric characters
  if (/^[a-z][a-zA-Z0-9]+$/.test(index)) {
    return true
  }

  // Also allow uppercase prefix for negative/very high indices
  if (/^[A-Z][a-zA-Z0-9]+$/.test(index)) {
    return true
  }

  return false
}

/**
 * Migrate old data to fix invalid index values
 * tldraw requires indices to be in a specific format (fractional indexing)
 * Old data may have simple indices like "b1" which are invalid
 */
function migrateStoreData(store: Record<string, any>): Record<string, any> {
  if (!store) return store

  const migratedStore: Record<string, any> = {}
  let currentIndex: IndexKey = 'a1' as IndexKey // Start with a valid index

  // Sort shapes by their old index to maintain relative ordering
  const entries = Object.entries(store)
  const shapes = entries.filter(([_, record]) => record?.typeName === 'shape')
  const nonShapes = entries.filter(([_, record]) => record?.typeName !== 'shape')

  // Check if any shapes have invalid indices
  const hasInvalidIndices = shapes.some(([_, record]) => {
    const index = record?.index
    if (!index) return false
    return !isValidTldrawIndex(index)
  })

  if (!hasInvalidIndices) {
    // No migration needed
    return store
  }


  // Copy non-shape records as-is
  for (const [id, record] of nonShapes) {
    migratedStore[id] = record
  }

  // Sort shapes by their original index (alphabetically) to maintain order
  shapes.sort((a, b) => {
    const indexA = a[1]?.index || ''
    const indexB = b[1]?.index || ''
    return indexA.localeCompare(indexB)
  })

  // Regenerate valid indices for shapes
  for (const [id, record] of shapes) {
    const migratedRecord = { ...record }

    // Generate a new valid index
    try {
      currentIndex = getIndexAbove(currentIndex)
    } catch {
      // Fallback if getIndexAbove fails - generate simple sequential index
      const num = parseInt(currentIndex.slice(1) || '1') + 1
      currentIndex = `a${num}` as IndexKey
    }

    migratedRecord.index = currentIndex
    migratedStore[id] = migratedRecord
  }

  return migratedStore
}

interface AutomergeSyncConfig {
  uri: string
  assets?: any
  shapeUtils?: any[]
  bindingUtils?: any[]
  user?: {
    id: string
    name: string
  }
}

export function useAutomergeSync(config: AutomergeSyncConfig): TLStoreWithStatus & {
  handle: DocHandle<any> | null;
  presence: ReturnType<typeof useAutomergePresence>;
  connectionState: ConnectionState;
  isNetworkOnline: boolean;
  syncVersion: number;
} {
  const { uri, user } = config

  // Extract roomId from URI (e.g., "https://worker.com/connect/room123" -> "room123")
  const roomId = useMemo(() => {
    const match = uri.match(/\/connect\/([^\/]+)$/)
    return match ? match[1] : "default-room"
  }, [uri])

  // Extract worker URL from URI (remove /connect/roomId part)
  const workerUrl = useMemo(() => {
    return uri.replace(/\/connect\/.*$/, '')
  }, [uri])

  const [handle, setHandle] = useState<any>(null)
  const [isLoading, setIsLoading] = useState(true)
  const [connectionState, setConnectionState] = useState<ConnectionState>('connecting')
  const [isNetworkOnline, setIsNetworkOnline] = useState(typeof navigator !== 'undefined' ? navigator.onLine : true)
  // Sync version counter - increments when server data is merged, forces re-render
  const [syncVersion, setSyncVersion] = useState(0)
  const handleRef = useRef<any>(null)
  const storeRef = useRef<any>(null)
  const adapterRef = useRef<any>(null)
  
  // Update refs when handle/store changes
  useEffect(() => {
    handleRef.current = handle
  }, [handle])
  
  // JSON sync callback - receives changed records from other clients
  // Apply to Automerge document which will emit patches to update the store
  const applyJsonSyncData = useCallback((data: TLStoreSnapshot & { deleted?: string[] }) => {
    const currentHandle = handleRef.current
    if (!currentHandle || (!data?.store && !data?.deleted)) {
      // No handle or data available for JSON sync - expected during initialization
      return
    }

    const changedRecordCount = data.store ? Object.keys(data.store).length : 0
    const shapeRecords = data.store ? Object.values(data.store).filter((r: any) => r?.typeName === 'shape') : []
    const deletedRecordIds = data.deleted || []
    const deletedShapes = deletedRecordIds.filter(id => id.startsWith('shape:'))

    // Apply changes to the Automerge document
    // This will trigger patches which will update the TLDraw store
    // NOTE: We do NOT increment pendingLocalChanges here because these are REMOTE changes
    // that we WANT to be processed by automergeChangeHandler and applied to the store
    currentHandle.change((doc: any) => {
      if (!doc.store) {
        doc.store = {}
      }
      // Merge the changed records into the Automerge document
      if (data.store) {
        Object.entries(data.store).forEach(([id, record]) => {
          doc.store[id] = record
        })
      }
      // Delete records that were removed on the other client
      if (deletedRecordIds.length > 0) {
        deletedRecordIds.forEach(id => {
          if (doc.store[id]) {
            delete doc.store[id]
          }
        })
      }
    })

  }, [])

  // Presence update batching to prevent "Maximum update depth exceeded" errors
  // We batch presence updates and apply them in a single mergeRemoteChanges call
  const pendingPresenceUpdates = useRef<Map<string, any>>(new Map())
  const presenceUpdateTimer = useRef<NodeJS.Timeout | null>(null)
  const PRESENCE_BATCH_INTERVAL_MS = 16 // ~60fps, batch updates every frame

  // Flush pending presence updates to the store
  const flushPresenceUpdates = useCallback(() => {
    const currentStore = storeRef.current
    if (!currentStore || pendingPresenceUpdates.current.size === 0) {
      return
    }

    const updates = Array.from(pendingPresenceUpdates.current.values())
    pendingPresenceUpdates.current.clear()

    try {
      currentStore.mergeRemoteChanges(() => {
        currentStore.put(updates)
      })
    } catch (error) {
      console.error('❌ Error flushing presence updates:', error)
    }
  }, [])

  // Presence update callback - applies presence from other clients
  // Presence is ephemeral (cursors, selections) and goes directly to the store
  // Note: This callback is passed to the adapter but accesses storeRef which updates later
  const applyPresenceUpdate = useCallback((userId: string, presenceData: any, senderId?: string, userName?: string, userColor?: string) => {
    // CRITICAL: Don't apply our own presence back to ourselves (avoid echo)
    // Use senderId (sessionId) instead of userId since multiple users can have the same userId
    const currentAdapter = adapterRef.current
    const ourSessionId = currentAdapter?.sessionId

    if (senderId && ourSessionId && senderId === ourSessionId) {
      return
    }

    // Access the CURRENT store ref (not captured in closure)
    const currentStore = storeRef.current

    if (!currentStore) {
      return
    }

    try {
      // CRITICAL: Transform remote user's instance/pointer/page_state into a proper instance_presence record
      // TLDraw expects instance_presence records for remote users, not their local instance records

      // Extract data from the presence message
      const pointerRecord = presenceData['pointer:pointer']
      const pageStateRecord = presenceData['instance_page_state:page:page']
      const instanceRecord = presenceData['instance:instance']

      if (!pointerRecord) {
        return
      }

      // Create a proper instance_presence record for this remote user
      // Use senderId to create a unique presence ID for each session
      const presenceId = InstancePresenceRecordType.createId(senderId || userId)

      const instancePresence = InstancePresenceRecordType.create({
        id: presenceId,
        currentPageId: pageStateRecord?.pageId || 'page:page', // Default to main page
        userId: userId,
        userName: userName || userId, // Use provided userName or fall back to userId
        color: userColor || '#000000', // Use provided color or default to black
        cursor: {
          x: pointerRecord.x || 0,
          y: pointerRecord.y || 0,
          type: pointerRecord.type || 'default',
          rotation: pointerRecord.rotation || 0
        },
        chatMessage: '', // Empty by default
        lastActivityTimestamp: Date.now()
      })

      // Queue the presence update for batched application
      pendingPresenceUpdates.current.set(presenceId, instancePresence)

      // Schedule a flush if not already scheduled
      if (!presenceUpdateTimer.current) {
        presenceUpdateTimer.current = setTimeout(() => {
          presenceUpdateTimer.current = null
          flushPresenceUpdates()
        }, PRESENCE_BATCH_INTERVAL_MS)
      }

    } catch (error) {
      console.error('❌ Error applying presence:', error)
    }
  }, [flushPresenceUpdates])

  // Handle presence leave - remove the user's presence record from the store
  const handlePresenceLeave = useCallback((sessionId: string) => {
    const currentStore = storeRef.current
    if (!currentStore) return

    try {
      // Find and remove the presence record for this session
      // Presence IDs are formatted as "instance_presence:{sessionId}"
      const presenceId = `instance_presence:${sessionId}`

      // Check if this record exists before trying to remove it
      const allRecords = currentStore.allRecords()
      const presenceRecord = allRecords.find((r: any) =>
        r.id === presenceId ||
        r.id?.includes(sessionId)
      )

      if (presenceRecord) {
        currentStore.remove([presenceRecord.id])
      }
    } catch (error) {
      console.error('Error removing presence on leave:', error)
    }
  }, [])

  const { repo, adapter, storageAdapter } = useMemo(() => {
    const adapter = new CloudflareNetworkAdapter(
      workerUrl,
      roomId,
      applyJsonSyncData,
      applyPresenceUpdate,
      handlePresenceLeave
    )

    // Store adapter ref for use in callbacks
    adapterRef.current = adapter

    // Create IndexedDB storage adapter for offline persistence
    // This stores Automerge documents locally in the browser
    const storageAdapter = new IndexedDBStorageAdapter()

    const repo = new Repo({
      network: [adapter],
      storage: storageAdapter, // Add IndexedDB storage for offline support
      // Enable sharing of all documents with all peers
      sharePolicy: async () => true
    })

    // Log when sync messages are sent/received
    adapter.on('message', (_msg: any) => {
      // Message received from network
    })

    return { repo, adapter, storageAdapter }
  }, [workerUrl, roomId, applyJsonSyncData, applyPresenceUpdate, handlePresenceLeave])

  // Subscribe to connection state changes
  useEffect(() => {
    const unsubscribe = adapter.onConnectionStateChange((state) => {
      setConnectionState(state)
      setIsNetworkOnline(adapter.isNetworkOnline)
    })
    return unsubscribe
  }, [adapter])

  // Initialize Automerge document handle
  useEffect(() => {
    let mounted = true

    const initializeHandle = async () => {
      try {
        // OFFLINE-FIRST: Load from IndexedDB immediately, don't wait for network
        // Network sync happens in the background after local data is loaded

        let handle: DocHandle<TLStoreSnapshot>
        let loadedFromLocal = false

        // Check if we have a stored document ID mapping for this room
        // This allows us to load the same document from IndexedDB on subsequent visits
        const storedDocumentId = await getDocumentId(roomId)

        if (storedDocumentId) {
          try {
            // Parse the URL to get the DocumentId
            const parsed = parseAutomergeUrl(storedDocumentId as AutomergeUrl)
            const docId = parsed.documentId

            // Check if the document is already loaded in the repo's handles cache
            // This prevents "Cannot create a reference to an existing document object" error
            const existingHandle = repo.handles[docId] as DocHandle<TLStoreSnapshot> | undefined

            let foundHandle: DocHandle<TLStoreSnapshot>
            if (existingHandle) {
              foundHandle = existingHandle
            } else {
              // Try to find the existing document in the repo (loads from IndexedDB)
              // repo.find() returns a Promise<DocHandle>
              foundHandle = await repo.find<TLStoreSnapshot>(storedDocumentId as AutomergeUrl)
            }
            await foundHandle.whenReady()
            handle = foundHandle

            // Check if document has data
            const localDoc = handle.doc()
            const localRecordCount = localDoc?.store ? Object.keys(localDoc.store).length : 0
            const localShapeCount = localDoc?.store ? Object.values(localDoc.store).filter((r: any) => r?.typeName === 'shape').length : 0

            if (localRecordCount > 0) {

              // CRITICAL: Migrate local IndexedDB data to fix any invalid indices
              // This ensures shapes with old-format indices like "b1" are fixed
              if (localDoc?.store) {
                const migratedStore = migrateStoreData(localDoc.store)
                if (migratedStore !== localDoc.store) {
                  handle.change((doc: any) => {
                    doc.store = migratedStore
                  })
                }
              }

              loadedFromLocal = true
            } else {
            }
          } catch (error) {
            console.warn(`Failed to load document ${storedDocumentId} from IndexedDB:`, error)
            // Fall through to create a new document
          }
        }

        // If we didn't load from local storage, create a new document
        if (!loadedFromLocal || !handle!) {
          handle = repo.create<TLStoreSnapshot>()
          await handle.whenReady()

          // Save the mapping between roomId and the new document ID
          const documentId = handle.url
          if (documentId) {
            await saveDocumentId(roomId, documentId)
          }
        }

        if (!mounted) return

        // OFFLINE-FIRST: Set the handle and mark as ready BEFORE network sync
        // This allows the UI to render immediately with local data
        if (handle.url) {
          adapter.setDocumentId(handle.url)
        }

        // If we loaded from local, set handle immediately so UI can render
        if (loadedFromLocal) {
          const localDoc = handle.doc() as any
          const localShapeCount = localDoc?.store ? Object.values(localDoc.store).filter((r: any) => r?.typeName === 'shape').length : 0
          setHandle(handle)
          setIsLoading(false)
        }

        // Sync with server in the background (non-blocking for offline-first)
        // This runs in parallel - if it fails, we still have local data
        const syncWithServer = async () => {
          try {
            // Wait for network adapter with a timeout
            const networkReadyPromise = adapter.whenReady()
            const timeoutPromise = new Promise<'timeout'>((resolve) =>
              setTimeout(() => resolve('timeout'), 5000)
            )

            const result = await Promise.race([networkReadyPromise, timeoutPromise])

            if (result === 'timeout') {
              // If we haven't set the handle yet (no local data), set it now
              if (!loadedFromLocal && mounted) {
                setHandle(handle)
                setIsLoading(false)
              }
              return
            }

            if (!mounted) return

            const response = await fetch(`${workerUrl}/room/${roomId}`)
            if (response.ok) {
              let serverDoc = await response.json() as TLStoreSnapshot

              // Migrate server data to fix any invalid indices
              if (serverDoc.store) {
                serverDoc = {
                  ...serverDoc,
                  store: migrateStoreData(serverDoc.store)
                }
              }

              const serverShapeCount = serverDoc.store ? Object.values(serverDoc.store).filter((r: any) => r?.typeName === 'shape').length : 0
              const serverRecordCount = Object.keys(serverDoc.store || {}).length

              // Get current local state
              const localDoc = handle.doc()
              const localRecordCount = localDoc?.store ? Object.keys(localDoc.store).length : 0

              // Merge server data with local data
              // Strategy (IMPROVED):
              // 1. Server is the source of truth for initial page load
              // 2. Always update local with server data for shape records
              // 3. Keep local-only records (potential offline additions not yet synced)
              // 4. This ensures stale IndexedDB cache doesn't override server data
              if (serverDoc.store && serverRecordCount > 0) {
                // Track if we merged any data (needed outside the change callback)
                let totalMerged = 0

                handle.change((doc: any) => {
                  // Initialize store if it doesn't exist
                  if (!doc.store) {
                    doc.store = {}
                  }

                  // Count LOCAL SHAPES (not just records - ignore ephemeral camera/instance records)
                  const localShapeCount = Object.values(doc.store).filter((r: any) => r?.typeName === 'shape').length
                  const localIsEmpty = Object.keys(doc.store).length === 0

                  // IMPROVED: Server is source of truth on initial load
                  // Prefer server if:
                  // - Local is empty (first load or cleared cache)
                  // - Server has more shapes (local is likely stale/incomplete)
                  // - Local has shapes but server has different/more content
                  const serverHasMoreContent = serverShapeCount > localShapeCount
                  const shouldPreferServer = localIsEmpty || localShapeCount === 0 || serverHasMoreContent

                  let addedFromServer = 0
                  let updatedFromServer = 0
                  let keptLocal = 0

                  Object.entries(serverDoc.store).forEach(([id, record]) => {
                    const existsLocally = !!doc.store[id]

                    if (!existsLocally) {
                      // Record doesn't exist locally - add from server
                      doc.store[id] = record
                      addedFromServer++
                    } else if (shouldPreferServer) {
                      // Record exists locally but server has more content - update with server version
                      // This handles stale IndexedDB cache scenarios
                      doc.store[id] = record
                      updatedFromServer++
                    } else {
                      // Local has equal or more content - keep local version
                      // Local changes will sync to server via normal CRDT mechanism
                      keptLocal++
                    }
                  })

                  totalMerged = addedFromServer + updatedFromServer
                  console.log(`🔄 Server sync: added=${addedFromServer}, updated=${updatedFromServer}, keptLocal=${keptLocal}, serverShapes=${serverShapeCount}, localShapes=${localShapeCount}, preferServer=${shouldPreferServer}`)
                })

                const finalDoc = handle.doc()
                const finalRecordCount = finalDoc?.store ? Object.keys(finalDoc.store).length : 0

                // CRITICAL: Force React to re-render after merging server data
                // The handle object reference doesn't change, so we increment syncVersion
                if (totalMerged > 0 && mounted) {
                  console.log(`🔄 Forcing UI update after server sync (${totalMerged} records merged)`)
                  // Increment sync version to trigger React re-render
                  setSyncVersion(v => v + 1)
                }
              } else if (!loadedFromLocal) {
                // Server is empty and we didn't load from local - fresh start
              }
            } else if (response.status === 404) {
              // No document found on server
              if (loadedFromLocal) {
              } else {
              }
            } else {
              console.warn(`Failed to load document from server: ${response.status} ${response.statusText}`)
            }
          } catch (error) {
            // Network error - continue with local data if available
            if (loadedFromLocal) {
            } else {
              console.error("Error loading from server (offline?):", error)
            }
          }

          // Verify final document state
          const finalDoc = handle.doc() as any
          const finalStoreKeys = finalDoc?.store ? Object.keys(finalDoc.store).length : 0
          const finalShapeCount = finalDoc?.store ? Object.values(finalDoc.store).filter((r: any) => r?.typeName === 'shape').length : 0

          // If we haven't set the handle yet (no local data), set it now after server sync
          if (!loadedFromLocal && mounted) {
            setHandle(handle)
            setIsLoading(false)
          }
        }

        // Start server sync in background (don't await - non-blocking)
        syncWithServer()

      } catch (error) {
        console.error("Error initializing Automerge handle:", error)
        if (mounted) {
          setIsLoading(false)
        }
      }
    }

    initializeHandle()

    return () => {
      mounted = false
      // Clear any pending presence update timer
      if (presenceUpdateTimer.current) {
        clearTimeout(presenceUpdateTimer.current)
        presenceUpdateTimer.current = null
      }
      // Disconnect adapter on unmount to clean up WebSocket connection
      if (adapter) {
        adapter.disconnect?.()
      }
    }
  }, [repo, adapter, roomId, workerUrl])

  // BINARY CRDT SYNC: The Automerge Repo now handles sync automatically via the NetworkAdapter
  // The NetworkAdapter sends binary sync messages when documents change
  // Local persistence is handled by IndexedDB via the storage adapter
  // Server persistence is handled by the worker receiving binary sync messages
  //
  // We keep a lightweight change logger for debugging, but no HTTP POST sync
  useEffect(() => {
    if (!handle) return

    // Listen for changes to log sync activity (debugging only)
    const changeHandler = (payload: any) => {
      const patchCount = payload.patches?.length || 0

      if (!patchCount) return

      // Filter out ephemeral record changes for logging
      const ephemeralIdPatterns = [
        'instance:',
        'instance_page_state:',
        'instance_presence:',
        'camera:',
        'pointer:'
      ]

      const hasOnlyEphemeralChanges = payload.patches.every((p: any) => {
        const id = p.path?.[1]
        if (!id || typeof id !== 'string') return false
        return ephemeralIdPatterns.some(pattern => id.startsWith(pattern))
      })

      if (hasOnlyEphemeralChanges) {
        // Don't log ephemeral changes
        return
      }

    }

    handle.on('change', changeHandler)

    return () => {
      handle.off('change', changeHandler)
    }
  }, [handle])

  // Generate a unique color for each user based on their userId
  const generateUserColor = (userId: string): string => {
    // Use a simple hash of the userId to generate a consistent color
    let hash = 0
    for (let i = 0; i < userId.length; i++) {
      hash = userId.charCodeAt(i) + ((hash << 5) - hash)
    }

    // Generate a vibrant color using HSL (hue varies, saturation and lightness fixed for visibility)
    const hue = hash % 360
    return `hsl(${hue}, 70%, 50%)`
  }

  // Get user metadata for presence
  // Color is generated from the username (name) for consistency across sessions,
  // not from the unique session ID (userId) which changes per tab/session
  const userMetadata: { userId: string; name: string; color: string } = (() => {
    if (user && 'userId' in user) {
      const uid = (user as { userId: string; name: string; color?: string }).userId
      const name = (user as { userId: string; name: string; color?: string }).name
      return {
        userId: uid,
        name: name,
        // Use name for color (consistent across sessions), fall back to uid if no name
        color: (user as { userId: string; name: string; color?: string }).color || generateUserColor(name || uid)
      }
    }
    const uid = user?.id || 'anonymous'
    const name = user?.name || 'Anonymous'
    return {
      userId: uid,
      name: name,
      // Use name for color (consistent across sessions), fall back to uid if no name
      color: generateUserColor(name !== 'Anonymous' ? name : uid)
    }
  })()
  
  // Use useAutomergeStoreV2 to create a proper TLStore instance that syncs with Automerge
  const storeWithStatus = useAutomergeStoreV2({
    handle: handle || null as any,
    userId: userMetadata.userId,
    adapter: adapter, // Pass adapter for JSON sync broadcasting
    isNetworkOnline // Pass network state for offline support
  })
  
  // Update store ref when store is available
  useEffect(() => {
    if (storeWithStatus.store) {
      storeRef.current = storeWithStatus.store
    }
  }, [storeWithStatus.store])
  
  // Get presence data (only when handle is ready)
  const presence = useAutomergePresence({
    handle: handle || null,
    store: storeWithStatus.store || null,
    userMetadata,
    adapter: adapter // Pass adapter for presence broadcasting
  })

  return {
    ...storeWithStatus,
    handle,
    presence,
    connectionState,
    isNetworkOnline,
    syncVersion // Increments when server data is merged, forces re-render
  }
}
