import { useCallback, useEffect, useState } from "react";

const BASE = import.meta.env.BASE_URL.replace(/\/$/, "");

type PushStatus =
  | "unsupported"
  | "denied"
  | "default"
  | "subscribed";

/**
 * Granular reason returned by `enable()` so the calling UI can show a
 * specific toast instead of a generic "could not enable" message.
 *
 * - `unsupported`         — browser lacks ServiceWorker / PushManager / Notification.
 * - `unsupported_ios_safari` — iPhone/iPad Safari that isn't installed as a PWA.
 *                           iOS only delivers Web Push when the page runs
 *                           from the Home Screen (`navigator.standalone === true`).
 * - `permission_denied`   — user (or OS) declined the system permission prompt.
 * - `vapid_unavailable`   — backend `/api/push/vapid-public-key` is missing/down.
 * - `subscribe_failed`    — `pushManager.subscribe()` threw (e.g. AbortError).
 * - `server_rejected`     — backend `/api/push/subscribe` returned non-2xx.
 */
export type EnableReason =
  | "unsupported"
  | "unsupported_ios_safari"
  | "permission_denied"
  | "permission_dismissed"
  | "vapid_unavailable"
  | "subscribe_failed"
  | "server_rejected";

export type EnableResult =
  | { ok: true }
  | { ok: false; reason: EnableReason };

function urlBase64ToUint8Array(base64: string): Uint8Array {
  const padding = "=".repeat((4 - (base64.length % 4)) % 4);
  const b64 = (base64 + padding).replace(/-/g, "+").replace(/_/g, "/");
  const raw = atob(b64);
  const out = new Uint8Array(raw.length);
  for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i);
  return out;
}

/**
 * True when the page is running on iPhone/iPad Safari that hasn't been
 * installed to the Home Screen. iOS Safari exposes the Push APIs even
 * in normal browsing, but `subscribe()` only succeeds in standalone
 * PWA mode — calling it from a regular tab fails silently in some
 * iOS builds, which is what was producing the generic
 * "تعذّر تفعيل الإشعارات" toast.
 */
export function isIosSafariNonStandalone(): boolean {
  if (typeof window === "undefined" || typeof navigator === "undefined") {
    return false;
  }
  const ua = navigator.userAgent || "";
  // iPhone / iPod / iPad. Modern iPadOS reports MacIntel + touch
  // support instead of "iPad", so we check for that too.
  const isIos =
    /iPad|iPhone|iPod/.test(ua) ||
    (navigator.platform === "MacIntel" &&
      typeof (navigator as Navigator & { maxTouchPoints?: number })
        .maxTouchPoints === "number" &&
      (navigator as Navigator & { maxTouchPoints: number }).maxTouchPoints >
        1);
  if (!isIos) return false;
  // On iOS, Chrome (CriOS), Firefox (FxiOS), Edge (EdgiOS), Opera
  // (OPiOS), DuckDuckGo (DuckDuckGo), etc. all wrap WebKit but cannot
  // register a push subscription even after Add-to-Home-Screen — only
  // mobile Safari can. So only show the install-steps prompt when the
  // user is actually in Safari; otherwise there's nothing actionable
  // they can do regardless of installing to the Home Screen.
  const isSafari =
    /Safari\//.test(ua) &&
    !/CriOS|FxiOS|EdgiOS|OPiOS|DuckDuckGo|YaBrowser|GSA\//.test(ua);
  if (!isSafari) return false;
  const standalone =
    (navigator as Navigator & { standalone?: boolean }).standalone === true ||
    (typeof window.matchMedia === "function" &&
      window.matchMedia("(display-mode: standalone)").matches);
  return !standalone;
}

function isSupported(): boolean {
  return (
    typeof window !== "undefined" &&
    "serviceWorker" in navigator &&
    "PushManager" in window &&
    "Notification" in window
  );
}

async function getRegistration(): Promise<ServiceWorkerRegistration | null> {
  if (!("serviceWorker" in navigator)) return null;
  // Wait for the SPA's registration in main.tsx to settle. `ready`
  // resolves once an active SW controls the page.
  return navigator.serviceWorker.ready;
}

/**
 * Hook that exposes the user's current Web Push subscription state and
 * `enable()` / `disable()` actions. Talks to the backend at
 * `${BASE}/api/push/*`.
 *
 * Web Push on iOS Safari only works when the page is installed as a
 * PWA (Add to Home Screen). The hook surfaces "unsupported" in that
 * case so the UI can prompt the user to install first.
 *
 * #576: `enable()` now returns `{ ok, reason }` instead of a bare
 * boolean so the calling UI can render a specific toast (iOS-needs-PWA,
 * permission denied, VAPID missing, subscribe failed, server rejected)
 * instead of a generic "could not enable" message that left the user
 * unable to diagnose why notifications wouldn't turn on.
 */
export function usePushSubscription() {
  const [status, setStatus] = useState<PushStatus>(() =>
    !isSupported()
      ? "unsupported"
      : Notification.permission === "denied"
        ? "denied"
        : "default",
  );
  const [busy, setBusy] = useState(false);

  const refresh = useCallback(async () => {
    if (!isSupported()) {
      setStatus("unsupported");
      return;
    }
    if (Notification.permission === "denied") {
      setStatus("denied");
      return;
    }
    const reg = await getRegistration();
    const sub = await reg?.pushManager.getSubscription();
    if (sub) setStatus("subscribed");
    else setStatus(Notification.permission === "granted" ? "default" : "default");
  }, []);

  useEffect(() => {
    void refresh();
  }, [refresh]);

  const enable = useCallback(async (): Promise<EnableResult> => {
    if (!isSupported()) {
      setStatus("unsupported");
      return { ok: false, reason: "unsupported" };
    }
    // #576: iOS Safari outside Home Screen — subscribe() will fail with
    // a confusing AbortError. Surface the real cause so the user knows
    // to install the PWA first.
    if (isIosSafariNonStandalone()) {
      return { ok: false, reason: "unsupported_ios_safari" };
    }
    setBusy(true);
    try {
      const perm = await Notification.requestPermission();
      if (perm !== "granted") {
        setStatus(perm === "denied" ? "denied" : "default");
        // Distinguish a hard "blocked" decision from the user just
        // dismissing the prompt without choosing — the messages need
        // to differ so we don't tell users to "unblock" when they
        // simply haven't decided yet.
        return {
          ok: false,
          reason:
            perm === "denied" ? "permission_denied" : "permission_dismissed",
        };
      }
      const reg = await getRegistration();
      if (!reg) return { ok: false, reason: "subscribe_failed" };

      let publicKey: string;
      try {
        const keyRes = await fetch(`${BASE}/api/push/vapid-public-key`, {
          credentials: "include",
        });
        if (!keyRes.ok) {
          return { ok: false, reason: "vapid_unavailable" };
        }
        publicKey = ((await keyRes.json()) as { publicKey: string }).publicKey;
        if (!publicKey) return { ok: false, reason: "vapid_unavailable" };
      } catch {
        return { ok: false, reason: "vapid_unavailable" };
      }

      // If a stale subscription exists from a previous VAPID key,
      // unsubscribe locally first so applicationServerKey isn't
      // mismatched.
      const existing = await reg.pushManager.getSubscription();
      if (existing) {
        try {
          await existing.unsubscribe();
        } catch {
          /* ignore */
        }
      }

      let sub: PushSubscription;
      try {
        sub = await reg.pushManager.subscribe({
          userVisibleOnly: true,
          applicationServerKey: urlBase64ToUint8Array(publicKey)
            .buffer as ArrayBuffer,
        });
      } catch {
        return { ok: false, reason: "subscribe_failed" };
      }
      const json = sub.toJSON();
      const subRes = await fetch(`${BASE}/api/push/subscribe`, {
        method: "POST",
        credentials: "include",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          endpoint: json.endpoint,
          keys: json.keys,
        }),
      });
      if (!subRes.ok) {
        try {
          await sub.unsubscribe();
        } catch {
          /* ignore */
        }
        return { ok: false, reason: "server_rejected" };
      }
      setStatus("subscribed");
      return { ok: true };
    } catch {
      return { ok: false, reason: "subscribe_failed" };
    } finally {
      setBusy(false);
    }
  }, []);

  const disable = useCallback(async (): Promise<void> => {
    if (!isSupported()) return;
    setBusy(true);
    try {
      const reg = await getRegistration();
      const sub = await reg?.pushManager.getSubscription();
      if (sub) {
        const endpoint = sub.endpoint;
        try {
          await sub.unsubscribe();
        } catch {
          /* ignore */
        }
        await fetch(`${BASE}/api/push/unsubscribe`, {
          method: "POST",
          credentials: "include",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ endpoint }),
        }).catch(() => {});
      }
      setStatus(Notification.permission === "denied" ? "denied" : "default");
    } finally {
      setBusy(false);
    }
  }, []);

  return { status, busy, enable, disable, refresh };
}
