import { useMemo, useState, useEffect, useRef, useCallback } from 'react'
import { Hooks } from 'applesauce-react'
import { Helpers, IEventStore } from 'applesauce-core'
import { getContentPointers } from 'applesauce-factory/helpers'
import { RelayPool } from 'applesauce-relay'
import { NostrEvent } from 'nostr-tools'
import { fetchProfiles, loadCachedProfiles } from '../services/profileService'
import { getNpubFallbackDisplay } from '../utils/nostrUriResolver'
import { extractProfileDisplayName } from '../utils/profileUtils'

const { getPubkeyFromDecodeResult, encodeDecodeResult } = Helpers

/**
 * Hook to resolve profile labels from content containing npub/nprofile identifiers
 * Returns an object with labels Map and loading Map that updates progressively as profiles load
 */
export function useProfileLabels(
  content: string, 
  relayPool?: RelayPool | null
): { labels: Map<string, string>; loading: Map<string, boolean> } {
  const eventStore = Hooks.useEventStore()
  
  // Extract profile pointers (npub and nprofile) using applesauce helpers
  const profileData = useMemo(() => {
      try {
        const pointers = getContentPointers(content)
        const filtered = pointers.filter(p => p.type === 'npub' || p.type === 'nprofile')
      const result: Array<{ pubkey: string; encoded: string }> = []
      filtered.forEach(pointer => {
        try {
          const pubkey = getPubkeyFromDecodeResult(pointer)
          const encoded = encodeDecodeResult(pointer)
          if (pubkey && encoded) {
            result.push({ pubkey, encoded: encoded as string })
          }
        } catch {
          // Ignore errors, continue processing other pointers
        }
      })
      return result
    } catch (error) {
      console.warn(`[profile-labels] Error extracting profile pointers:`, error)
      return []
    }
  }, [content])

  // Initialize labels synchronously from cache on first render to avoid delay
  // Use pubkey (hex) as the key instead of encoded string for canonical identification
  const initialLabels = useMemo(() => {
    if (profileData.length === 0) {
      return new Map<string, string>()
    }
    
    const allPubkeys = profileData.map(({ pubkey }) => pubkey)
    const cachedProfiles = loadCachedProfiles(allPubkeys)
    const labels = new Map<string, string>()
    
    profileData.forEach(({ pubkey }) => {
      const cachedProfile = cachedProfiles.get(pubkey)
      if (cachedProfile) {
        const displayName = extractProfileDisplayName(cachedProfile)
        if (displayName) {
          // Add @ prefix (extractProfileDisplayName returns name without @)
          const label = `@${displayName}`
          labels.set(pubkey, label)
        } else {
          // Use fallback npub display if profile has no name (add @ prefix)
          const fallback = getNpubFallbackDisplay(pubkey)
          labels.set(pubkey, `@${fallback}`)
        }
      }
    })
    
    return labels
  }, [profileData])

  const [profileLabels, setProfileLabels] = useState<Map<string, string>>(initialLabels)
  const [profileLoading, setProfileLoading] = useState<Map<string, boolean>>(new Map())
  
  // Batching strategy: Collect profile updates and apply them in batches via RAF to prevent UI flicker
  // when many profiles resolve simultaneously. We use refs to avoid stale closures in async callbacks.
  // Use pubkey (hex) as the key for canonical identification
  const pendingUpdatesRef = useRef<Map<string, string>>(new Map())
  const rafScheduledRef = useRef<number | null>(null)
  
  /**
   * Helper to apply pending batched updates to state
   * Cancels any scheduled RAF and applies updates synchronously
   */
  const applyPendingUpdates = () => {
    const pendingUpdates = pendingUpdatesRef.current
    if (pendingUpdates.size === 0) {
      return
    }
    
    // Cancel scheduled RAF since we're applying synchronously
    if (rafScheduledRef.current !== null) {
      cancelAnimationFrame(rafScheduledRef.current)
      rafScheduledRef.current = null
    }
    
    // Apply all pending updates in one batch
    setProfileLabels(prevLabels => {
      const updatedLabels = new Map(prevLabels)
      for (const [pubkey, label] of pendingUpdates.entries()) {
        updatedLabels.set(pubkey, label)
      }
      pendingUpdates.clear()
      return updatedLabels
    })
  }
  
  /**
   * Helper to schedule a batched update via RAF (if not already scheduled)
   * This prevents multiple RAF calls when many profiles resolve at once
   * Wrapped in useCallback for stable reference in dependency arrays
   */
  const scheduleBatchedUpdate = useCallback(() => {
    if (rafScheduledRef.current === null) {
      rafScheduledRef.current = requestAnimationFrame(() => {
        applyPendingUpdates()
        rafScheduledRef.current = null
      })
    }
  }, []) // Empty deps: only uses refs which are stable
  
    // Sync state when initialLabels changes (e.g., when content changes)
  // This ensures we start with the correct cached labels even if profiles haven't loaded yet
  useEffect(() => {
    // Use a functional update to access current state without including it in dependencies
    setProfileLabels(prevLabels => {
      const currentPubkeys = new Set(Array.from(prevLabels.keys()))
      const newPubkeys = new Set(profileData.map(p => p.pubkey))
      
      // If the content changed significantly (different set of profiles), reset state
      const hasDifferentProfiles = 
        currentPubkeys.size !== newPubkeys.size ||
        !Array.from(newPubkeys).every(pk => currentPubkeys.has(pk))
      
      if (hasDifferentProfiles) {
        // Clear pending updates and cancel RAF for old profiles
        pendingUpdatesRef.current.clear()
        if (rafScheduledRef.current !== null) {
          cancelAnimationFrame(rafScheduledRef.current)
          rafScheduledRef.current = null
        }
        // Reset to initial labels
        return new Map(initialLabels)
      } else {
        // Same profiles, merge initial labels with existing state
        // IMPORTANT: Preserve existing labels (from pending updates) and only add initial labels if missing
        const merged = new Map(prevLabels)
        for (const [pubkey, label] of initialLabels.entries()) {
          // Only add initial label if we don't already have a label for this pubkey
          // This preserves labels that were added via applyPendingUpdates
          if (!merged.has(pubkey)) {
            merged.set(pubkey, label)
          }
        }
        return merged
      }
    })
    
    // Reset loading state when content changes significantly
    setProfileLoading(prevLoading => {
      const currentPubkeys = new Set(Array.from(prevLoading.keys()))
      const newPubkeys = new Set(profileData.map(p => p.pubkey))
      
      const hasDifferentProfiles = 
        currentPubkeys.size !== newPubkeys.size ||
        !Array.from(newPubkeys).every(pk => currentPubkeys.has(pk))
      
      if (hasDifferentProfiles) {
        return new Map()
      }
      return prevLoading
    })
  }, [initialLabels, profileData])

  // Build initial labels: localStorage cache -> eventStore -> fetch from relays
  useEffect(() => {
    // Extract all pubkeys
    const allPubkeys = profileData.map(({ pubkey }) => pubkey)
    
    if (allPubkeys.length === 0) {
      setProfileLabels(new Map())
      setProfileLoading(new Map())
      // Clear pending updates and cancel RAF when clearing labels
      pendingUpdatesRef.current.clear()
      if (rafScheduledRef.current !== null) {
        cancelAnimationFrame(rafScheduledRef.current)
        rafScheduledRef.current = null
      }
      return
    }
    
    // Add cached profiles to EventStore for consistency
    const cachedProfiles = loadCachedProfiles(allPubkeys)
    if (eventStore) {
      for (const profile of cachedProfiles.values()) {
        eventStore.add(profile)
      }
    }
    
    // Build labels from localStorage cache and eventStore
    // initialLabels already has all cached profiles, so we only need to check eventStore
    // Use pubkey (hex) as the key for canonical identification
    const labels = new Map<string, string>(initialLabels)
    const loading = new Map<string, boolean>()
    
    const pubkeysToFetch: string[] = []
    
    profileData.forEach(({ pubkey }) => {
      // Skip if already resolved from initial cache
      if (labels.has(pubkey)) {
        loading.set(pubkey, false)
        return
      }
      
      // Check EventStore for profiles that weren't in cache
      const eventStoreProfile = eventStore?.getEvent(pubkey + ':0')
      
      if (eventStoreProfile && eventStore) {
        // Extract display name using centralized utility
        const displayName = extractProfileDisplayName(eventStoreProfile as NostrEvent)
        if (displayName) {
          // Add @ prefix (extractProfileDisplayName returns name without @)
          const label = `@${displayName}`
          labels.set(pubkey, label)
        } else {
          // Use fallback npub display if profile has no name (add @ prefix)
          const fallback = getNpubFallbackDisplay(pubkey)
          labels.set(pubkey, `@${fallback}`)
        }
        loading.set(pubkey, false)
      } else {
      // No profile found yet, will use fallback after fetch or keep empty
      // We'll set fallback labels for missing profiles at the end
      // Mark as loading since we'll fetch it
      pubkeysToFetch.push(pubkey)
      loading.set(pubkey, true)
      }
    })
    
    // Don't set fallback labels in the Map - we'll use fallback directly when rendering
    // This allows us to distinguish between "no label yet" (use fallback) vs "resolved label" (use Map value)
    
    setProfileLabels(new Map(labels))
    setProfileLoading(new Map(loading))
    
    // Fetch missing profiles asynchronously with reactive updates
    if (pubkeysToFetch.length > 0 && relayPool && eventStore) {
      
      // Reactive callback: collects profile updates and batches them via RAF to prevent flicker
      // Strategy: Apply label immediately when profile resolves, but still batch for multiple profiles
      const handleProfileEvent = (event: NostrEvent) => {
        // Use pubkey directly as the key
        const pubkey = event.pubkey
        
        // Determine the label for this profile using centralized utility
        // Add @ prefix (both extractProfileDisplayName and getNpubFallbackDisplay return names without @)
        const displayName = extractProfileDisplayName(event)
        const label = displayName ? `@${displayName}` : `@${getNpubFallbackDisplay(pubkey)}`
        
        // Apply label immediately to prevent race condition with loading state
        // This ensures labels are available when isLoading becomes false
        setProfileLabels(prevLabels => {
          const updated = new Map(prevLabels)
          updated.set(pubkey, label)
          return updated
        })
        
        // Clear loading state for this profile when it resolves
        setProfileLoading(prevLoading => {
          const updated = new Map(prevLoading)
          updated.set(pubkey, false)
          return updated
        })
      }
      
      fetchProfiles(relayPool, eventStore as unknown as IEventStore, pubkeysToFetch, undefined, handleProfileEvent)
        .then(() => {
          // After EOSE: apply any remaining pending updates immediately
          // This ensures all profile updates are applied even if RAF hasn't fired yet
          applyPendingUpdates()
          
          // Clear loading state for all fetched profiles
          setProfileLoading(prevLoading => {
            const updated = new Map(prevLoading)
            pubkeysToFetch.forEach(pubkey => {
              updated.set(pubkey, false)
            })
            return updated
          })
        })
        .catch((error) => {
          console.error(`[profile-labels] Error fetching profiles:`, error)
          // Silently handle fetch errors, but still clear any pending updates
          pendingUpdatesRef.current.clear()
          if (rafScheduledRef.current !== null) {
            cancelAnimationFrame(rafScheduledRef.current)
            rafScheduledRef.current = null
          }
          
          // Clear loading state on error (show fallback)
          setProfileLoading(prevLoading => {
            const updated = new Map(prevLoading)
            pubkeysToFetch.forEach(pubkey => {
              updated.set(pubkey, false)
            })
            return updated
          })
        })
      
      // Cleanup: apply any pending updates before unmount to avoid losing them
      return () => {
        applyPendingUpdates()
      }
    }
  }, [profileData, eventStore, relayPool, initialLabels, scheduleBatchedUpdate])
  
  return { labels: profileLabels, loading: profileLoading }
}

