import { FormattedTimestamp } from '@/components/FormattedTimestamp'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import UserAvatar, { SimpleUserAvatar } from '@/components/UserAvatar'
import Username, { SimpleUsername } from '@/components/Username'
import { isMentioningMutedUsers } from '@/lib/event'
import { toNote, toUserAggregationDetail } from '@/lib/link'
import { cn, isTouchDevice } from '@/lib/utils'
import { useSecondaryPage } from '@/PageManager'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useDeletedEvent } from '@/providers/DeletedEventProvider'
import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider'
import { usePinnedUsers } from '@/providers/PinnedUsersProvider'
import { useReply } from '@/providers/ReplyProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
import client from '@/services/client.service'
import userAggregationService, { TUserAggregation } from '@/services/user-aggregation.service'
import { TFeedSubRequest } from '@/types'
import dayjs from 'dayjs'
import { History, Loader, Pin, PinOff } from 'lucide-react'
import { Event, kinds } from 'nostr-tools'
import {
  forwardRef,
  useCallback,
  useEffect,
  useImperativeHandle,
  useMemo,
  useRef,
  useState
} from 'react'
import { useTranslation } from 'react-i18next'
import PullToRefresh from 'react-simple-pull-to-refresh'
import { toast } from 'sonner'
import { LoadingBar } from '../LoadingBar'
import NewNotesButton from '../NewNotesButton'

const LIMIT = 500
const SHOW_COUNT = 20

export type TUserAggregationListRef = {
  scrollToTop: (behavior?: ScrollBehavior) => void
  refresh: () => void
}

const UserAggregationList = forwardRef<
  TUserAggregationListRef,
  {
    subRequests: TFeedSubRequest[]
    showKinds?: number[]
    filterFn?: (event: Event) => boolean
    filterMutedNotes?: boolean
    areAlgoRelays?: boolean
    showRelayCloseReason?: boolean
  }
>(
  (
    {
      subRequests,
      showKinds,
      filterFn,
      filterMutedNotes = true,
      areAlgoRelays = false,
      showRelayCloseReason = false
    },
    ref
  ) => {
    const { t } = useTranslation()
    const { pubkey: currentPubkey, startLogin } = useNostr()
    const { push } = useSecondaryPage()
    const { hideUntrustedNotes, isUserTrusted } = useUserTrust()
    const { mutePubkeySet } = useMuteList()
    const { pinnedPubkeySet } = usePinnedUsers()
    const { hideContentMentioningMutedUsers } = useContentPolicy()
    const { isEventDeleted } = useDeletedEvent()
    const { addReplies } = useReply()
    const [since, setSince] = useState(() => dayjs().subtract(1, 'day').unix())
    const [events, setEvents] = useState<Event[]>([])
    const [newEvents, setNewEvents] = useState<Event[]>([])
    const [newEventPubkeys, setNewEventPubkeys] = useState<Set<string>>(new Set())
    const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
    const [loading, setLoading] = useState(true)
    const [showLoadingBar, setShowLoadingBar] = useState(true)
    const [refreshCount, setRefreshCount] = useState(0)
    const [showCount, setShowCount] = useState(SHOW_COUNT)
    const [hasMore, setHasMore] = useState(true)
    const supportTouch = useMemo(() => isTouchDevice(), [])
    const feedId = useMemo(() => {
      return userAggregationService.getFeedId(subRequests, showKinds)
    }, [JSON.stringify(subRequests), JSON.stringify(showKinds)])
    const bottomRef = useRef<HTMLDivElement | null>(null)
    const topRef = useRef<HTMLDivElement | null>(null)
    const nonPinnedTopRef = useRef<HTMLDivElement | null>(null)

    const scrollToTop = (behavior: ScrollBehavior = 'instant') => {
      setTimeout(() => {
        topRef.current?.scrollIntoView({ behavior, block: 'start' })
      }, 20)
    }

    const refresh = () => {
      scrollToTop()
      setTimeout(() => {
        setRefreshCount((count) => count + 1)
      }, 500)
    }

    useImperativeHandle(ref, () => ({ scrollToTop, refresh }), [])

    useEffect(() => {
      return () => {
        userAggregationService.clearAggregations(feedId)
      }
    }, [feedId])

    useEffect(() => {
      if (!subRequests.length) return

      setSince(dayjs().subtract(1, 'day').unix())
      setHasMore(true)

      async function init() {
        setLoading(true)
        setEvents([])
        setNewEvents([])
        setHasMore(true)

        if (showKinds?.length === 0 && subRequests.every(({ filter }) => !filter.kinds)) {
          setLoading(false)
          setHasMore(false)
          return () => {}
        }

        const { closer, timelineKey } = await client.subscribeTimeline(
          subRequests.map(({ urls, filter }) => ({
            urls,
            filter: {
              kinds: showKinds ?? [],
              ...filter,
              limit: LIMIT
            }
          })),
          {
            onEvents: (events, eosed) => {
              if (events.length > 0) {
                setEvents(events)
              }
              if (areAlgoRelays) {
                setHasMore(false)
              }
              if (eosed) {
                setLoading(false)
                setHasMore(events.length > 0)
                addReplies(events)
              }
            },
            onNew: (event) => {
              setNewEvents((oldEvents) =>
                [event, ...oldEvents].sort((a, b) => b.created_at - a.created_at)
              )
              addReplies([event])
            },
            onClose: (url, reason) => {
              if (!showRelayCloseReason) return
              // ignore reasons from nostr-tools
              if (
                [
                  'closed by caller',
                  'relay connection errored',
                  'relay connection closed',
                  'pingpong timed out',
                  'relay connection closed by us'
                ].includes(reason)
              ) {
                return
              }

              toast.error(`${url}: ${reason}`)
            }
          },
          {
            startLogin,
            needSort: !areAlgoRelays
          }
        )
        setTimelineKey(timelineKey)

        return closer
      }

      const promise = init()
      return () => {
        promise.then((closer) => closer())
      }
    }, [feedId, refreshCount])

    useEffect(() => {
      if (loading || !hasMore || !timelineKey || !events.length) {
        return
      }

      const until = events[events.length - 1].created_at - 1
      if (until < since) {
        return
      }

      setLoading(true)
      client.loadMoreTimeline(timelineKey, until, LIMIT).then((moreEvents) => {
        if (moreEvents.length === 0) {
          setHasMore(false)
          setLoading(false)
          return
        }
        setEvents((oldEvents) => [...oldEvents, ...moreEvents])
        setLoading(false)
      })
    }, [loading, timelineKey, events, since, hasMore])

    useEffect(() => {
      if (loading) {
        setShowLoadingBar(true)
        return
      }

      const timeout = setTimeout(() => {
        setShowLoadingBar(false)
      }, 1000)

      return () => clearTimeout(timeout)
    }, [loading])

    const shouldHideEvent = useCallback(
      (evt: Event) => {
        if (evt.pubkey === currentPubkey) return true
        if (isEventDeleted(evt)) return true
        if (hideUntrustedNotes && !isUserTrusted(evt.pubkey)) return true
        if (filterMutedNotes && mutePubkeySet.has(evt.pubkey)) return true
        if (
          filterMutedNotes &&
          hideContentMentioningMutedUsers &&
          isMentioningMutedUsers(evt, mutePubkeySet)
        ) {
          return true
        }
        if (filterFn && !filterFn(evt)) {
          return true
        }

        return false
      },
      [
        hideUntrustedNotes,
        mutePubkeySet,
        isEventDeleted,
        filterFn,
        currentPubkey,
        filterMutedNotes,
        isUserTrusted,
        hideContentMentioningMutedUsers,
        isMentioningMutedUsers
      ]
    )

    const lastXDays = useMemo(() => {
      return dayjs().diff(dayjs.unix(since), 'day')
    }, [since])

    const filteredEvents = useMemo(() => {
      return events.filter((evt) => evt.created_at >= since && !shouldHideEvent(evt))
    }, [events, since, shouldHideEvent])

    const filteredNewEvents = useMemo(() => {
      return newEvents.filter((evt) => evt.created_at >= since && !shouldHideEvent(evt))
    }, [newEvents, since, shouldHideEvent])

    const aggregations = useMemo(() => {
      const aggs = userAggregationService.aggregateByUser(filteredEvents)
      userAggregationService.saveAggregations(feedId, aggs)
      return aggs
    }, [feedId, filteredEvents])

    const pinnedAggregations = useMemo(() => {
      return aggregations.filter((agg) => pinnedPubkeySet.has(agg.pubkey))
    }, [aggregations, pinnedPubkeySet])

    const normalAggregations = useMemo(() => {
      return aggregations.filter((agg) => !pinnedPubkeySet.has(agg.pubkey))
    }, [aggregations, pinnedPubkeySet])

    const displayedNormalAggregations = useMemo(() => {
      return normalAggregations.slice(0, showCount)
    }, [normalAggregations, showCount])

    const hasMoreToDisplay = useMemo(() => {
      return normalAggregations.length > displayedNormalAggregations.length
    }, [normalAggregations, displayedNormalAggregations])

    useEffect(() => {
      const options = {
        root: null,
        rootMargin: '10px',
        threshold: 1
      }
      if (!hasMoreToDisplay) return

      const observerInstance = new IntersectionObserver((entries) => {
        if (entries[0].isIntersecting) {
          setShowCount((count) => count + SHOW_COUNT)
        }
      }, options)

      const currentBottomRef = bottomRef.current
      if (currentBottomRef) {
        observerInstance.observe(currentBottomRef)
      }

      return () => {
        if (observerInstance && currentBottomRef) {
          observerInstance.unobserve(currentBottomRef)
        }
      }
    }, [hasMoreToDisplay])

    const handleViewUser = (agg: TUserAggregation) => {
      // Mark as viewed when user clicks
      userAggregationService.markAsViewed(feedId, agg.pubkey)
      setNewEventPubkeys((prev) => {
        const newSet = new Set(prev)
        newSet.delete(agg.pubkey)
        return newSet
      })

      if (agg.count === 1) {
        const evt = agg.events[0]
        if (evt.kind !== kinds.Repost && evt.kind !== kinds.GenericRepost) {
          push(toNote(agg.events[0]))
          return
        }
      }

      push(toUserAggregationDetail(feedId, agg.pubkey))
    }

    const handleLoadEarlier = () => {
      setSince((prevSince) => dayjs.unix(prevSince).subtract(1, 'day').unix())
      setShowCount(SHOW_COUNT)
    }

    const showNewEvents = () => {
      const pubkeySet = new Set<string>()
      let hasPinnedUser = false
      newEvents.forEach((evt) => {
        pubkeySet.add(evt.pubkey)
        if (pinnedPubkeySet.has(evt.pubkey)) {
          hasPinnedUser = true
        }
      })
      setNewEventPubkeys(pubkeySet)
      setEvents((oldEvents) => [...newEvents, ...oldEvents])
      setNewEvents([])
      setTimeout(() => {
        if (hasPinnedUser) {
          scrollToTop('smooth')
          return
        }
        nonPinnedTopRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
      }, 0)
    }

    const list = (
      <div className="min-h-screen">
        {pinnedAggregations.map((agg) => (
          <UserAggregationItem
            key={agg.pubkey}
            feedId={feedId}
            aggregation={agg}
            onClick={() => handleViewUser(agg)}
            isNew={newEventPubkeys.has(agg.pubkey)}
          />
        ))}

        <div ref={nonPinnedTopRef} className="scroll-mt-[calc(6rem+1px)]" />
        {normalAggregations.map((agg) => (
          <UserAggregationItem
            key={agg.pubkey}
            feedId={feedId}
            aggregation={agg}
            onClick={() => handleViewUser(agg)}
            isNew={newEventPubkeys.has(agg.pubkey)}
          />
        ))}

        {loading || hasMoreToDisplay ? (
          <div ref={bottomRef}>
            <UserAggregationItemSkeleton />
          </div>
        ) : aggregations.length === 0 ? (
          <div className="flex justify-center w-full mt-2">
            <Button size="lg" onClick={() => setRefreshCount((count) => count + 1)}>
              {t('Reload')}
            </Button>
          </div>
        ) : (
          <div className="text-center text-sm text-muted-foreground mt-2">{t('no more notes')}</div>
        )}
      </div>
    )

    return (
      <div>
        <div ref={topRef} className="scroll-mt-[calc(6rem+1px)]" />
        {showLoadingBar && <LoadingBar />}
        <div className="border-b h-12 pl-4 pr-1 flex items-center justify-between gap-2">
          <div className="text-sm text-muted-foreground flex items-center gap-1.5 min-w-0">
            <span className="font-medium text-foreground">
              {lastXDays === 1
                ? t('Last 24 hours')
                : t('Last {{count}} days', { count: lastXDays })}
            </span>
            ·
            <span>
              {filteredEvents.length} {t('notes')}
            </span>
          </div>
          <Button
            variant="ghost"
            className="h-10 px-3 shrink-0 rounded-lg text-muted-foreground hover:text-foreground"
            disabled={showLoadingBar || !hasMore}
            onClick={handleLoadEarlier}
          >
            {showLoadingBar ? <Loader className="animate-spin" /> : <History />}
            {t('Load earlier')}
          </Button>
        </div>
        {supportTouch ? (
          <PullToRefresh
            onRefresh={async () => {
              refresh()
              await new Promise((resolve) => setTimeout(resolve, 1000))
            }}
            pullingContent=""
          >
            {list}
          </PullToRefresh>
        ) : (
          list
        )}
        <div className="h-40" />
        {filteredNewEvents.length > 0 && (
          <NewNotesButton newEvents={filteredNewEvents} onClick={showNewEvents} />
        )}
      </div>
    )
  }
)
UserAggregationList.displayName = 'UserAggregationList'
export default UserAggregationList

function UserAggregationItem({
  feedId,
  aggregation,
  onClick,
  isNew
}: {
  feedId: string
  aggregation: TUserAggregation
  onClick: () => void
  isNew?: boolean
}) {
  const { t } = useTranslation()
  const supportTouch = useMemo(() => isTouchDevice(), [])
  const [hasNewEvents, setHasNewEvents] = useState(true)
  const [loading, setLoading] = useState(false)
  const { isPinned, togglePin } = usePinnedUsers()
  const pinned = useMemo(() => isPinned(aggregation.pubkey), [aggregation.pubkey, isPinned])

  useEffect(() => {
    const update = () => {
      const lastViewedTime = userAggregationService.getLastViewedTime(feedId, aggregation.pubkey)
      setHasNewEvents(aggregation.lastEventTime > lastViewedTime)
    }

    const unSub = userAggregationService.subscribeViewedTimeChange(
      feedId,
      aggregation.pubkey,
      () => {
        update()
      }
    )

    update()

    return unSub
  }, [feedId, aggregation])

  const onTogglePin = (e: React.MouseEvent) => {
    e.stopPropagation()
    setLoading(true)
    togglePin(aggregation.pubkey).finally(() => {
      setLoading(false)
    })
  }

  const onToggleViewed = (e: React.MouseEvent) => {
    e.stopPropagation()
    if (hasNewEvents) {
      userAggregationService.markAsViewed(feedId, aggregation.pubkey)
    } else {
      userAggregationService.markAsUnviewed(feedId, aggregation.pubkey)
    }
  }

  return (
    <div
      className={cn(
        'group relative flex items-center gap-4 px-4 py-3 border-b hover:bg-accent/30 cursor-pointer transition-all duration-200',
        isNew && 'bg-primary/15 hover:bg-primary/20'
      )}
      onClick={onClick}
    >
      {supportTouch ? (
        <SimpleUserAvatar
          userId={aggregation.pubkey}
          className={!hasNewEvents ? 'grayscale' : ''}
        />
      ) : (
        <UserAvatar userId={aggregation.pubkey} className={!hasNewEvents ? 'grayscale' : ''} />
      )}

      <div className="flex-1 min-w-0 flex flex-col">
        {supportTouch ? (
          <SimpleUsername
            userId={aggregation.pubkey}
            className={cn(
              'font-semibold text-base truncate max-w-fit',
              !hasNewEvents && 'text-muted-foreground'
            )}
            skeletonClassName="h-4"
          />
        ) : (
          <Username
            userId={aggregation.pubkey}
            className={cn(
              'font-semibold text-base truncate max-w-fit',
              !hasNewEvents && 'text-muted-foreground'
            )}
            skeletonClassName="h-4"
          />
        )}
        <FormattedTimestamp
          timestamp={aggregation.lastEventTime}
          className="text-sm text-muted-foreground"
        />
      </div>

      <Button
        variant="ghost"
        size="icon"
        onClick={onTogglePin}
        className={`flex-shrink-0 ${
          pinned
            ? 'text-primary hover:text-primary/80'
            : 'text-muted-foreground hover:text-foreground'
        }`}
        title={pinned ? t('Unpin') : t('Pin')}
      >
        {loading ? <Loader className="animate-spin" /> : pinned ? <PinOff /> : <Pin />}
      </Button>

      <button
        className={cn(
          'flex-shrink-0 size-10 rounded-full font-bold tabular-nums text-primary border border-primary/80 bg-primary/10 hover:border-primary hover:bg-primary/20 flex flex-col items-center justify-center transition-colors',
          !hasNewEvents &&
            'border-muted-foreground/80 text-muted-foreground/80 bg-muted-foreground/10 hover:border-muted-foreground hover:text-muted-foreground hover:bg-muted-foreground/20'
        )}
        onClick={onToggleViewed}
      >
        {aggregation.count > 99 ? '99+' : aggregation.count}
      </button>
    </div>
  )
}

function UserAggregationItemSkeleton() {
  return (
    <div className="flex items-center gap-4 px-4 py-3">
      <Skeleton className="size-10 rounded-full" />
      <div className="flex-1">
        <Skeleton className="h-4 w-36 my-1" />
        <Skeleton className="h-3 w-14 my-1" />
      </div>
      <Skeleton className="size-10 rounded-full" />
    </div>
  )
}
