import type { Plugin, UserConfig, ViteBuilder } from "vite";
import type { VitePluginFn } from "../types.js";

import { resolveAutoDiscover } from "../config/autoDiscover/resolveAutoDiscover.js";
import { resolveUserConfig } from "../config/resolveUserConfig.js";
import { resolveOptions } from "../config/resolveOptions.js";
import { handleError } from "../error/handleError.js";
import { createDefaultModuleID } from "../config/createModuleID.js";
import { createLogger } from "vite";
import { join } from "node:path";
import { DEFAULT_LOADER_CONFIG } from "../config/defaults.js";
import { runDeferredStaticGeneration } from "../bundle/deferredStaticGeneration.js";

/**
 * Creates a plugin that ensures consistent hash generation across environments
 * by using the original source file content as the basis for hash generation.

*/
// Note: Path normalization should be handled in the file naming functions, not in writeBundle

/**
 * Environment Configuration Plugin
 *
 * This plugin configures Vite Environment API environments for React Server Components.
 * It's separate from the env plugin which handles process environment variables.
 *
 * Environment mapping:
 * - client (Vite client = browser) → dist/client (React client components - real implementations)
 * - ssr (Vite SSR = SSR-safe) → dist/static (SSR-compatible static files)
 * - server (custom) → dist/server (React server components with registerClientReference)
 */
export const createEnvironmentPlugin: VitePluginFn = (options): Plugin => {
  const environmentPlugin: Plugin = {
    name: "vite:plugin-react-server/environments",
    enforce: "pre",

    async config(config: UserConfig, configEnv) {
      // Resolve plugin options
      const resolvedOptionsResult = resolveOptions(options);
      if (resolvedOptionsResult.type === "error") {
        throw resolvedOptionsResult.error;
      }
      const userOptions = resolvedOptionsResult.userOptions;

      // Add transformer plugins at the Vite level with proper environment filtering
      if (!config.plugins) {
        config.plugins = [];
      }

      // Note: Transformer is now added via orchestrator, so skip adding it here
      // to avoid duplicates and ensure proper registration

      // Note: Hash coordination is handled by the sequential build approach
      // Each environment will use the manifest from the previous build

      // Set up logger and moduleID
      const logger = config.customLogger || createLogger();
      if (typeof userOptions.moduleID !== "function") {
        userOptions.moduleID = createDefaultModuleID(
          userOptions,
          configEnv,
          userOptions.loader?.mode
        );
      }
      // Always override the moduleID function to ensure it has the forTransformer logic
      if (!userOptions.loader) {
        userOptions.loader = DEFAULT_LOADER_CONFIG;
      }
      userOptions.loader.moduleID = createDefaultModuleID(
        userOptions,
        configEnv,
        userOptions.loader?.mode
      );

      // Run auto-discovery once to get all files - we don't need separate calls since
      // the file discovery process is identical, only the organization differs
      const autoDiscoverResult = await resolveAutoDiscover({
        config,
        configEnv,
        userOptions,
        logger,
      });

      if (autoDiscoverResult.type === "error") {
        const panicError = handleError({
          error: autoDiscoverResult.error,
          logger,
          context: "createEnvironmentPlugin(autoDiscover)",
          panicThreshold: userOptions.panicThreshold,
          critical: true, // Auto-discovery is critical for environment setup
        });
        if (panicError != null) {
          throw panicError;
        } else {
          // If handleError returns null but this is critical, we can't continue
          throw new Error("Cannot continue without auto-discovery");
        }
      }

      // Get the auto-discovered files (safe to access since we checked for errors above)
      const autoDiscoveredFiles = autoDiscoverResult.autoDiscoveredFiles!;

      // Define environment configurations
      const allEnvironmentConfigs = [
        {
          name: "client",
          condition: "react-client" as const,
          ssr: false,
          outDir: join(userOptions.build.outDir, userOptions.build.static),
        },
        {
          name: "ssr",
          condition: "react-client" as const,
          ssr: true,
          outDir: join(userOptions.build.outDir, userOptions.build.client),
        },
        {
          name: "server",
          condition: "react-server" as const,
          ssr: true,
          outDir: join(userOptions.build.outDir, userOptions.build.server),
        },
      ];

      // Filter environments based on availableEnvironments from orchestrator

      const availableEnvironments = (userOptions as any)
        .availableEnvironments || ["client", "ssr", "server"];

      const environmentConfigs = allEnvironmentConfigs.filter((config) =>
        availableEnvironments.includes(config.name)
      );


      // Resolve all environment configurations using resolveUserConfig
      const environments: Record<string, import("vite").EnvironmentOptions> =
        {};

      // Sort environments to process static first (to establish hashes)
      // Use the environment configs as-is
      const sortedEnvConfigs = environmentConfigs;

      for (const envConfig of sortedEnvConfigs) {
        const configResult = resolveUserConfig({
          condition: envConfig.condition,
          config,
          configEnv,
          userOptions,
          autoDiscoveredFiles,
          ssr: envConfig.ssr,
        });

        if (configResult.type === "error") {
          const panicError = handleError({
            error: configResult.error,
            logger,
            context: `createEnvironmentPlugin(${envConfig.name}Config)`,
            panicThreshold: userOptions.panicThreshold,
            critical: true,
          });
          if (panicError != null) {
            throw panicError;
          } else {
            throw new Error(
              `Cannot continue without ${envConfig.name} environment configuration`
            );
          }
        }

        // Map the resolved user config to Environment API compatible options
        const userConfig = configResult.userConfig;

        // Log the rollup inputs for this environment (only in verbose mode)
        if (userOptions.verbose) {
          logger?.info(
            `${envConfig.name} environment rollup inputs: ${JSON.stringify(
              userConfig.build.rollupOptions.input,
              null,
              2
            )}`
          );
          logger?.info(
            `${
              envConfig.name
            } environment output preserveModulesRoot: ${JSON.stringify(
              userConfig.build.rollupOptions.output,
              null,
              2
            )}`
          );
        }

        // Debug: Log what resolveUserConfig provided
        if (userOptions.verbose) {
          logger?.info(
            `${envConfig.name} userConfig.resolve: ${JSON.stringify(
              userConfig.resolve,
              null,
              2
            )}`
          );
          logger?.info(
            `${
              envConfig.name
            } userConfig.build.rollupOptions.external: ${JSON.stringify(
              userConfig.build.rollupOptions.external,
              null,
              2
            )}`
          );
        }
        // detect if legacy build or not
        const legacyBuild = userOptions.strategy?.legacyBuilder && !config?.builder;
        const implicitSsr =
          userOptions.strategy?.mainThreadCondition === "react-server" &&
          userOptions.strategy?.legacyBuilder;
        // this follows vite's logic for legacy builds
        const implicitViteBuildName =
          userOptions.strategy?.legacyBuilder && !config.build?.ssr
            ? "client"
            : "ssr";
        const consumer = legacyBuild
          ? implicitViteBuildName === "ssr"
            ? "server"
            : "client"
          : envConfig.name === "server" || envConfig.name === "ssr"
          ? "server"
          : "client";

        // Note: Path normalization should be handled in the file naming functions
        environments[envConfig.name] = {
          keepProcessEnv: envConfig.name === "server" ? true : false,
          define: userConfig.define,
          consumer: consumer,
          resolve: {
            ...userConfig.resolve,
            // IMPORTANT: Map externals from resolveUserConfig (rollupOptions.external) to Environment API format
            // In Environment API, externals go in resolve.external, not build.rollupOptions.external
            // For static builds (browser/ESM): don't externalize anything - bundle everything to avoid _virtual files
            // For client/server builds (SSR): externalize as configured
            external: (() => {
              const isStaticBuild = envConfig.name === "static" || (!envConfig.ssr && envConfig.name === "client");
              if (isStaticBuild) {
                // For static builds, don't externalize anything (bundle everything)
                return [];
              }
              // For SSR builds, use configured externals
              return Array.isArray(userConfig.build.rollupOptions.external)
                ? userConfig.build.rollupOptions.external.filter(
                    (item): item is string => typeof item === "string"
                  )
                : [];
            })(),
          },
          build: {
            ...userConfig.build,
            ssr:
              envConfig.name === "server"
                ? true
                : legacyBuild
                ? implicitSsr
                : envConfig.name === "ssr",
            target: userConfig.build.target,
            // Remove externals from rollupOptions since they should be in resolve.external for Environment API
            rollupOptions: {
              ...userConfig.build.rollupOptions,
              external: undefined, // Remove external from rollupOptions, it's now in resolve.external
              // Set preserveModules in the output configuration, not at the top level
              output: (() => {
                const output = userConfig.build.rollupOptions.output;
                
                // Handle array output configuration - extract the plugin output that contains preserveModulesRoot
                if (Array.isArray(output)) {
                  const pluginOutput = output.find(o => o && typeof o === 'object' && 'preserveModulesRoot' in o);
                  if (pluginOutput) {
                    return pluginOutput;
                  }
                  // If no pluginOutput found, use the first output configuration
                  if (output.length > 0) {
                    return output[0];
                  }
                }
                
                // Ensure preserveModulesRoot is always present in the output configuration
                if (output && typeof output === 'object' && !Array.isArray(output)) {
                  // Check if the property exists in the object (not just checking the value)
                  const hasPreserveModulesRoot = 'preserveModulesRoot' in output;
                  
                  if (hasPreserveModulesRoot) {
                    // Property exists, preserve the preserveModules value from the output (don't override it)
                    // This is critical for static builds where preserveModules: false is set
                    return output; // Return as-is, preserveModules is already set correctly
                  } else {
                    // Property missing, add it based on user options
                    const preserveModulesRootString = userOptions.build.preserveModulesRoot === false
                      ? userOptions.moduleBase
                      : undefined;
                    return { ...output, preserveModulesRoot: preserveModulesRootString };
                  }
                }
                
                return output;
              })(),
            },
          },
        };
      }

      // Force the PRODUCTION JSX transform for every build environment.
      //
      // Under a dev-mode build (`NODE_ENV=development … vite build --mode
      // development`) esbuild's automatic-runtime JSX transform emits the
      // dev call shape `jsxDEV(type, props, key, isStaticChildren, source,
      // self)`. esbuild renders the trailing `self` argument as a bare
      // `module` reference when it can't prove the file is ESM at
      // per-file transform time. The vprs server bundle is pure ESM
      // (`dist/server/*.js`), so at SSG-prerender time that `module`
      // identifier is undefined and the very first server component to
      // render (e.g. the built-in `Html` component) throws
      // `ReferenceError: module is not defined`.
      //
      // The server bundle never consumes jsxDEV's client-warning
      // `source`/`self` info, so dropping to the production transform
      // (`jsx`/`jsxs`, no `self` arg) is a pure win for builds:
      //   - It only changes the JSX *call shape*, NOT which React build is
      //     bundled — `NODE_ENV=development` still resolves the development
      //     (non-minified) React, so dev builds keep surfacing the errors
      //     production minifies away.
      //   - Production builds already use the production JSX transform
      //     (esbuild only emits jsxDEV in dev), so this is a no-op there.
      //   - Scoped to `command === "build"`, so the dev SERVER / client
      //     Fast Refresh path (`command === "serve"`) is untouched.
      const esbuildJsxDevOverride =
        configEnv.command === "build" && config.esbuild !== false
          ? { esbuild: { ...config.esbuild, jsxDev: false } }
          : {};

      // Return the configuration with all environments
      // Build order: client → ssr → server → static generation (step 4)
      // Server build runs LAST so dist/client exists when HTML rendering references client components
      // Static generation is deferred to run after ALL environments complete (needs server manifest)
      return {
        root: userOptions.projectRoot,
        ...config,
        ...esbuildJsxDevOverride,
        environments,
        builder: {
          async buildApp(builder: ViteBuilder) {
            // Build all environments in definition order
            for (const environment of Object.values(builder.environments)) {
              await builder.build(environment);
            }
            // Step 4: Run deferred static generation now that all manifests are available
            await runDeferredStaticGeneration();
          },
        },
      };
    },
  };

  return environmentPlugin;
};
