import { createRoot } from "react-dom/client";
import { lazy, Suspense, useEffect } from "react";
import { ErrorBoundary } from "./components/ErrorBoundary";
import { ThemeProvider } from "./components/ThemeProvider";
import "./index.css";
import { initPerfMetrics } from "./lib/perfMetrics";
import { captureException as sentryCaptureException, initSentry, isSentryEnabled } from "./lib/sentry";
import { isReloadInProgress, reloadWithCacheBust, shouldReloadForChunkError } from "./lib/lazyWithRetry";
import { rescueLegacyAdminPwaInstall } from "./lib/adminPwaRescue";

// [Task #821 - Bug B] 救回舊的、已加桌面但 start_url=/ 的後台 PWA icon。
// 必須在 React mount 前就決定要不要 redirect — 越早離開白屏 race 視窗越好。
// 詳見 client/src/lib/adminPwaRescue.ts 註解。沒有觸發條件時 zero-cost。
//
// rescue 命中時 location.replace 已 fire — 不再 mount React 避免 chunk
// 載入跟 navigation 競爭。用顯式 boolean short-circuit (非 throw) 因為
// throw module-level error 會讓 Sentry / 瀏覽器 console / Vite overlay
// 報噪音，即使 user 已經在 navigate 也會留在錯誤回報。
const adminPwaRescued = rescueLegacyAdminPwaInstall();

// [T-481] App, i18n, react-query, wouter all live behind this single dynamic
// import so the entry chunk only ships the React runtime, ErrorBoundary,
// ThemeProvider and our boot/Sentry plumbing. Without a static `import App`
// at the top, Vite no longer emits modulepreload links for vendor-query /
// vendor-router / vendor-i18n in index.html — they fetch in parallel right
// after main.js executes and starts this dynamic import. Mobile Safari can
// finish parsing the entry chunk much sooner.
//
// Awaiting i18n's `i18nReady` inside the lazy factory guarantees the very
// first paint of Landing already has zh-TW translations (no flash of raw
// translation keys). On warm caches the awaited promises resolve on the
// next microtask. A short timeout fallback prevents a permanently stuck
// splash if the locale chunk fails to load — i18next falls back to keys.
const LazyApp = lazy(async () => {
  const i18nReadyWithTimeout = (async () => {
    try {
      const { i18nReady } = await import("./lib/i18n");
      await Promise.race([
        i18nReady,
        new Promise<void>((resolve) => window.setTimeout(resolve, 1500)),
      ]);
    } catch {
      // Defensive: i18n failure must not block React mount.
    }
  })();
  const [, AppModule] = await Promise.all([
    i18nReadyWithTimeout,
    import("./App"),
  ]);
  return AppModule;
});

// [T-479] Sentry's heavy @sentry/react chunk used to load before createRoot,
// competing with critical-path JS on first paint. We now defer the real init
// until after the first render and use these tiny pre-mount listeners to
// buffer any error that happens during boot. The buffered events flow into
// `captureException` (which itself buffers until the core finishes loading)
// so nothing is lost. We keep these listeners attached *until* initSentry()
// resolves successfully — only then does Sentry's own global handler take
// over, at which point we detach to avoid double-reporting. The
// `sentryCoreReady` guard short-circuits during the brief window between
// Sentry installing its handlers and us getting a chance to detach ours, so
// a single error never lands in Sentry twice.
let sentryCoreReady = false;
const earlyErrorListener = (event: ErrorEvent) => {
  if (sentryCoreReady) return;
  try {
    sentryCaptureException(event.error ?? new Error(event.message || "early error"), {
      earlyBoot: { source: "window.onerror", filename: event.filename, lineno: event.lineno },
    });
  } catch {
    // Defensive: pre-init buffering must never crash the boot path.
  }
};
const earlyRejectionListener = (event: PromiseRejectionEvent) => {
  if (sentryCoreReady) return;
  try {
    sentryCaptureException(event.reason ?? new Error("early unhandled rejection"), {
      earlyBoot: { source: "unhandledrejection" },
    });
  } catch {
    // Defensive
  }
};
window.addEventListener("error", earlyErrorListener);
window.addEventListener("unhandledrejection", earlyRejectionListener);

// [Task #618 / Task #619] 三方共用同一個節流（sessionStorage key + in-memory
// reloadInProgress 旗標）由 `@/lib/lazyWithRetry` 集中管理，三條路徑
// （unhandled rejection / Suspense lazy / React tree error）共用同一個
// 計數器，行為一致。第一次失敗（節流外、且沒有 in-flight reload）走
// reloadWithCacheBust，會帶 `?_cb=<ts>` 強制繞過 bfcache；節流內第二次
// 失敗則 fall through，讓事件走預設行為（瀏覽器 console + Sentry early
// listener 會接走），不再強制再次 reload／導頁。
//
// Task #619：reload 視窗內的並行失敗用 isReloadInProgress() 提前 short
// circuit。例如 deploy 切換瞬間有 4 個 prefetch 同時打 stale chunk，會被
// server 端 410 handler 全部 reject，第一個 reject 觸發 reloadWithCacheBust
// 並打開 reloadInProgress；其餘 3 個 reject 在 unload 真的發生前到達這個
// listener，看到旗標就什麼都不做，避免重複 reload／重複寫節流時間戳。
window.addEventListener("unhandledrejection", (event) => {
  const reason = event.reason;
  if (reason instanceof Error) {
    const msg = reason.message || "";
    const isRecoverable =
      msg.includes("Importing a module script failed") ||
      msg.includes("Failed to fetch dynamically imported module") ||
      msg.includes("error loading dynamically imported module") ||
      msg.includes("ChunkLoadError") ||
      msg.includes("Loading chunk") ||
      msg.includes("Loading CSS chunk") ||
      (msg.includes("Cannot read properties of null") &&
        /reading '(useState|useEffect|useRef|useCallback|useMemo|useContext|useReducer|useLayoutEffect)'/.test(msg)) ||
      msg.includes("Invalid hook call");
    if (isRecoverable) {
      if (isReloadInProgress()) return;
      if (shouldReloadForChunkError()) {
        reloadWithCacheBust();
        return;
      }
    }
  }
});

declare global {
  interface Window {
    __APP_MOUNTED?: boolean;
    __cancelMountTimer?: () => void;
  }
}

// [T-479] Kick off the deferred Sentry init. Keeps the early global
// listeners attached for the entire async window (idle wait + dynamic
// chunk fetch + Sentry.init) so an error during boot is never dropped.
// Only after the init promise resolves do we mark the core ready (which
// short-circuits the early listeners on the next event) and detach them.
function finalizeSentryReady(): void {
  // Only hand off to Sentry's own global handlers if the core actually
  // initialized. If the dynamic import or Sentry.init failed (or DSN is not
  // configured), keep our early listeners attached so we still capture
  // errors via the buffered no-op path instead of going completely blind.
  if (!isSentryEnabled()) return;
  sentryCoreReady = true;
  window.removeEventListener("error", earlyErrorListener);
  window.removeEventListener("unhandledrejection", earlyRejectionListener);
}

function startDeferredSentryInit(): void {
  const run = () => {
    initSentry().then(finalizeSentryReady);
  };
  try {
    if (typeof window.requestIdleCallback === "function") {
      window.requestIdleCallback(run, { timeout: 3000 });
    } else {
      setTimeout(run, 1500);
    }
  } catch {
    // Defensive: if scheduling fails, just init now.
    run();
  }
}

// [T-481] Marks the boot as complete after React actually commits the lazy
// App tree (not when createRoot is first called against a Suspense fallback
// that paints nothing). We mount this tiny effect inside the Suspense
// boundary so __APP_MOUNTED flips at the same moment users see real UI.
function MountSignal() {
  useEffect(() => {
    if (window.__APP_MOUNTED) return;
    window.__APP_MOUNTED = true;
    if (window.__cancelMountTimer) window.__cancelMountTimer();
    initPerfMetrics();
    startDeferredSentryInit();
  }, []);
  return null;
}

try {
  if (adminPwaRescued) {
    // location.replace 已 fire — short-circuit boot 避免跟 navigation race
  } else {
    const root = document.getElementById("root");
    if (!root) throw new Error("Root element not found");
    createRoot(root).render(
      <ErrorBoundary>
        <ThemeProvider>
          <Suspense fallback={null}>
            <LazyApp />
            <MountSignal />
          </Suspense>
        </ThemeProvider>
      </ErrorBoundary>
    );
  }
} catch (err) {
  handleMountError(err);
}

function handleMountError(err: unknown): void {
  console.error("[main] Fatal mount error:", err);
  try {
    sentryCaptureException(err, { mount: { phase: "createRoot" } });
  } catch {}
  // [T-479] Mount failed — load Sentry immediately (don't wait for idle) so
  // the buffered fatal error gets flushed ASAP. The beacon below is the
  // primary report path; Sentry is a secondary signal.
  void initSentry().then(finalizeSentryReady);
  try {
    navigator.sendBeacon("/api/client-error", JSON.stringify({
      message: "Fatal mount error: " + (err instanceof Error ? err.message : String(err)),
      stack: err instanceof Error ? err.stack : undefined,
      url: window.location.href,
      ua: navigator.userAgent,
    }));
  } catch {}
  const shell = document.getElementById("app-shell");
  if (shell) {
    shell.innerHTML = '<p style="color:#ff6b6b;font-size:14px;padding:20px">載入錯誤，請重新整理頁面</p>';
  }
}
