"use client";

import { useEffect, useRef } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { getEcho } from "@/lib/echo";
import { queryKeys } from "@/lib/query-keys";
import { useSession } from "@/features/auth/session";

/** How long a burst of order events is allowed to settle before the screens that show orders ask the server again. */
const REFETCH_WINDOW_MS = 1000;

/**
 * Keeps the back office current without asking the API on a timer.
 *
 * **The events carry facts, not data.** "Order 799078 changed" arrives as a handful of
 * identifiers and this marks the matching queries stale; React Query then refetches only
 * what is actually on screen. So the socket traffic is the same whether the fleet is running
 * ten orders or ten thousand, and a screen nobody is looking at costs nothing — which is the
 * opposite of a refresh interval, where every open tab pays for every table it has ever
 * rendered, forever, awake or not.
 *
 * The one exception is a notification: there the payload *is* the data, it is three short
 * strings, and the bell should move the instant it is written rather than on the next sweep.
 *
 * **Reconnection is the part that is easy to get wrong.** A socket that drops and comes back
 * has missed everything in between, and no event will ever arrive to say so. So every
 * reconnect refetches outright — the one moment polling's "ask again" behaviour is exactly
 * what is needed.
 */
export function RealtimeProvider({ children }: { children: React.ReactNode }) {
  const queryClient = useQueryClient();
  const { admin } = useSession();

  // Held in a ref so the effect below does not re-subscribe whenever a render happens.
  const adminUuid = admin?.uuid ?? null;
  const reconnected = useRef(false);

  useEffect(() => {
    const echo = getEcho();

    // No key configured, or nobody signed in: there is nothing to listen to and nothing to
    // listen as. The screens that still hold a refresh interval carry on as before.
    if (!echo || adminUuid === null) {
      return;
    }

    /*
     * Coalesced, and this is the part that decides whether any of this is actually faster.
     *
     * Events arrive per change, and changes come in bursts — a dispatcher assigning a dozen
     * orders, a sweep moving twenty at once. Refetching on each one would ask the API a dozen
     * times in a second for the same list, which is worse than the poll it replaced. So the
     * key prefixes pile up in a set and one refetch goes out when the burst stops.
     *
     * A trailing delay rather than a leading one, deliberately: the first event of a burst is
     * rarely the interesting one, and waiting a moment means the screen lands on the state
     * after the burst rather than flickering through the middle of it.
     *
     * One second, not a quarter of one: every refetch is a sorted, counted list query on the server,
     * and under steady order traffic a quarter second let each open screen ask four times a second.
     * A second still reads as live, and it caps each screen at one refresh a second however busy the
     * orders are.
     */
    const pending = new Set<string>();
    let flush: number | null = null;

    const invalidate = (keys: readonly unknown[][]) => {
      for (const key of keys) {
        pending.add(JSON.stringify(key));
      }

      if (flush !== null) {
        return;
      }

      flush = window.setTimeout(() => {
        flush = null;

        const keysToRefetch = [...pending].map((key) => JSON.parse(key) as unknown[]);
        pending.clear();

        /*
         * A hidden tab is marked stale but not refetched: nobody is reading it, and a wall of
         * background tabs each refetching on every event is exactly the cost this change was
         * meant to remove. The visibility listener below refetches when it is looked at again.
         */
        const refetchType = document.visibilityState === "visible" ? "active" : "none";

        for (const key of keysToRefetch) {
          void queryClient.invalidateQueries({ queryKey: key, refetchType });
        }
      }, REFETCH_WINDOW_MS);
    };

    const orders = echo
      .private("dashboard.orders")
      .listen(".order.touched", () => {
        /*
         * Both the lists and the overview: an order changing status moves a tile on the
         * dashboard as surely as it moves a row in the table. The detail of whichever order
         * is open refetches through the same prefix.
         */
        invalidate([["orders"], ["stats"], ["drivers"]]);
      });

    const personal = echo
      .private(`admin.${adminUuid}`)
      .listen(".notification.created", () => {
        invalidate([["notifications"], [queryKeys.notifications.unreadCount]]);
      });

    const push = echo
      .private("dashboard.push")
      .listen(".push.progressed", () => {
        invalidate([["push"]]);
      });

    /*
     * The gap. A dropped connection misses every event while it is down, and the server has
     * no way to tell us what we missed — so the first thing a restored connection does is
     * ask for everything again. Skipped on the very first connect, where there is no gap and
     * every screen has just loaded its own data.
     */
    const connection = (echo.connector as { pusher?: { connection?: { bind: (event: string, handler: () => void) => void } } }).pusher?.connection;

    connection?.bind("connected", () => {
      if (reconnected.current) {
        void queryClient.invalidateQueries();
      }
      reconnected.current = true;
    });

    /*
     * Coming back to the tab. Everything that changed while it was hidden was marked stale
     * and left alone, so this is where that debt is paid — once, for whatever screen is
     * actually open, instead of continuously for a tab nobody was reading.
     */
    const onVisible = () => {
      if (document.visibilityState === "visible") {
        void queryClient.invalidateQueries({ stale: true, refetchType: "active" });
      }
    };

    document.addEventListener("visibilitychange", onVisible);

    return () => {
      if (flush !== null) {
        window.clearTimeout(flush);
      }

      document.removeEventListener("visibilitychange", onVisible);

      orders.stopListening(".order.touched");
      personal.stopListening(".notification.created");
      push.stopListening(".push.progressed");

      echo.leave("dashboard.orders");
      echo.leave(`admin.${adminUuid}`);
      echo.leave("dashboard.push");
    };
  }, [adminUuid, queryClient]);

  return <>{children}</>;
}
