import type { PreRenderedAsset, PreRenderedChunk } from "rollup";
import type {
  StreamPluginOptions,
  ResolvedUserOptions,
  PageName,
  PropsName,
  HtmlName,
  RootName,
} from "../types.js";
import {
  BASE_PATTERNS,
  DEFAULT_CONFIG,
  DEFAULT_LOADER_CONFIG,
} from "./defaults.js";
import { join, resolve } from "node:path";
import { pluginRoot } from "../root.js";
import { createInputNormalizer } from "../helpers/inputNormalizer.js";
import { resolveDirectiveMatcher } from "./resolveDirectiveMatcher.js";
import { resolveAllowedDirectives } from "./resolveAllowedDirectives.js";
import { resolveRegExp } from "./resolveRegExp.js";
import type { LoaderConfig } from "../loader/types.js";
import { handleError } from "../error/handleError.js";
import { createLogger, type Logger } from "vite";
import { getCondition } from "./getCondition.js";
import { getNodeEnv } from "./getNodeEnv.js";
import { stashUserOptions, getStashedUserOptions, getEnvironmentId } from "./stashedOptionsState.js";
import { readFileSync, existsSync } from "node:fs";
import { createRollupLikeHash } from "./createRollupLikeHash.js";

export type ResolveOptionsReturn =
  | {
      type: "success";
      userOptions: ResolvedUserOptions;
      error?: never;
    }
  | { type: "error"; error: unknown; userOptions?: never };

export type ResolveOptionsFn = (
  options: StreamPluginOptions,
  forceResolve?: boolean,
  logger?: Logger
) => ResolveOptionsReturn;

// /**
//  * Ensures a path ends with .js extension
//  */
// const addExtension = (path: string, extension: string = "js") => {
//   if (path.endsWith(`.${extension}`)) return path;
//   if (path.endsWith("/.")) return path.slice(0, -2) + "." + extension;
//   if (path.endsWith(".")) return path + "." + extension;
//   return path + "." + extension;
// };

/**
 * Handles search query parameters in file paths
 */
const handleSearchQuery = (path: string) => {
  const searchQuery = path.split("?")[1];
  if (!searchQuery) return path;
  const folder = path.split("/").slice(0, -1).join("/");
  const filename = path.split("/").pop();
  const fileNameExtIndex = filename?.lastIndexOf(".");
  const fileNameWithoutExt = filename?.slice(0, fileNameExtIndex);
  const extension = filename?.slice(fileNameExtIndex);
  return `${folder}/${fileNameWithoutExt}.${searchQuery}.${extension}`;
};

/**
 * Registers a path with an optional pattern matcher and extension.
 * If a pattern matches and the path doesn't end with the extension, the extension is appended.
 *
 * @param path - The path to register
 * @param pattern - Optional pattern matcher function that returns true if the path matches
 * @param ext - Optional extension to append if pattern matches and path doesn't already end with it
 */
const registerPath = (path: string, pattern?: RegExp, ext?: string) => {
  // If we have a pattern and it doesn't match, or we have an extension and the path doesn't end with it, append the extension
  if ((pattern && !pattern.test(path)) || (ext && !path.endsWith(ext))) {
    return path + ext;
  }
  return path;
};

// ============================================================================
// Main Options Resolver
// ============================================================================

let originalOptions: any | null = null;
/**
 * Resolves the user options for the plugin.
 *
 * @param options - The user options to resolve.
 * @returns The resolved options.
 */
export const resolveOptions: ResolveOptionsFn = function _resolveOptions(
  options = {} as StreamPluginOptions,
  forceResolve = false,
  logger = createLogger(!options.verbose ? "error" : "info", {
    prefix: "vite:plugin-react-server/config#resolveOptions",
  })
) {
  if (!forceResolve && originalOptions == null) {
    originalOptions = options;
  } else if (originalOptions != null && options !== originalOptions) {
    if (options.verbose) {
      logger.info("options changed, forcing re-resolve");
    }
    forceResolve = true;
  }
  
  // Force re-resolve if projectRoot changed
  if (originalOptions != null && originalOptions.projectRoot !== options.projectRoot) {
    if (options.verbose) {
      logger.info(`projectRoot changed from ${originalOptions.projectRoot} to ${options.projectRoot}, forcing re-resolve`);
    }
    forceResolve = true;
  }
  
  const panicThreshold =
    typeof options.panicThreshold === "string"
      ? options.panicThreshold
      : DEFAULT_CONFIG.PANIC_THRESHOLD;
  // since panicThreshold affects the behavior of the plugin, we need to re-resolve the options if it changes
  const envId = getEnvironmentId(getCondition(), process.env.NODE_ENV ?? "production");

  // Return stashed options if available
  const stashedOptions = getStashedUserOptions(envId);
  if (stashedOptions && !forceResolve) {
    return {
      type: "success",
      userOptions: stashedOptions as never,
    };
  }

  const loaderMode = options.loader?.mode ?? undefined;
  // Module path configuration
  const moduleBase =
    typeof options.moduleBase === "string"
      ? options.moduleBase
      : DEFAULT_CONFIG.MODULE_BASE;

  // Basic configuration - use the first projectRoot that was provided, or fall back to current options
  const projectRoot = options.projectRoot ?? originalOptions?.projectRoot ?? process.cwd();
  
  if (options.verbose && options.projectRoot != originalOptions?.projectRoot) {
    logger.info(`[resolveOptions] new projectRoot: ${projectRoot}`);
  }

  // Build options
  const preserveModulesRoot =
    options.build?.preserveModulesRoot ??
    DEFAULT_CONFIG.BUILD.preserveModulesRoot;

  // Get current mode to check if we're in development
  const currentMode = getNodeEnv(process.env.NODE_ENV);
  const isDevelopment = currentMode === "development";

  // Rollup's preserveModulesRoot works in reverse of what you'd expect:
  // - When user wants preservation (true): pass undefined to Rollup (don't strip anything)
  // - When user wants stripping (false): pass moduleBase to Rollup (strip this path)
  // CRITICAL: In development mode, NEVER strip moduleBase (src/) because Vite serves from source locations
  const preserveModulesRootString =
    !isDevelopment && preserveModulesRoot === false
      ? moduleBase // Strip src/ from output paths (production only)
      : undefined; // Keep src/ in output paths

  const client =
    typeof options.build?.client === "string"
      ? options.build.client
      : DEFAULT_CONFIG.BUILD.client;

  const outDir =
    typeof options.build?.outDir === "string"
      ? options.build.outDir
      : DEFAULT_CONFIG.BUILD.outDir;

  // Use defaults for now - these will be updated later when we have the config with proper prefix
  const moduleBasePath =
    typeof options.moduleBasePath === "string"
      ? options.moduleBasePath
      : DEFAULT_CONFIG.MODULE_BASE_PATH;

  const moduleBaseURL =
    typeof options.moduleBaseURL === "string"
      ? options.moduleBaseURL
      : DEFAULT_CONFIG.MODULE_BASE_URL;

  const moduleRootPath =
    typeof options.moduleRootPath === "string"
      ? options.moduleRootPath
      : join(projectRoot, outDir, client);

  const publicOrigin =
    typeof options.publicOrigin === "string"
      ? options.publicOrigin
      : DEFAULT_CONFIG.PUBLIC_ORIGIN;


  const rscWorkerPath =
    typeof options.rscWorkerPath === "string"
      ? resolve(projectRoot, options.rscWorkerPath)
      : resolve(pluginRoot, DEFAULT_CONFIG.RSC_WORKER_PATH);

  const htmlWorkerPath =
    typeof options.htmlWorkerPath === "string"
      ? resolve(projectRoot, options.htmlWorkerPath)
      : resolve(pluginRoot, DEFAULT_CONFIG.HTML_WORKER_PATH);

  const loaderPath =
    typeof options.loaderPath === "string"
      ? resolve(projectRoot, options.loaderPath)
      : resolve(pluginRoot, DEFAULT_CONFIG.LOADER_PATH);

  const jsExtension =
    typeof options.build?.jsExtension === "string"
      ? options.build.jsExtension
      : DEFAULT_CONFIG.BUILD.jsExtension;
  const cssExtension =
    typeof options.build?.cssExtension === "string"
      ? options.build.cssExtension
      : DEFAULT_CONFIG.BUILD.cssExtension;
  const cssModuleExtension =
    typeof options.build?.cssModuleExtension === "string"
      ? options.build.cssModuleExtension
      : DEFAULT_CONFIG.BUILD.cssModuleExtension;
  const htmlExtension =
    typeof options.build?.htmlExtension === "string"
      ? options.build.htmlExtension
      : DEFAULT_CONFIG.BUILD.htmlExtension;
  const jsonExtension =
    typeof options.build?.jsonExtension === "string"
      ? options.build.jsonExtension
      : DEFAULT_CONFIG.BUILD.jsonExtension;
  const rscExtension =
    typeof options.build?.rscExtension === "string"
      ? options.build.rscExtension
      : DEFAULT_CONFIG.BUILD.rscExtension;

  const rscOutputPath =
    typeof options.build?.rscOutputPath === "string"
      ? options.build.rscOutputPath
      : DEFAULT_CONFIG.BUILD.rscOutputPath;
  const htmlOutputPath =
    typeof options.build?.htmlOutputPath === "string"
      ? options.build.htmlOutputPath
      : DEFAULT_CONFIG.BUILD.htmlOutputPath;

  const modulePattern = resolveRegExp(
    options.autoDiscover?.modulePattern,
    DEFAULT_CONFIG.AUTO_DISCOVER.modulePattern
  );

  const jsonPattern = resolveRegExp(
    options.autoDiscover?.jsonPattern,
    DEFAULT_CONFIG.AUTO_DISCOVER.jsonPattern
  );

  const cssPattern = resolveRegExp(
    options.autoDiscover?.cssPattern,
    DEFAULT_CONFIG.AUTO_DISCOVER.cssPattern
  );

  const htmlPattern = resolveRegExp(
    options.autoDiscover?.htmlPattern,
    DEFAULT_CONFIG.AUTO_DISCOVER.htmlPattern
  );

  const rscPattern = resolveRegExp(
    options.autoDiscover?.rscPattern,
    DEFAULT_CONFIG.AUTO_DISCOVER.rscPattern
  );

  const clientPattern = resolveRegExp(
    options.autoDiscover?.clientPattern,
    DEFAULT_CONFIG.AUTO_DISCOVER.clientPattern
  );

  const serverPattern = resolveRegExp(
    options.autoDiscover?.serverPattern,
    DEFAULT_CONFIG.AUTO_DISCOVER.serverPattern
  );

  const nodePattern = resolveRegExp(
    options.autoDiscover?.nodePattern,
    DEFAULT_CONFIG.AUTO_DISCOVER.nodeOnly
  );

  const propsPattern = resolveRegExp(
    options.autoDiscover?.propsPattern,
    DEFAULT_CONFIG.AUTO_DISCOVER.propsPattern
  );

  const pagePattern = resolveRegExp(
    options.autoDiscover?.pagePattern,
    DEFAULT_CONFIG.AUTO_DISCOVER.pagePattern
  );

  const cssModulePattern = resolveRegExp(
    options.autoDiscover?.cssModulePattern,
    DEFAULT_CONFIG.AUTO_DISCOVER.cssModulePattern
  );

  const vendorPattern = resolveRegExp(
    options.autoDiscover?.vendorPattern,
    DEFAULT_CONFIG.AUTO_DISCOVER.vendorPattern
  );

  const virtualPattern = resolveRegExp(
    options.autoDiscover?.virtualPattern,
    DEFAULT_CONFIG.AUTO_DISCOVER.virtualPattern
  );

  const dotPattern = resolveRegExp(
    options.autoDiscover?.dotPattern,
    BASE_PATTERNS.DOT_FILES
  );

  /** Loader options */
  const serverDirective = resolveRegExp(
    options.loader?.serverDirective,
    DEFAULT_LOADER_CONFIG.serverDirective
  );
  const clientDirective = resolveRegExp(
    options.loader?.clientDirective,
    DEFAULT_LOADER_CONFIG.clientDirective
  );
  const isServerFunctionCode = resolveDirectiveMatcher(
    serverDirective,
    (code: string, moduleId?: string) =>
      code.match(serverDirective) != null ||
      (typeof moduleId === "string" && serverPattern.test(moduleId)) ||
      false
  );

  const isClientComponentCode = resolveDirectiveMatcher(
    clientDirective,
    (code: string, moduleId?: string) =>
      code.match(clientDirective) != null ||
      (typeof moduleId === "string" && clientPattern.test(moduleId)) ||
      false
  );

  const isClientComponentByCode = resolveDirectiveMatcher(
    clientDirective,
    (code: string) => code.match(clientDirective) != null || false
  );

  const isClientComponentByName = (moduleId: string, _transformedModuleId?: string) => 
    (typeof moduleId === "string" && clientPattern.test(moduleId)) || false;

  const hashOption = options.build?.hash ?? DEFAULT_CONFIG.BUILD.hash;

  // User-facing hash function that can take source content or filename
  const hash = (input: string | null, _ssr: boolean, sourceContent?: string) => {
    if (!input) return "";
    if (new RegExp(BASE_PATTERNS.EXT.NODE).test(input)) {
      return input;
    }
    
    // CRITICAL: Never hash node_modules files - Vite/Rollup handles those
    if (input.includes("node_modules")) {
      return input;
    }
    
    // CRITICAL: Never hash virtual modules (_virtual or matching virtualPattern) - Vite handles those
    const virtualPattern = resolveRegExp(
      options.autoDiscover?.virtualPattern,
      DEFAULT_CONFIG.AUTO_DISCOVER.virtualPattern
    );
    if (input.includes("_virtual") || (virtualPattern && virtualPattern.test(input))) {
      return input;
    }
    
    // Check if hashing is disabled
    if (hashOption === "false") {
      return input;
    }
    
    // Skip outputs that are addressed by canonical path rather than via the
    // build manifest, so renaming them would break their callers:
    //   - htmlPattern / rscPattern: SSG outputs at fixed route URLs
    //   - pagePattern / propsPattern / serverPattern: SSR entry modules the
    //     runtime imports by literal path (see resolvePageAndProps), not
    //     browser-served assets so caching isn't a concern either
    // Everything else falls through to content-based hashing so prod deploys
    // bust browser/CDN caches. Cross-environment hash consistency is provided
    // by handleSsrEntryName / handleSsrAssetName in resolveUserConfig.ts
    // (which feed sourceContent through to this function) and the
    // augmentChunkHash plugin.
    if (
      htmlPattern.test(input) ||
      rscPattern.test(input) ||
      pagePattern.test(input) ||
      propsPattern.test(input) ||
      serverPattern.test(input)
    ) {
      return input;
    }

    // Determine what to hash based on available input
    let contentToHash: string;
    
    if (sourceContent) {
      // Use provided source content (preferred)
      contentToHash = sourceContent;
    } else {
           // Try to read source file content
     try {
       const sourcePath = resolve(projectRoot, input);
       if (existsSync(sourcePath)) {
         contentToHash = readFileSync(sourcePath, 'utf-8');
         // Debug logging
         if (options.verbose) {
           logger.info(`[hash] Reading source: ${sourcePath} (${contentToHash.length} chars)`);
         }
       } else {
         // Fallback to filename
         contentToHash = input;
         if (options.verbose) {
           logger.warn(`[hash] File not found: ${sourcePath}, using filename: ${input}`);
         }
       }
     } catch (error) {
       // Fallback to filename
       contentToHash = input;
       if (options.verbose) {
         logger.warn(`[hash] Error reading ${input}: ${error}`);
       }
     }
    }
    
    // Generate hash using Rollup-like algorithm
    const hashCharacters = typeof hashOption === 'object' && hashOption?.format === 'hex' ? 'hex' : 'base36';
    const contentHash = createRollupLikeHash(contentToHash, hashCharacters);
    
    // Apply naming logic
    const extensionIndex = input.lastIndexOf(".");
    if (extensionIndex !== -1) {
      const extension = input.slice(extensionIndex);
      const filename = input.slice(0, extensionIndex);
      return filename + "-" + contentHash + extension;
    } else {
      return input + "-" + contentHash;
    }
  };

  // Output path resolution
  const getOutputPath = (n: string | null, isAsset: boolean = false) => {
    if (!n) return "";
    let path = handleSearchQuery(n);
    path = path.startsWith(moduleBase + moduleBasePath)
      ? path.slice(moduleBase.length + moduleBasePath.length)
      : path;

    // For assets that are not modules, preserve the original extension
    // Only apply module transformations if the file actually matches module patterns
    if (isAsset) {
      // Check if this is a module file (JS/TS/JSX/TSX) - if so, apply module transformations
      const isModuleFile = modulePattern.test(path) || 
                          clientPattern.test(path) || 
                          serverPattern.test(path) ||
                          propsPattern.test(path) ||
                          pagePattern.test(path);
      
      // If it's not a module file, preserve the original extension
      if (!isModuleFile) {
        return path;
      }
    }

    if (vendorPattern.test(path))
      return registerPath(path, vendorPattern, jsExtension);
    if (cssModulePattern.test(path))
      return registerPath(path, cssModulePattern, cssExtension);
    if (cssPattern.test(path))
      return registerPath(path, cssPattern, cssExtension);
    if (clientPattern.test(path))
      return registerPath(path, clientPattern, jsExtension);
    if (htmlPattern.test(path))
      return registerPath(path, htmlPattern, htmlExtension);
    if (jsonPattern.test(path))
      return registerPath(path, jsonPattern, jsonExtension);
    if (propsPattern.test(path))
      return registerPath(path, propsPattern, jsExtension);
    if (pagePattern.test(path))
      return registerPath(path, pagePattern, jsExtension);
    if (serverPattern.test(path))
      return registerPath(path, serverPattern, jsExtension);
    if (modulePattern.test(path))
      return registerPath(path, modulePattern, jsExtension);
    
    // Fallback: only apply module pattern if we're sure it's a module
    // For assets, we've already returned above, so this is safe
    return registerPath(path, modulePattern, jsExtension);
  };

  const normalizer =
    options.normalizer ??
    createInputNormalizer({
      root: projectRoot,
      preserveModulesRoot: preserveModulesRootString,
      removeExtension: true,
      moduleBasePath,
      moduleBaseURL,
    });
  // File naming functions - defined as regular functions to avoid closure issues
  function entryFile(n: PreRenderedChunk, ssr: boolean, sourceContent?: string): string {
    const normalizedName = normalizer(n.name)[0];
    let outputPath = getOutputPath(normalizedName);
    
    // When preserveModulesRoot is true, preserve the src/ prefix in output paths
    if (preserveModulesRoot && n.name.startsWith("src/")) {
      // If the normalized name doesn't have src/, add it back
      if (!outputPath.startsWith("src/")) {
        // Handle the case where outputPath starts with a slash
        if (outputPath.startsWith("/")) {
          outputPath = "src" + outputPath;
        } else {
          outputPath = "src/" + outputPath;
        }
      }
    }
    
    return hash(outputPath, ssr, sourceContent);
  }

  function chunkFile(n: PreRenderedChunk, ssr: boolean, sourceContent?: string): string {
    const normalizedName = normalizer(n.name)[0];
    let outputPath = getOutputPath(normalizedName);
    
    // When preserveModulesRoot is true, preserve the src/ prefix in output paths
    if (preserveModulesRoot && n.name.startsWith("src/")) {
      // If the normalized name doesn't have src/, add it back
      if (!outputPath.startsWith("src/")) {
        // Handle the case where outputPath starts with a slash
        if (outputPath.startsWith("/")) {
          outputPath = "src" + outputPath;
        } else {
          outputPath = "src/" + outputPath;
        }
      }
    }
    
    return hash(outputPath, ssr, sourceContent);
  }

  function assetFile(n: PreRenderedAsset, _ssr: boolean = false): string {
    // Rollup may report the same asset under several `names`; dedupe so we
    // don't emit invalid comma-joined filenames like `Foo.eot,Foo.eot`. When
    // multiple distinct names survive they all reference the same emitted
    // file, so use the first as the canonical output path.
    const uniqueNames = Array.from(new Set(n.names));
    let firstName = uniqueNames[0];

    // Clean up asset paths by removing the moduleBase from within assets directory
    // Transform: assets/src/page/file.css -> assets/page/file.css
    const assetsDir = build.assetsDir || "assets";
    if (firstName.startsWith(assetsDir + "/" + moduleBase + "/")) {
      firstName =
        assetsDir +
        "/" +
        firstName.slice((assetsDir + "/" + moduleBase + "/").length);
    }
    // Handle moduleBasePath removal
    else if (moduleBasePath && firstName.startsWith(moduleBasePath)) {
      firstName = firstName.slice(moduleBasePath.length);
    } else if (
      moduleBaseURL &&
      moduleBaseURL !== "/" &&
      moduleBaseURL !== "" &&
      firstName.startsWith(moduleBaseURL)
    ) {
      firstName = firstName.slice(moduleBaseURL.length);
    }
    // Handle direct moduleBase removal
    else if (firstName.startsWith(moduleBase + "/")) {
      firstName = firstName.slice(moduleBase.length + 1);
    }

    // For CSS files, ensure they go into the assets directory
    if (firstName.endsWith(".css")) {
      // Add assets directory prefix if not already present
      if (!firstName.startsWith(assetsDir + "/")) {
        firstName = assetsDir + "/" + firstName;
      }
      return hash(firstName, false);
    }

    // For other assets, apply the extension mapping if needed
    // Pass isAsset=true to preserve extensions for non-module assets
    return hash(getOutputPath(firstName, true), false);
  }

  /**
   * pages
   * assetsDir
   * client
   * server
   * static
   * api
   * outDir
   * hash

   * preserveModulesRoot
   * rscOutputPath
   * htmlOutputPath
   * entryFile
   * chunkFile
   * assetFile
   * extensionMap
   * moduleExtension
   * jsExtension
   * cssExtension
   * htmlExtension
   * jsonExtension
   * rscExtension
   * cssModuleExtension
   * nodeExtension
   */
  const build = {
    pages: options.build?.pages ?? DEFAULT_CONFIG.BUILD.pages,
    assetsDir: options.build?.assetsDir ?? DEFAULT_CONFIG.BUILD.assetsDir,
    client: options.build?.client ?? DEFAULT_CONFIG.BUILD.client,
    server: options.build?.server ?? DEFAULT_CONFIG.BUILD.server,
    static: options.build?.static ?? DEFAULT_CONFIG.BUILD.static,
    api: options.build?.api ?? DEFAULT_CONFIG.BUILD.api,
    preserveModulesRoot:
      options.build?.preserveModulesRoot ??
      DEFAULT_CONFIG.BUILD.preserveModulesRoot,
    outDir: options.build?.outDir ?? DEFAULT_CONFIG.BUILD.outDir,
    hash: options.build?.hash ?? DEFAULT_CONFIG.BUILD.hash,
    extensionMap: {
      // File extensions first (more specific patterns should come first)
      [BASE_PATTERNS.EXT.CSS]: cssExtension,
      [BASE_PATTERNS.EXT.JSON]: jsonExtension,
      [BASE_PATTERNS.EXT.HTML]: htmlExtension,
      [BASE_PATTERNS.EXT.RSC]: rscExtension,
      // Special case for .node files
      [BASE_PATTERNS.EXT.NODE]:
        BASE_PATTERNS.EXT.NODE +
        (options.build?.jsExtension ?? DEFAULT_CONFIG.BUILD.jsExtension),
      // General module pattern last (less specific)
      [BASE_PATTERNS.MODULE]: jsExtension,
      ...options.build?.extensionMap,
    },
    entryFile,
    chunkFile,
    assetFile,

    rscOutputPath: rscOutputPath,
    htmlOutputPath: htmlOutputPath,
    moduleExtension: jsExtension,
    jsExtension: jsExtension,
    cssExtension: cssExtension,
    htmlExtension: htmlExtension,
    jsonExtension: jsonExtension,
    rscExtension: rscExtension,
    cssModuleExtension: cssModuleExtension,
    nodeExtension: DEFAULT_CONFIG.BUILD.nodeExtension,
    useRscWorker: options.build?.useRscWorker ?? DEFAULT_CONFIG.BUILD.useRscWorker,
    useHtmlWorker: options.build?.useHtmlWorker ?? 
      // Force useHtmlWorker to true when build.pages is explicitly configured, regardless of default logic
      (options.build?.pages && (Array.isArray(options.build.pages) || typeof options.build.pages === 'function')) ? true : DEFAULT_CONFIG.BUILD.useHtmlWorker,
    renderMode: options.build?.renderMode ?? "parallel",
    batchSize: options.build?.batchSize ?? 8,
  } satisfies ResolvedUserOptions["build"];

  // Development configuration
  const dev = {
    useHtmlWorker: options.dev?.useHtmlWorker ?? DEFAULT_CONFIG.DEV.useHtmlWorker,
    useRscWorker: options.dev?.useRscWorker ?? DEFAULT_CONFIG.DEV.useRscWorker,
  } satisfies ResolvedUserOptions["dev"];

  // Auto-discovery configuration
  const autoDiscover = {
    clientEntry: options.autoDiscover?.clientEntry ?? DEFAULT_CONFIG.AUTO_DISCOVER.clientEntry,
    serverEntry: options.autoDiscover?.serverEntry ?? DEFAULT_CONFIG.AUTO_DISCOVER.serverEntry,
    cssEntry: options.autoDiscover?.cssEntry ?? DEFAULT_CONFIG.AUTO_DISCOVER.cssEntry,
    jsonEntry: options.autoDiscover?.jsonEntry ?? DEFAULT_CONFIG.AUTO_DISCOVER.jsonEntry,
    htmlEntry: options.autoDiscover?.htmlEntry ?? DEFAULT_CONFIG.AUTO_DISCOVER.htmlEntry,
    modulePattern,
    jsonPattern,
    cssPattern,
    htmlPattern,
    rscPattern,
    clientPattern,
    serverPattern,
    nodePattern,
    propsPattern,
    pagePattern,
    cssModulePattern,
    vendorPattern,
    virtualPattern,
    dotPattern,
  } satisfies ResolvedUserOptions["autoDiscover"];

  const allowedDirectives = resolveAllowedDirectives(
    options.loader?.allowedDirectives ?? DEFAULT_LOADER_CONFIG.allowedDirectives
  );

  // Create loader configuration
  const loader = loaderMode
    ? ({
        serverDirective: resolveRegExp(
          options.loader?.serverDirective,
          DEFAULT_LOADER_CONFIG.serverDirective
        ),
        clientDirective: resolveRegExp(
          options.loader?.clientDirective,
          DEFAULT_LOADER_CONFIG.clientDirective
        ),
        allowedDirectives: allowedDirectives,
        getDirectiveType:
          options.loader?.getDirectiveType ?? options.loader?.allowedDirectives
            ? (directive: string) => {
                if (options.loader?.allowedDirectives) {
                  if (Array.isArray(options.loader?.allowedDirectives)) {
                    return options.loader?.allowedDirectives.includes(directive)
                      ? directive === "use server"
                        ? "server"
                        : "client"
                      : undefined;
                  } else {
                    const config = options.loader?.allowedDirectives[directive];
                    return config
                      ? directive === "use server"
                        ? "server"
                        : "client"
                      : undefined;
                  }
                }
                return undefined;
              }
            : DEFAULT_LOADER_CONFIG.getDirectiveType,
        mode: loaderMode,
        importServerPath:
          options.loader?.importServerPath ??
          DEFAULT_CONFIG.RSC_LOADER[loaderMode].importServerPath,
        importClientPath:
          options.loader?.importClientPath ??
          DEFAULT_CONFIG.RSC_LOADER[loaderMode].importClientPath,
        registerClientReferenceName:
          options.loader?.registerClientReferenceName ??
          DEFAULT_CONFIG.RSC_LOADER[loaderMode].registerClientReferenceName,
        registerServerReferenceName:
          options.loader?.registerServerReferenceName ??
          DEFAULT_CONFIG.RSC_LOADER[loaderMode].registerServerReferenceName,
        isServerFunctionCode,
        isClientComponentCode,
        isClientComponentByCode,
        isClientComponentByName,
        parse: options?.loader?.parse ?? DEFAULT_LOADER_CONFIG.parse,
        moduleID: options?.loader?.moduleID ?? options?.moduleID,
      } as Required<LoaderConfig>)
    : undefined;

  const pipeableStreamOptions = options.pipeableStreamOptions
    ? options.pipeableStreamOptions
    : {};

  // Return resolved options
  try {
    const userOptions: ResolvedUserOptions = {
      projectRoot,
      moduleBase,
      moduleBasePath,
      moduleBaseURL,
      moduleRootPath,
      publicOrigin,
      build: build,
      dev: dev,
      verbose: options.verbose ?? DEFAULT_CONFIG.VERBOSE,
      availableEnvironments: (options as any).availableEnvironments,
      strategy: (options as any).strategy,
      onMetrics:
        typeof options.onMetrics === "function"
          ? options.onMetrics
          : DEFAULT_CONFIG.ON_METRICS,
      onEvent:
        typeof options.onEvent === "function"
          ? options.onEvent
          : DEFAULT_CONFIG.ON_EVENT,
      Page: options.Page,
      props: options.props,
      Html: options.Html ?? DEFAULT_CONFIG.HTML,
      Root: options.Root ?? DEFAULT_CONFIG.ROOT,
      components: options.components,
      normalizer: normalizer,
      moduleID: options.moduleID, // if not provided, will be created when config hook is called
      pageExportName:
        options.pageExportName ?? (DEFAULT_CONFIG.PAGE_EXPORT_NAME as PageName),
      propsExportName:
        options.propsExportName ??
        (DEFAULT_CONFIG.PROPS_EXPORT_NAME as PropsName),
      htmlExportName:
        options.htmlExportName ?? (DEFAULT_CONFIG.HTML_EXPORT_NAME as HtmlName),
      rootExportName:
        options.rootExportName ?? (DEFAULT_CONFIG.ROOT_EXPORT_NAME as RootName),
      css: {
        inlineCss: options.css?.inlineCss ?? DEFAULT_CONFIG.CSS.inlineCss,
        inlineThreshold:
          options.css?.inlineThreshold ?? DEFAULT_CONFIG.CSS.inlineThreshold,
        inlinePatterns:
          options.css?.inlinePatterns ?? DEFAULT_CONFIG.CSS.inlinePatterns,
        linkPatterns:
          options.css?.linkPatterns ?? DEFAULT_CONFIG.CSS.linkPatterns,
      },
      htmlWorkerPath: htmlWorkerPath,
      rscWorkerPath: rscWorkerPath,
      loaderPath: loaderPath,
      reactLoaderPath:
        options.reactLoaderPath ?? DEFAULT_CONFIG.REACT_LOADER_PATH,
      cssLoaderPath: options.cssLoaderPath ?? DEFAULT_CONFIG.CSS_LOADER_PATH,
      envLoaderPath: options.envLoaderPath ?? DEFAULT_CONFIG.ENV_LOADER_PATH,
      clientEntry: options.clientEntry ?? DEFAULT_CONFIG.CLIENT_ENTRY,
      serverEntry: options.serverEntry ?? DEFAULT_CONFIG.SERVER_ENTRY,
      clientPackages: (options as { clientPackages?: readonly string[] }).clientPackages,
      autoDiscover: autoDiscover,
      loader: loader,
      pipeableStreamOptions,
      rscTimeout:
        typeof options.rscTimeout === "number"
          ? options.rscTimeout
          : DEFAULT_CONFIG.RSC_TIMEOUT,
      htmlTimeout:
        typeof options.htmlTimeout === "number"
          ? options.htmlTimeout
          : DEFAULT_CONFIG.HTML_TIMEOUT,
      htmlWorkerStartupTimeout:
        typeof options.htmlWorkerStartupTimeout === "number"
          ? options.htmlWorkerStartupTimeout
          : DEFAULT_CONFIG.HTML_WORKER_STARTUP_TIMEOUT,
      rscWorkerStartupTimeout:
        typeof options.rscWorkerStartupTimeout === "number"
          ? options.rscWorkerStartupTimeout
          : DEFAULT_CONFIG.RSC_WORKER_STARTUP_TIMEOUT,
      fileWriteTimeout:
        typeof options.fileWriteTimeout === "number"
          ? options.fileWriteTimeout
          : DEFAULT_CONFIG.FILE_WRITE_TIMEOUT,
      workerShutdownTimeout:
        typeof options.workerShutdownTimeout === "number"
          ? options.workerShutdownTimeout
          : DEFAULT_CONFIG.WORKER_SHUTDOWN_TIMEOUT,
      panicThreshold: panicThreshold,
    };

    // Stash the resolved options
    stashUserOptions(envId, userOptions);

    return {
      type: "success",
      userOptions,
    };
  } catch (error) {
    return {
      type: "error",
      error: handleError({
        error,
        logger: logger,
        panicThreshold,
        context: "config(resolveOptions)",
      }),
    };
  }
};


