/* This file is a part of @mdn/browser-compat-data
 * See LICENSE file for more information. */

import { readdir, readFile, stat, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { styleText } from 'node:util';

import esMain from 'es-main';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';

import dataFolders from '../scripts/lib/data-folders.js';

import fixBrowserOrder from './fixer/browser-order.js';
import fixCommonErrors from './fixer/common-errors.js';
import fixFeatureOrder from './fixer/feature-order.js';
import fixPropertyOrder from './fixer/property-order.js';
import fixStatementOrder from './fixer/statement-order.js';
import fixDescriptions from './fixer/descriptions.js';
import fixFlags from './fixer/flags.js';
import fixLinks from './fixer/links.js';
import fixMDNURLs from './fixer/mdn-urls.js';
import fixStatus from './fixer/status.js';
import fixMirror from './fixer/mirror.js';
import fixOverlap from './fixer/overlap.js';
import fixStandardTrackExceptions from './fixer/standard-track-exceptions.js';
import { IS_WINDOWS } from './utils.js';

/** @import {Stats} from 'node:fs' */
/** @import {LintOptions} from './types.js' */

const dirname = fileURLToPath(new URL('.', import.meta.url));

/** @type {Readonly<Record<string, function(string, string): Promise<string> | string>>} */
const FIXES = Object.freeze({
  descriptions: fixDescriptions,
  common_errors: fixCommonErrors,
  flags: fixFlags,
  links: fixLinks,
  mdn_urls: fixMDNURLs,
  status: fixStatus,
  mirror: fixMirror,
  overlap: fixOverlap,
  browser_order: fixBrowserOrder,
  feature_order: fixFeatureOrder,
  property_order: fixPropertyOrder,
  statement_order: fixStatementOrder,
  standard_track_exceptions: fixStandardTrackExceptions,
});

/**
 * Recursively load one or more files and/or directories passed as arguments and perform automatic fixes.
 * @param {LintOptions} options The lint options
 * @param {...string} files The files to load and perform fix upon
 * @returns {Promise<void>}
 */
const load = async (options, ...files) => {
  const fixes = Object.entries(FIXES)
    .filter(([key]) => !options.only || options.only.includes(key))
    .map(([, fix]) => fix);

  for (let file of files) {
    if (file.indexOf(dirname) !== 0) {
      file = path.resolve(dirname, '..', file);
    }

    /** @type {Stats} */
    let fsStats;

    try {
      fsStats = await stat(file);
    } catch {
      console.warn(
        styleText('yellow', `File ${styleText('bold', file)} doesn't exist!`),
      );
      continue;
    }

    if (fsStats.isFile()) {
      if (path.extname(file) === '.json' && !file.endsWith('.schema.json')) {
        let initial = (await readFile(file, 'utf-8')).trim();
        let expected = initial;

        for (const fix of fixes) {
          expected = await fix(file, expected);
        }

        if (IS_WINDOWS) {
          // prevent false positives from git.core.autocrlf on Windows
          initial = initial.replace(/\r/g, '');
          expected = expected.replace(/\r/g, '');
        }

        if (initial !== expected) {
          await writeFile(file, expected + '\n', 'utf-8');
        }
      }
    } else {
      const subFiles = (await readdir(file)).map((subfile) =>
        path.join(file, subfile),
      );

      // Sort so files come before directories (e.g., meta.json before meta/).
      // This ensures parent features are fixed before their sub-features.
      subFiles.sort((a, b) => {
        const aIsJson = a.endsWith('.json');
        const bIsJson = b.endsWith('.json');
        if (aIsJson !== bIsJson) {
          return aIsJson ? -1 : 1;
        }
        return a.localeCompare(b);
      });

      await load(options, ...subFiles);
    }
  }
};

/**
 * Fix any errors in specified file(s) and/or folder(s), or all of BCD
 * @param {string[]} files The file(s) and/or folder(s) to fix. Leave undefined for everything.
 * @param {LintOptions} options Lint options
 * @returns {Promise<void>}
 */
const main = async (files, options) => {
  await load(options, ...files);
};

if (esMain(import.meta)) {
  const argv = yargs(hideBin(process.argv))
    .command('$0 [files..]', false)
    .positional('files', {
      array: true,
      description: 'The files to fix (leave blank to test everything)',
      type: 'string',
    })
    .option('only', {
      array: true,
      description: 'The checks to run',
      choices: Object.keys(FIXES),
    })
    .parseSync();

  const { files = dataFolders, only } = argv;

  await main(files, { only });
}

export default load;
