"use client";

import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
import { useRouter } from "next/navigation";
import { useQueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { HAS_WEB_PUSH } from "@/config/site";
import { queryKeys } from "@/lib/query-keys";
import { browserSupportsPush, chosenOff, disableWebPush, enableWebPush, hasRegisteredToken, onForegroundPush, permission } from "@/lib/web-push";
import { useCan } from "@/features/auth/use-can";
import { useT } from "@/i18n";

/**
 * Where this browser stands on notifications, and the two moves the admin can make.
 *
 * - `unavailable`: not configured, or this browser cannot do push. Nothing is offered.
 * - `blocked`: the browser's permission was refused; only the browser's own settings can undo that.
 * - `off`: available, not on (never asked, or switched off here).
 * - `on`: this browser is registered and is pushed what lands in the inbox.
 *
 * Once on, opening the dashboard re-registers quietly (it is how a token Firebase rotated reaches the server), and a push that
 * arrives while the dashboard is in front refreshes the bell and shows a toast, instead of a system notification on top of it.
 */
export type WebPushStatus = "unavailable" | "blocked" | "off" | "on";

type WebPushValue = {
  status: WebPushStatus;
  working: boolean;
  enable: () => Promise<void>;
  disable: () => Promise<void>;
};

const WebPushContext = createContext<WebPushValue>({ status: "unavailable", working: false, enable: async () => undefined, disable: async () => undefined });

export function useWebPush(): WebPushValue {
  return useContext(WebPushContext);
}

function currentStatus(): WebPushStatus {
  if (!HAS_WEB_PUSH || !browserSupportsPush()) return "unavailable";
  if (permission() === "denied") return "blocked";

  // "On" means registered, not just allowed: a browser that was allowed but never got its token to the server is not being pushed to.
  return permission() === "granted" && !chosenOff() && hasRegisteredToken() ? "on" : "off";
}

export function WebPushProvider({ children }: { children: ReactNode }) {
  const t = useT();
  const router = useRouter();
  const queryClient = useQueryClient();
  const { can } = useCan();
  const allowed = can("notifications.view");

  // Read after mount: the server render knows nothing about this browser.
  const [status, setStatus] = useState<WebPushStatus>("unavailable");
  const [working, setWorking] = useState(false);

  useEffect(() => {
    // eslint-disable-next-line react-hooks/set-state-in-effect
    setStatus(allowed ? currentStatus() : "unavailable");
  }, [allowed]);

  const refreshBell = useCallback(() => {
    void queryClient.invalidateQueries({ queryKey: ["notifications"] });
    void queryClient.invalidateQueries({ queryKey: queryKeys.notifications.unreadCount });
  }, [queryClient]);

  // Allowed and not switched off: register quietly, which is also how a token Firebase rotated reaches the server. "On" only once it worked.
  useEffect(() => {
    // Asked of the browser itself, not of `status`: the first render has not read the browser yet, and this runs once.
    const now = currentStatus();

    if (!allowed || now === "unavailable" || now === "blocked" || chosenOff() || permission() !== "granted") return;

    let cancelled = false;

    void enableWebPush(false)
      .then((token) => {
        if (!cancelled && token) setStatus("on");
      })
      .catch(() => undefined);

    return () => {
      cancelled = true;
    };
  }, [allowed]);

  // A push while the dashboard is in front: the bell moves and a toast says what it was.
  useEffect(() => {
    if (!allowed || status !== "on") return;

    let stop: (() => void) | undefined;
    let cancelled = false;

    void onForegroundPush((push) => {
      refreshBell();
      toast(push.title, { description: push.body, action: { label: t("notifications.push.open"), onClick: () => router.push(push.path) } });
    }).then((unsubscribe) => {
      if (cancelled) unsubscribe();
      else stop = unsubscribe;
    });

    return () => {
      cancelled = true;
      stop?.();
    };
  }, [allowed, status, refreshBell, router, t]);

  // A click on a system notification while the dashboard is open: the service worker asks the page to go there.
  useEffect(() => {
    if (!browserSupportsPush()) return;

    const onMessage = (event: MessageEvent) => {
      const data = event.data as { type?: string; path?: string } | null;

      if (data?.type === "kapitano-open-path" && typeof data.path === "string" && data.path.startsWith("/")) {
        refreshBell();
        router.push(data.path);
      }
    };

    navigator.serviceWorker.addEventListener("message", onMessage);

    return () => navigator.serviceWorker.removeEventListener("message", onMessage);
  }, [refreshBell, router]);

  const enable = useCallback(async () => {
    setWorking(true);

    try {
      const token = await enableWebPush(true);

      setStatus(token ? "on" : currentStatus());

      if (token) {
        toast.success(t("notifications.push.enabled"));
      } else if (permission() !== "denied") {
        // Allowed, or never answered, and still no token: Firebase refused or the browser could not (say so rather than nothing).
        toast.error(t("notifications.push.failed"));
      }
    } catch {
      toast.error(t("notifications.push.failed"));
    } finally {
      setWorking(false);
    }
  }, [t]);

  const disable = useCallback(async () => {
    setWorking(true);

    try {
      await disableWebPush(true);
      setStatus(currentStatus());
    } finally {
      setWorking(false);
    }
  }, []);

  const value = useMemo<WebPushValue>(() => ({ status, working, enable, disable }), [status, working, enable, disable]);

  return <WebPushContext.Provider value={value}>{children}</WebPushContext.Provider>;
}
