import { fileURLToPath } from "node:url";
import { defineConfig, loadEnv, type Plugin } from "vite";
import { cloudflare } from "@cloudflare/vite-plugin";
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import executorVitePlugin from "@executor-js/vite-plugin";
import { unstable_readConfig } from "wrangler";

import { routes } from "./tsr.routes";

// Dev-only: the cloudflare vite-plugin bridges outbound fetches (JWKS,
// OAuth metadata proxy, etc.) through node undici in the host process. If
// a pooled keep-alive socket gets RST'd while no listener is attached, the
// `'error'` emit is unhandled and tears down the whole dev server. Log
// enough to identify the offender and keep the server alive.
const devCrashGuard = (): Plugin => {
  let installed = false;
  const install = () => {
    if (installed) return;
    installed = true;
    process.on("uncaughtException", (err, origin) => {
      console.error(`[dev-crash-guard] uncaughtException (origin=${origin}):`, err);
    });
    process.on("unhandledRejection", (reason, promise) => {
      console.error("[dev-crash-guard] unhandledRejection:", reason, promise);
    });
  };
  return {
    name: "dev-crash-guard",
    apply: "serve",
    configureServer: install,
  };
};

const loadWranglerPublicVars = () => {
  const wranglerConfig = unstable_readConfig(
    { config: fileURLToPath(new URL("./wrangler.jsonc", import.meta.url)) },
    { hideWarnings: true },
  );
  return Object.fromEntries(
    Object.entries(wranglerConfig.vars ?? {}).filter(([key]) => key.startsWith("VITE_PUBLIC_")),
  );
};

// VITE_PUBLIC_ANALYTICS_PATH is generated once per build by `scripts/build.mjs`
// and inherited via process.env, so the client and SSR/Cloudflare environment
// builds bake the same value. The fallback "a" is for `vite dev`, where the
// proxy isn't routed anyway.
const ANALYTICS_PATH = process.env.VITE_PUBLIC_ANALYTICS_PATH ?? "a";

export default defineConfig(({ command, mode }) => {
  const env = loadEnv(mode, process.cwd(), "");
  const publicEnv = {
    ...loadWranglerPublicVars(),
    VITE_PUBLIC_ANALYTICS_PATH: ANALYTICS_PATH,
    ...env,
  };
  // The wrangler-declared OTLP endpoint is for DEPLOYED workers (the
  // /v1/traces forwarding route). Under `vite dev` that path is only the
  // proxy below — keep the exporter off unless something actually listens
  // (e2e/dev sets MOTEL_URL or the env var itself), or every dev session
  // posts spans into a dead proxy once a second.
  if (command === "serve" && !process.env.MOTEL_URL && !env.VITE_PUBLIC_OTLP_TRACES_URL) {
    delete (publicEnv as Record<string, string | undefined>).VITE_PUBLIC_OTLP_TRACES_URL;
  }

  // Deps vite only discovers once a lazy-loaded React chunk actually renders
  // (e.g. opening the MCP/OpenAPI "add source" flow). Discovering them mid-run
  // forces a re-optimize + full program reload; in workerd (apps/cloud's SSR
  // worker) each reload stacks a new isolate's heap on top of the last one
  // without freeing it, so a handful of reloads exhausts the worker's heap
  // limit and crashes the dev server. Pre-bundling them at boot means vite
  // never discovers them mid-run, so it never reloads. Keep this list scoped
  // to deps actually imported by UI code (grep `from "effect/` under
  // packages/react/src and packages/plugins/*/src/react, plus js-yaml pulled
  // in via packages/plugins/openapi/src/sdk) rather than including the world.
  // js-yaml is a transitive dep (via @executor-js/plugin-openapi), not hoisted
  // into apps/cloud/node_modules under bun's isolated install, so a plain
  // "js-yaml" specifier fails to resolve here and silently falls out of the
  // pre-bundle. The "<pkg> > <dep>" syntax resolves it starting from that
  // package's own node_modules instead.
  const lateDiscoveredDeps = [
    "effect/Match",
    "effect/Predicate",
    "effect/Exit",
    "effect/Option",
    "effect/Cause",
    "effect/Data",
    "effect/Schema",
    "@executor-js/plugin-openapi > js-yaml",
  ];

  return {
    define: Object.fromEntries(
      Object.entries(publicEnv)
        .filter(([key]) => key.startsWith("VITE_PUBLIC_"))
        .map(([key, value]) => [`import.meta.env.${key}`, JSON.stringify(value)]),
    ),
    // Browser OTLP spans (VITE_PUBLIC_OTLP_TRACES_URL=/v1/traces, set by the
    // e2e global setup) go same-origin and proxy to the local motel server —
    // motel serves no CORS headers, so a direct cross-origin post would die
    // in preflight. Dev-only; unrouted when nothing listens.
    server: {
      proxy: {
        "/v1/traces": process.env.MOTEL_URL ?? "http://127.0.0.1:27686",
      },
    },
    resolve: { tsconfigPaths: true },
    // Client-side pre-bundle. See lateDiscoveredDeps comment above.
    optimizeDeps: {
      include: lateDiscoveredDeps,
    },
    // SSR/worker-side pre-bundle for the same deps, keyed under the "ssr"
    // environment name that `cloudflare({ viteEnvironment: { name: "ssr" } })`
    // below uses. The cloudflare plugin sets its own environments.ssr.optimizeDeps
    // (entries/exclude for worker externals); vite merges include arrays rather
    // than replacing them, so this only adds to that, it doesn't fight it.
    environments: {
      ssr: {
        optimizeDeps: {
          include: lateDiscoveredDeps,
        },
      },
    },
    plugins: [
      devCrashGuard(),
      tailwindcss(),
      executorVitePlugin(),
      cloudflare({ viteEnvironment: { name: "ssr" }, inspectorPort: false }),
      tanstackStart({
        // The route tree definition lives in tsr.routes.ts (shared with
        // tsr.config.json so `bunx tsr generate` produces the same tree).
        router: {
          virtualRouteConfig: routes,
        },
      }),
      react(),
    ],
  };
});
