import { useEffect, useState, useMemo, useRef, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useNostr } from '@/providers/NostrProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import rssFeedService, { RssFeedItem as TRssFeedItem } from '@/services/rss-feed.service'
import { DEFAULT_RSS_FEEDS } from '@/constants'
import RssFeedItem from '../RssFeedItem'
import RssWebFeedCard from '../RssWebFeedCard'
import { ArticleUrlsSection } from './ArticleUrlsSection'
import { RssUnifiedScopeSection } from './RssUnifiedScopeSection'
import { canonicalizeRssArticleUrl, isClawstrDotComHttpUrl } from '@/lib/rss-article'
import {
  addManualRssWebUrl,
  fetchDiscoveredWebUrlsFromRelays,
  loadManualRssWebUrls,
  loadPromotedRssThreadUrls,
  loadRssWebFeedScopePreference,
  loadRssWebHideUnifiedClutterPreference,
  loadRssWebSuppressClawstrPreference,
  buildArticleUrlFeedRows,
  isHttpArticleUrl,
  isRssWebUnifiedClutterUrl,
  mergeDiscoveredRssWebUrls,
  rssWebRowHasRealFeedItems,
  saveRssWebFeedScopePreference,
  saveRssWebHideUnifiedClutterPreference,
  saveRssWebSuppressClawstrPreference,
  WEB_EXTERNAL_REACTION_PUBLISHED_EVENT,
  type ManualRssWebUrlEntry,
  type RssWebFeedScope
} from '@/lib/rss-web-feed'
import { RssFeedDisplayPrefsProvider } from './RssFeedDisplayPrefsContext'
import { Checkbox } from '@/components/ui/checkbox'
import { Skeleton } from '@/components/ui/skeleton'
import { AlertCircle, Search, Plus } from 'lucide-react'
import logger from '@/lib/logger'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { Button } from '@/components/ui/button'
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle
} from '@/components/ui/dialog'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Check, ChevronDown } from 'lucide-react'
import { normalizeHttpArticleUrl } from '@/lib/rss-article'
import {
  getRssFeedUrlHostname,
  getStandardRssFeedProfile
} from '@/lib/standard-rss-feed-url'
import { StandardRssFeedUrlInline } from '@/components/StandardRssFeedUrlRow'

/** Cutoff timestamp (ms) for RSS+Web time filters; `null` = all time. */
function getRssTimeFilterCutoffMs(timeFilter: string): number | null {
  if (timeFilter === 'all') return null
  const now = Date.now()
  switch (timeFilter) {
    case 'hour':
      return now - 60 * 60 * 1000
    case 'hours24':
      return now - 24 * 60 * 60 * 1000
    case 'hours48':
      return now - 48 * 60 * 60 * 1000
    case 'week':
      return now - 7 * 24 * 60 * 60 * 1000
    case 'month':
      return now - 30 * 24 * 60 * 60 * 1000
    default:
      return null
  }
}

function ManualRssUrlAddRow({
  className,
  onUrlAdded
}: {
  className?: string
  onUrlAdded: () => void | Promise<void>
}) {
  const { t } = useTranslation()
  const [open, setOpen] = useState(false)
  const [value, setValue] = useState('')
  const [error, setError] = useState('')
  const [saving, setSaving] = useState(false)

  const submit = () => {
    setError('')
    const url = normalizeHttpArticleUrl(value)
    if (!url) {
      setError(t('Enter a valid http(s) URL'))
      return
    }
    setSaving(true)
    void (async () => {
      try {
        await addManualRssWebUrl(url)
        setOpen(false)
        setValue('')
        await Promise.resolve(onUrlAdded())
      } finally {
        setSaving(false)
      }
    })()
  }

  return (
    <>
      <Button
        type="button"
        variant="outline"
        className={className ?? 'w-full justify-start gap-2 text-muted-foreground border-dashed'}
        onClick={() => setOpen(true)}
      >
        <Plus className="h-4 w-4 shrink-0" />
        {t('+ Add a URL to this list')}
      </Button>
      <Dialog open={open} onOpenChange={setOpen}>
        <DialogContent className="sm:max-w-md">
          <DialogHeader>
            <DialogTitle>{t('Add a web URL')}</DialogTitle>
            <DialogDescription>
              {t('Add web URL to feed description')}
            </DialogDescription>
          </DialogHeader>
          <Input
            placeholder="https://example.com/article"
            value={value}
            onChange={(e) => {
              setValue(e.target.value)
              setError('')
            }}
            onKeyDown={(e) => {
              if (e.key === 'Enter') {
                e.preventDefault()
                submit()
              }
            }}
            autoFocus
          />
          {error ? <p className="text-sm text-destructive">{error}</p> : null}
          <DialogFooter className="gap-2 sm:gap-0">
            <Button type="button" variant="secondary" onClick={() => setOpen(false)}>
              {t('Cancel')}
            </Button>
            <Button type="button" disabled={saving} onClick={submit}>
              {t('Add to feed')}
            </Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
    </>
  )
}

function rssFeedItemArticleIsClawstrHost(item: TRssFeedItem): boolean {
  const l = item.link?.trim()
  if (!l || (!l.startsWith('http://') && !l.startsWith('https://'))) return false
  return isClawstrDotComHttpUrl(l) || isClawstrDotComHttpUrl(canonicalizeRssArticleUrl(l))
}

export default function RssFeedList() {
  const { t } = useTranslation()
  const { pubkey, rssFeedListEvent } = useNostr()
  const { favoriteRelays, blockedRelays } = useFavoriteRelays()
  const { isSmallScreen } = useScreenSize()
  const [items, setItems] = useState<TRssFeedItem[]>([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)
  const [refreshing, setRefreshing] = useState(false)
  
  // Filter states
  const [selectedFeeds, setSelectedFeeds] = useState<string[]>(['all'])
  const [timeFilter, setTimeFilter] = useState<string>('all')
  const [searchQuery, setSearchQuery] = useState<string>('')
  const [showFilters, setShowFilters] = useState<boolean>(false)
  const [feedPopoverOpen, setFeedPopoverOpen] = useState<boolean>(false)
  
  // Pagination state (merged RSS+Web rows)
  const [showRowCount, setShowRowCount] = useState<number>(20)
  const bottomRef = useRef<HTMLDivElement>(null)
  /** True after user changes RSS+Web scope or “only my web events”; blocks async prefs from overwriting. */
  const rssWebPrefsUserTouchedRef = useRef(false)
  const [manualWebEntries, setManualWebEntries] = useState<ManualRssWebUrlEntry[]>([])
  /** Latest relay discovery (in-memory); URLs appear as faux cards even before IndexedDB merge. */
  const [relayDiscoveredUrls, setRelayDiscoveredUrls] = useState<ManualRssWebUrlEntry[]>([])
  const [suppressClawstrLinks, setSuppressClawstrLinks] = useState(true)
  const [hideUnifiedClutter, setHideUnifiedClutter] = useState(true)

  const refreshManualWebUrls = useCallback(() => {
    void loadManualRssWebUrls().then(setManualWebEntries)
  }, [])

  const [promotedThreadUrls, setPromotedThreadUrls] = useState<string[]>([])
  const promotedThreadUrlSet = useMemo(() => new Set(promotedThreadUrls), [promotedThreadUrls])

  const refreshPromotedThreadUrls = useCallback(() => {
    void loadPromotedRssThreadUrls().then(setPromotedThreadUrls)
  }, [])

  useEffect(() => {
    void loadManualRssWebUrls().then(setManualWebEntries)
  }, [])

  useEffect(() => {
    void loadPromotedRssThreadUrls().then(setPromotedThreadUrls)
  }, [])

  /** Bump to re-run relay URL discovery after publishing a kind-17 reaction. */
  const [relayDiscoveryTick, setRelayDiscoveryTick] = useState(0)

  /** Subscribed feed URLs (same rules as RSS load effect) — used for IndexedDB snapshot. */
  const resolvedFeedUrls = useMemo((): string[] => {
    if (pubkey && rssFeedListEvent) {
      try {
        return rssFeedListEvent.tags
          .filter((tag) => tag[0] === 'u' && tag[1])
          .map((tag) => tag[1] as string)
          .filter((url): url is string => {
            if (typeof url !== 'string') return false
            return url.trim().length > 0
          })
      } catch {
        return []
      }
    }
    return DEFAULT_RSS_FEEDS
  }, [pubkey, rssFeedListEvent])

  /** Full IndexedDB slice for current subscriptions; merged into the filter pipeline so search/time/feed use all cached rows. */
  const [idbRssSnapshot, setIdbRssSnapshot] = useState<TRssFeedItem[]>([])

  const refreshIdbRssSnapshot = useCallback(() => {
    if (resolvedFeedUrls.length === 0) {
      setIdbRssSnapshot([])
      return
    }
    void rssFeedService.getCachedItemsForFeedUrls(resolvedFeedUrls).then(setIdbRssSnapshot)
  }, [resolvedFeedUrls])

  useEffect(() => {
    refreshIdbRssSnapshot()
  }, [refreshIdbRssSnapshot, items])

  useEffect(() => {
    const id = window.setInterval(() => refreshIdbRssSnapshot(), 20_000)
    return () => clearInterval(id)
  }, [refreshIdbRssSnapshot])

  const mergedRssItems = useMemo(
    () => rssFeedService.mergeRssFeedItemLists([items, idbRssSnapshot]),
    [items, idbRssSnapshot]
  )

  // Listen for filter toggle events
  useEffect(() => {
    const handleToggleFilters = () => {
      setShowFilters(prev => !prev)
    }

    window.addEventListener('toggleRssFilters', handleToggleFilters)
    return () => {
      window.removeEventListener('toggleRssFilters', handleToggleFilters)
    }
  }, [])

  useEffect(() => {
    // Create AbortController for this effect
    let abortController = new AbortController()
    let isMounted = true
    let isLoading = false
    let timeoutId: NodeJS.Timeout | null = null

    const loadRssFeeds = async (forceNewController = false) => {
      // If forced, create a new controller (for manual refreshes)
      if (forceNewController) {
        abortController.abort() // Abort old one
        abortController = new AbortController()
      }

      // Check if already aborted or if a load is already in progress
      if (abortController.signal.aborted || isLoading) {
        logger.debug('[RssFeedList] Skipping load - already aborted or loading', { 
          aborted: abortController.signal.aborted, 
          isLoading 
        })
        return
      }

      // Clear any existing timeout
      if (timeoutId) {
        clearTimeout(timeoutId)
        timeoutId = null
      }

      isLoading = true
      setLoading(true)
      setError(null)
      
      // Set a timeout to prevent infinite loading (30 seconds)
      timeoutId = setTimeout(() => {
        if (isMounted && isLoading) {
          logger.warn('[RssFeedList] Feed loading timeout - aborting and showing partial results')
          abortController.abort()
          isLoading = false
          if (isMounted) {
            setLoading(false)
          }
        }
      }, 30000)

      try {
        // Get feed URLs from event or use default
        let feedUrls: string[] = []

        if (pubkey && rssFeedListEvent) {
          // User has an event - use only feeds from that event (even if empty)
          try {
            // Extract URLs from "u" tags
            const urls = rssFeedListEvent.tags
              .filter(tag => tag[0] === 'u' && tag[1])
              .map(tag => tag[1] as string)
              .filter((url): url is string => {
                if (typeof url !== 'string') {
                  logger.warn('[RssFeedList] Invalid RSS feed URL (not a string)', { url, type: typeof url })
                  return false
                }
                const trimmed = url.trim()
                if (trimmed.length === 0) {
                  logger.warn('[RssFeedList] Empty RSS feed URL found')
                  return false
                }
                return true
              })
            
            feedUrls = urls
            if (urls.length > 0) {
              logger.info('[RssFeedList] Loaded RSS feed list from context', { 
                feedCount: urls.length,
                eventId: rssFeedListEvent.id,
                urls
              })
            } else {
              logger.info('[RssFeedList] RSS feed list event exists but is empty - will show empty feed')
            }
          } catch (e) {
            logger.error('[RssFeedList] Failed to parse RSS feed list from tags', { 
              error: e,
              tags: rssFeedListEvent.tags
            })
            // On parse error, treat as empty event (don't use defaults)
            feedUrls = []
          }
        } else if (pubkey) {
          // No event exists - use default feeds for demo
          logger.info('[RssFeedList] No RSS feed list event in context, using default feeds')
          feedUrls = DEFAULT_RSS_FEEDS
          // Trigger background refresh for default feeds when no event exists
          rssFeedService.backgroundRefreshFeeds(feedUrls, abortController.signal).catch(err => {
            if (!(err instanceof DOMException && err.name === 'AbortError')) {
              logger.error('[RssFeedList] Background refresh of default feeds failed', { error: err })
            }
          })
        } else {
          // No pubkey - use default feeds
          feedUrls = DEFAULT_RSS_FEEDS
        }

        // Check if aborted before fetching
        if (abortController.signal.aborted || !isMounted) {
          return
        }

        // Fetch and merge feeds (cache-first: returns cached items immediately, background-refreshes)
        // Show refreshing indicator (background refresh will run in background, or we'll wait if cache is empty)
        if (isMounted) {
          setRefreshing(true)
        }
        
        const fetchedItems = await rssFeedService.fetchMultipleFeeds(feedUrls, abortController.signal)
        
        // Always set items if we got them, even if signal was aborted (abort might happen after fetch completes)
        // Only skip setting items if component unmounted
        if (!isMounted) {
          setRefreshing(false)
          return
        }
        
        // Set items regardless of abort status (abort might have happened after fetch completed)
        if (fetchedItems.length === 0) {
          // No items were successfully fetched, but don't show error if we tried
          // The fetchMultipleFeeds already logs warnings for failed feeds
          setError(null) // Clear any previous error
        }
        
        setItems(fetchedItems)
        
        // Check if aborted after setting items (for cleanup)
        if (abortController.signal.aborted) {
          logger.debug('[RssFeedList] Signal was aborted after fetching, but items were set', {
            itemCount: fetchedItems.length
          })
        }
        
        // Set up a listener for cache updates (background refresh may add new items)
        // Re-check cache after a delay to see if background refresh added items
        const checkForUpdates = async () => {
          if (abortController.signal.aborted || !isMounted) {
            if (isMounted) {
              setRefreshing(false)
            }
            return
          }
          
          try {
            const updatedItems = await rssFeedService.fetchMultipleFeeds(feedUrls, abortController.signal)
            if (!abortController.signal.aborted && isMounted) {
              setRefreshing(false)
              if (updatedItems.length > fetchedItems.length) {
                // New items were added by background refresh
                setItems(updatedItems)
                logger.info('[RssFeedList] Updated items from background refresh', {
                  previousCount: fetchedItems.length,
                  newCount: updatedItems.length
                })
              }
            }
          } catch {
            if (isMounted) {
              setRefreshing(false)
            }
            // Ignore errors in update check
          }
        }
        
        // Check for updates after 5 seconds (background refresh should be done by then)
        setTimeout(checkForUpdates, 5000)
      } catch (err) {
        // Don't handle abort errors - they're expected during cleanup
        if (err instanceof DOMException && err.name === 'AbortError') {
          return
        }

        // Check if still mounted before setting error
        if (!isMounted) {
          return
        }

        logger.error('[RssFeedList] Error loading RSS feeds', { error: err })
        // Don't set error state - fetchMultipleFeeds handles individual feed failures gracefully
        // Only set error if there's a critical issue (like network completely down)
        if (err instanceof TypeError && err.message.includes('Failed to fetch')) {
          // Network error - might be temporary, don't show persistent error
          setError(null)
        } else {
          setError(err instanceof Error ? err.message : t('Failed to load RSS feeds'))
        }
      } finally {
        isLoading = false
        if (timeoutId) {
          clearTimeout(timeoutId)
          timeoutId = null
        }
        // Only update loading state if still mounted
        if (isMounted) {
          setLoading(false)
          // If we had no cached items, background refresh was awaited, so stop refreshing indicator
          if (items.length === 0) {
            setRefreshing(false)
          }
        }
      }
    }

    loadRssFeeds()

    // Listen for RSS feed list updates
    const handleRssFeedListUpdate = (event: CustomEvent) => {
      const detail = event.detail as { pubkey: string; feedUrls: string[]; eventId: string }
      // Only refresh if it's for the current user
      if (detail.pubkey === pubkey && isMounted) {
        logger.info('[RssFeedList] Received RSS feed list update event, refreshing...', { 
          eventId: detail.eventId,
          feedCount: detail.feedUrls.length 
        })
        
        // For manual refresh, show refreshing indicator
        if (detail.eventId === 'manual-refresh' && isMounted) {
          setRefreshing(true)
        }
        
        // For manual refresh, the background refresh is already triggered by the button
        // Just reload to show updated items (background refresh will update cache in the background)
        // For other updates (like event changes), also just reload
        loadRssFeeds(true)
      }
    }

    window.addEventListener('rssFeedListUpdated', handleRssFeedListUpdate as EventListener)

    return () => {
      isMounted = false
      isLoading = false
      if (timeoutId) {
        clearTimeout(timeoutId)
      }
      // Abort any in-flight requests
      abortController.abort()
      window.removeEventListener('rssFeedListUpdated', handleRssFeedListUpdate as EventListener)
    }
  }, [pubkey, rssFeedListEvent, t])

  // Normalize feed URL to prevent duplicates (e.g., with/without trailing slash)
  // This matches the normalization used in rss-feed.service.ts
  const normalizeFeedUrl = (url: string): string => {
    return url.trim().replace(/\/$/, '')
  }

  // Get unique feed URLs and titles from items
  // Normalize URLs to prevent duplicates (e.g., with/without trailing slash)
  const availableFeeds = useMemo(() => {
    const feedMap = new Map<string, { url: string; title: string }>()

    mergedRssItems.forEach((item) => {
      const normalizedUrl = normalizeFeedUrl(item.feedUrl)
      if (!feedMap.has(normalizedUrl)) {
        const profile = getStandardRssFeedProfile(normalizedUrl)
        const fallback = profile
          ? t(profile.labelKey, { defaultValue: profile.defaultLabel })
          : getRssFeedUrlHostname(normalizedUrl)
        feedMap.set(normalizedUrl, {
          url: normalizedUrl,
          title: item.feedTitle?.trim() || fallback
        })
      }
    })
    return Array.from(feedMap.values())
  }, [mergedRssItems, t])

  // Helper function to truncate text
  const truncateText = (text: string, maxLength: number): string => {
    if (text.length <= maxLength) return text
    return text.slice(0, maxLength) + '...'
  }

  // Handle feed selection change
  const handleFeedToggle = (feedUrl: string, checked: boolean) => {
    if (feedUrl === 'all') {
      // If "all" is checked, clear all other selections
      setSelectedFeeds(checked ? ['all'] : [])
    } else {
      // If a specific feed is checked, remove "all" if present
      setSelectedFeeds(prev => {
        const newSelection = checked
          ? [...prev.filter(f => f !== 'all'), feedUrl]
          : prev.filter(f => f !== feedUrl)
        // If nothing is selected, default to "all"
        return newSelection.length === 0 ? ['all'] : newSelection
      })
    }
  }

  /** Single-select value for the RSS tab source dropdown (multi-select from filter popover → `__multi__`). */
  const rssSourceDropdownValue = useMemo(() => {
    if (selectedFeeds.includes('all') || selectedFeeds.length === 0) return 'all'
    const urls = selectedFeeds.filter((f) => f !== 'all').map((f) => normalizeFeedUrl(f))
    if (urls.length === 1) return urls[0]!
    return '__multi__'
  }, [selectedFeeds])

  const onRssSourceSelect = useCallback((value: string) => {
    if (value === 'all') setSelectedFeeds(['all'])
    else setSelectedFeeds([value.trim().replace(/\/$/, '')])
  }, [])

  /** Feed + time only (search is applied after merge so URL rows and links match too). */
  const baseFilteredItems = useMemo(() => {
    let filtered = mergedRssItems

    if (!selectedFeeds.includes('all') && selectedFeeds.length > 0) {
      const normalizedSelectedFeeds = selectedFeeds.map((f) => normalizeFeedUrl(f))
      filtered = filtered.filter((item) =>
        normalizedSelectedFeeds.includes(normalizeFeedUrl(item.feedUrl))
      )
    }

    const cutoffTime = getRssTimeFilterCutoffMs(timeFilter)
    if (cutoffTime !== null) {
      filtered = filtered.filter((item) => {
        if (!item.pubDate) return false
        return item.pubDate.getTime() >= cutoffTime
      })
    }

    return filtered
  }, [mergedRssItems, selectedFeeds, timeFilter])

  /** When “hide clutter” is on, drop those entries from the feed (not only from URL cards). */
  const rssWebItemsRespectingClutterPref = useMemo(() => {
    if (!hideUnifiedClutter) return baseFilteredItems
    return baseFilteredItems.filter((item) => {
      const link = item.link?.trim()
      if (!link || !isHttpArticleUrl(link)) return true
      return !isRssWebUnifiedClutterUrl(link)
    })
  }, [baseFilteredItems, hideUnifiedClutter])

  const rssItemMatchesSearch = useCallback((item: TRssFeedItem, q: string) => {
    const query = q.toLowerCase().trim()
    if (!query) return true
    return (
      item.title.toLowerCase().includes(query) ||
      item.description.toLowerCase().includes(query) ||
      (item.feedTitle || '').toLowerCase().includes(query) ||
      (item.link || '').toLowerCase().includes(query) ||
      (item.guid || '').toLowerCase().includes(query) ||
      (item.feedUrl || '').toLowerCase().includes(query) ||
      (item.feedDescription || '').toLowerCase().includes(query) ||
      (item.feedImage || '').toLowerCase().includes(query)
    )
  }, [])

  /** Match article / preview URLs and hostname (e.g. "spiegel" → www.spiegel.de paths). */
  const articleHttpUrlMatchesSearch = useCallback((url: string, q: string) => {
    const query = q.toLowerCase().trim()
    if (!query) return true
    const lower = url.toLowerCase()
    if (lower.includes(query)) return true
    try {
      const host = new URL(url).hostname.toLowerCase()
      if (host.includes(query)) return true
    } catch {
      /* ignore */
    }
    return false
  }, [])

  type CombinedFeedRow =
    | {
        kind: 'web'
        canonicalUrl: string
        rssItems: TRssFeedItem[]
        latestPub: number
      }
    | { kind: 'rss'; item: TRssFeedItem }

  type UnifiedFeedRow =
    | { kind: 'url'; canonicalUrl: string; rssItems: TRssFeedItem[] }
    | { kind: 'rssEntry'; item: TRssFeedItem }

  const [feedScope, setFeedScope] = useState<RssWebFeedScope>('urls')

  useEffect(() => {
    const handler = () => {
      setRelayDiscoveryTick((n) => n + 1)
      refreshManualWebUrls()
      refreshPromotedThreadUrls()
    }
    window.addEventListener(WEB_EXTERNAL_REACTION_PUBLISHED_EVENT, handler)
    return () => window.removeEventListener(WEB_EXTERNAL_REACTION_PUBLISHED_EVENT, handler)
  }, [refreshManualWebUrls, refreshPromotedThreadUrls])

  useEffect(() => {
    let cancelled = false
    void (async () => {
      try {
        const discovered = await fetchDiscoveredWebUrlsFromRelays({
          accountPubkey: pubkey,
          favoriteRelays: favoriteRelays ?? [],
          blockedRelays: blockedRelays ?? [],
          excludeClutterUrls: hideUnifiedClutter
        })
        if (cancelled) return
        setRelayDiscoveredUrls(discovered)
        const didMerge = await mergeDiscoveredRssWebUrls(discovered)
        if (didMerge && !cancelled) refreshManualWebUrls()
      } catch {
        if (!cancelled) setRelayDiscoveredUrls([])
      }
    })()
    return () => {
      cancelled = true
    }
  }, [pubkey, favoriteRelays, blockedRelays, refreshManualWebUrls, relayDiscoveryTick, hideUnifiedClutter])

  const combinedFeedRows = useMemo((): CombinedFeedRow[] => {
    const { webRows, nonHttpItems } = buildArticleUrlFeedRows(
      rssWebItemsRespectingClutterPref,
      manualWebEntries,
      relayDiscoveredUrls,
      { excludeClutterLinks: hideUnifiedClutter }
    )
    const rest: CombinedFeedRow[] = nonHttpItems.map((item) => ({
      kind: 'rss' as const,
      item
    }))
    const merged = [...webRows, ...rest].sort((a, b) => {
      const ta = a.kind === 'web' ? a.latestPub : (a.item.pubDate?.getTime() ?? 0)
      const tb = b.kind === 'web' ? b.latestPub : (b.item.pubDate?.getTime() ?? 0)
      return tb - ta
    })
    if (!suppressClawstrLinks) return merged
    return merged.filter((row) => {
      if (row.kind === 'web') return !isClawstrDotComHttpUrl(row.canonicalUrl)
      return !rssFeedItemArticleIsClawstrHost(row.item)
    })
  }, [
    rssWebItemsRespectingClutterPref,
    manualWebEntries,
    relayDiscoveredUrls,
    suppressClawstrLinks,
    hideUnifiedClutter
  ])

  /** Time window applies to URL cards too (latestPub / item pubDate), not only RSS rows. */
  const combinedFeedRowsInTimeRange = useMemo((): CombinedFeedRow[] => {
    const cutoff = getRssTimeFilterCutoffMs(timeFilter)
    if (cutoff === null) return combinedFeedRows
    return combinedFeedRows.filter((row) => {
      if (row.kind === 'rss') {
        const ts = row.item.pubDate?.getTime() ?? 0
        return ts >= cutoff
      }
      return row.latestPub >= cutoff
    })
  }, [combinedFeedRows, timeFilter])

  const combinedFeedRowsForSearch = useMemo((): CombinedFeedRow[] => {
    const q = searchQuery.trim()
    if (!q) return combinedFeedRowsInTimeRange
    return combinedFeedRowsInTimeRange.filter((row) => {
      if (row.kind === 'rss') {
        return rssItemMatchesSearch(row.item, q)
      }
      if (articleHttpUrlMatchesSearch(row.canonicalUrl, q)) return true
      return row.rssItems.some((it) => rssItemMatchesSearch(it, q))
    })
  }, [combinedFeedRowsInTimeRange, searchQuery, rssItemMatchesSearch, articleHttpUrlMatchesSearch])

  const urlScopeRows = useMemo((): UnifiedFeedRow[] => {
    return combinedFeedRowsForSearch
      .filter(
        (r): r is Extract<CombinedFeedRow, { kind: 'web' }> =>
          r.kind === 'web' &&
          (!rssWebRowHasRealFeedItems(r.rssItems) || promotedThreadUrlSet.has(r.canonicalUrl))
      )
      .sort((a, b) => b.latestPub - a.latestPub)
      .map((r) => ({
        kind: 'url' as const,
        canonicalUrl: r.canonicalUrl,
        rssItems: r.rssItems
      }))
  }, [combinedFeedRowsForSearch, promotedThreadUrlSet])

  const rssScopeRows = useMemo((): UnifiedFeedRow[] => {
    const picked = combinedFeedRowsForSearch.filter((r) => {
      if (r.kind === 'rss') {
        const link = r.item.link?.trim()
        if (link && isHttpArticleUrl(link)) {
          if (promotedThreadUrlSet.has(canonicalizeRssArticleUrl(link))) return false
        }
        return true
      }
      if (r.kind === 'web' && rssWebRowHasRealFeedItems(r.rssItems)) {
        return !promotedThreadUrlSet.has(r.canonicalUrl)
      }
      return false
    })
    const sorted = [...picked].sort((a, b) => {
      const ta = a.kind === 'web' ? a.latestPub : (a.item.pubDate?.getTime() ?? 0)
      const tb = b.kind === 'web' ? b.latestPub : (b.item.pubDate?.getTime() ?? 0)
      return tb - ta
    })
    return sorted.map((r) =>
      r.kind === 'web'
        ? {
            kind: 'url' as const,
            canonicalUrl: r.canonicalUrl,
            rssItems: r.rssItems
          }
        : { kind: 'rssEntry' as const, item: r.item }
    )
  }, [combinedFeedRowsForSearch, promotedThreadUrlSet])

  const feedDisplayBase = useMemo(
    () => ({ rows: feedScope === 'urls' ? urlScopeRows : rssScopeRows }),
    [feedScope, urlScopeRows, rssScopeRows]
  )

  const persistSuppressClawstr = useCallback((checked: boolean) => {
    rssWebPrefsUserTouchedRef.current = true
    setSuppressClawstrLinks(checked)
    void saveRssWebSuppressClawstrPreference(checked)
  }, [])

  const persistHideUnifiedClutter = useCallback((checked: boolean) => {
    rssWebPrefsUserTouchedRef.current = true
    setHideUnifiedClutter(checked)
    void saveRssWebHideUnifiedClutterPreference(checked)
  }, [])

  const persistFeedScope = useCallback((scope: RssWebFeedScope) => {
    rssWebPrefsUserTouchedRef.current = true
    setFeedScope(scope)
    void saveRssWebFeedScopePreference(scope)
  }, [])

  useEffect(() => {
    let cancelled = false
    void (async () => {
      const [suppressClawstr, hideClutter, scope] = await Promise.all([
        loadRssWebSuppressClawstrPreference(),
        loadRssWebHideUnifiedClutterPreference(),
        loadRssWebFeedScopePreference()
      ])
      if (cancelled || rssWebPrefsUserTouchedRef.current) return
      setSuppressClawstrLinks(suppressClawstr)
      setHideUnifiedClutter(hideClutter)
      setFeedScope(scope)
    })()
    return () => {
      cancelled = true
    }
  }, [])

  const feedTotalCount = feedDisplayBase.rows.length

  // Reset pagination when filters change
  useEffect(() => {
    setShowRowCount(20)
  }, [selectedFeeds, timeFilter, searchQuery, feedScope, suppressClawstrLinks, hideUnifiedClutter])

  const displayedFeed = useMemo(
    () => ({ rows: feedDisplayBase.rows.slice(0, showRowCount) }),
    [feedDisplayBase, showRowCount]
  )

  const displayedCount = displayedFeed.rows.length

  // IntersectionObserver for infinite scroll
  useEffect(() => {
    if (!bottomRef.current || displayedCount >= feedTotalCount) return

    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && displayedCount < feedTotalCount) {
          setShowRowCount((prev) => Math.min(prev + 20, feedTotalCount))
        }
      },
      { root: null, rootMargin: '100px', threshold: 0.1 }
    )

    observer.observe(bottomRef.current)

    return () => {
      observer.disconnect()
    }
  }, [displayedCount, feedTotalCount])

  // Get display text for feed selector
  const feedSelectorText = useMemo(() => {
    if (selectedFeeds.includes('all') || selectedFeeds.length === 0) {
      return t('All feeds')
    }
    if (selectedFeeds.length === 1) {
      const feed = availableFeeds.find(f => f.url === selectedFeeds[0])
      return feed ? truncateText(feed.title, 50) : t('All feeds')
    }
    return t('{{count}} feeds', { count: selectedFeeds.length })
  }, [selectedFeeds, availableFeeds, t])

  if (loading) {
    return (
      <div className="space-y-3 px-4 py-8" role="status" aria-busy="true" aria-live="polite">
        <p className="text-sm text-muted-foreground">{t('Loading RSS feeds...')}</p>
        {Array.from({ length: 6 }).map((_, i) => (
          <Skeleton key={i} className="h-24 w-full rounded-lg" />
        ))}
      </div>
    )
  }

  if (error) {
    return (
      <div className="flex flex-col items-center justify-center py-12 px-4">
        <AlertCircle className="h-8 w-8 text-destructive mb-4" />
        <p className="text-sm text-destructive text-center">{error}</p>
      </div>
    )
  }

  if (mergedRssItems.length === 0 && manualWebEntries.length === 0) {
    return (
      <div className="space-y-4 px-4 py-6">
        <ManualRssUrlAddRow onUrlAdded={refreshManualWebUrls} />
        <p className="text-sm text-muted-foreground text-center">{t('No RSS feed items available')}</p>
      </div>
    )
  }

  return (
    <RssFeedDisplayPrefsProvider value={{ suppressClawstrLinks }}>
    <div className="space-y-3">
      {/* Feed header — view mode, display prefs, counts */}
      <div className="sticky top-0 z-10 space-y-1.5 border-b bg-background px-4 py-2">
        <div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between">
          <div className="flex flex-wrap items-center gap-2 sm:gap-3">
            <div
              className="inline-flex max-w-full flex-wrap rounded-md border border-border bg-muted/30 p-0.5 sm:flex-nowrap"
              role="group"
              aria-label={t('RSS feed view mode')}
            >
              <Button
                type="button"
                variant={feedScope === 'urls' ? 'secondary' : 'ghost'}
                size="sm"
                className="h-7 rounded-sm px-2 text-[11px] font-normal shadow-none sm:px-2.5 sm:text-xs"
                onClick={() => persistFeedScope('urls')}
              >
                {t('URLs')}
              </Button>
              <Button
                type="button"
                variant={feedScope === 'rss' ? 'secondary' : 'ghost'}
                size="sm"
                className="h-7 rounded-sm px-2 text-[11px] font-normal shadow-none sm:px-2.5 sm:text-xs"
                onClick={() => persistFeedScope('rss')}
              >
                {t('RSS')}
              </Button>
            </div>
            <div className="flex flex-wrap items-center gap-x-4 gap-y-2">
              <div className="flex items-center gap-2">
                <Checkbox
                  id="suppress-clawstr-links"
                  checked={suppressClawstrLinks}
                  onCheckedChange={(c) => persistSuppressClawstr(c === true)}
                />
                <Label
                  htmlFor="suppress-clawstr-links"
                  className="cursor-pointer text-xs text-muted-foreground"
                >
                  {t('Suppress Clawstr links in RSS previews')}
                </Label>
              </div>
              <div className="flex items-center gap-2">
                <Checkbox
                  id="hide-unified-clutter"
                  checked={hideUnifiedClutter}
                  onCheckedChange={(c) => persistHideUnifiedClutter(c === true)}
                />
                <Label
                  htmlFor="hide-unified-clutter"
                  className="cursor-pointer text-xs text-muted-foreground"
                >
                  {t('Hide local, media & feed URLs from URL cards')}
                </Label>
              </div>
            </div>
          </div>
          <p className="text-xs text-muted-foreground sm:text-right">
            {t('Showing {{filtered}} of {{total}} entries', {
              filtered: displayedCount,
              total: feedTotalCount
            })}
          </p>
        </div>
        <div className="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-end">
          <div className="relative w-full max-w-xl flex-1 min-w-0">
            <Search className="absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground sm:h-4 sm:w-4" />
            <Input
              type="search"
              placeholder={t('Search...')}
              value={searchQuery}
              onChange={(e) => setSearchQuery(e.target.value)}
              className="h-8 w-full pl-8 text-xs sm:h-9 sm:pl-9 sm:text-sm"
              aria-label={t('Search...')}
            />
          </div>
          <div className="flex flex-wrap items-end gap-3">
            <div className="flex min-w-[10rem] flex-col gap-0.5">
              <Label htmlFor="rss-time-range" className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground sm:text-xs">
                {t('Time range')}
              </Label>
              <Select value={timeFilter} onValueChange={setTimeFilter}>
                <SelectTrigger id="rss-time-range" className="h-8 text-xs sm:h-9 sm:text-sm">
                  <SelectValue placeholder={t('All time')} />
                </SelectTrigger>
                <SelectContent>
                  <SelectItem value="all">{t('All time')}</SelectItem>
                  <SelectItem value="hour">{t('Last hour')}</SelectItem>
                  <SelectItem value="hours24">{t('Last 24 hours')}</SelectItem>
                  <SelectItem value="hours48">{t('Last 48 hours')}</SelectItem>
                  <SelectItem value="week">{t('Last week')}</SelectItem>
                  <SelectItem value="month">{t('Last month')}</SelectItem>
                </SelectContent>
              </Select>
            </div>
            {feedScope === 'rss' ? (
              <div className="flex min-w-[12rem] flex-col gap-0.5">
                <Label htmlFor="rss-source-filter" className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground sm:text-xs">
                  {t('Filter by RSS source')}
                </Label>
                <Select value={rssSourceDropdownValue} onValueChange={onRssSourceSelect}>
                  <SelectTrigger id="rss-source-filter" className="h-8 text-xs sm:h-9 sm:text-sm">
                    <SelectValue placeholder={t('All feeds')} />
                  </SelectTrigger>
                  <SelectContent>
                    <SelectItem value="all">{t('All feeds')}</SelectItem>
                    {rssSourceDropdownValue === '__multi__' ? (
                      <SelectItem value="__multi__" disabled>
                        {t('{{count}} feeds', { count: selectedFeeds.filter((f) => f !== 'all').length })}
                      </SelectItem>
                    ) : null}
                    {availableFeeds.map((feed) => (
                      <SelectItem key={feed.url} value={feed.url}>
                        <span className="truncate">{feed.title}</span>
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              </div>
            ) : null}
          </div>
        </div>
      </div>

      {/* Filter Bar - Collapsible */}
      {showFilters && (
        <div className="sticky top-[2.5rem] z-10 bg-background border-b px-4 py-2">
          <div className={`flex ${isSmallScreen ? 'flex-col' : 'flex-row'} items-stretch gap-2`}>
            {/* Feed Selector - Multi-select with Popover */}
            <Popover open={feedPopoverOpen} onOpenChange={setFeedPopoverOpen}>
              <PopoverTrigger asChild>
                <Button
                  variant="outline"
                  role="combobox"
                  className="h-8 text-xs md:text-sm md:h-9 flex-shrink-0 w-full md:w-auto justify-between"
                  style={{ minWidth: isSmallScreen ? '100%' : '300px' }}
                >
                  <span className="truncate">{feedSelectorText}</span>
                  <ChevronDown className="ml-2 h-3 w-3 md:h-4 md:w-4 shrink-0 opacity-50" />
                </Button>
              </PopoverTrigger>
              <PopoverContent className={`${isSmallScreen ? 'w-[calc(100vw-2rem)]' : 'w-[calc(100vw-2rem)] max-w-[400px]'} p-0`} align="start">
                <div className="max-h-[300px] overflow-y-auto">
                  <div className="p-2">
                    {/* All feeds option */}
                    <div
                      className="flex items-center space-x-2 p-2 rounded-sm hover:bg-accent cursor-pointer"
                      onClick={() => {
                        const isAllSelected = selectedFeeds.includes('all')
                        handleFeedToggle('all', !isAllSelected)
                      }}
                    >
                      <div className="flex items-center justify-center w-4 h-4 border border-border rounded">
                        {selectedFeeds.includes('all') && <Check className="w-3 h-3" />}
                      </div>
                      <label className="text-sm cursor-pointer flex-1">
                        {t('All feeds')}
                      </label>
                    </div>
                    {/* Individual feed options */}
                    {availableFeeds.map((feed) => {
                      const isChecked = selectedFeeds.includes(feed.url)
                      return (
                        <div
                          key={feed.url}
                          className="flex items-center space-x-2 p-2 rounded-sm hover:bg-accent cursor-pointer"
                          onClick={() => handleFeedToggle(feed.url, !isChecked)}
                        >
                          <div className="flex items-center justify-center w-4 h-4 border border-border rounded">
                            {isChecked && <Check className="w-3 h-3" />}
                          </div>
                          <label className="text-sm cursor-pointer flex-1 min-w-0" title={feed.title}>
                            <StandardRssFeedUrlInline
                              feedUrl={feed.url}
                              title={feed.title}
                              maxLength={50}
                            />
                          </label>
                        </div>
                      )
                    })}
                  </div>
                </div>
              </PopoverContent>
            </Popover>
          </div>
        </div>
      )}

      {/* Content */}
      <div className="space-y-4 px-4 py-3">
        <ManualRssUrlAddRow onUrlAdded={refreshManualWebUrls} />
        {refreshing && (
          <div className="flex items-center gap-2 border-b py-2" role="status" aria-busy="true">
            <Skeleton className="h-4 w-4 shrink-0 rounded-sm" aria-hidden />
            <Skeleton className="h-4 flex-1 max-w-[200px]" />
          </div>
        )}
        
        {feedTotalCount === 0 ? (
          <div className="flex flex-col items-center justify-center py-12">
            <p className="text-sm text-muted-foreground">
              {searchQuery || (!selectedFeeds.includes('all') && selectedFeeds.length > 0) || timeFilter !== 'all'
                ? t('No items match your filters')
                : feedScope === 'urls'
                  ? t('No URL-only items yet')
                  : t('No RSS feed items available')}
            </p>
          </div>
        ) : feedScope === 'urls' ? (
          <>
            <ArticleUrlsSection subtitleKey="Article URLs Nostr manual subtitle">
              {displayedFeed.rows
                .filter((r): r is Extract<UnifiedFeedRow, { kind: 'url' }> => r.kind === 'url')
                .map((row) => (
                  <RssWebFeedCard
                    key={row.canonicalUrl}
                    canonicalUrl={row.canonicalUrl}
                    rssItems={row.rssItems}
                  />
                ))}
            </ArticleUrlsSection>
            {displayedCount < feedTotalCount ? (
              <div ref={bottomRef} className="flex justify-center py-4">
                <Skeleton className="h-8 w-8 rounded-md" aria-hidden />
              </div>
            ) : null}
          </>
        ) : (
          <>
            <RssUnifiedScopeSection>
              {displayedFeed.rows.map((row) =>
                row.kind === 'url' ? (
                  <RssWebFeedCard
                    key={row.canonicalUrl}
                    canonicalUrl={row.canonicalUrl}
                    rssItems={row.rssItems}
                    rssColumnReadOnly
                  />
                ) : (
                  <div
                    key={`${row.item.feedUrl}-${row.item.guid}`}
                    className="overflow-hidden rounded-xl border border-border bg-card"
                  >
                    <RssFeedItem
                      item={row.item}
                      layout="list"
                      sourceStrip="rss"
                      className="rounded-none border-0 bg-transparent shadow-none"
                      rssEntryReadOnlyMode
                      onAfterPromoteRss={() => {
                        refreshManualWebUrls()
                        refreshPromotedThreadUrls()
                      }}
                    />
                  </div>
                )
              )}
            </RssUnifiedScopeSection>
            {displayedCount < feedTotalCount ? (
              <div ref={bottomRef} className="flex justify-center py-4">
                <Skeleton className="h-8 w-8 rounded-md" aria-hidden />
              </div>
            ) : null}
          </>
        )}
      </div>
    </div>
    </RssFeedDisplayPrefsProvider>
  )
}

