// @ts-check

/**
 * @typedef {PrettierOptions & {
 *   onDiskFilepath: string;
 *   parserMeta?: ESLint.ObjectMetaProperties['meta'];
 *   parserPath?: string;
 *   usePrettierrc?: boolean;
 * }} Options
 * @import {FileInfoOptions, Options as PrettierOptions} from 'prettier'
 * @import {ESLint} from 'eslint'
 */

import prettier from 'prettier';
import { runAsWorker } from 'synckit';

runAsWorker(
  /**
   * @param {string} source - The source code to format.
   * @param {Options} options - The prettier options.
   * @param {FileInfoOptions} eslintFileInfoOptions - The file info options.
   * @returns {Promise<string | undefined>} The formatted source code.
   */
  async (
    source,
    {
      filepath,
      onDiskFilepath,
      parserMeta,
      parserPath,
      usePrettierrc,
      ...eslintPrettierOptions
    },
    eslintFileInfoOptions,
  ) => {
    const prettierRcOptions = usePrettierrc
      ? await prettier.resolveConfig(onDiskFilepath, {
          editorconfig: true,
        })
      : null;

    const { ignored, inferredParser } = await prettier.getFileInfo(
      onDiskFilepath,
      {
        resolveConfig: false,
        withNodeModules: false,
        ignorePath: '.prettierignore',
        ...prettierRcOptions,
        ...eslintFileInfoOptions,
      },
    );

    // Skip if file is ignored using a .prettierignore file
    if (ignored) {
      return;
    }

    const initialOptions = { parser: inferredParser ?? 'babel' };

    // ESLint supports processors that let you extract and lint JS
    // fragments within a non-JS language. In the cases where prettier
    // supports the same language as a processor, we want to process
    // the provided source code as javascript (as ESLint provides the
    // rules with fragments of JS) instead of guessing the parser
    // based off the filename. Otherwise, for instance, on a .md file we
    // end up trying to run prettier over a fragment of JS using the
    // markdown parser, which throws an error.
    // Processors may set virtual filenames for these extracted blocks.
    // If they do so then we want to trust the file extension they
    // provide, and no override is needed.
    // If the processor does not set any virtual filename (signified by
    // `filepath` and `onDiskFilepath` being equal) AND we can't
    // infer the parser from the filename, either because no filename
    // was provided or because there is no parser found for the
    // filename, use javascript.
    // This is added to the options first, so that
    // prettierRcOptions and eslintPrettierOptions can still override
    // the parser.
    //
    // `parserBlocklist` should contain the list of prettier parser
    // names for file types where:
    // * Prettier supports parsing the file type
    // * There is an ESLint processor that extracts JavaScript snippets
    //   from the file type.
    if (filepath === onDiskFilepath) {
      // The following list means the plugin process source into js content
      // but with same filename, so we need to change the parser to `babel`
      // by default.
      // Related ESLint plugins are:
      // 1. `eslint-plugin-graphql` (replacement: `@graphql-eslint/eslint-plugin`)
      // 2. `eslint-plugin-html`
      // 3. `eslint-plugin-markdown@1` (replacement: `eslint-plugin-markdown@2+`)
      // 4. `eslint-plugin-svelte3` (replacement: `eslint-plugin-svelte@2+`)
      let inferParserToBabel = false;

      switch (inferredParser) {
        // it could be processed by `@graphql-eslint/eslint-plugin` or `eslint-plugin-graphql`
        case 'graphql': {
          if (
            // for `eslint-plugin-graphql`, see https://github.com/apollographql/eslint-plugin-graphql/blob/64c524cd6607358803d5fcb7cead0a383a125ccb/src/index.js#L416
            source.startsWith('ESLintPluginGraphQLFile`')
          ) {
            inferParserToBabel = true;
          }
          break;
        }
        case 'html': {
          // it could be processed by `eslint-plugin-html` or correctly parsed by `@html-eslint/parser`
          if (
            (typeof parserMeta !== 'undefined' &&
              parserMeta.name !== '@html-eslint/parser') ||
            (typeof parserPath === 'string' &&
              !/([\\/])@html-eslint\1parser\1/.test(parserPath))
          ) {
            inferParserToBabel = true;
          }
          break;
        }
        case 'markdown': {
          // it could be processed by `eslint-plugin-markdown@1` or correctly parsed by `eslint-mdx`
          if (
            (typeof parserMeta !== 'undefined' &&
              parserMeta.name !== 'eslint-mdx') ||
            (typeof parserPath === 'string' &&
              !/([\\/])eslint-mdx\1/.test(parserPath))
          ) {
            inferParserToBabel = true;
          }
          break;
        }
        // it could be processed by `@ota-meshi/eslint-plugin-svelte`, `eslint-plugin-svelte` or `eslint-plugin-svelte3`
        case 'svelte': {
          // The `source` would be modified by `eslint-plugin-svelte3`
          if (
            typeof parserPath === 'string' &&
            !/([\\/])svelte-eslint-parser\1/.test(parserPath)
          ) {
            // We do not support `eslint-plugin-svelte3`,
            // the users should run `prettier` on `.svelte` files manually
            return;
          }
        }
      }

      if (inferParserToBabel) {
        initialOptions.parser = 'babel';
      }
    } else {
      // Similar to https://github.com/prettier/stylelint-prettier/pull/22
      // In all of the following cases ESLint extracts a part of a file to
      // be formatted and there exists a prettier parser for the whole file.
      // If you're interested in prettier you'll want a fully formatted file so
      // you're about to run prettier over the whole file anyway.
      // Therefore running prettier over just the style section is wasteful, so
      // skip it.
      const parserBlocklist = [
        'babel',
        'babylon',
        'flow',
        'typescript',
        'vue',
        'markdown',
        'html',
        'mdx',
        'angular',
        'svelte',
        'pug',
        'oxc',
        'oxc-ts',
        'hermes',
      ];
      if (parserBlocklist.includes(/** @type {string} */ (inferredParser))) {
        return;
      }
    }

    /** @type {PrettierOptions} */
    const prettierOptions = {
      ...initialOptions,
      ...prettierRcOptions,
      ...eslintPrettierOptions,
      filepath,
    };

    return prettier.format(source, prettierOptions);
  },
);
