import { posix } from "node:path";
import { parse } from "@babel/parser";
import * as t from "@babel/types";
import ts from "typescript";
import type { ComponentId } from "@fragments-sdk/core";
import type { ComponentImportResolution } from "../usage/jsx.js";
import { BABEL_PARSER_OPTIONS } from "../usage/babel-config.js";
import { normalizeScanSourcePath } from "./paths.js";
import type { ScanSource, ScanSourceFile } from "./types.js";

const MODULE_EXTENSIONS = [".tsx", ".ts", ".jsx", ".js", ".mjs", ".cjs"];

export interface SourceComponentAlias {
  componentId: ComponentId;
  exportName?: string;
  modulePath?: string;
  source?: string;
  fromFilePath?: string;
}

export interface CreateSourceComponentResolverOptions {
  source: ScanSource;
  files: readonly ScanSourceFile[];
  aliases: readonly SourceComponentAlias[];
  tsconfigPath?: string;
}

type ResolvedModule = {
  path: string;
  key: string;
};

type PathRule = {
  prefix: string;
  suffix: string;
  targets: string[];
  wildcard: boolean;
};

type ExportTarget = {
  source: string;
  imported: string;
};

type ExportResolution =
  | { kind: "component"; componentId: ComponentId }
  | { kind: "local" }
  | { kind: "absent" };

type PackageInfo = {
  root: string;
  name: string;
  exports?: unknown;
  source?: string;
  module?: string;
  main?: string;
};

type PackageSpecifier = {
  packageName: string;
  subpath: string;
};

type TsconfigResolution = {
  tsconfigDir: string;
  baseUrl: string;
  paths: Record<string, string[]>;
};

interface ResolverState {
  source: ScanSource;
  pathRules: PathRule[];
  baseUrl: string;
  tsconfigDir: string;
  packageByName: Map<string, PackageInfo>;
  importsByFileAndSource: Map<string, ResolvedModule | null>;
  sourceAliases: Map<string, ComponentId>;
  componentAliases: Map<string, ComponentId>;
  modulePathByKey: Map<string, string>;
  exportMapByModule: Map<string, Map<string, ExportTarget>>;
  localExportsByModule: Map<string, Set<string>>;
  starExportsByModule: Map<string, string[]>;
  readCache: Map<string, Promise<string | null>>;
}

export async function createSourceBackedComponentResolver(
  options: CreateSourceComponentResolverOptions
): Promise<(resolution: ComponentImportResolution) => ComponentId | null | undefined> {
  const tsconfig = await loadTsconfig(options.source, options.tsconfigPath);
  const state: ResolverState = {
    source: options.source,
    pathRules: buildPathRules(tsconfig.paths),
    baseUrl: tsconfig.baseUrl,
    tsconfigDir: tsconfig.tsconfigDir,
    packageByName: new Map(),
    importsByFileAndSource: new Map(),
    sourceAliases: new Map(),
    componentAliases: new Map(),
    modulePathByKey: new Map(),
    exportMapByModule: new Map(),
    localExportsByModule: new Map(),
    starExportsByModule: new Map(),
    readCache: new Map(),
  };

  await registerAliases(state, options.aliases);
  await discoverPackageMetadata(state, options.files);
  await indexImports(state, options.files);

  return (resolution) => resolveComponentId(state, resolution);
}

async function loadTsconfig(
  source: ScanSource,
  tsconfigPath = "tsconfig.json",
  seen = new Set<string>()
): Promise<TsconfigResolution> {
  const relPath = normalizeScanSourcePath(tsconfigPath) ?? "tsconfig.json";
  if (seen.has(relPath)) {
    return { tsconfigDir: ".", baseUrl: ".", paths: {} as Record<string, string[]> };
  }
  seen.add(relPath);

  const content = await readOptionalSourceFile(source, relPath);
  if (!content) {
    return { tsconfigDir: ".", baseUrl: ".", paths: {} as Record<string, string[]> };
  }

  const parsed = ts.parseConfigFileTextToJson(relPath, content);
  const config =
    parsed.config && typeof parsed.config === "object"
      ? (parsed.config as {
          extends?: unknown;
          compilerOptions?: { baseUrl?: string; paths?: Record<string, string[]> };
        })
      : {};
  const base = await loadExtendedTsconfig(source, relPath, config.extends, seen);
  const compilerOptions = config.compilerOptions ?? {};
  const hasOwnCompilerResolution =
    compilerOptions.baseUrl !== undefined || compilerOptions.paths !== undefined;

  if (!hasOwnCompilerResolution && base) return base;

  return {
    tsconfigDir: posix.dirname(relPath) === "." ? "." : posix.dirname(relPath),
    baseUrl: compilerOptions.baseUrl ?? base?.baseUrl ?? ".",
    paths: compilerOptions.paths ?? base?.paths ?? {},
  };
}

async function loadExtendedTsconfig(
  source: ScanSource,
  currentPath: string,
  extendsValue: unknown,
  seen: Set<string>
): Promise<TsconfigResolution | null> {
  const extendedPath = resolveExtendsPath(currentPath, extendsValue);
  return extendedPath ? loadTsconfig(source, extendedPath, seen) : null;
}

function resolveExtendsPath(currentPath: string, extendsValue: unknown): string | null {
  if (typeof extendsValue !== "string" || !extendsValue.trim()) return null;
  if (!isRelativeSource(extendsValue) && !extendsValue.startsWith("/")) return null;

  const currentDir = posix.dirname(currentPath);
  const withoutSlash = extendsValue.startsWith("/")
    ? extendsValue.slice(1)
    : posix.join(currentDir === "." ? "" : currentDir, extendsValue);
  const normalized = normalizeScanSourcePath(withoutSlash);
  if (!normalized) return null;
  return normalized.endsWith(".json") ? normalized : `${normalized}.json`;
}

function buildPathRules(paths: Record<string, string[]>): PathRule[] {
  return Object.entries(paths)
    .map(([pattern, targets]) => {
      const star = pattern.indexOf("*");
      return {
        prefix: star >= 0 ? pattern.slice(0, star) : pattern,
        suffix: star >= 0 ? pattern.slice(star + 1) : "",
        targets,
        wildcard: star >= 0,
      };
    })
    .sort((a, b) => b.prefix.length - a.prefix.length);
}

async function registerAliases(state: ResolverState, aliases: readonly SourceComponentAlias[]) {
  for (const alias of aliases) {
    const exportName = alias.exportName ?? "default";
    if (alias.source && !alias.fromFilePath && !alias.modulePath) {
      addSourceAlias(state, alias.source, exportName, alias.componentId);
      continue;
    }

    const resolved = alias.modulePath
      ? await resolvePathLikeModule(state, alias.modulePath)
      : alias.source && alias.fromFilePath
        ? await resolveModuleSpecifier(state, alias.fromFilePath, alias.source)
        : null;
    if (!resolved) continue;
    addComponentAlias(state, resolved, exportName, alias.componentId);
  }
}

function addSourceAlias(
  state: ResolverState,
  source: string,
  exportName: string,
  componentId: ComponentId
): void {
  state.sourceAliases.set(aliasKey(source, exportName), componentId);
}

function addComponentAlias(
  state: ResolverState,
  module: ResolvedModule,
  exportName: string,
  componentId: ComponentId
) {
  state.componentAliases.set(aliasKey(module.key, exportName), componentId);
  state.modulePathByKey.set(module.key, module.path);

  const indexSuffix = "/index";
  if (module.key.endsWith(indexSuffix)) {
    state.componentAliases.set(
      aliasKey(module.key.slice(0, -indexSuffix.length), exportName),
      componentId
    );
  }
}

async function discoverPackageMetadata(state: ResolverState, files: readonly ScanSourceFile[]) {
  const visitedDirs = new Set<string>();
  await loadPackageAtDir(state, ".", visitedDirs);

  const sourcePaths = new Set<string>();
  for (const file of files) {
    const relPath = normalizeScanSourcePath(file.relPath);
    if (relPath) sourcePaths.add(relPath);
  }
  for (const path of state.modulePathByKey.values()) {
    sourcePaths.add(path);
  }

  for (const path of sourcePaths) {
    let dir = posix.dirname(path);
    while (true) {
      await loadPackageAtDir(state, dir, visitedDirs);
      if (dir === ".") break;
      dir = posix.dirname(dir);
    }
  }
}

async function loadPackageAtDir(state: ResolverState, dir: string, visitedDirs: Set<string>) {
  const normalizedDir = normalizeScanSourcePath(dir) ?? ".";
  if (visitedDirs.has(normalizedDir)) return;
  visitedDirs.add(normalizedDir);

  const packagePath = normalizedDir === "." ? "package.json" : `${normalizedDir}/package.json`;
  const content = await readStateSourceFile(state, packagePath);
  if (content === null) return;

  const parsed = parsePackageJson(content, normalizedDir);
  if (!parsed) return;
  state.packageByName.set(parsed.name, parsed);
}

function parsePackageJson(content: string, root: string): PackageInfo | null {
  try {
    const parsed = JSON.parse(content) as {
      name?: unknown;
      exports?: unknown;
      source?: unknown;
      module?: unknown;
      main?: unknown;
    };
    if (typeof parsed.name !== "string" || !parsed.name) return null;
    return {
      root,
      name: parsed.name,
      exports: parsed.exports,
      source: typeof parsed.source === "string" ? parsed.source : undefined,
      module: typeof parsed.module === "string" ? parsed.module : undefined,
      main: typeof parsed.main === "string" ? parsed.main : undefined,
    };
  } catch {
    return null;
  }
}

async function indexImports(state: ResolverState, files: readonly ScanSourceFile[]) {
  for (const file of files) {
    const relPath = normalizeScanSourcePath(file.relPath);
    if (!relPath || !/\.[jt]sx?$/.test(relPath)) continue;
    const content = await readStateSourceFile(state, relPath);
    if (content === null) continue;

    for (const source of collectImportSources(content)) {
      const resolved = await resolveModuleSpecifier(state, relPath, source);
      if (
        resolved ||
        isLocalSourceReference(state, source) ||
        isKnownPackageSpecifier(state, source)
      ) {
        state.importsByFileAndSource.set(importKey(relPath, source), resolved);
      }
    }
  }
}

function collectImportSources(source: string): string[] {
  let ast: t.File;
  try {
    ast = parse(source, BABEL_PARSER_OPTIONS);
  } catch {
    return [];
  }

  const out = new Set<string>();
  for (const node of ast.program.body) {
    if (t.isImportDeclaration(node)) out.add(node.source.value);
  }
  return [...out];
}

function resolveComponentId(
  state: ResolverState,
  resolution: ComponentImportResolution
): ComponentId | null | undefined {
  const module = state.importsByFileAndSource.get(
    importKey(resolution.filePath, resolution.source)
  );
  if (module === null) {
    return resolveSourceAlias(state, resolution.source, resolution.imported) ?? null;
  }
  if (module === undefined) return undefined;

  const resolved = resolveExportedComponent(state, module, resolution.imported);
  return resolved.kind === "component" ? resolved.componentId : null;
}

function resolveSourceAlias(
  state: ResolverState,
  source: string,
  exportName: string
): ComponentId | undefined {
  for (const candidate of exportCandidates(exportName)) {
    const direct = state.sourceAliases.get(aliasKey(source, candidate));
    if (direct) return direct;
  }
  return undefined;
}

function resolveExportedComponent(
  state: ResolverState,
  module: ResolvedModule,
  exportName: string,
  seen = new Set<string>()
): ExportResolution {
  for (const candidate of exportCandidates(exportName)) {
    const direct = state.componentAliases.get(aliasKey(module.key, candidate));
    if (direct) return { kind: "component", componentId: direct };
  }

  const seenKey = aliasKey(module.key, exportName);
  if (seen.has(seenKey)) return { kind: "absent" };
  seen.add(seenKey);

  const exports = state.exportMapByModule.get(module.key);
  const target = exports?.get(exportName) ?? exports?.get(exportHead(exportName));
  if (target) {
    const tail = exportTail(exportName);
    const targetModule = resolveModuleSpecifierSync(state, module.path, target.source);
    if (!targetModule) {
      return isLocalSourceReference(state, target.source) ? { kind: "local" } : { kind: "absent" };
    }
    const targetName = tail ? `${target.imported}.${tail}` : target.imported;
    return resolveExportedComponent(state, targetModule, targetName, seen);
  }

  if (hasLocalExport(state, module, exportName)) return { kind: "local" };

  const starResolutions: ExportResolution[] = [];
  for (const star of state.starExportsByModule.get(module.key) ?? []) {
    const targetModule = resolveModuleSpecifierSync(state, module.path, star);
    if (!targetModule) {
      if (isLocalSourceReference(state, star)) starResolutions.push({ kind: "local" });
      continue;
    }
    const resolved = resolveExportedComponent(state, targetModule, exportName, seen);
    if (resolved.kind !== "absent") starResolutions.push(resolved);
  }

  if (starResolutions.length === 1) return starResolutions[0];
  if (starResolutions.length > 1) return { kind: "local" };
  return { kind: "absent" };
}

function exportCandidates(exportName: string): string[] {
  const [, ...tail] = exportName.split(".");
  if (tail.length === 0) return [exportName];
  return [exportName, tail.join(".")].filter(Boolean);
}

function exportHead(exportName: string): string {
  return exportName.split(".")[0] ?? exportName;
}

function exportTail(exportName: string): string {
  return exportName.split(".").slice(1).join(".");
}

async function resolveModuleSpecifier(
  state: ResolverState,
  importerFile: string,
  specifier: string
): Promise<ResolvedModule | null> {
  const cached = state.importsByFileAndSource.get(importKey(importerFile, specifier));
  if (cached !== undefined) return cached;

  const resolved = await resolveModuleCandidates(
    state,
    moduleCandidates(state, importerFile, specifier)
  );
  if (resolved) await indexBarrelExports(state, resolved);
  return resolved;
}

function resolveModuleSpecifierSync(
  state: ResolverState,
  importerFile: string,
  specifier: string
): ResolvedModule | null {
  return state.importsByFileAndSource.get(importKey(importerFile, specifier)) ?? null;
}

async function resolvePathLikeModule(
  state: ResolverState,
  path: string
): Promise<ResolvedModule | null> {
  return resolveModuleCandidates(state, [path]);
}

async function resolveModuleCandidates(
  state: ResolverState,
  candidates: string[]
): Promise<ResolvedModule | null> {
  for (const candidate of candidates) {
    const resolved = await resolveCandidatePath(state, candidate);
    if (resolved) return resolved;
  }
  return null;
}

async function resolveCandidatePath(
  state: ResolverState,
  candidate: string
): Promise<ResolvedModule | null> {
  const normalized = normalizeScanSourcePath(candidate);
  if (!normalized) return null;

  for (const path of candidateFilePaths(normalized)) {
    const content = await readStateSourceFile(state, path);
    if (content === null) continue;
    const resolved = { path, key: moduleKey(path) };
    state.modulePathByKey.set(resolved.key, resolved.path);
    await indexBarrelExports(state, resolved, content);
    return resolved;
  }

  return null;
}

function candidateFilePaths(path: string): string[] {
  if (path.endsWith(".d.ts")) return [];
  if (MODULE_EXTENSIONS.some((ext) => path.endsWith(ext))) return [path];
  return [
    ...MODULE_EXTENSIONS.map((ext) => `${path}${ext}`),
    ...MODULE_EXTENSIONS.map((ext) => `${path}/index${ext}`),
  ];
}

function moduleCandidates(state: ResolverState, importerFile: string, specifier: string): string[] {
  if (isRelativeSource(specifier)) {
    return [posix.normalize(posix.join(posix.dirname(importerFile), specifier))];
  }

  const out: string[] = [];
  for (const rule of state.pathRules) {
    const replacement = matchPathRule(rule, specifier);
    if (replacement === null) continue;
    for (const target of rule.targets) {
      out.push(
        posix.normalize(
          posix.join(
            state.tsconfigDir,
            state.baseUrl,
            rule.wildcard ? target.replace("*", replacement) : target
          )
        )
      );
    }
  }
  out.push(...packageModuleCandidates(state, specifier));
  out.push(posix.normalize(posix.join(state.tsconfigDir, state.baseUrl, specifier)));
  return out;
}

function matchPathRule(rule: PathRule, specifier: string): string | null {
  if (!rule.wildcard) return rule.prefix === specifier ? "" : null;
  if (!specifier.startsWith(rule.prefix) || !specifier.endsWith(rule.suffix)) {
    return null;
  }
  return specifier.slice(rule.prefix.length, specifier.length - rule.suffix.length);
}

function isLocalSourceReference(state: ResolverState, specifier: string): boolean {
  return (
    isRelativeSource(specifier) ||
    state.pathRules.some((rule) => matchPathRule(rule, specifier) !== null)
  );
}

function isKnownPackageSpecifier(state: ResolverState, specifier: string): boolean {
  return packageInfoForSpecifier(state, specifier) !== null;
}

function packageModuleCandidates(state: ResolverState, specifier: string): string[] {
  const packageInfo = packageInfoForSpecifier(state, specifier);
  if (!packageInfo) return [];
  return packageExportTargets(packageInfo.pkg, packageInfo.specifier)
    .map((target) => packageTargetPath(packageInfo.pkg, target))
    .filter((target): target is string => Boolean(target));
}

function packageInfoForSpecifier(
  state: ResolverState,
  specifier: string
): { pkg: PackageInfo; specifier: PackageSpecifier } | null {
  const parsed = parsePackageSpecifier(specifier);
  if (!parsed) return null;
  const pkg = state.packageByName.get(parsed.packageName);
  return pkg ? { pkg, specifier: parsed } : null;
}

function parsePackageSpecifier(specifier: string): PackageSpecifier | null {
  if (!specifier || isRelativeSource(specifier) || specifier.startsWith("/")) {
    return null;
  }

  const parts = specifier.split("/");
  if (specifier.startsWith("@")) {
    if (parts.length < 2 || !parts[0] || !parts[1]) return null;
    return {
      packageName: `${parts[0]}/${parts[1]}`,
      subpath: parts.length > 2 ? `./${parts.slice(2).join("/")}` : ".",
    };
  }

  if (!parts[0]) return null;
  return {
    packageName: parts[0],
    subpath: parts.length > 1 ? `./${parts.slice(1).join("/")}` : ".",
  };
}

function packageExportTargets(pkg: PackageInfo, specifier: PackageSpecifier): string[] {
  const exported = resolvePackageExports(pkg.exports, specifier.subpath);
  if (exported.length > 0) return exported;

  if (specifier.subpath === ".") {
    return [pkg.source, pkg.module, pkg.main, "./src/index", "./index"].filter(
      (target): target is string => Boolean(target)
    );
  }

  return [specifier.subpath, `./src/${specifier.subpath.slice(2)}`];
}

function resolvePackageExports(exportsValue: unknown, subpath: string): string[] {
  if (exportsValue === undefined) return [];
  if (isExportMap(exportsValue)) {
    const exact = resolvePackageTarget(exportsValue[subpath]);
    if (exact.length > 0) return exact;

    for (const [pattern, target] of Object.entries(exportsValue)) {
      const replacement = matchPackageExportPattern(pattern, subpath);
      if (replacement === null) continue;
      return resolvePackageTarget(target).map((value) => value.replace("*", replacement));
    }
    return [];
  }

  return subpath === "." ? resolvePackageTarget(exportsValue) : [];
}

function isExportMap(value: unknown): value is Record<string, unknown> {
  if (!isRecord(value)) return false;
  return Object.keys(value).some((key) => key === "." || key.startsWith("./"));
}

function matchPackageExportPattern(pattern: string, subpath: string): string | null {
  const star = pattern.indexOf("*");
  if (star < 0) return null;
  const prefix = pattern.slice(0, star);
  const suffix = pattern.slice(star + 1);
  if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix)) return null;
  return subpath.slice(prefix.length, subpath.length - suffix.length);
}

function resolvePackageTarget(value: unknown): string[] {
  if (typeof value === "string") return [value];
  if (Array.isArray(value)) return value.flatMap(resolvePackageTarget);
  if (!isRecord(value)) return [];

  for (const condition of [
    "source",
    "development",
    "import",
    "module",
    "browser",
    "default",
    "require",
  ]) {
    if (condition in value) {
      const resolved = resolvePackageTarget(value[condition]);
      if (resolved.length > 0) return resolved;
    }
  }

  return Object.values(value).flatMap(resolvePackageTarget);
}

function packageTargetPath(pkg: PackageInfo, target: string): string | null {
  const normalizedTarget = normalizeScanSourcePath(target);
  if (!normalizedTarget) return null;
  return pkg.root === "." ? normalizedTarget : `${pkg.root}/${normalizedTarget}`;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}

async function indexBarrelExports(state: ResolverState, module: ResolvedModule, content?: string) {
  if (state.exportMapByModule.has(module.key)) return;
  const source = content ?? (await readStateSourceFile(state, module.path));
  if (source === null) return;

  const exportMap = new Map<string, ExportTarget>();
  const localExports = new Set<string>();
  const starExports: string[] = [];
  for (const node of parseExportDeclarations(source)) {
    if (node.kind === "named") {
      addNamedExport(exportMap, localExports, node.exported, node.target);
    } else if (node.kind === "star") {
      starExports.push(node.source);
    } else {
      localExports.add(node.exported);
    }
  }
  state.exportMapByModule.set(module.key, exportMap);
  state.localExportsByModule.set(module.key, localExports);
  state.starExportsByModule.set(module.key, starExports);

  for (const specifier of [...exportMap.values()]
    .map((target) => target.source)
    .concat(starExports)) {
    const resolved = await resolveModuleSpecifier(state, module.path, specifier);
    state.importsByFileAndSource.set(importKey(module.path, specifier), resolved);
  }
}

type ExportDeclaration =
  | { kind: "named"; exported: string; target: ExportTarget }
  | { kind: "star"; source: string }
  | { kind: "local"; exported: string };

function parseExportDeclarations(source: string): ExportDeclaration[] {
  let ast: t.File;
  try {
    ast = parse(source, BABEL_PARSER_OPTIONS);
  } catch {
    return [];
  }

  const importedBindings = importBindingsByLocalName(ast);
  const out: ExportDeclaration[] = [];
  for (const node of ast.program.body) {
    if (t.isExportAllDeclaration(node)) {
      out.push({ kind: "star", source: node.source.value });
      continue;
    }

    if (t.isExportDefaultDeclaration(node)) {
      out.push({ kind: "local", exported: "default" });
      continue;
    }

    if (!t.isExportNamedDeclaration(node)) continue;

    if (!node.source) {
      out.push(...localExportDeclarations(node, importedBindings));
      continue;
    }

    for (const spec of node.specifiers) {
      if (!t.isExportSpecifier(spec)) continue;
      out.push({
        kind: "named",
        exported: exportName(spec.exported),
        target: {
          source: node.source.value,
          imported: exportName(spec.local),
        },
      });
    }
  }
  return out;
}

function importBindingsByLocalName(ast: t.File): Map<string, ExportTarget> {
  const bindings = new Map<string, ExportTarget>();
  for (const node of ast.program.body) {
    if (!t.isImportDeclaration(node)) continue;
    for (const spec of node.specifiers) {
      if (t.isImportSpecifier(spec)) {
        bindings.set(spec.local.name, {
          source: node.source.value,
          imported: exportName(spec.imported),
        });
      } else if (t.isImportDefaultSpecifier(spec)) {
        bindings.set(spec.local.name, {
          source: node.source.value,
          imported: "default",
        });
      }
    }
  }
  return bindings;
}

function localExportDeclarations(
  node: t.ExportNamedDeclaration,
  importedBindings: ReadonlyMap<string, ExportTarget>
): ExportDeclaration[] {
  const out: ExportDeclaration[] = [];
  for (const spec of node.specifiers) {
    if (!t.isExportSpecifier(spec)) continue;
    const localName = exportName(spec.local);
    const imported = importedBindings.get(localName);
    if (imported) {
      out.push({
        kind: "named",
        exported: exportName(spec.exported),
        target: imported,
      });
    } else {
      out.push({ kind: "local", exported: exportName(spec.exported) });
    }
  }

  const declaration = node.declaration;
  if (t.isFunctionDeclaration(declaration) || t.isClassDeclaration(declaration)) {
    if (declaration.id?.name) out.push({ kind: "local", exported: declaration.id.name });
  } else if (t.isVariableDeclaration(declaration)) {
    for (const declarator of declaration.declarations) {
      if (t.isIdentifier(declarator.id)) {
        out.push({ kind: "local", exported: declarator.id.name });
      }
    }
  }

  return out;
}

function addNamedExport(
  exportMap: Map<string, ExportTarget>,
  localExports: Set<string>,
  exported: string,
  target: ExportTarget
): void {
  if (localExports.has(exported)) return;
  if (exportMap.has(exported)) {
    exportMap.delete(exported);
    localExports.add(exported);
    return;
  }
  exportMap.set(exported, target);
}

function hasLocalExport(state: ResolverState, module: ResolvedModule, exportName: string): boolean {
  const localExports = state.localExportsByModule.get(module.key);
  if (!localExports) return false;
  return exportCandidates(exportName).some((candidate) => {
    return localExports.has(candidate) || localExports.has(exportHead(candidate));
  });
}

function exportName(node: t.Identifier | t.StringLiteral): string {
  return t.isIdentifier(node) ? node.name : node.value;
}

function moduleKey(path: string): string {
  let key = path.replace(/\\/g, "/");
  for (const ext of MODULE_EXTENSIONS) {
    if (key.endsWith(ext)) {
      key = key.slice(0, -ext.length);
      break;
    }
  }
  return key;
}

function importKey(filePath: string, source: string): string {
  return `${filePath}\0${source}`;
}

function aliasKey(module: string, exportName: string): string {
  return `${module}#${exportName}`;
}

function isRelativeSource(source: string): boolean {
  return source.startsWith("./") || source.startsWith("../") || source === ".";
}

async function readOptionalSourceFile(source: ScanSource, path: string): Promise<string | null> {
  try {
    return await source.readFile(path);
  } catch {
    return null;
  }
}

async function readStateSourceFile(state: ResolverState, path: string): Promise<string | null> {
  let cached = state.readCache.get(path);
  if (!cached) {
    cached = readOptionalSourceFile(state.source, path);
    state.readCache.set(path, cached);
  }
  return cached;
}
