import { getNoteBech32Id } from '@/lib/event'
import { useIsEventDeleted } from '@/providers/DeletedEventProvider'
import { useReplyIngress } from '@/hooks/useReplyIngress'
import { eventService } from '@/services/client.service'
import { navigationEventStore } from '@/services/navigation-event-store'
import { Event } from 'nostr-tools'
import { useCallback, useEffect, useState } from 'react'

export function useFetchEvent(
  eventId?: string,
  initialEvent?: Event,
  fetchOpts?: { relayHints?: string[] }
) {
  const isEventDeleted = useIsEventDeleted()
  const { addReplies } = useReplyIngress()
  const [error, setError] = useState<Error | null>(null)
  const [event, setEvent] = useState<Event | undefined>(initialEvent)
  const [isFetching, setIsFetching] = useState(!initialEvent)
  const [refetchToken, setRefetchToken] = useState(0)

  const refetch = useCallback(() => {
    setRefetchToken((n) => n + 1)
  }, [])

  /** Content-based key so a new `relayHints` array with the same URLs does not restart the fetch. */
  const relayHintsSerialized = fetchOpts?.relayHints?.join('\0') ?? ''

  useEffect(() => {
    let cancelled = false

    if (!eventId) {
      setIsFetching(false)
      setEvent(undefined)
      // Do not setError here: this effect re-runs when callback deps (e.g. addReplies) change identity;
      // allocating a new Error each time would force updates and can exceed React's max update depth.
      return () => {
        cancelled = true
      }
    }

    const skipShortcuts = refetchToken > 0

    // If we have an initial event that matches the eventId, use it and skip fetching
    const initialMatches =
      initialEvent &&
      (initialEvent.id === eventId ||
        (() => {
          try {
            return getNoteBech32Id(initialEvent) === eventId
          } catch {
            return false
          }
        })())
    if (!skipShortcuts && initialMatches && initialEvent) {
      if (!isEventDeleted(initialEvent)) {
        setEvent(initialEvent)
        addReplies([initialEvent])
        setIsFetching(false)
      }
      return () => {
        cancelled = true
      }
    }

    // Check navigation event store first (events passed through navigation) — peek so remounts still see it.
    if (!skipShortcuts) {
      const navigationEvent = navigationEventStore.peekEvent(eventId)
      if (navigationEvent && !isEventDeleted(navigationEvent)) {
        setEvent(navigationEvent)
        addReplies([navigationEvent])
        setIsFetching(false)
        return () => {
          cancelled = true
        }
      }
    }

    // New target without a synchronous hit: drop the previous note immediately so the panel does not
    // keep showing the last-opened article (or fail to show a skeleton) while the new fetch runs or
    // after it returns empty.
    setEvent(undefined)
    setError(null)
    setIsFetching(true)

    const fetchEvent = async () => {
      try {
        // First load: DataLoader dedupes. Refetches (incl. session-waiter) clear a prior undefined so
        // timeline-cached events resolve after the embed mounted first.
        const opts = fetchOpts?.relayHints?.length ? fetchOpts : undefined
        const fetchedEvent = skipShortcuts
          ? await eventService.fetchEventForceRetry(eventId, opts)
          : await eventService.fetchEvent(eventId, opts)
        if (cancelled) return
        if (fetchedEvent && !isEventDeleted(fetchedEvent)) {
          setEvent(fetchedEvent)
          addReplies([fetchedEvent])
        } else {
          setEvent(undefined)
        }
      } catch (error) {
        if (!cancelled) {
          setError(error as Error)
          setEvent(undefined)
        }
      } finally {
        if (!cancelled) {
          setIsFetching(false)
        }
      }
    }

    void fetchEvent()

    return () => {
      cancelled = true
      // If deps change (e.g. embed relay hints) or Strict Mode re-runs the effect while a fetch is
      // still in flight, `finally` skips `setIsFetching(false)` when `cancelled` — without this,
      // loading can stay true forever and embeds show an endless skeleton.
      setIsFetching(false)
    }
  }, [eventId, initialEvent, isEventDeleted, addReplies, refetchToken, relayHintsSerialized])

  useEffect(() => {
    if (event && isEventDeleted(event)) {
      setEvent(undefined)
    }
  }, [isEventDeleted, event])

  // Parent notes often render before the embedded event arrives from the same timeline; refetch when it hits session cache.
  useEffect(() => {
    if (!eventId || event !== undefined) return undefined
    return eventService.subscribeWhenSessionHasEvent(eventId, refetch)
  }, [eventId, event, refetch])

  return { isFetching, error, event, refetch }
}
