/**
 * Bootstraps Firebase Cloud Messaging on Android (and iOS via Capacitor).
 *
 * STABILITY FIX (v2.1.0 — Asus ROG Phone 8 Pro / Android 14-15 silent
 * crash when the "Allow notifications" system dialog appears or is
 * dismissed):
 *
 *   1. Wait until the DOM has fully loaded (document.readyState ===
 *      "complete") AND a 5-second timer has elapsed AND the browser is
 *      idle (requestIdleCallback) before EVER calling
 *      `PushNotifications.requestPermissions()`. This guarantees the
 *      Warm Decoder + Fast-Zapping bootstrap in v2.0 has already run
 *      its native handshakes — pinning the AdMob initialiser, ExoPlayer
 *      surface allocator and FCM permission dialog into the same
 *      microtask was the trigger for the WebView SIGSEGV on ROG 8 Pro.
 *
 *   2. Listeners are registered BEFORE `PushNotifications.register()`,
 *      so the native callback can never fire on a not-yet-existing
 *      handler (which on Android 15 surfaces as a hard crash inside
 *      the Capacitor bridge — "attempt to invoke virtual method ...
 *      on a null object reference").
 *
 *   3. Everything is wrapped in try/catch — the whole hook is a no-op
 *      on web / Tizen / webOS and CAN NEVER bubble an exception up
 *      into React's render cycle.
 *
 * IMPORTANT: This file deliberately does NOT touch ExoPlayer, the
 * channel preview decoder, or any AdMob code paths. The Warm Decoder
 * and Fast-Zapping logic shipped in v2.0 remain untouched — we only
 * defer the FCM permission flow so it stops racing them at cold start.
 */
import { useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { Capacitor } from "@capacitor/core";

import { apiRegisterDeviceToken, getToken } from "@/lib/nadiAuth";
import { fetchAppInfo } from "@/lib/appInfo";

const STORAGE_KEY = "nadiplay_fcm_registered_for";
// 5 seconds gives Warm Decoder + Fast-Zapping enough breathing room to
// finish their native handshakes. The 3 s value used in v1.x was still
// racing the player on ROG 8 Pro / Android 15.
const PUSH_DEFER_MS = 5000;

/**
 * Resolve when the page is fully painted AND the browser has a moment
 * of idle time. Falls back gracefully if requestIdleCallback is missing
 * (e.g. older Android WebView). Maxes out at 8 s total wait so we never
 * leave permission flow indefinitely deferred on a busy thread.
 */
const waitForIdle = (): Promise<void> =>
  new Promise<void>((resolve) => {
    let settled = false;
    const done = () => {
      if (settled) return;
      settled = true;
      resolve();
    };

    const onReady = () => {
      // Coalesce requestIdleCallback (Chromium ≥ 47, present on WebView 95+)
      // with a hard ceiling so we ALWAYS resolve.
      const ric =
        (globalThis as unknown as {
          requestIdleCallback?: (
            cb: () => void,
            opts?: { timeout?: number },
          ) => number;
        }).requestIdleCallback;
      if (typeof ric === "function") {
        try {
          ric(done, { timeout: 1500 });
        } catch {
          setTimeout(done, 800);
        }
      } else {
        setTimeout(done, 800);
      }
    };

    if (typeof document === "undefined") {
      // SSR / no-DOM path — resolve immediately.
      done();
      return;
    }

    if (document.readyState === "complete") {
      onReady();
    } else {
      const handler = () => {
        if (document.readyState === "complete") {
          window.removeEventListener("load", handler);
          onReady();
        }
      };
      window.addEventListener("load", handler, { once: true });
      // Hard ceiling: don't wait more than 8 s even if 'load' never fires
      // (rare, but happens behind aggressive ad/script blockers).
      setTimeout(() => {
        window.removeEventListener("load", handler);
        onReady();
      }, 8000);
    }
  });

export const usePushNotifications = (
  passedUser?: { id: string; role: string } | null,
) => {
  const navigate = useNavigate();
  const registeredFor = useRef<string | null>(null);

  const userId = passedUser?.id ?? null;

  // ── 1. Token registration (deferred, per logged-in user) ──────
  useEffect(() => {
    if (!userId || !getToken()) return;
    if (!Capacitor.isNativePlatform()) return;
    if (registeredFor.current === userId) return;

    let cancelled = false;
    let timer: ReturnType<typeof setTimeout> | null = null;

    // CRITICAL: defer until BOTH the document is fully loaded AND a 5 s
    // grace period has elapsed AND the browser is idle. Without this,
    // requestPermissions() races the AdMob initialiser + ExoPlayer
    // surface allocation on Android 14/15 and SIGSEGVs the WebView.
    timer = setTimeout(() => {
      if (cancelled) return;
      void waitForIdle().then(async () => {
        if (cancelled) return;

        // ── SERVER KILL-SWITCH ───────────────────────────────────
        // Before touching any native FCM API, ask the backend if
        // push registration is currently enabled.
        //
        // Why this matters: an APK that was built BEFORE
        // `google-services.json` was bundled will crash NATIVELY the
        // moment `PushNotifications.register()` is called (the
        // Firebase initialiser fails inside JNI and takes the
        // WebView with it). This kill-switch is the v2.1.0 hotfix
        // for the ROG 8 Pro / Android 14-15 silent post-login crash
        // — and lets the admin re-enable push instantly from the
        // panel once a Firebase-aware APK is distributed (no second
        // crash-fix rebuild needed).
        try {
          const info = await fetchAppInfo();
          if (info?.flags?.push_enabled === false) {
            console.log(
              "[push] kill-switch off (flags.push_enabled === false) — skipping FCM init",
            );
            return;
          }
        } catch (e) {
          // If app-info is unreachable, continue with best-effort
          // registration so installs are still captured and admin can
          // see active devices.
          console.warn(
            "[push] kill-switch check failed, continuing FCM init",
            e,
          );
        }

        if (cancelled) return;

        // Wrap the entire init in its own try/catch so even a synchronous
        // throw can never propagate into React's lifecycle. If anything
        // blows up here the Warm Decoder / Fast-Zapping path is
        // unaffected — the app just won't have push notifications.
        initPushNotifications(userId, () => cancelled, registeredFor, navigate)
          .catch((e) => {
            console.warn("[push] init failed (caught at top level)", e);
          });
      });
    }, PUSH_DEFER_MS);

    return () => {
      cancelled = true;
      if (timer) clearTimeout(timer);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [userId]);

  // ── 2. Cold-start deep link via native window event ──────────
  useEffect(() => {
    const handler = (ev: Event) => {
      try {
        const route = (ev as CustomEvent<{ route: string }>).detail?.route;
        if (route && typeof route === "string" && route.startsWith("/")) {
          navigate(route);
        }
      } catch (e) {
        console.warn("[push] cold-start route handler error", e);
      }
    };
    window.addEventListener("nadiplay:notification-route", handler);
    return () =>
      window.removeEventListener("nadiplay:notification-route", handler);
  }, [navigate]);
};

/**
 * Separated into a standalone function so the entire init is wrapped
 * in a single try/catch — can NEVER crash the app.
 *
 * Order of operations is INTENTIONAL and load-bearing for the ROG 8
 * Pro / Android 15 crash fix:
 *
 *   a. Lazy-import the plugin (so the JS bundle for unauthenticated
 *      / web users doesn't even reference the native module).
 *   b. Check permissions FIRST — bail out for an already-denied user
 *      so we never re-show the dialog and never race the player.
 *   c. Attach every listener BEFORE register() is called. On
 *      Android 14/15 the JNI callback can fire before the listener
 *      promise resolves; if no JS handler exists at that instant the
 *      bridge crashes.
 *   d. Only then request permissions + register.
 */
async function initPushNotifications(
  userId: string,
  isCancelled: () => boolean,
  registeredFor: React.MutableRefObject<string | null>,
  navigate: ReturnType<typeof useNavigate>,
): Promise<void> {
  try {
    if (isCancelled()) return;

    const { PushNotifications } = await import(
      "@capacitor/push-notifications"
    );

    // ── (b) Read current permission state. NEVER prompt a user who
    //         has already denied (Android will show a one-time toast
    //         then suppress, but the JNI round-trip is still risky
    //         during Warm Decoder bringup).
    let state: string;
    try {
      const perm = await PushNotifications.checkPermissions();
      state = perm?.receive ?? "denied";
    } catch (permErr) {
      console.warn("[push] checkPermissions failed", permErr);
      return;
    }

    if (state === "denied") {
      console.log("[push] permission denied previously — skipping");
      return;
    }

    if (isCancelled()) return;

    // ── (c) Wire ALL listeners up FRONT so register()'s native
    //         callback can never fire into a void.
    try {
      await PushNotifications.removeAllListeners();
    } catch { /* ignore */ }

    try {
      await PushNotifications.addListener("registration", async (t) => {
        if (isCancelled() || !t?.value) return;
        try {
          await apiRegisterDeviceToken({
            token: t.value,
            device_id: getDeviceId(),
            platform: Capacitor.getPlatform() === "ios" ? "ios" : "android",
          });
          try { localStorage.setItem(STORAGE_KEY, userId); } catch { /* ignore */ }
          registeredFor.current = userId;
        } catch (e) {
          console.warn("[push] failed to send token to backend", e);
        }
      });
    } catch (e) {
      console.warn("[push] registration listener attach failed", e);
    }

    try {
      await PushNotifications.addListener("registrationError", (err) => {
        console.warn("[push] registration error", err);
      });
    } catch { /* ignore */ }

    try {
      await PushNotifications.addListener("pushNotificationReceived", (n) => {
        console.log("[push] received in foreground", n);
      });
    } catch { /* ignore */ }

    try {
      await PushNotifications.addListener(
        "pushNotificationActionPerformed",
        (action) => {
          try {
            const data = action?.notification?.data as
              | Record<string, string>
              | undefined;
            const route = data?.route || "/";
            navigate(route);
          } catch (navErr) {
            console.warn("[push] notification action nav error", navErr);
          }
        },
      );
    } catch { /* ignore */ }

    if (isCancelled()) return;

    // ── (d) Now (and only now) prompt + register.
    if (state === "prompt" || state === "prompt-with-rationale") {
      try {
        const ask = await PushNotifications.requestPermissions();
        state = ask?.receive ?? "denied";
      } catch (askErr) {
        console.warn("[push] requestPermissions failed", askErr);
        return;
      }
    }

    if (state !== "granted") {
      console.log("[push] permission not granted — skipping registration");
      return;
    }

    if (isCancelled()) return;

    try {
      await PushNotifications.register();
    } catch (regErr) {
      console.warn("[push] register() failed", regErr);
    }
  } catch (e) {
    // CRITICAL: never let push notification init crash the app
    console.warn("[push] init failed (non-fatal)", e);
  }
}

/**
 * Stable per-install device id.
 */
const getDeviceId = (): string => {
  const KEY = "nadiplay_device_id";
  try {
    const cur = localStorage.getItem(KEY);
    if (cur) return cur;
    const fresh =
      "dev-" +
      Math.random().toString(36).slice(2, 10) +
      "-" +
      Date.now().toString(36);
    localStorage.setItem(KEY, fresh);
    return fresh;
  } catch {
    return "dev-anon-" + Date.now().toString(36);
  }
};
