import { PROFILE_FETCH_PROMISE_TIMEOUT_MS } from '@/constants'
import { getProfileFromEvent } from '@/lib/event-metadata'
import { userIdToPubkey } from '@/lib/pubkey'
import { useNostr } from '@/providers/NostrProvider'
import { useNoteFeedProfileContext } from '@/providers/NoteFeedProfileContext'
import { replaceableEventService } from '@/services/client.service'
import { TProfile } from '@/types'
import { useEffect, useState, useRef, useCallback } from 'react'
import logger from '@/lib/logger'

// CRITICAL: Global deduplication - shared across ALL hook instances
// This prevents multiple components from fetching the same profile simultaneously
const globalFetchPromises = new Map<string, Promise<TProfile | null>>()
const globalFetchingPubkeys = new Set<string>()
// Cooldown period after timeout to prevent cascade of duplicate fetches
const globalFetchCooldowns = new Map<string, number>() // pubkey -> timestamp when cooldown expires

export function useFetchProfile(id?: string, skipCache = false) {
  // CRITICAL: Reduce logging to prevent performance issues during infinite loops
  // Only log if we're actually going to process (not just checking)
  // logger.info('[useFetchProfile] Hook called', { 
  //   id: id || 'undefined',
  //   skipCache,
  //   stack: new Error().stack?.split('\n').slice(1, 4).join('\n')
  // })
  
  const { profile: currentAccountProfile } = useNostr()
  const noteFeed = useNoteFeedProfileContext()
  /** Hex/npub ids can show npub fallback immediately; avoid a skeleton frame before the first effect. */
  const [isFetching, setIsFetching] = useState(() => {
    if (!id) return false
    const pk = userIdToPubkey(id)
    return !(pk.length === 64 && /^[0-9a-f]{64}$/.test(pk))
  })
  const [error, setError] = useState<Error | null>(null)
  const [profile, setProfile] = useState<TProfile | null>(null)
  const [pubkey, setPubkey] = useState<string | null>(null)
  const checkIntervalRef = useRef<NodeJS.Timeout | null>(null)
  const processingPubkeyRef = useRef<string | null>(null) // Track which pubkey we're currently processing (prevents duplicate fetches)
  const effectRunCountRef = useRef<Map<string, number>>(new Map()) // Track how many times effect has run for each pubkey (safety guard against infinite loops)
  const initializedPubkeysRef = useRef<Set<string>>(new Set()) // Track pubkeys we've successfully initialized (have profile or failed)

  // Function to check for profile updates with GLOBAL deduplication
  // fetchProfileEvent already checks: 1) IndexedDB, 2) network (with author's relays)
  // Memoize to prevent recreation on every render
  const checkProfile = useCallback(async (pubkey: string, cancelled: { current: boolean }): Promise<TProfile | null> => {
    // CRITICAL: Reduce logging during rapid scrolling to prevent performance issues
    // Only log at debug level during normal operations
    logger.debug('[useFetchProfile] checkProfile called', {
      pubkey: pubkey.substring(0, 8),
      cancelled: cancelled.current,
      skipCache
    })
    
    if (cancelled.current) {
      logger.debug('[useFetchProfile] Already cancelled, returning null')
      return null
    }
    
    // CRITICAL: Check cooldown period first to prevent cascade of duplicate fetches after timeout
    const cooldownExpiry = globalFetchCooldowns.get(pubkey)
    if (cooldownExpiry && Date.now() < cooldownExpiry) {
      logger.debug('[useFetchProfile] In cooldown period after timeout, skipping fetch', {
        pubkey: pubkey.substring(0, 8),
        remainingMs: cooldownExpiry - Date.now()
      })
      return null
    }
    // Clean up expired cooldowns
    if (cooldownExpiry && Date.now() >= cooldownExpiry) {
      globalFetchCooldowns.delete(pubkey)
    }
    
    // CRITICAL: Check if another hook instance is already fetching this pubkey
    // If so, wait for that fetch to complete instead of starting a new one
    // Add timeout protection to prevent infinite waits
    const existingPromise = globalFetchPromises.get(pubkey)
    if (existingPromise) {
      logger.debug('[useFetchProfile] Reusing existing fetch promise', {
        pubkey: pubkey.substring(0, 8)
      })
      try {
        // Await the shared promise only — it already races fetchProfileEvent with
        // PROFILE_FETCH_PROMISE_TIMEOUT_MS. Per-waiter Promise.race timers caused N identical
        // "timeout" warnings (one per mounted component) and premature map deletion.
        const existingProfile = await existingPromise
        if (cancelled.current) return null

        if (existingProfile) {
          // Update state for this instance
          setProfile(existingProfile)
          setIsFetching(false)
          initializedPubkeysRef.current.add(pubkey)
          if (checkIntervalRef.current) {
            clearInterval(checkIntervalRef.current)
            checkIntervalRef.current = null
          }
          effectRunCountRef.current.delete(pubkey)
          return existingProfile
        } else {
          setIsFetching(false)
          return null
        }
      } catch (err) {
        // If the existing promise failed, we'll try again below
        logger.debug('[useFetchProfile] Existing promise failed, will retry', {
          pubkey: pubkey.substring(0, 8),
          error: err instanceof Error ? err.message : String(err)
        })
        // Clear the failed promise so we can start fresh
        globalFetchPromises.delete(pubkey)
        globalFetchingPubkeys.delete(pubkey)
      }
    }
    
    // Mark as fetching globally to prevent other instances from starting
    if (globalFetchingPubkeys.has(pubkey)) {
      // Another instance is fetching, wait a bit and check again
      await new Promise(resolve => setTimeout(resolve, 50))
      const retryPromise = globalFetchPromises.get(pubkey)
      if (retryPromise) {
        try {
          const retryProfile = await retryPromise
          if (cancelled.current) return null

          if (retryProfile) {
            // Update state for this instance
            setProfile(retryProfile)
            setIsFetching(false)
            initializedPubkeysRef.current.add(pubkey)
            if (checkIntervalRef.current) {
              clearInterval(checkIntervalRef.current)
              checkIntervalRef.current = null
            }
            effectRunCountRef.current.delete(pubkey)
            return retryProfile
          } else {
            setIsFetching(false)
            return null
          }
        } catch (err) {
          logger.debug('[useFetchProfile] Retry promise failed', {
            pubkey: pubkey.substring(0, 8),
            error: err instanceof Error ? err.message : String(err)
          })
          // Clear the failed promise
          globalFetchPromises.delete(pubkey)
          globalFetchingPubkeys.delete(pubkey)
          // Fall through to start our own fetch
        }
      }
    }
    
    // Create a new fetch promise with timeout protection
    const fetchPromise = (async (): Promise<TProfile | null> => {
      try {
        globalFetchingPubkeys.add(pubkey)
        const startTime = Date.now()
        
        // CRITICAL: Add timeout to prevent infinite hangs (must exceed batched metadata query globalTimeout)
        const timeoutPromise = new Promise<never>((_, reject) => {
          setTimeout(() => {
            reject(
              new Error(
                `Profile fetch timeout after ${PROFILE_FETCH_PROMISE_TIMEOUT_MS}ms for pubkey ${pubkey.substring(0, 8)}`
              )
            )
          }, PROFILE_FETCH_PROMISE_TIMEOUT_MS)
        })
        
        // Use fetchProfileEvent which includes author's relay list for better profile discovery
        const profileEvent = await Promise.race([
          replaceableEventService.fetchProfileEvent(pubkey, skipCache),
          timeoutPromise
        ])
        const fetchTime = Date.now() - startTime
        
        if (profileEvent || fetchTime > 1000) {
          logger.debug('[useFetchProfile] fetchProfileEvent completed', {
            pubkey: pubkey.substring(0, 8),
            hasEvent: !!profileEvent,
            eventId: profileEvent?.id?.substring(0, 8),
            fetchTime: `${fetchTime}ms`
          })
        }

        if (profileEvent) {
          // getProfileFromEvent always returns a profile object (with fallback username)
          const newProfile = getProfileFromEvent(profileEvent)
          // Only log at debug level to reduce noise during rapid scrolling
          logger.debug('[useFetchProfile] Profile found', {
            pubkey: pubkey.substring(0, 8),
            username: newProfile.username,
            hasAvatar: !!newProfile.avatar,
            fetchTime: `${fetchTime}ms`,
            unmounted: cancelled.current
          })
          // CRITICAL: Always return the profile from this shared promise, even when the
          // originating hook cleaned up (list virtualization, Strict Mode, feed switch).
          // Returning null here made every waiter treat the result like a timeout, applied
          // cooldowns, and left avatars empty (especially busy feeds e.g. all-favorites).
          return newProfile
        }
        // Only log warnings for missing profiles if skipCache is true (user explicitly requested)
        if (skipCache) {
          logger.debug('[useFetchProfile] No profile event found', {
            pubkey: pubkey.substring(0, 8),
            fetchTime: `${fetchTime}ms`
          })
        }
        return null
      } catch (err) {
        const isTimeout = err instanceof Error && err.message.includes('timeout')
        if (isTimeout) {
          logger.warn('[useFetchProfile] Profile fetch timed out', {
            pubkey: pubkey.substring(0, 8),
            error: err.message
          })
          // Set cooldown period after timeout to prevent cascade of duplicate fetches
          globalFetchCooldowns.set(pubkey, Date.now() + 10000) // 10 second cooldown
          // Return null on timeout instead of throwing - allows UI to show fallback
          return null
        }
        logger.error('[useFetchProfile] Profile fetch error', {
          pubkey: pubkey.substring(0, 8),
          error: err instanceof Error ? err.message : String(err),
          stack: err instanceof Error ? err.stack : undefined,
          cancelled: cancelled.current
        })
        // For non-timeout errors, still throw to allow retry logic
        throw err
      } finally {
        // Clean up global tracking
        globalFetchingPubkeys.delete(pubkey)
        // Keep promise in cache for a short time to allow other instances to reuse it
        // But remove it immediately on timeout/error to allow retries
        setTimeout(() => {
          globalFetchPromises.delete(pubkey)
        }, 1000) // 1 second cache retention
      }
    })()
    
    // Store the promise globally so other instances can reuse it
    globalFetchPromises.set(pubkey, fetchPromise)
    
    try {
      const profile = await fetchPromise
      if (cancelled.current) return null
      
      if (profile) {
        setProfile(profile)
        setIsFetching(false)
        // Mark as initialized
        initializedPubkeysRef.current.add(pubkey)
        // Keep processingPubkeyRef set so we don't re-fetch
        // Clear interval once we have a profile
        if (checkIntervalRef.current) {
          clearInterval(checkIntervalRef.current)
          checkIntervalRef.current = null
        }
        // Clear run count when profile is found
        effectRunCountRef.current.delete(pubkey)
        return profile
      }
      
      if (!cancelled.current) {
        setIsFetching(false)
      }
      return null
    } catch (err) {
      if (!cancelled.current) {
        setError(err as Error)
        setIsFetching(false)
      }
      return null
    }
  }, [skipCache])

  useEffect(() => {
    // Early exit when id is missing (e.g. truncated or undefined) - use debug to avoid console spam
    if (!id) {
      logger.debug('[useFetchProfile] No id provided')
      setProfile(null)
      setPubkey(null)
      setIsFetching(false)
      setError(new Error('No id provided'))
      processingPubkeyRef.current = null
      return
    }

    // Extract pubkey early to check if id has changed
    const extractedPubkey = userIdToPubkey(id)

    // Note feeds: profiles are batch-fetched in NoteList — skip per-row relay storms while pending
    if (extractedPubkey && noteFeed && !skipCache) {
      const fromBatch = noteFeed.profiles.get(extractedPubkey)
      if (fromBatch) {
        setProfile(fromBatch)
        setPubkey(extractedPubkey)
        setIsFetching(false)
        setError(null)
        processingPubkeyRef.current = extractedPubkey
        initializedPubkeysRef.current.add(extractedPubkey)
        effectRunCountRef.current.delete(extractedPubkey)
        return
      }
      if (noteFeed.pendingPubkeys.has(extractedPubkey)) {
        setPubkey(extractedPubkey)
        setIsFetching(false)
        setError(null)
        return
      }
    }
    
    // CRITICAL: Early exit if already processing this exact pubkey - prevents infinite loops
    // This check must happen FIRST, before any other logic
    // Set processingPubkeyRef IMMEDIATELY after extraction to prevent race conditions
    if (extractedPubkey) {
      if (processingPubkeyRef.current === extractedPubkey) {
        // Silently exit - no logging to reduce noise
        return
      }
      // Mark that we're processing this pubkey IMMEDIATELY to prevent concurrent runs
      // We'll clear it later if we early exit for other reasons
      processingPubkeyRef.current = extractedPubkey
    }
    
    // CRITICAL: Early exit if we already have a profile for this pubkey
    // This prevents re-fetching when we already have the profile
    if (extractedPubkey && profile && profile.pubkey === extractedPubkey) {
      // Ensure processingPubkeyRef is set to prevent re-fetch
      if (processingPubkeyRef.current !== extractedPubkey) {
        processingPubkeyRef.current = extractedPubkey
      }
      // Mark as initialized
      initializedPubkeysRef.current.add(extractedPubkey)
      // Ensure fetching is false (but don't call setState if already false to avoid re-renders)
      if (isFetching) {
        setIsFetching(false)
      }
      // Clear run count since we have the profile
      effectRunCountRef.current.delete(extractedPubkey)
      return
    }
    
    // CRITICAL: Early exit if we've already initialized this pubkey (even if profile is null)
    // This prevents re-fetching when we've already tried and failed
    // BUT: Allow retry if skipCache is true (user explicitly wants to refresh)
    if (extractedPubkey && initializedPubkeysRef.current.has(extractedPubkey) && !profile) {
      if (skipCache) {
        // User wants to refresh - clear initialized flag to allow fresh fetch
        initializedPubkeysRef.current.delete(extractedPubkey)
        // Also clear run count to allow fresh attempt
        effectRunCountRef.current.delete(extractedPubkey)
      } else {
        // Already tried and failed - don't retry unless explicitly requested
        // Ensure fetching is false
        if (isFetching) {
          setIsFetching(false)
        }
        return
      }
    }
    
    // CRITICAL: Guard against infinite loops - limit effect runs per pubkey (reduced from 10 to 3)
    // Only increment if we're actually going to process (not early exiting)
    if (extractedPubkey) {
      const runCount = effectRunCountRef.current.get(extractedPubkey) || 0
      if (runCount >= 3) {
        logger.warn('[useFetchProfile] Too many effect runs for this pubkey, preventing infinite loop', {
          extractedPubkey,
          runCount
        })
        // Clear the run count after a delay to allow retries later
        setTimeout(() => {
          effectRunCountRef.current.delete(extractedPubkey)
        }, 30000) // Clear after 30 seconds
        return
      }
      // Only increment if we're actually going to process
      effectRunCountRef.current.set(extractedPubkey, runCount + 1)
    }
    
    // If id has changed (extractedPubkey is different from processingPubkeyRef), clear the refs
    // This allows a new fetch to start for a different pubkey
    if (extractedPubkey && processingPubkeyRef.current && processingPubkeyRef.current !== extractedPubkey) {
      const oldPubkey = processingPubkeyRef.current
      // Clear run count and initialized status for old pubkey before clearing ref
      effectRunCountRef.current.delete(oldPubkey)
      initializedPubkeysRef.current.delete(oldPubkey)
      processingPubkeyRef.current = null
    }
    
    const cancelled = { current: false }
    // CRITICAL: Reduce logging during rapid scrolling - only log at debug level
    logger.debug('[useFetchProfile] Extracting pubkey', {
      idLength: id.length,
      idStartsWithNpub: id.startsWith('npub1'),
      idStartsWithNprofile: id.startsWith('nprofile1')
    })
    
    if (!extractedPubkey) {
      logger.error('[useFetchProfile] Invalid id - could not extract pubkey', {
        id,
        idLength: id.length
      })
      setProfile(null)
      setPubkey(null)
      setIsFetching(false)
      setError(new Error('Invalid id: could not extract pubkey'))
      processingPubkeyRef.current = null
      return
    }
    
    // Validate pubkey format
    if (extractedPubkey.length !== 64 || !/^[0-9a-f]{64}$/.test(extractedPubkey)) {
      logger.error('[useFetchProfile] Invalid pubkey format', {
        id,
        extractedPubkey,
        pubkeyLength: extractedPubkey.length,
        expectedLength: 64
      })
      setProfile(null)
      setPubkey(null)
      setIsFetching(false)
      setError(new Error(`Invalid pubkey format: expected 64 hex chars, got ${extractedPubkey.length}`))
      processingPubkeyRef.current = null
      return
    }
    
    // These checks are now done earlier in the effect (before incrementing run count)
    // Keeping this as a safety check, but it should rarely be hit now that we set processingPubkeyRef earlier
    if (processingPubkeyRef.current !== extractedPubkey) {
      // This should never happen now, but keep as safety check
      logger.warn('[useFetchProfile] processingPubkeyRef mismatch (safety check)', {
        extractedPubkey,
        processingPubkey: processingPubkeyRef.current
      })
      processingPubkeyRef.current = extractedPubkey
    }
    
    if (profile && profile.pubkey === extractedPubkey) {
      logger.debug('[useFetchProfile] Already have profile for this pubkey (safety check)', {
        extractedPubkey
      })
      setIsFetching(false)
      effectRunCountRef.current.delete(extractedPubkey)
      return
    }
    
    // processingPubkeyRef is already set earlier (right after extraction)
    // No need to set it again here
    
    // CRITICAL: Only update pubkey state if it's actually different
    // Avoid state updates that could trigger re-renders and loops
    if (pubkey !== extractedPubkey) {
      setPubkey(extractedPubkey)
    }
    // CRITICAL: Reduce logging during rapid scrolling
    logger.debug('[useFetchProfile] Starting profile fetch', {
      pubkey: extractedPubkey?.substring(0, 8) || 'null'
    })

    const run = async () => {
      try {
        setIsFetching(true)
        setError(null)
        
        // Initial fetch - fetchReplaceableEvent checks: 1) in-memory, 2) IndexedDB, 3) network
        // checkProfile now returns the profile directly (or null) and handles global deduplication
        const profile = await checkProfile(extractedPubkey, cancelled)
        
        // Only log if profile was found or if cancelled (important events)
        if (profile || cancelled.current) {
          logger.debug('[useFetchProfile] checkProfile completed', {
            pubkey: extractedPubkey?.substring(0, 8),
            found: !!profile,
            cancelled: cancelled.current
          })
        }
        
        if (cancelled.current) {
          logger.debug('[useFetchProfile] Cancelled after checkProfile, cleaning up')
          setIsFetching(false)
          return
        }
        
        if (profile) {
          // Profile found (from cache or network), we're done
          // checkProfile already set the profile state, so we're done
          return
        }
        
        logger.debug('[useFetchProfile] No profile found, considering retry')
        // No profile found yet - set fetching to false so UI can show fallback
        // The profile will remain null, allowing components to show npub fallback
        setIsFetching(false)
        setError(null) // Clear any previous errors
        
        // CRITICAL FIX: Disable retry intervals during rapid scrolling to prevent browser crashes
        // Only retry if skipCache is true (user explicitly wants to refresh)
        // For normal feed scrolling, missing profiles are acceptable and will be fetched on-demand
        // This prevents accumulation of hundreds of intervals during rapid scrolling
        if (skipCache) {
          // If no profile was found, periodically re-check (profiles might load asynchronously)
          // REDUCED: Check every 10 seconds for up to 30 seconds (3 checks) to prevent too many intervals
          // This reduces memory usage when many profiles are being fetched (e.g., trending page)
          let checkCount = 0
          const maxChecks = 3 // Reduced from 4 to further reduce load
          const startTime = Date.now()
          const maxTotalTime = 20000 // 20 seconds total timeout (3 checks * ~5s + buffer)
          
          checkIntervalRef.current = setInterval(async () => {
            // CRITICAL: Check for timeout to prevent infinite retries
            const elapsed = Date.now() - startTime
            if (elapsed > maxTotalTime) {
              logger.warn('[useFetchProfile] Retry interval timeout reached, stopping retries', {
                pubkey: extractedPubkey?.substring(0, 8),
                elapsed: `${elapsed}ms`
              })
              if (checkIntervalRef.current) {
                clearInterval(checkIntervalRef.current)
                checkIntervalRef.current = null
              }
              return
            }
            
            if (cancelled.current || checkCount >= maxChecks) {
              if (checkIntervalRef.current) {
                clearInterval(checkIntervalRef.current)
                checkIntervalRef.current = null
              }
              return
            }
            
            checkCount++
            const profile = await checkProfile(extractedPubkey, cancelled)
            if (profile || cancelled.current) {
              // Profile found or cancelled, stop checking
              if (checkIntervalRef.current) {
                clearInterval(checkIntervalRef.current)
                checkIntervalRef.current = null
              }
            }
          }, 10000) // Increased from 5 seconds to 10 seconds to reduce load
        } else {
          // For normal feed scrolling, don't set up retry intervals
          // Profiles will be fetched on-demand when user navigates to profile page
          // This prevents accumulation of intervals during rapid scrolling
          logger.debug('[useFetchProfile] Skipping retry intervals for normal feed scrolling', {
            pubkey: extractedPubkey
          })
        }
      } catch (err) {
                 logger.error('[useFetchProfile] run() error', {
                   pubkey: extractedPubkey,
                   error: err instanceof Error ? err.message : String(err),
                   stack: err instanceof Error ? err.stack : undefined
                 })
        if (!cancelled.current) {
          setError(err as Error)
          setIsFetching(false)
        }
      }
    }

    logger.debug('[useFetchProfile] About to call run()', {
      pubkey: extractedPubkey?.substring(0, 8)
    })
    run().catch((err) => {
      logger.error('[useFetchProfile] Unhandled error in run()', {
        pubkey: extractedPubkey,
        error: err instanceof Error ? err.message : String(err),
        stack: err instanceof Error ? err.stack : undefined
      })
    })
    return () => {
      cancelled.current = true
      // Only clear processingPubkeyRef if it matches the current pubkey
      // This prevents clearing it if a new fetch has already started
      if (processingPubkeyRef.current === extractedPubkey) {
        processingPubkeyRef.current = null
      }
      // CRITICAL: Always clear interval on cleanup to prevent memory leaks
      // This is especially important during rapid scrolling when many components mount/unmount
      if (checkIntervalRef.current) {
        clearInterval(checkIntervalRef.current)
        checkIntervalRef.current = null
      }
      // Clear run count and initialized status on cleanup to allow fresh fetches if component remounts
      if (extractedPubkey) {
        effectRunCountRef.current.delete(extractedPubkey)
        // Don't clear initializedPubkeysRef here - keep it to prevent re-fetching on remount
        // Only clear it if explicitly requested via skipCache
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [id, skipCache, noteFeed?.version]) // checkProfile is memoized; noteFeed.version hydrates batch profiles

  useEffect(() => {
    // CRITICAL: Only use currentAccountProfile if it matches the pubkey we're looking for
    // Use pubkey from the profile object to avoid reference equality issues
    // Only update if we don't have a profile yet AND we're not currently processing
    // CRITICAL FIX: Don't include profile in dependencies to prevent infinite loops
    // We only read profile to check if it exists, we don't need to re-run when it changes
    if (currentAccountProfile?.pubkey && pubkey && pubkey === currentAccountProfile.pubkey) {
      // Only update if we don't have a profile yet (avoid unnecessary updates)
      // Also check that we're processing this pubkey to prevent race conditions
      if (!profile && processingPubkeyRef.current === pubkey) {
        setProfile(currentAccountProfile)
        setIsFetching(false)
        // Clear interval if we got the profile from current account
        if (checkIntervalRef.current) {
          clearInterval(checkIntervalRef.current)
          checkIntervalRef.current = null
        }
        // Clear run count since we have the profile
        effectRunCountRef.current.delete(pubkey)
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [currentAccountProfile?.pubkey, pubkey]) // Removed profile from dependencies to prevent infinite loops

  return { isFetching, error, profile }
}
