'use client';

import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
import { useAuth } from '@/lib/auth-context';
import { adminApiFetch } from '@/lib/api';
import { useToast } from '@/lib/toast';

export type AppNotification = {
  id: number; section: string; type: string; title: string; body: string | null;
  href: string | null; refId: number | null; readAt: string | null; createdAt: string;
};

type Counts = { bySection: Record<string, number>; total: number };

type RealtimeValue = {
  connected: boolean;
  counts: Counts;
  notifications: AppNotification[];
  refreshNotifications: () => Promise<void>;
  /** Clear the badge for a menu section (or for one item inside it). */
  markRead: (section: string, refId?: number) => Promise<void>;
  markAllRead: () => Promise<void>;
  subscribe: (topics: string[], cb: () => void) => () => void;
};

const RealtimeContext = createContext<RealtimeValue | null>(null);
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || '';

/**
 * Live updates without refresh.
 *  • One WebSocket to the API (authenticated with a one-shot ticket).
 *  • Server says "tickets changed" → every page that subscribed to `tickets` re-fetches.
 *  • Server says "new notification" → sidebar badges + bell update instantly.
 *  • If the socket can't connect (proxy blocks it, offline), it retries with
 *    back-off AND falls back to light polling, so pages still stay fresh.
 */
export function RealtimeProvider({ children }: { children: React.ReactNode }) {
  const { accessToken, appSecretProof, locked, user } = useAuth();
  const { show } = useToast();
  const [connected, setConnected] = useState(false);
  const [counts, setCounts] = useState<Counts>({ bySection: {}, total: 0 });
  const [notifications, setNotifications] = useState<AppNotification[]>([]);

  const auth = useRef({ accessToken, appSecretProof });
  auth.current = { accessToken, appSecretProof };
  const listeners = useRef(new Map<string, Set<() => void>>());
  const debounce = useRef(new Map<string, ReturnType<typeof setTimeout>>());

  const fire = useCallback((topic: string) => {
    const set = listeners.current.get(topic);
    if (!set) return;
    // Bursts (a bulk update fires many events) collapse into one refetch.
    clearTimeout(debounce.current.get(topic));
    debounce.current.set(topic, setTimeout(() => set.forEach((cb) => cb()), 350));
  }, []);

  const subscribe = useCallback((topics: string[], cb: () => void) => {
    topics.forEach((t) => {
      if (!listeners.current.has(t)) listeners.current.set(t, new Set());
      listeners.current.get(t)!.add(cb);
    });
    return () => topics.forEach((t) => listeners.current.get(t)?.delete(cb));
  }, []);

  const refreshCounts = useCallback(async () => {
    const { accessToken: t, appSecretProof: p } = auth.current;
    if (!t || !p) return;
    try { setCounts(await adminApiFetch('/admin/notifications/counts', t, p)); } catch { /* keep last counts */ }
  }, []);

  const refreshNotifications = useCallback(async () => {
    const { accessToken: t, appSecretProof: p } = auth.current;
    if (!t || !p) return;
    try { setNotifications((await adminApiFetch('/admin/notifications', t, p)).notifications || []); } catch { /* ignore */ }
  }, []);

  const markRead = useCallback(async (section: string, refId?: number) => {
    const { accessToken: t, appSecretProof: p } = auth.current;
    if (!t || !p) return;
    // Optimistic: the badge clears the instant the page opens.
    setCounts((c) => {
      if (refId) return c; // one item: wait for the server's exact number
      const bySection = { ...c.bySection }; const n = bySection[section] || 0; delete bySection[section];
      return { bySection, total: Math.max(0, c.total - n) };
    });
    try {
      await adminApiFetch('/admin/notifications/read', t, p, { method: 'POST', body: JSON.stringify(refId ? { section, refId } : { section }) });
    } catch { /* counts reconcile on next refresh */ }
    refreshCounts(); refreshNotifications();
  }, [refreshCounts, refreshNotifications]);

  const markAllRead = useCallback(async () => {
    const { accessToken: t, appSecretProof: p } = auth.current;
    if (!t || !p) return;
    setCounts({ bySection: {}, total: 0 });
    try { await adminApiFetch('/admin/notifications/read', t, p, { method: 'POST', body: JSON.stringify({ all: true }) }); } catch { /* ignore */ }
    refreshNotifications();
  }, [refreshNotifications]);

  // Initial numbers as soon as we hold a valid session.
  const signedIn = !!accessToken && !locked;
  useEffect(() => { if (signedIn) { refreshCounts(); refreshNotifications(); } }, [signedIn, refreshCounts, refreshNotifications]);

  // WebSocket lifecycle.
  useEffect(() => {
    if (!signedIn || !user) return;
    let ws: WebSocket | null = null;
    let closed = false;
    let attempt = 0;
    let retry: ReturnType<typeof setTimeout>;

    async function connect() {
      const { accessToken: t, appSecretProof: p } = auth.current;
      if (closed || !t || !p) return;
      try {
        const { ticket, path } = await adminApiFetch('/admin/realtime/ticket', t, p, { method: 'POST' });
        const origin = API_BASE.replace(/\/api\/v1\/?$/, '').replace(/^http/, 'ws');
        ws = new WebSocket(`${origin}${path}?ticket=${encodeURIComponent(ticket)}`);
      } catch { schedule(); return; }

      ws.onopen = () => { attempt = 0; setConnected(true); };
      ws.onmessage = (ev) => {
        try {
          const msg = JSON.parse(ev.data);
          if (msg.type === 'entity') fire(msg.topic);
          else if (msg.type === 'notification') {
            refreshCounts(); refreshNotifications();
            show('info', msg.title);
          }
        } catch { /* ignore malformed frame */ }
      };
      ws.onclose = () => { setConnected(false); if (!closed) schedule(); };
      ws.onerror = () => { try { ws?.close(); } catch { /* ignore */ } };
    }
    function schedule() {
      attempt += 1;
      retry = setTimeout(connect, Math.min(30_000, 1000 * 2 ** Math.min(attempt, 5)));
    }
    connect();
    return () => { closed = true; clearTimeout(retry); try { ws?.close(); } catch { /* ignore */ } setConnected(false); };
  }, [signedIn, user, fire, refreshCounts, refreshNotifications, show]);

  // Safety net: while the socket is down, poll counts and nudge every subscribed page every 30 s.
  // While it's up, a slow reconcile (2 min) still corrects any missed event.
  useEffect(() => {
    if (!signedIn) return;
    const ms = connected ? 120_000 : 30_000;
    const t = setInterval(() => {
      refreshCounts();
      if (!connected) listeners.current.forEach((_set, topic) => fire(topic));
    }, ms);
    return () => clearInterval(t);
  }, [signedIn, connected, refreshCounts, fire]);

  return (
    <RealtimeContext.Provider value={{ connected, counts, notifications, refreshNotifications, markRead, markAllRead, subscribe }}>
      {children}
    </RealtimeContext.Provider>
  );
}

export function useRealtime() {
  const ctx = useContext(RealtimeContext);
  if (!ctx) throw new Error('useRealtime must be used within RealtimeProvider');
  return ctx;
}

/**
 * Re-run `refetch` whenever any of `topics` changes on the server — no refresh needed.
 *   useLiveRefresh(['tickets'], load);
 */
export function useLiveRefresh(topics: string[], refetch: () => void) {
  const { subscribe } = useRealtime();
  const ref = useRef(refetch);
  ref.current = refetch;
  const key = topics.join('|');
  useEffect(() => subscribe(topics, () => ref.current()), [subscribe, key]); // eslint-disable-line react-hooks/exhaustive-deps
}
