import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { AlertTriangle, Clock, Hash, Ticket, Loader2, RefreshCw } from 'lucide-react'
import { aiSessionsApi } from '@/api'
import type { AISessionSummary } from '@/types/ai-session'
import { timeAgo } from '@/lib/timeAgo'
import { cn } from '@/lib/utils'

interface EscalationQueueProps {
  onPickup?: (sessionId: string) => void
  onCountChange?: (count: number) => void
}

// Static list sort: oldest-first. Longest waiting = most urgent.
const sortOldestFirst = (a: AISessionSummary, b: AISessionSummary) =>
  new Date(a.created_at).getTime() - new Date(b.created_at).getTime()

// Live-arrival bucket sort: newest-first so the most recent escalation is at
// the very top of the list.
const sortNewestFirst = (a: AISessionSummary, b: AISessionSummary) =>
  new Date(b.created_at).getTime() - new Date(a.created_at).getTime()

// How long a freshly-arrived card keeps the slide-in animation class. The
// keyframe itself runs 200ms; this just keeps the class on the DOM long
// enough for the animation to finish before React removes it on the next
// state transition.
const NEW_CARD_HIGHLIGHT_MS = 800

// localStorage key for the per-user "seen" set. Tracks session IDs the user
// has acknowledged so the unread dot doesn't reappear on refresh. Bounded to
// the last `SEEN_CAP` entries to avoid unbounded growth on long-lived
// accounts.
const SEEN_STORAGE_KEY = 'rf-escalation-seen'
const SEEN_CAP = 200

function loadSeenIds(): Set<string> {
  try {
    const raw = localStorage.getItem(SEEN_STORAGE_KEY)
    if (!raw) return new Set()
    const parsed = JSON.parse(raw) as unknown
    if (!Array.isArray(parsed)) return new Set()
    return new Set(parsed.filter((v): v is string => typeof v === 'string'))
  } catch {
    return new Set()
  }
}

function saveSeenIds(ids: Set<string>): void {
  try {
    const arr = Array.from(ids).slice(-SEEN_CAP)
    localStorage.setItem(SEEN_STORAGE_KEY, JSON.stringify(arr))
  } catch {
    // localStorage unavailable / quota — silent. The dot just won't persist.
  }
}

function waitTimeColor(createdAt: string): string {
  const hours = (Date.now() - new Date(createdAt).getTime()) / 3_600_000
  if (hours >= 4) return '#f87171'   // danger
  if (hours >= 1) return '#fbbf24'   // warning/amber
  return '#848b9b'                    // muted
}

export function EscalationQueue({ onPickup, onCountChange }: EscalationQueueProps) {
  const navigate = useNavigate()
  const [sessions, setSessions] = useState<AISessionSummary[]>([])
  const [isLoading, setIsLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)
  // Session IDs that arrived via SSE and should still play the slide-in.
  const [newIds, setNewIds] = useState<Set<string>>(new Set())
  // Track count of unseen arrivals while the tab is backgrounded.
  const [unseenCount, setUnseenCount] = useState(0)
  // Per-user seen set persisted in localStorage. Cleared on open, claim, or
  // explicit dismiss (NOT on hover — Codex correction). The unread dot is
  // shown for any session id NOT in this set.
  const [seenIds, setSeenIds] = useState<Set<string>>(() => loadSeenIds())

  const markSeen = useCallback((sessionId: string) => {
    setSeenIds(prev => {
      if (prev.has(sessionId)) return prev
      const next = new Set(prev)
      next.add(sessionId)
      saveSeenIds(next)
      return next
    })
  }, [])

  // Ref mirrors the latest sessions so the SSE handler can diff without
  // re-binding on every state change.
  const sessionsRef = useRef<AISessionSummary[]>([])
  useEffect(() => {
    sessionsRef.current = sessions
  }, [sessions])

  const prefersReducedMotion = useMemo(() => {
    if (typeof window === 'undefined' || !window.matchMedia) return false
    return window.matchMedia('(prefers-reduced-motion: reduce)').matches
  }, [])

  // ── Tab title flash ──
  // Capture the original title once at mount. While unseen > 0, prefix it.
  const originalTitleRef = useRef<string>('')
  useEffect(() => {
    originalTitleRef.current = document.title
    return () => {
      // Restore on unmount so a leftover "(N) ..." prefix doesn't bleed
      // into the next page.
      document.title = originalTitleRef.current
    }
  }, [])

  useEffect(() => {
    const base = originalTitleRef.current || document.title
    document.title = unseenCount > 0 ? `(${unseenCount}) ${base}` : base
  }, [unseenCount])

  useEffect(() => {
    const clearUnseen = () => {
      if (!document.hidden) setUnseenCount(0)
    }
    const onFocus = () => setUnseenCount(0)
    document.addEventListener('visibilitychange', clearUnseen)
    window.addEventListener('focus', onFocus)
    return () => {
      document.removeEventListener('visibilitychange', clearUnseen)
      window.removeEventListener('focus', onFocus)
    }
  }, [])

  const loadQueue = useCallback(async () => {
    setIsLoading(true)
    setError(null)
    try {
      const data = await aiSessionsApi.getEscalationQueue()
      const sorted = [...data].sort(sortOldestFirst)
      setSessions(sorted)
      setNewIds(new Set())
      onCountChange?.(sorted.length)
    } catch {
      setError('Failed to load escalation queue')
    } finally {
      setIsLoading(false)
    }
  }, [onCountChange])

  useEffect(() => {
    loadQueue()
  }, [loadQueue])

  // ── SSE subscription ──
  // Refetch the queue on each `handoff_created` event (the event payload is
  // intentionally thin — it's a trigger, not the full card data). Diff
  // against the previous list to identify newly-arrived sessions; prepend
  // them at the top with the slide-in animation, then keep the rest of the
  // queue in oldest-first order below.
  const handleHandoffCreated = useCallback(async () => {
    let fresh: AISessionSummary[]
    try {
      fresh = await aiSessionsApi.getEscalationQueue()
    } catch {
      return
    }

    const prevIds = new Set(sessionsRef.current.map((s) => s.id))
    const arrived = fresh.filter((s) => !prevIds.has(s.id)).sort(sortNewestFirst)
    const established = fresh.filter((s) => prevIds.has(s.id)).sort(sortOldestFirst)
    const next = [...arrived, ...established]
    setSessions(next)
    onCountChange?.(next.length)

    if (arrived.length === 0) return

    const arrivedIds = arrived.map((s) => s.id)
    setNewIds((prev) => {
      const merged = new Set(prev)
      arrivedIds.forEach((id) => merged.add(id))
      return merged
    })
    if (document.hidden) {
      setUnseenCount((c) => c + arrived.length)
    }
    window.setTimeout(() => {
      setNewIds((prev) => {
        const remaining = new Set(prev)
        arrivedIds.forEach((id) => remaining.delete(id))
        return remaining
      })
    }, NEW_CARD_HIGHLIGHT_MS)
  }, [onCountChange])

  useEffect(() => {
    const abort = new AbortController()
    let reconnectTimer: number | null = null
    let attempt = 0
    let cancelled = false

    const connect = async () => {
      if (cancelled) return
      try {
        await aiSessionsApi.streamEscalations(
          {
            onReady: () => {
              attempt = 0
            },
            onHandoffCreated: () => {
              void handleHandoffCreated()
            },
          },
          abort.signal,
        )
        // Stream ended cleanly (server hung up). Reconnect quickly.
        if (!cancelled) {
          reconnectTimer = window.setTimeout(connect, 1000)
        }
      } catch (err) {
        if (cancelled || abort.signal.aborted) return
        if (err instanceof DOMException && err.name === 'AbortError') return
        // Exponential backoff: 1s, 2s, 4s, 8s, 16s, capped at 30s.
        const delay = Math.min(30_000, 1000 * 2 ** attempt)
        attempt += 1
        reconnectTimer = window.setTimeout(connect, delay)
      }
    }

    void connect()

    return () => {
      cancelled = true
      abort.abort()
      if (reconnectTimer !== null) window.clearTimeout(reconnectTimer)
    }
  }, [handleHandoffCreated])

  const handlePickup = (sessionId: string) => {
    markSeen(sessionId)
    if (onPickup) {
      onPickup(sessionId)
    } else {
      navigate(`/pilot/${sessionId}?pickup=true`)
    }
  }

  // Click on the card body (anywhere outside Pick Up) marks the session as
  // seen — the "open" affordance from the unread-dot spec. Pick Up handles
  // its own marking via handlePickup. Hover deliberately does NOT clear
  // (Codex correction).
  const handleCardOpen = (sessionId: string) => {
    markSeen(sessionId)
  }

  if (isLoading) {
    return (
      <div className="flex items-center justify-center py-12">
        <Loader2 size={20} className="animate-spin text-muted-foreground" />
      </div>
    )
  }

  if (error) {
    return (
      <div className="py-12 text-center">
        <p className="text-sm text-danger">{error}</p>
        <button
          onClick={loadQueue}
          className="mt-2 text-xs text-muted-foreground hover:text-foreground transition-colors"
        >
          Try again
        </button>
      </div>
    )
  }

  if (sessions.length === 0) {
    return (
      <div className="py-12 text-center">
        <AlertTriangle size={24} className="mx-auto mb-2 text-muted-foreground/40" />
        <p className="text-sm text-muted-foreground">No sessions awaiting escalation</p>
        <button
          onClick={loadQueue}
          className="mt-3 flex items-center gap-1.5 mx-auto text-xs text-muted-foreground hover:text-foreground transition-colors"
        >
          <RefreshCw size={12} />
          Refresh
        </button>
      </div>
    )
  }

  return (
    <div className="space-y-3">
      <div className="flex items-center justify-between px-1">
        <h3
          className="font-sans text-[0.625rem] uppercase tracking-wider text-muted-foreground"
          aria-label={`${sessions.length} escalations awaiting pickup`}
        >
          Awaiting pickup ({sessions.length})
        </h3>
        <button
          onClick={loadQueue}
          className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
        >
          <RefreshCw size={10} />
          Refresh
        </button>
      </div>

      <div role="region" aria-live="polite" className="space-y-3">
        {sessions.map((session) => {
          const isNew = newIds.has(session.id)
          const isUnread = !seenIds.has(session.id)
          return (
            <div
              key={session.id}
              onClick={() => handleCardOpen(session.id)}
              className={cn(
                'relative card-flat p-3 sm:p-4 space-y-3 cursor-pointer',
                isNew && !prefersReducedMotion && 'animate-slide-in-bottom',
                isNew && prefersReducedMotion && 'animate-fade-in',
              )}
            >
              {/* Unread indicator: 6px dot, top-right corner. Cleared on
                  open (card click) or claim (Pick Up). Persists across
                  refresh via localStorage. */}
              {isUnread && (
                <span
                  aria-label="Unread escalation"
                  className="absolute top-2 right-2 inline-block w-1.5 h-1.5 rounded-full bg-accent"
                />
              )}
              <div>
                <p className="text-sm font-semibold text-foreground">
                  {session.problem_summary || 'Untitled session'}
                </p>
                {session.escalation_reason && (
                  <p className="mt-1 text-xs text-warning line-clamp-2">
                    Reason: {session.escalation_reason}
                  </p>
                )}
              </div>

              <div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
                {session.problem_domain && (
                  <span className="font-sans rounded-md bg-accent-dim px-1.5 py-0.5 text-[0.5625rem] uppercase tracking-wider text-accent-text">
                    {session.problem_domain}
                  </span>
                )}
                <span className="flex items-center gap-1">
                  <Hash size={10} />
                  {session.step_count} steps
                </span>
                <span
                  className="flex items-center gap-1 font-medium"
                  style={{ color: waitTimeColor(session.created_at) }}
                >
                  <Clock size={10} />
                  {timeAgo(session.created_at)}
                </span>
                {session.psa_ticket_id && (
                  <span className="flex items-center gap-1 text-accent-text">
                    <Ticket size={10} />
                    #{session.psa_ticket_id}
                  </span>
                )}
              </div>

              <div className="flex justify-end">
                <button
                  onClick={(e) => {
                    e.stopPropagation()
                    handlePickup(session.id)
                  }}
                  className="rounded-lg bg-primary text-white px-4 py-2.5 text-sm font-semibold hover:brightness-110 active:scale-[0.98] transition-all"
                >
                  Pick Up
                </button>
              </div>
            </div>
          )
        })}
      </div>
    </div>
  )
}
