import {
  HeadContent,
  Scripts,
  createRootRouteWithContext,
  Outlet,
  Link,
  redirect,
  useRouter,
} from "@tanstack/react-router";
import { useCallback, useEffect, useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
  BetterI18nProvider,
  getLocaleFromPath,
  useTranslations,
} from "@better-i18n/use-intl";
import { ContentProvider } from "@better-i18n/content/adapters/react";
import type { Messages } from "@better-i18n/use-intl";
import { getMessages } from "@better-i18n/use-intl/server";
import { detectLocale } from "@better-i18n/core";
import { getRequestHeader } from "@tanstack/react-start/server";
import { i18nConfig } from "../i18n.config";
import { filterMessagesByPath, getCdnNamespacesForPage, extractPagePath } from "../lib/page-namespaces";
import { storeMessages, readMessages } from "../lib/ssr-messages";
import { fetchLocales } from "../lib/locales";
import appCss from "../styles.css?url";
import { MarketingLayout } from "../components/MarketingLayout";
import { SvgSprite } from "../components/SvgSprite";
import { CookieBanner } from "../components/CookieBanner";
import { WebMcpRegistrar } from "../components/WebMcpRegistrar";
import { IconArrowLeft } from "@central-icons-react/round-outlined-radius-2-stroke-2";
import { lazy, Suspense } from "react";

const LazyHelpwayWidget = lazy(() =>
  import("@helpway/react").then((m) => ({ default: m.HelpwayWidget })),
);

/**
 * Mounts the Helpway widget client-side only without causing hydration
 * mismatch. Renders nothing on SSR and on the first client render so
 * server HTML and the initial client render match. After the post-mount
 * effect flips `mounted` to true, the widget is rendered on the client
 * only — at that point React has already committed the matching server
 * tree, so there's no mismatch to detect.
 */
function HelpwayWidgetMount({ locale }: { locale: string }) {
  const [mounted, setMounted] = useState(false);
  useEffect(() => setMounted(true), []);
  if (!mounted) return null;
  return (
    <Suspense fallback={null}>
      <LazyHelpwayWidget
        apiKey="pk_live_Nir-sHLl1_qc9S9EuV9RdNN5"
        locale={locale}
      />
    </Suspense>
  );
}

/**
 * Per-request SSR side-channel for i18n messages.
 *
 * Keyed by a unique requestId generated in beforeLoad — safe under concurrent
 * requests on CF Workers (unlike a single module-scoped variable).
 *
 * Entries are deleted immediately after the synchronous React render reads them.
 * Safety cap of 50 entries prevents leaks from errored requests that never render.
 */
const ssrMessagesByRequest = new Map<string, Messages>();
const SSR_MAP_MAX_SIZE = 50;

/**
 * Safely serialize JSON for embedding inside <script type="application/json"> tags.
 * Escapes characters that would break HTML parsing: <, >, &, and line separators.
 */
function safeJsonForScript(value: unknown): string {
  // Guard: JSON.stringify(undefined) returns undefined (not "undefined") —
  // .replace() on undefined throws. Falls back to empty object so the script
  // tag is always valid JSON; client hydration can refetch via loader.
  return JSON.stringify(value ?? {})
    .replace(/</g, "\\u003c")
    .replace(/>/g, "\\u003e")
    .replace(/&/g, "\\u0026")
    .replace(/\u2028/g, "\\u2028")
    .replace(/\u2029/g, "\\u2029");
}

/**
 * Reads i18n messages from the `<script id="__i18n_messages__">` tag
 * injected during SSR. Called synchronously during React hydration.
 */
function getClientMessages(): Messages | undefined {
  if (typeof document === "undefined") return undefined;
  const el = document.getElementById("__i18n_messages__");
  if (!el?.textContent) return undefined;
  try {
    return JSON.parse(el.textContent) as Messages;
  } catch {
    return undefined;
  }
}

function createQueryClient() {
  return new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 5 * 60 * 1000, // 5 minutes cache
        refetchOnWindowFocus: false,
      },
    },
  });
}

interface RouterContext {
  locale: string;
  locales: string[];
  requestId: string; // Unique per SSR request — keys into ssrMessagesByRequest
}

/**
 * Paths that are handled by dedicated non-locale routes (API, etc.)
 * and should NOT be redirected to /$locale/...
 */
const BYPASS_LOCALE_CHECK = new Set(["api", "brand"]);

export const Route = createRootRouteWithContext<RouterContext>()({
  staleTime: 0, // locale değişince loader'ın yeniden çalışması gerekiyor
  beforeLoad: async ({ location }) => {
    const locales = await fetchLocales();
    const localeConfig = {
      locales,
      defaultLocale: i18nConfig.defaultLocale,
    };

    // Detect locale from URL path or geo-IP.
    // Routes without a valid locale prefix (including root /) get redirected
    // to /{detectedLocale}/{path} using CF Worker's X-Country header.
    const segments = location.pathname.split("/").filter(Boolean);
    const firstSegment = segments[0];
    const hasLocalePrefix = firstSegment && locales.includes(firstSegment);
    const isBypassPath = firstSegment && BYPASS_LOCALE_CHECK.has(firstSegment);

    if (!hasLocalePrefix && !isBypassPath) {
      const search = location.searchStr || "";
      const hash = location.hash || "";

      // Detect locale: cookie (manual user choice) > geo (CF X-Country) > default
      let countryCode: string | undefined;
      let cookieLocale: string | undefined;
      if (typeof document === "undefined") {
        try {
          countryCode = getRequestHeader("X-Country") ?? undefined;
          const cookieHeader = getRequestHeader("Cookie") ?? "";
          const cookieMatch = /preferred-locale=([^;]+)/.exec(cookieHeader);
          if (cookieMatch?.[1] && locales.includes(cookieMatch[1])) {
            cookieLocale = cookieMatch[1];
          }
        } catch {
          // getRequestHeader may throw outside request context (dev server)
        }
      }

      const { locale: detectedLocale } = detectLocale({
        project: i18nConfig.project,
        defaultLocale: i18nConfig.defaultLocale,
        availableLocales: locales,
        cookieLocale,
        countryCode,
      });

      // Cache warming: pre-load messages before redirect so loader gets a TtlCache hit
      await getMessages({ project: i18nConfig.project, locale: detectedLocale }).catch(() => {});

      const targetPath = location.pathname === "/" ? "" : location.pathname;
      throw redirect({
        href: `/${detectedLocale}${targetPath}${search}${hash}`,
        // 302 (not 301) — locale redirect is personalized (cookie + geo).
        // A 301 is cached indefinitely by browsers, so users who first landed
        // on /tr/ would never get re-detected after switching to /en/.
        statusCode: 302,
      });
    }

    const locale = getLocaleFromPath(location.pathname, localeConfig);
    const requestId = crypto.randomUUID();

    // Load messages ONCE here (beforeLoad runs sequentially parent→child).
    // Child loaders read from the shared Map via requestId — zero race condition.
    const pagePath = extractPagePath(location.pathname);
    const cdnNamespaces = getCdnNamespacesForPage(pagePath) ?? undefined;

    if (import.meta.env.DEV && cdnNamespaces) {
      console.info(`[i18n] ${pagePath || "home"}: fetching ${cdnNamespaces.length} namespaces (of 103)`, cdnNamespaces);
    }

    const allMessages = await getMessages({
      project: i18nConfig.project,
      locale,
      namespaces: cdnNamespaces,
    });
    storeMessages(requestId, allMessages);

    return { locale, locales, requestId };
  },

  loader: async ({ context, location }) => {
    const allMessages = readMessages(context.requestId) ?? await getMessages({
      project: i18nConfig.project,
      locale: context.locale,
      namespaces: getCdnNamespacesForPage(extractPagePath(location.pathname)) ?? undefined,
    });
    const messages = filterMessagesByPath(allMessages, location.pathname);

    const isSSR = typeof document === "undefined";
    if (isSSR) {
      // SSR: store in per-request Map, keep out of loader data (avoids dehydration).
      if (ssrMessagesByRequest.size >= SSR_MAP_MAX_SIZE) {
        const firstKey = ssrMessagesByRequest.keys().next().value;
        if (firstKey) ssrMessagesByRequest.delete(firstKey);
      }
      ssrMessagesByRequest.set(context.requestId, messages);
    }
    // SSR: messages=undefined (tiny in dehydration). Client nav: messages=full data.
    return { locale: context.locale, messages: isSSR ? undefined : messages };
  },

  head: () => {
    return {
      meta: [
        {
          charSet: "utf-8",
        },
        {
          name: "viewport",
          content: "width=device-width, initial-scale=1",
        },
        {
          name: "google",
          content: "notranslate",
        },
        {
          name: "theme-color",
          content: "#fafafa",
          media: "(prefers-color-scheme: light)",
        },
        {
          name: "theme-color",
          content: "#0a0a0a",
          media: "(prefers-color-scheme: dark)",
        },
      ],
      links: [
        { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
        { rel: "icon", type: "image/x-icon", href: "/favicon.ico" },
        // Always serve the dark-scheme PNG set. The prefers-color-scheme
        // split was previously serving a dark glyph on a transparent
        // background in light mode, which some browsers render as an empty
        // square against matching tab chrome (Safari private, some strict
        // tab themes). The dark-scheme variant is a light glyph that
        // stays visible on every tab background regardless of theme.
        { rel: "icon", type: "image/png", sizes: "32x32", href: "/favicon-32x32-dark.png" },
        { rel: "icon", type: "image/png", sizes: "16x16", href: "/favicon-16x16-dark.png" },
        { rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png" },
        { rel: "manifest", href: "/site.webmanifest" },
        // TODO: restore dns-prefetch for og.better-i18n.com when OG service is live
        {
          rel: "dns-prefetch",
          href: "https://dash.better-i18n.com",
        },
        {
          rel: "dns-prefetch",
          href: "https://docs.better-i18n.com",
        },
        {
          rel: "preload",
          href: "/fonts/Geist-Variable.woff2",
          as: "font",
          type: "font/woff2",
          crossOrigin: "anonymous",
        },
        {
          rel: "stylesheet",
          href: appCss,
        },
      ],
      scripts: [
        // Google Consent Mode v2 — must load BEFORE any GTM/GA4 scripts.
        // Sets default denied state; analytics.ts updates after user consent.
        {
          children: `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments)}gtag('consent','default',{analytics_storage:'denied',ad_storage:'denied',ad_user_data:'denied',ad_personalization:'denied',wait_for_update:500});`,
        },
      ],
    };
  },

  component: RootComponent,
  notFoundComponent: NotFoundPage,
});

function NotFoundPage() {
  const t = useTranslations("common");
  const { locale } = Route.useRouteContext();

  return (
    <MarketingLayout showCTA={false} bgClassName="bg-white">
      <div className="py-24 sm:py-32">
        <div className="mx-auto max-w-2xl px-6 text-center">
          <p className="font-display text-6xl font-medium tracking-[-0.02em] text-mist-300 sm:text-8xl">
            404
          </p>
          <h1 className="mt-4 font-display text-3xl/[1.1] font-medium tracking-[-0.02em] text-mist-950 sm:text-4xl/[1.1]">
            {t("notFound.title", { defaultValue: "Page not found" })}
          </h1>
          <p className="mt-4 text-lg text-mist-600">
            {t("notFound.description", {
              defaultValue:
                "The page you're looking for doesn't exist or has been moved.",
            })}
          </p>
          <Link
            to="/$locale/"
            params={{ locale }}
            className="mt-8 inline-flex items-center gap-2 rounded-full bg-mist-950 px-5 py-2.5 text-sm font-medium text-white hover:bg-mist-800 transition-colors"
          >
            <IconArrowLeft className="size-4" />
            {t("notFound.backHome", { defaultValue: "Back to Home" })}
          </Link>
        </div>
      </div>
    </MarketingLayout>
  );
}

function RootComponent() {
  const { locale, locales, requestId } = Route.useRouteContext();
  const router = useRouter();
  // Per-mount QueryClient — prevents cross-request cache leak on CF Workers
  const [queryClient] = useState(createQueryClient);

  // Locale change handler — triggers TanStack Router navigation so loaders
  // re-execute with the new locale. Without this, setLocale() would only
  // update history.replaceState which TanStack Router doesn't observe.
  const handleLocaleChange = useCallback(
    (newLocale: string) => {
      const path = window.location.pathname;
      const segments = path.split("/").filter(Boolean);
      const firstSegment = segments[0];
      if (firstSegment && /^[a-z]{2}$/i.test(firstSegment)) {
        segments[0] = newLocale;
      } else {
        segments.unshift(newLocale);
      }
      router.navigate({ to: "/" + segments.join("/") + window.location.search });
    },
    [router],
  );

  const loaderData = Route.useLoaderData();

  // SSR: per-request Map (race-free across concurrent CF Worker requests)
  // Client hydration: <script id="__i18n_messages__"> tag from SSR HTML
  // Client navigation: loader data (fresh messages for the new page)
  const messages = (() => {
    if (typeof document === "undefined") {
      const msgs = ssrMessagesByRequest.get(requestId);
      if (msgs) ssrMessagesByRequest.delete(requestId);
      return msgs;
    }
    return loaderData.messages ?? getClientMessages();
  })();

  useEffect(() => {
    if (import.meta.env.DEV) {
      import("react-grab");
      import("react-scan").then(({ scan }) => {
        scan({ enabled: true });
      });
    }
    // Initialize consent-gated analytics (loads scripts only if consent exists)
    import("../lib/analytics").then(({ initAnalytics }) => initAnalytics());
  }, []);

  return (
    <html lang={locale} translate="no" className="notranslate">
      <head>
        <HeadContent />
        {/* SSR locales → prevents client fetchLocales() from making a CDN round-trip.
            type="application/json" means the browser never executes this — no XSS risk. */}
        <script
          type="application/json"
          id="__i18n_locales__"
          suppressHydrationWarning
        >
          {safeJsonForScript(locales)}
        </script>
      </head>
      <body className="no-dark text-mist-950">
        {/* SSR messages — keeps ~30-60 KB out of TSR's dehydration pipeline.
            type="application/json" = browser never executes this (no XSS risk).
            Client reads synchronously during hydration via getClientMessages(). */}
        <script
          type="application/json"
          id="__i18n_messages__"
          suppressHydrationWarning
        >
          {safeJsonForScript(messages)}
        </script>
        <SvgSprite />
        <QueryClientProvider client={queryClient}>
          <BetterI18nProvider
            project={i18nConfig.project}
            locale={locale}
            messages={messages}
            timeZone="UTC"
            localeCookie="preferred-locale"
            onLocaleChange={handleLocaleChange}
            getMessageFallback={({ key }) => {
              const lastSegment = key.split(".").pop() || key;
              return lastSegment
                .replace(/([A-Z])/g, " $1")
                .replace(/^./, (c) => c.toUpperCase())
                .trim();
            }}
          >
            <ContentProvider
              config={{
                projectId: "better-i18n/landing",
                apiKey: "bi_pub_qJLePAhVnKMPVpLevSMlQaGqxWQoHGMCAOFWSfoCabCHJjMNFxZxLGeFUmIBetyB",
                analytics: { debug: import.meta.env.DEV },
              }}
            >
              <Outlet />
              <CookieBanner />
              <WebMcpRegistrar locale={locale} />
            </ContentProvider>
          </BetterI18nProvider>
        </QueryClientProvider>
        <Scripts />
        <HelpwayWidgetMount locale={locale} />
      </body>
    </html>
  );
}
