import { loadPartialConfigAsync } from "@babel/core";
import type { InputOptions, NormalizedOptions } from "@babel/core";
import type { Options } from "../types";
import type { PartialConfig } from "@babel/core";
type ParserPlugin = NonNullable<
  NonNullable<InputOptions["parserOpts"]>["plugins"]
>[number];

/**
 * Merge user supplied estree plugin options to default estree plugin options
 *
 * @returns {Array} Merged parser plugin descriptors
 */
export function getParserPlugins(babelOptions: InputOptions): ParserPlugin[] {
  const babelParserPlugins = babelOptions.parserOpts?.plugins ?? [];
  const estreeOptions = { classFeatures: true };
  for (const plugin of babelParserPlugins) {
    if (Array.isArray(plugin) && plugin[0] === "estree") {
      Object.assign(estreeOptions, plugin[1]);
      break;
    }
  }
  // estree must be the first parser plugin to work with other parser plugins
  return [["estree", estreeOptions], ...babelParserPlugins];
}

function normalizeParserOptions(options: Options): InputOptions & {
  showIgnoredFiles?: boolean;
} {
  return {
    sourceType: options.sourceType,
    filename: options.filePath,
    ...options.babelOptions,
    parserOpts: {
      ...(options.sourceType !== "commonjs"
        ? {
            allowReturnOutsideFunction:
              options.ecmaFeatures?.globalReturn ?? false,
          }
        : {}),
      ...options.babelOptions.parserOpts,
      plugins: getParserPlugins(options.babelOptions),
      // skip comment attaching for parsing performance
      attachComment: false,
      ranges: true,
      tokens: true,
    },
    caller: {
      name: "@babel/eslint-parser",
      ...options.babelOptions.caller,
    },
  };
}

function validateResolvedConfig(
  config: PartialConfig,
  options: Options,
  parseOptions: InputOptions,
): InputOptions | NormalizedOptions {
  if (config !== null) {
    if (options.requireConfigFile !== false) {
      if (!config.hasFilesystemConfig()) {
        let error = `No Babel config file detected for ${config.options.filename}. Either disable config file checking with requireConfigFile: false, or configure Babel so that it can find the config files.`;

        if (config.options.filename!.includes("node_modules")) {
          error += `\nIf you have a .babelrc.js file or use package.json#babel, keep in mind that it's not used when parsing dependencies. If you want your config to be applied to your whole app, consider using babel.config.js or babel.config.json instead.`;
        }

        throw new Error(error);
      }
    }
    if (config.options) return config.options;
  }

  return getDefaultParserOptions(parseOptions);
}

function getDefaultParserOptions(options: InputOptions): InputOptions {
  return {
    plugins: [],
    ...options,
    babelrc: false,
    configFile: false,
    ignore: undefined,
    only: undefined,
  };
}

export async function normalizeBabelParseConfig(
  options: Options,
): Promise<InputOptions | NormalizedOptions> {
  const parseOptions = normalizeParserOptions(options);
  const config = await loadPartialConfigAsync(parseOptions);
  return validateResolvedConfig(config!, options, parseOptions);
}
