import { createRoot } from "react-dom/client";
import { useEffect } from "react";
import App from "./App.tsx";
import "./index.css";
import { applyTheme, loadThemeLocal } from "./lib/theme";
import {
  SPLASH_COMPLETE_HOLD_MS,
  SPLASH_FADE_OUT_MS,
  SPLASH_HARD_FALLBACK_MS,
} from "./lib/splashTiming";

// Normalize legacy shell routes before React mounts so routing and the bottom
// nav always treat home as `/`, not `/index` or `/index.html`.
{
  const { pathname, search, hash } = window.location;
  if (pathname === "/index" || pathname === "/index.html") {
    window.history.replaceState({}, "", `/${search}${hash}`);
  }
}

// Apply saved theme before first paint to prevent a flash of default colours.
// The full theme (mode + accent) is also re-loaded from the database on login
// in ThemeLoader, so it follows the user's account across devices.
{
  const { mode, accent } = loadThemeLocal();
  applyTheme(mode, accent);
}

// Dismiss the native splash only after the React splash has painted
// Dismiss the native splash only once the first real screen has actually
// rendered AND critical web fonts have loaded. Previously we faded out
// after two RAFs, which could reveal a half-laid-out page or unstyled
// text. Now we wait for:
//   1. The #root element to contain real content (route mounted)
//   2. document.fonts.ready (critical fonts decoded)
//   3. Two RAFs so the browser has painted that first frame
// A hard fallback at 2.5s guarantees the splash never sticks forever.
const Root = () => {
  useEffect(() => {
    const splash = document.getElementById("splash-screen");
    if (!splash) return;

    let removed = false;
    const removeSplash = () => {
      if (removed) return;
      removed = true;
      // Snap the loading bar to 100% so the user actually sees it complete
      // before the splash fades out. This guarantees a clean finish in
      // every code path:
      //   • Warm load — bar is mid-fill, snaps from ~50% → 100%.
      //   • Cold load — bar has crept to ~95%, snaps to 100%.
      //   • Hard fallback (no readiness signal) — same path runs, so the
      //     bar still completes before fade-out. Users never see the bar
      //     freeze or get cut off.
      const bar = document.getElementById("splash-loading-bar") as HTMLElement | null;
      if (bar) {
        bar.style.animation = "none";
        // Force a reflow so the cancelled animations' current values are
        // committed before our overriding transform kicks in.
        void bar.offsetWidth;
        bar.style.transform = "scaleX(1)";
      }
      // Hold so the bar's 200ms ease-to-100% finishes on screen, then start
      // the fade-out and remove on completion. Durations are sourced from
      // src/lib/splashTiming.ts so the HTML splash and React splash stay
      // in lock-step — change values there, mirror in index.html.
      window.setTimeout(() => {
        splash.style.opacity = "0";
        splash.style.visibility = "hidden";
        window.setTimeout(() => splash.remove(), SPLASH_FADE_OUT_MS);
      }, SPLASH_COMPLETE_HOLD_MS);
    };

    const root = document.getElementById("root");

    const waitForFirstScreen = () =>
      new Promise<void>((resolve) => {
        if (!root) return resolve();
        // Resolve once the root has any non-empty child (the first route
        // has mounted real DOM, not just a Suspense fallback wrapper).
        const check = () => {
          if (root.childElementCount > 0 && root.textContent?.trim()) {
            return true;
          }
          return false;
        };
        if (check()) return resolve();
        const obs = new MutationObserver(() => {
          if (check()) {
            obs.disconnect();
            resolve();
          }
        });
        obs.observe(root, { childList: true, subtree: true, characterData: true });
        // Safety: stop observing after 2s regardless.
        window.setTimeout(() => {
          obs.disconnect();
          resolve();
        }, 2000);
      });

    const waitForFonts = () => {
      const fonts = (document as any).fonts;
      if (fonts?.ready && typeof fonts.ready.then === "function") {
        return Promise.race([
          fonts.ready,
          new Promise((r) => window.setTimeout(r, 1500)),
        ]);
      }
      return Promise.resolve();
    };

    let cancelled = false;
    Promise.all([waitForFirstScreen(), waitForFonts()]).then(() => {
      if (cancelled) return;
      // Two RAFs to ensure the first frame with real content has painted.
      window.requestAnimationFrame(() => {
        window.requestAnimationFrame(removeSplash);
      });
    });

    // Hard fallback so the splash can never get stuck.
    const fallback = window.setTimeout(removeSplash, SPLASH_HARD_FALLBACK_MS);

    return () => {
      cancelled = true;
      window.clearTimeout(fallback);
    };
  }, []);
  return <App />;
};

// Register service worker for push notifications (only in production, not in iframes)
const isInIframe = (() => {
  try {
    return window.self !== window.top;
  } catch (e) {
    return true;
  }
})();

const isPreviewHost =
  window.location.hostname.includes("id-preview--") ||
  window.location.hostname.includes("lovableproject.com");

createRoot(document.getElementById("root")!).render(<Root />);

// Defer service worker work until the browser is idle so it doesn't add to
// Total Blocking Time during initial hydration. Behavior is unchanged — only
// the timing shifts off the critical path.
const scheduleIdle = (cb: () => void) => {
  const ric = (window as any).requestIdleCallback as
    | ((cb: () => void, opts?: { timeout: number }) => number)
    | undefined;
  if (ric) ric(cb, { timeout: 3000 });
  else window.setTimeout(cb, 2000);
};

scheduleIdle(() => {
  if ("serviceWorker" in navigator && !isInIframe && !isPreviewHost) {
    navigator.serviceWorker.register("/sw.js").catch(() => {
      // SW registration failed silently
    });
  } else if (isPreviewHost || isInIframe) {
    // Unregister any existing service workers in preview/iframe contexts
    navigator.serviceWorker?.getRegistrations().then((registrations) => {
      registrations.forEach((r) => r.unregister());
    });
  }
});
