import process$1 from 'node:process';
import tseslint from 'typescript-eslint';
import pluginVue from 'eslint-plugin-vue';
import fs from 'node:fs';
import fg from 'fast-glob';
import path from 'node:path';
import vueParser from 'vue-eslint-parser';

const CONFIG_NAMES = [
  "all",
  "base",
  "disableTypeChecked",
  "eslintRecommended",
  "recommended",
  "recommendedTypeChecked",
  "recommendedTypeCheckedOnly",
  "strict",
  "strictTypeChecked",
  "strictTypeCheckedOnly",
  "stylistic",
  "stylisticTypeChecked",
  "stylisticTypeCheckedOnly"
];
function toArray(value) {
  return Array.isArray(value) ? value : [value];
}
class TsEslintConfigForVue {
  /**
   * The name of the config object as defined in `typescript-eslint`.
   */
  configName;
  /**
   * the name property is here to provide better error messages when ESLint throws an error
   */
  name;
  constructor(configName) {
    this.configName = configName;
    this.name = `vueTsConfigs.${configName}`;
  }
  extendsOptions;
  /**
   * Create a new instance of `TsEslintConfigForVue` with the `restOfConfig` merged into it.
   * Should be used when the config is used in the `extends` field of another config.
   */
  asExtendedWith(restOfConfig) {
    const extendedConfig = new TsEslintConfigForVue(this.configName);
    extendedConfig.extendsOptions = {
      name: [restOfConfig.name, this.name].filter(Boolean).join("__"),
      ...restOfConfig.files && { files: restOfConfig.files },
      ...restOfConfig.ignores && { ignores: restOfConfig.ignores }
    };
    return extendedConfig;
  }
  needsTypeChecking() {
    if (this.configName === "disableTypeChecked") {
      return false;
    }
    if (this.configName === "all") {
      return true;
    }
    return this.configName.includes("TypeChecked");
  }
  toConfigArray() {
    return toArray(tseslint.configs[this.configName]).flat().map((config) => ({
      ...config,
      ...config.files && config.files.includes("**/*.ts") ? { files: [...config.files, "**/*.vue"] } : {},
      ...this.extendsOptions
    }));
  }
}
const vueTsConfigs = Object.fromEntries(
  CONFIG_NAMES.map((name) => [
    name,
    new Proxy(new TsEslintConfigForVue(name), {
      // `ownKeys` is called by ESLint when validating the config object.
      // The only possible scenario where this is called is when the placeholder object
      // isn't replaced, which means it's passed to ESLint without being wrapped by
      // `defineConfigWithVueTs()`
      // We throw an error here to provide a better error message to the user.
      ownKeys() {
        throw new Error(
          "Please wrap the config object with `defineConfigWithVueTs()`"
        );
      }
    })
  ])
);

function groupVueFiles(rootDir) {
  const { vueFilesWithScriptTs, otherVueFiles } = fg.sync(["**/*.vue"], {
    cwd: rootDir,
    ignore: ["**/node_modules/**"]
  }).reduce(
    (acc, file) => {
      const absolutePath = path.resolve(rootDir, file);
      const contents = fs.readFileSync(absolutePath, "utf8");
      if (/<script[^>]*\blang\s*=\s*"ts"[^>]*>/i.test(contents)) {
        acc.vueFilesWithScriptTs.push(file);
      } else {
        acc.otherVueFiles.push(file);
      }
      return acc;
    },
    { vueFilesWithScriptTs: [], otherVueFiles: [] }
  );
  return {
    // Only `.vue` files with `<script lang="ts">` or `<script setup lang="ts">` can be type-checked.
    typeCheckable: vueFilesWithScriptTs,
    nonTypeCheckable: otherVueFiles
  };
}

const extraFileExtensions = [".vue"];
function escapePathForGlob(path) {
  return path.replace(/([*?{}[\]()])/g, "[$1]");
}
const additionalRulesRequiringParserServices = [
  "@typescript-eslint/consistent-type-imports",
  "@typescript-eslint/prefer-optional-chain"
];
function createBasicSetupConfigs(tsSyntaxInTemplates, scriptLangs) {
  const mayHaveJsxInSfc = scriptLangs.includes("jsx") || scriptLangs.includes("tsx");
  const parser = {
    // Fallback to espree for js/jsx scripts, as well as SFCs without scripts
    // for better performance.
    js: "espree",
    jsx: "espree",
    ts: tseslint.parser,
    tsx: tseslint.parser
    // Leave the template parser unspecified,
    // so that it could be determined by `<script lang="...">`
  };
  if (!tsSyntaxInTemplates) {
    parser["<template>"] = "espree";
  }
  return [
    // Must set eslint-plugin-vue's base config again no matter whether the user
    // has set it before. Otherwise it would be overridden by the tseslint's config.
    ...pluginVue.configs["flat/base"],
    {
      name: "@vue/typescript/setup",
      files: ["*.vue", "**/*.vue"],
      languageOptions: {
        parser: vueParser,
        parserOptions: {
          parser,
          // The internal espree version used by vue-eslint-parser is 9.x, which supports ES2024 at most.
          // While the parser may try to load the latest version of espree, it's not guaranteed to work.
          // For example, if npm accidentally hoists the older version to the top of the node_modules,
          // or if the user installs the older version of espree at the project root,
          // the older versions would be used.
          // But ESLint 9 allows setting the ecmaVersion to 2025, which may cause a crash.
          // So we set the ecmaVersion to 2024 here to avoid the potential issue.
          ecmaVersion: 2024,
          ecmaFeatures: {
            jsx: mayHaveJsxInSfc
          },
          extraFileExtensions
        }
      },
      rules: {
        "vue/block-lang": [
          "error",
          {
            script: {
              lang: scriptLangs,
              allowNoLang: scriptLangs.includes("js")
            }
          }
        ]
      }
    }
  ];
}
function createSkipTypeCheckingConfigs(nonTypeCheckableVueFiles) {
  const configs = [
    {
      name: "@vue/typescript/skip-type-checking-for-js-files",
      files: ["**/*.js", "**/*.jsx", "**/*.cjs", "**/*.mjs"],
      ...tseslint.configs.disableTypeChecked
    }
  ];
  if (nonTypeCheckableVueFiles.length > 0) {
    configs.push({
      name: "@vue/typescript/skip-type-checking-for-vue-files-without-ts",
      files: nonTypeCheckableVueFiles.map(escapePathForGlob),
      ...tseslint.configs.disableTypeChecked,
      rules: {
        ...tseslint.configs.disableTypeChecked.rules,
        ...Object.fromEntries(
          additionalRulesRequiringParserServices.map((rule) => [rule, "off"])
        )
      }
    });
  }
  return configs;
}
function createTypeCheckingConfigs(typeCheckableVueFiles, allowComponentTypeUnsafety) {
  const configs = [
    {
      name: "@vue/typescript/default-project-service-for-ts-files",
      files: ["**/*.ts", "**/*.tsx", "**/*.mts"],
      languageOptions: {
        parser: tseslint.parser,
        parserOptions: {
          projectService: true,
          extraFileExtensions
        }
      }
    }
  ];
  if (allowComponentTypeUnsafety) {
    configs.push(
      // Due to limitations in the integration between Vue and TypeScript-ESLint,
      // TypeScript-ESLint cannot get the full type information for `.vue` files
      // and will use fallback types that contain some `any`s.
      // Therefore, we need to disable some `no-unsafe-*` rules that would error on idiomatic Vue code.
      {
        name: "@vue/typescript/type-aware-rules-in-conflict-with-vue",
        files: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.vue"],
        rules: {
          // Would error on `createApp(App)`
          "@typescript-eslint/no-unsafe-argument": "off",
          // Would error on route component configuration
          "@typescript-eslint/no-unsafe-assignment": "off",
          // Would error on async components
          "@typescript-eslint/no-unsafe-return": "off",
          // Might error on `defineExpose` + `useTemplateRef` usages
          "@typescript-eslint/no-unsafe-call": "off",
          "@typescript-eslint/no-unsafe-member-access": "off"
        }
      }
    );
  }
  if (typeCheckableVueFiles.length > 0) {
    configs.push({
      name: "@vue/typescript/default-project-service-for-vue-files",
      files: typeCheckableVueFiles.map(escapePathForGlob),
      languageOptions: {
        parser: vueParser,
        parserOptions: {
          projectService: true,
          parser: tseslint.parser,
          extraFileExtensions
        }
      }
    });
  }
  return configs;
}

function omit(obj, keys) {
  return Object.fromEntries(
    Object.entries(obj).filter(([key]) => !keys.includes(key))
  );
}
const pipe = (value, ...fns) => {
  return fns.reduce((acc, fn) => fn(acc), value);
};
function partition(array, predicate) {
  const truthy = [];
  const falsy = [];
  for (const element of array) {
    if (predicate(element)) {
      truthy.push(element);
    } else {
      falsy.push(element);
    }
  }
  return [truthy, falsy];
}

let projectOptions = {
  tsSyntaxInTemplates: true,
  scriptLangs: ["ts"],
  allowComponentTypeUnsafety: true,
  rootDir: process$1.cwd()
};
function configureVueProject(userOptions) {
  if (userOptions.tsSyntaxInTemplates !== void 0) {
    projectOptions.tsSyntaxInTemplates = userOptions.tsSyntaxInTemplates;
  }
  if (userOptions.allowComponentTypeUnsafety !== void 0) {
    projectOptions.allowComponentTypeUnsafety = userOptions.allowComponentTypeUnsafety;
  }
  if (userOptions.scriptLangs) {
    projectOptions.scriptLangs = userOptions.scriptLangs;
  }
  if (userOptions.rootDir) {
    projectOptions.rootDir = userOptions.rootDir;
  }
}
function defineConfigWithVueTs(...configs) {
  return pipe(
    configs,
    flattenConfigs,
    deduplicateVuePlugin,
    insertAndReorderConfigs,
    resolveVueTsConfigs,
    tseslint.config
    // this might not be necessary, but it doesn't hurt to keep it
  );
}
function flattenConfigs(configs) {
  return configs.flat(Infinity).flatMap(
    (c) => {
      if (c instanceof TsEslintConfigForVue) {
        return c;
      }
      const { extends: extendsArray, ...restOfConfig } = c;
      if (extendsArray == null || extendsArray.length === 0) {
        return restOfConfig;
      }
      const flattenedExtends = extendsArray.flatMap(
        (configToExtend) => Array.isArray(configToExtend) ? flattenConfigs(configToExtend) : [configToExtend]
      );
      return [
        ...flattenedExtends.map((extension) => {
          if (extension instanceof TsEslintConfigForVue) {
            return extension.asExtendedWith(restOfConfig);
          } else {
            const name = [restOfConfig.name, extension.name].filter(Boolean).join("__");
            return {
              ...extension,
              ...restOfConfig.files && { files: restOfConfig.files },
              ...restOfConfig.ignores && { ignores: restOfConfig.ignores },
              ...name && { name }
            };
          }
        }),
        // If restOfConfig contains nothing but `ignores`/`name`, we shouldn't return it
        // Because that would make it a global `ignores` config, which is not what we want
        ...Object.keys(omit(restOfConfig, ["ignores", "name"])).length > 0 ? [restOfConfig] : []
      ];
    }
  );
}
function resolveVueTsConfigs(configs) {
  return configs.flatMap(
    (config) => config instanceof TsEslintConfigForVue ? config.toConfigArray() : config
  );
}
const userTypeAwareConfigs = [];
function insertAndReorderConfigs(configs) {
  const lastExtendedConfigIndex = configs.findLastIndex(
    (config) => config instanceof TsEslintConfigForVue
  );
  if (lastExtendedConfigIndex === -1) {
    return configs;
  }
  const vueFiles = groupVueFiles(projectOptions.rootDir);
  const configsWithoutTypeAwareRules = configs.map(extractTypeAwareRules);
  const hasTypeAwareConfigs = configs.some(
    (config) => config instanceof TsEslintConfigForVue && config.needsTypeChecking()
  );
  const needsTypeAwareLinting = hasTypeAwareConfigs || userTypeAwareConfigs.length > 0;
  return [
    ...configsWithoutTypeAwareRules.slice(0, lastExtendedConfigIndex + 1),
    ...createBasicSetupConfigs(
      projectOptions.tsSyntaxInTemplates,
      projectOptions.scriptLangs
    ),
    // user-turned-off type-aware rules must come after the last extended config
    // in case some rules re-enabled by the extended config
    // user-turned-on type-aware rules must come before skipping type-checking
    // in case some rules targets those can't be type-checked files
    // So we extract all type-aware rules by users and put them in the middle
    ...userTypeAwareConfigs,
    ...needsTypeAwareLinting ? [
      ...createSkipTypeCheckingConfigs(vueFiles.nonTypeCheckable),
      ...createTypeCheckingConfigs(vueFiles.typeCheckable, projectOptions.allowComponentTypeUnsafety)
    ] : [],
    ...configsWithoutTypeAwareRules.slice(lastExtendedConfigIndex + 1)
  ];
}
function extractTypeAwareRules(config) {
  if (config instanceof TsEslintConfigForVue) {
    return config;
  }
  if (!config.rules) {
    return config;
  }
  const [typeAwareRuleEntries, otherRuleEntries] = partition(
    Object.entries(config.rules),
    ([name]) => doesRuleRequireTypeInformation(name)
  );
  if (typeAwareRuleEntries.length > 0) {
    userTypeAwareConfigs.push({
      rules: Object.fromEntries(typeAwareRuleEntries),
      ...config.files && { files: config.files }
    });
  }
  return {
    ...config,
    rules: Object.fromEntries(otherRuleEntries)
  };
}
const rulesRequiringTypeInformation = new Set(
  Object.entries(tseslint.plugin.rules).filter(([_name, def]) => def?.meta?.docs?.requiresTypeChecking).map(([name, _def]) => `@typescript-eslint/${name}`).concat(additionalRulesRequiringParserServices)
);
function doesRuleRequireTypeInformation(ruleName) {
  return rulesRequiringTypeInformation.has(ruleName);
}
function deduplicateVuePlugin(configs) {
  return configs.map((config) => {
    if (config instanceof TsEslintConfigForVue || !config.plugins?.vue) {
      return config;
    }
    const currentVuePlugin = config.plugins.vue;
    if (currentVuePlugin !== pluginVue) {
      const currentVersion = currentVuePlugin.meta?.version || "unknown";
      const expectedVersion = pluginVue.meta?.version || "unknown";
      const configName = config.name || "unknown config";
      console.warn(
        `Warning: Multiple instances of eslint-plugin-vue detected in ${configName}. Replacing version ${currentVersion} with version ${expectedVersion}.`
      );
      return {
        ...config,
        plugins: {
          ...config.plugins,
          vue: pluginVue
        }
      };
    }
    return config;
  });
}

function createConfig({
  extends: configNamesToExtend = ["recommended"],
  supportedScriptLangs = { ts: true, tsx: false, js: false, jsx: false },
  rootDir = process.cwd()
} = {}) {
  for (const name of configNamesToExtend) {
    if (!tseslint.configs[name]) {
      const nameInCamelCase = name.replace(
        /-([a-z])/g,
        (_, letter) => letter.toUpperCase()
      );
      if (tseslint.configs[nameInCamelCase]) {
        throw new Error(
          `The config name "${name}" is not supported in "extends". Please use "${nameInCamelCase}" instead.`
        );
      }
      throw new Error(`Unknown config name in "extends": ${name}.`);
    }
  }
  configureVueProject({
    scriptLangs: Object.keys(supportedScriptLangs).filter(
      (lang) => supportedScriptLangs[lang]
    ),
    rootDir
  });
  return defineConfigWithVueTs(
    ...configNamesToExtend.map((name) => vueTsConfigs[name])
  );
}

const defineConfig = defineConfigWithVueTs;

export { configureVueProject, createConfig, createConfig as default, defineConfig, defineConfigWithVueTs, vueTsConfigs };
