import { trpc } from "@/lib/trpc";
import { UNAUTHED_ERR_MSG } from '@shared/const';
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink, httpLink, splitLink, TRPCClientError } from "@trpc/client";
import { createRoot } from "react-dom/client";
import superjson from "superjson";
import App from "./App";
import { getLoginUrl } from "./const";
import "./index.css";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      // Show cached data instantly on navigation, refetch in background
      staleTime: 60_000, // 60s — data is "fresh" for 1 min (no refetch on remount)
      gcTime: 10 * 60_000, // 10 min — keep data in memory even after unmount
      refetchOnWindowFocus: false, // Don't refetch just because user alt-tabbed
      retry: 1, // Only retry once on failure
    },
  },
});

const redirectToLoginIfUnauthorized = (error: unknown) => {
  // Disabled: Allow app to work without authentication
  // if (!(error instanceof TRPCClientError)) return;
  // if (typeof window === "undefined") return;

  // const isUnauthorized = error.message === UNAUTHED_ERR_MSG;

  // if (!isUnauthorized) return;

  // window.location.href = getLoginUrl();
};

queryClient.getQueryCache().subscribe(event => {
  if (event.type === "updated" && event.action.type === "error") {
    const error = event.query.state.error;
    redirectToLoginIfUnauthorized(error);
    console.error("[API Query Error]", error);
  }
});

queryClient.getMutationCache().subscribe(event => {
  if (event.type === "updated" && event.action.type === "error") {
    const error = event.mutation.state.error;
    redirectToLoginIfUnauthorized(error);
    console.error("[API Mutation Error]", error);
  }
});

/**
 * Read the CSRF token from the __csrf cookie.
 * The server sets this as a non-httpOnly cookie so JS can read it.
 */
function getCsrfToken(): string | null {
  const match = document.cookie.match(/(?:^|;\s*)__csrf=([^;]*)/);
  return match ? decodeURIComponent(match[1]) : null;
}

/**
 * Fetch the initial CSRF token from the server on app load.
 * Retries up to 3 times with exponential backoff to handle cold-start delays.
 */
async function fetchCsrfWithRetry(retries = 3): Promise<void> {
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      const res = await fetch('/api/csrf-token', { credentials: 'include' });
      if (res.ok) return; // cookie is now set
      // Non-OK response — retry
      console.warn(`[CSRF] Token fetch returned ${res.status}, retrying (${attempt + 1}/${retries})...`);
    } catch {
      console.warn(`[CSRF] Token fetch failed, retrying (${attempt + 1}/${retries})...`);
    }
    // Exponential backoff: 1s, 2s, 4s
    if (attempt < retries - 1) {
      await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
    }
  }
  console.error('[CSRF] Failed to fetch token after all retries. Mutations may fail.');
}

const csrfReady = fetchCsrfWithRetry();

// Client-side fetch timeout for tRPC requests (3 minutes)
// Prevents infinite spinner when proxy/server drops the connection silently
const CLIENT_FETCH_TIMEOUT_MS = 180_000;

const fetchWithCredentials = async (input: RequestInfo | URL, init?: RequestInit) => {
  // If no CSRF token yet, wait for the initial fetch to complete (prevents race condition
  // where mutations fire before the CSRF cookie is set, causing "Unable to transform response")
  let csrfToken = getCsrfToken();
  if (!csrfToken) {
    await csrfReady;
    csrfToken = getCsrfToken();
  }
  // If still no token after initial fetch (server was cold-starting), try one more time
  if (!csrfToken) {
    try {
      await fetch('/api/csrf-token', { credentials: 'include' });
      csrfToken = getCsrfToken();
    } catch { /* proceed without token — server will set it in response */ }
  }
  const headers = new Headers(init?.headers);
  if (csrfToken) {
    headers.set('x-csrf-token', csrfToken);
  }

  // Add AbortController with timeout to prevent hanging forever
  const controller = new AbortController();
  const existingSignal = init?.signal;
  const timeoutId = setTimeout(() => controller.abort(), CLIENT_FETCH_TIMEOUT_MS);

  // If caller already has a signal, chain them
  if (existingSignal) {
    existingSignal.addEventListener('abort', () => controller.abort());
  }

  // Retry logic for 503 (Cloud Run cold-start) — up to 2 retries with backoff
  const MAX_RETRIES = 2;
  let lastResponse: Response | null = null;

  try {
    for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
      try {
        const response = await globalThis.fetch(input, {
          ...(init ?? {}),
          headers,
          credentials: "include",
          signal: controller.signal,
        });

        // If 503 (service unavailable / cold start), retry after a delay
        if (response.status === 503 && attempt < MAX_RETRIES) {
          lastResponse = response;
          console.warn(`[tRPC] Got 503, retrying (${attempt + 1}/${MAX_RETRIES})...`);
          await new Promise(r => setTimeout(r, 1500 * (attempt + 1))); // 1.5s, 3s
          continue;
        }

        return response;
      } catch (err: any) {
        if (err.name === 'AbortError') {
          throw new Error('Request timed out. The AI is taking longer than usual — please try again.');
        }
        // Network errors on non-final attempt — retry
        if (attempt < MAX_RETRIES) {
          console.warn(`[tRPC] Network error, retrying (${attempt + 1}/${MAX_RETRIES})...`);
          await new Promise(r => setTimeout(r, 1500 * (attempt + 1)));
          continue;
        }
        throw err;
      }
    }
    // Should not reach here, but return last response if we do
    return lastResponse!;
  } finally {
    clearTimeout(timeoutId);
  }
};

const trpcClient = trpc.createClient({
  links: [
    splitLink({
      // Use non-batched httpLink for mutations to prevent them from being
      // combined with heavy queries (like getById with full transcript).
      // This avoids proxy timeout issues where the batched response is too
      // large or takes too long, causing the proxy to return HTML error pages.
      condition: (op) => op.type === 'mutation',
      true: httpLink({
        url: "/api/trpc",
        transformer: superjson,
        fetch: fetchWithCredentials,
      }),
      false: httpBatchLink({
        url: "/api/trpc",
        transformer: superjson,
        maxURLLength: 2000,
        fetch: fetchWithCredentials,
      }),
    }),
  ],
});

// Detect PWA standalone mode and add class to <html> for CSS targeting
// iOS Safari doesn't support @media (display-mode: standalone), so we use JS detection
(function detectStandalone() {
  const nav = window.navigator as any;
  const isStandalone = nav.standalone === true || window.matchMedia('(display-mode: standalone)').matches;
  if (isStandalone) {
    document.documentElement.classList.add('pwa-standalone');
  }
})();

createRoot(document.getElementById("root")!).render(
  <trpc.Provider client={trpcClient} queryClient={queryClient}>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </trpc.Provider>
);

// Signal the splash screen that React has mounted.
// The splash screen will advance to 100% and fade out gracefully.
// If the splash screen is already gone (fast load), this is a no-op.
requestAnimationFrame(() => {
  (window as any).__strataReady = true;
  // Fallback: if the progress animation already stopped, force removal
  const loader = document.getElementById('strata-loader');
  if (loader) {
    const fill = document.getElementById('strata-progress-fill');
    if (fill) fill.style.width = '100%';
    setTimeout(() => {
      loader.classList.add('fade-out');
      setTimeout(() => loader.remove(), 600);
    }, 300);
  }
});

// Register service worker for PWA support
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker
      .register('/sw.js', { scope: '/' })
      .then((registration) => {
        console.log('[SW] Registered with scope:', registration.scope);

        // Check for updates periodically (every 60 minutes)
        setInterval(() => {
          registration.update();
        }, 60 * 60 * 1000);

        // Listen for new service worker activation
        registration.addEventListener('updatefound', () => {
          const newWorker = registration.installing;
          if (newWorker) {
            newWorker.addEventListener('statechange', () => {
              if (newWorker.state === 'activated' && navigator.serviceWorker.controller) {
                // New version available - could show update prompt
                console.log('[SW] New version available');
              }
            });
          }
        });
      })
      .catch((error) => {
        console.warn('[SW] Registration failed:', error);
      });
  });
}
