import { createRoot } from "react-dom/client";
import { Component, type ErrorInfo, type ReactNode } from "react";
import { HelmetProvider } from "react-helmet-async";
import App from "./App.tsx";
import "./index.css";
import { installInputZoomGuard } from "./lib/input-zoom-guard";
import { initPerfMode } from "./lib/perf";

// Auto-recover from stale chunk references after a new deploy.
// When index.html still in the browser points at vendor/asset hashes that no
// longer exist on the server, dynamic imports throw "Importing a module script
// failed". Reload once to fetch the new index.html and chunk hashes.
const RELOAD_KEY = "__module_reload_attempted__";
const RELOAD_PARAM = "module-reload";
const MAX_RELOADS = 2;
const isRecoverableModuleError = (value: unknown) => {
  const msg = typeof value === "string" ? value : "";
  return (
    msg.includes("Importing a module script failed") ||
    msg.includes("Failed to fetch dynamically imported module") ||
    msg.includes("error loading dynamically imported module") ||
    /Loading chunk \S+ failed/i.test(msg) ||
    // A long-lived Vite tab can briefly retain React from the previous
    // optimized-dependency generation while react-dom has already updated.
    // Hooks then reach the inactive singleton dispatcher and the app blanks.
    msg.includes("dispatcher.useState") ||
    msg.includes("useState") ||
    msg.includes("Invalid hook call")
  );
};
let reloading = false;
const tryReload = () => {
  if (reloading) return;
  const attempts = Number(sessionStorage.getItem(RELOAD_KEY) ?? "0");
  if (attempts >= MAX_RELOADS) return;
  reloading = true;
  sessionStorage.setItem(RELOAD_KEY, String(attempts + 1));
  const finish = () => {
    const url = new URL(window.location.href);
    // A fresh query string forces a network fetch of index.html, which in turn
    // hands the tab the module URLs of the current dependency generation
    // instead of the cached ones that reference a dead React copy.
    url.searchParams.set(RELOAD_PARAM, Date.now().toString());
    window.location.replace(url.toString());
  };
  // Drop any service-worker/HTTP cache copies of the stale module graph first.
  if (typeof caches !== "undefined" && caches.keys) {
    caches
      .keys()
      .then((keys) => Promise.all(keys.map((k) => caches.delete(k))))
      .catch(() => undefined)
      .finally(finish);
  } else {
    finish();
  }
};
window.addEventListener("error", (e) => {
  const details = `${e.message ?? ""}\n${e.error?.stack ?? ""}`;
  if (isRecoverableModuleError(details)) tryReload();
});
window.addEventListener("unhandledrejection", (e) => {
  const reason = e.reason;
  const details = typeof reason === "string"
    ? reason
    : `${reason?.message ?? ""}\n${reason?.stack ?? ""}`;
  if (isRecoverableModuleError(details)) tryReload();
});

interface ModuleRecoveryBoundaryProps {
  children: ReactNode;
}

class ModuleRecoveryBoundary extends Component<ModuleRecoveryBoundaryProps> {
  componentDidMount() {
    // This lifecycle runs only after React has committed successfully. The
    // static loader already inside #root must not be mistaken for a commit.
    sessionStorage.removeItem(RELOAD_KEY);
    const url = new URL(window.location.href);
    if (!url.searchParams.has(RELOAD_PARAM)) return;
    url.searchParams.delete(RELOAD_PARAM);
    window.history.replaceState(window.history.state, "", url);
  }

  componentDidCatch(error: Error, info: ErrorInfo) {
    const details = `${error.message}\n${error.stack ?? ""}\n${info.componentStack ?? ""}`;
    if (isRecoverableModuleError(details)) tryReload();
  }

  render() {
    return this.props.children;
  }
}


// Suspend viewport scaling while a field has focus, so iOS never zooms
// into a control and leaves the reader magnified afterwards.
installInputZoomGuard();

// Decide the motion budget BEFORE React renders, so no component ever mounts
// an ambient WebGL layer and then discards it. See lib/perf.ts: this is what
// keeps the in-app browsers of social media apps from stuttering through
// animations that Safari and Chrome handle without effort.
initPerfMode();

const rootElement = document.getElementById("root");
if (!rootElement) throw new Error("Application root element is missing");

createRoot(rootElement).render(
  <ModuleRecoveryBoundary>
    <HelmetProvider>
      <App />
    </HelmetProvider>
  </ModuleRecoveryBoundary>
);
