import { Event } from 'nostr-tools'
import { useGlobalRelayBootstrapDefaults } from '@/hooks/use-global-relay-bootstrap-defaults'
import {
  buildAuthorInboxOutboxRelayUrls,
  buildProfileAugmentedReadRelayUrls,
  PROFILE_PAGE_PINS_RESOLVE_LIMIT
} from '@/lib/favorites-feed-relays'
import {
  METADATA_BATCH_QUERY_EOSE_TIMEOUT_MS,
  METADATA_BATCH_QUERY_GLOBAL_TIMEOUT_MS
} from '@/constants'
import { PIN_LIST_UPDATED_EVENT, type PinListUpdatedDetail } from '@/lib/pin-list-events'
import { hexPubkeysEqual, normalizeHexPubkey } from '@/lib/pubkey'
import { normalizeUrl } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostrOptional } from '@/providers/nostr-context'
import client, { eventService, queryService } from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'

const CACHE_DURATION = 5 * 60 * 1000

type PinsCacheEntry = {
  events: Event[]
  lastUpdated: number
}

const pinsCache = new Map<string, PinsCacheEntry>()

function orderPinEvents(pinList: Event, eventsById: Map<string, Event>): Event[] {
  const ordered: Event[] = []
  const seen = new Set<string>()

  const eIds = pinList.tags
    .filter((tag) => tag[0] === 'e' && tag[1])
    .map((tag) => tag[1]!.toLowerCase())
    .reverse()

  for (const id of eIds) {
    const ev = eventsById.get(id)
    if (ev) {
      const k = ev.id.toLowerCase()
      if (!seen.has(k)) {
        ordered.push(ev)
        seen.add(k)
      }
    }
  }

  const aTags = pinList.tags.filter((tag) => tag[0] === 'a' && tag[1]).map((tag) => tag[1]!)
  for (const coord of aTags) {
    const want = coord.toLowerCase()
    const ev = [...eventsById.values()].find((e) => {
      const d = e.tags.find((t) => t[0] === 'd')?.[1] ?? ''
      return `${e.kind}:${e.pubkey}:${d}`.toLowerCase() === want
    })
    if (ev) {
      const k = ev.id.toLowerCase()
      if (!seen.has(k)) {
        ordered.push(ev)
        seen.add(k)
      }
    }
  }

  for (const ev of eventsById.values()) {
    const k = ev.id.toLowerCase()
    if (!seen.has(k)) {
      ordered.push(ev)
      seen.add(k)
    }
  }

  return ordered
}

function blockedRelaysContentKey(blockedRelays: string[]): string {
  return [...blockedRelays].map((u) => normalizeUrl(u) || u).filter(Boolean).sort().join('\u0001')
}

export function useProfilePins(pubkey: string | undefined) {
  const nostr = useNostrOptional()
  const { blockedRelays } = useFavoriteRelays()
  const viewerUsesGlobalBootstrap = useGlobalRelayBootstrapDefaults()
  const blockedKey = useMemo(() => blockedRelaysContentKey(blockedRelays), [blockedRelays])
  const includeAuthorLocalRelays = useMemo(() => {
    const me = nostr?.pubkey?.trim()
    const pk = pubkey?.trim()
    if (!me || !pk) return false
    try {
      return hexPubkeysEqual(normalizeHexPubkey(me), normalizeHexPubkey(pk))
    } catch {
      return false
    }
  }, [nostr?.pubkey, pubkey])
  const useGlobalRelayBootstrap = viewerUsesGlobalBootstrap || !includeAuthorLocalRelays
  const [pinEvents, setPinEvents] = useState<Event[]>([])
  const [loadingPins, setLoadingPins] = useState(false)

  /** Same-tab paint: show cached pins before async relay work (matches timeline showing memory cache). */
  useLayoutEffect(() => {
    if (!pubkey) {
      setPinEvents([])
      return
    }
    const cacheKey = `${pubkey}-pins-profile`
    const cached = pinsCache.get(cacheKey)
    if (
      cached &&
      cached.events.length > 0 &&
      Date.now() - cached.lastUpdated < CACHE_DURATION
    ) {
      setPinEvents(cached.events)
      cached.events.forEach((e) => client.addEventToCache(e))
    } else {
      setPinEvents([])
    }
  }, [pubkey])

  const loadPins = useCallback(
    async (forceRefresh = false) => {
      if (!pubkey) {
        setPinEvents([])
        return
      }
      const cacheKey = `${pubkey}-pins-profile`
      if (!forceRefresh) {
        const cached = pinsCache.get(cacheKey)
        // Only reuse cache for non-empty pin rows. Empty was previously cached on transient relay
        // failures / races, which hid pins for CACHE_DURATION with no refetch.
        if (
          cached &&
          cached.events.length > 0 &&
          Date.now() - cached.lastUpdated < CACHE_DURATION
        ) {
          setPinEvents(cached.events)
          cached.events.forEach((e) => client.addEventToCache(e))
          return
        }
      }

      setLoadingPins(true)
      try {
        const pk = normalizeHexPubkey(pubkey)
        let paintedLocalPins = false
        const localPinLists = eventService.listSessionEventsAuthoredBy(pk, { kinds: [10001], limit: 8 })
        const diskPinList = await indexedDb.getReplaceableEvent(pk, 10001).catch(() => undefined)
        if (diskPinList) localPinLists.push(diskPinList)
        const localPinList =
          localPinLists.length > 0
            ? localPinLists.reduce((best, event) =>
                event.created_at > best.created_at ? event : best
              )
            : null
        if (localPinList?.tags?.length) {
          const localIds = localPinList.tags
            .filter((tag) => tag[0] === 'e' && tag[1])
            .slice(0, PROFILE_PAGE_PINS_RESOLVE_LIMIT)
            .map((tag) => tag[1]!.toLowerCase())
          const [archiveHits, publicationHits] = await Promise.all([
            indexedDb.getArchivedEventsByIds(localIds),
            Promise.all(localIds.map((id) => indexedDb.getEventFromPublicationStore(id)))
          ])
          const localById = new Map<string, Event>()
          for (const event of archiveHits) localById.set(event.id.toLowerCase(), event)
          for (const event of publicationHits) {
            if (event) localById.set(event.id.toLowerCase(), event)
          }
          const orderedLocal = orderPinEvents(localPinList, localById).slice(0, PROFILE_PAGE_PINS_RESOLVE_LIMIT)
          if (orderedLocal.length > 0) {
            setPinEvents(orderedLocal)
            paintedLocalPins = true
            pinsCache.set(cacheKey, { events: orderedLocal, lastUpdated: Date.now() })
            orderedLocal.forEach((event) => client.addEventToCache(event))
          }
        }

        const [authorRl, pinListEarly] = await Promise.all([
          client.fetchRelayList(pk).catch(() => ({
            read: [] as string[],
            write: [] as string[]
          })),
          client.fetchPinListEvent(pk).catch(() => undefined)
        ])
        const authorRelays = buildAuthorInboxOutboxRelayUrls(authorRl, blockedRelays, includeAuthorLocalRelays)
        const pinsResolveRelays = buildProfileAugmentedReadRelayUrls(authorRelays, blockedRelays, 16, useGlobalRelayBootstrap)
        if (!pinsResolveRelays.length) {
          if (!paintedLocalPins) setPinEvents([])
          return
        }

        let pinList: Event | null = pinListEarly ?? null

        if (!pinList) {
          try {
            const rows = await queryService.fetchEvents(
              pinsResolveRelays,
              { authors: [pk], kinds: [10001], limit: 1 },
              {
                replaceableRace: true,
                eoseTimeout: METADATA_BATCH_QUERY_EOSE_TIMEOUT_MS,
                globalTimeout: METADATA_BATCH_QUERY_GLOBAL_TIMEOUT_MS
              }
            )
            pinList =
              rows.length > 0
                ? rows.reduce((best, e) => (e.created_at > best.created_at ? e : best))
                : null
          } catch {
            pinList = null
          }
        }

        if (!pinList) {
          if (!paintedLocalPins) setPinEvents([])
          return
        }

        if (!pinList.tags?.length) {
          if (!paintedLocalPins) setPinEvents([])
          return
        }

        const max = PROFILE_PAGE_PINS_RESOLVE_LIMIT
        const eventIds: string[] = []
        const aTags: string[] = []
        for (const tag of pinList.tags) {
          if (eventIds.length + aTags.length >= max) break
          if (tag[0] === 'e' && tag[1]) eventIds.push(tag[1].toLowerCase())
          else if (tag[0] === 'a' && tag[1]) aTags.push(tag[1])
        }

        const byId = new Map<string, Event>()
        if (eventIds.length > 0) {
          const sessionHits = await Promise.all(eventIds.map((id) => eventService.fetchEvent(id)))
          for (let i = 0; i < eventIds.length; i++) {
            const ev = sessionHits[i]
            if (ev) byId.set(ev.id.toLowerCase(), ev)
          }
          const missing = eventIds.filter((id) => !byId.has(id))
          if (missing.length > 0) {
            const rows = await queryService.fetchEvents(pinsResolveRelays, {
              ids: missing,
              limit: max
            })
            for (const e of rows) {
              byId.set(e.id.toLowerCase(), e)
            }
          }
        }

        const eventPromises: Promise<Event[]>[] = []
        if (aTags.length > 0) {
          const aTagFetches = aTags.map(async (aTagRaw) => {
            const parts = aTagRaw.trim().split(':')
            if (parts.length < 2) return null
            const kind = parseInt(parts[0], 10)
            const author = parts[1]?.trim().toLowerCase()
            if (!Number.isFinite(kind) || !author || !/^[0-9a-f]{64}$/.test(author)) return null
            const d = parts.slice(2).join(':')
            const filter = d
              ? { authors: [author], kinds: [kind], limit: 1, '#d': [d] as [string] }
              : { authors: [author], kinds: [kind], limit: 1 }
            const events = await queryService.fetchEvents(pinsResolveRelays, filter)
            return events[0] ?? null
          })
          eventPromises.push(
            Promise.all(aTagFetches).then((events) => events.filter((e): e is Event => e !== null))
          )
        }

        const eventArrays = await Promise.all(eventPromises)
        const flat = eventArrays.flat()
        flat.forEach((e) => client.addEventToCache(e))
        for (const e of flat) {
          byId.set(e.id.toLowerCase(), e)
        }

        const ordered = orderPinEvents(pinList, byId).slice(0, PROFILE_PAGE_PINS_RESOLVE_LIMIT)
        setPinEvents(ordered)
        if (ordered.length > 0) {
          pinsCache.set(cacheKey, { events: ordered, lastUpdated: Date.now() })
        }
      } catch {
        setPinEvents([])
      } finally {
        setLoadingPins(false)
      }
    },
    // `blockedKey` fingerprints `blockedRelays`; omit the array so new [] references do not recreate loadPins every render.
    [pubkey, blockedKey, includeAuthorLocalRelays]
  )

  useEffect(() => {
    if (!pubkey) {
      setPinEvents([])
      return
    }
    void loadPins(false)
  }, [pubkey, loadPins])

  useEffect(() => {
    if (!pubkey) return

    const handler = (raw: globalThis.Event) => {
      const detail = (raw as CustomEvent<PinListUpdatedDetail>).detail
      if (!detail) return
      let profilePk: string
      try {
        profilePk = normalizeHexPubkey(pubkey).toLowerCase()
      } catch {
        return
      }
      if (detail.ownerPubkey !== profilePk) return

      const cacheKey = `${pubkey}-pins-profile`
      pinsCache.delete(cacheKey)

      if (detail.toggledEvent && detail.pinned === false) {
        const removedId = detail.toggledEvent.id.toLowerCase()
        setPinEvents((prev) => {
          const next = prev.filter((ev) => ev.id.toLowerCase() !== removedId)
          if (next.length > 0) {
            pinsCache.set(cacheKey, { events: next, lastUpdated: Date.now() })
          }
          return next
        })
      } else {
        void loadPins(true)
      }
    }

    window.addEventListener(PIN_LIST_UPDATED_EVENT, handler)
    return () => window.removeEventListener(PIN_LIST_UPDATED_EVENT, handler)
  }, [pubkey, loadPins])

  const refreshPins = useCallback(() => {
    if (pubkey) {
      pinsCache.delete(`${pubkey}-pins-profile`)
    }
    void loadPins(true)
  }, [pubkey, loadPins])

  return { pinEvents, loadingPins, refreshPins }
}
