import { mkdir, rm } from 'node:fs/promises';
import path from 'node:path';
import stableStringify from 'fast-json-stable-stringify';
import type { FormatConfig } from 'oxfmt';
import { format } from 'oxfmt';
import { transformWithOxc } from 'vite';
import _oxfmtConfig from '../.oxfmtrc.json' with { type: 'json5' };
import { version } from '../lerna.json' with { type: 'json' };
import { catalogs } from '../package.json' with { type: 'json' };
import { configs } from './exampleConfigs.js';

const oxfmtConfig = _oxfmtConfig as FormatConfig;

interface PackageJSON {
  name: string;
  description: string;
  dependencies: Record<string, string>;
  devDependencies: Record<string, string>;
  peerDependencies: Record<string, string>;
}

console.log('Generating/updating examples');

const compileToJS = async (code: string, fileName: string) => {
  const compiled = await transformWithOxc(code, fileName, { jsx: 'preserve' });
  return compiled.code.replaceAll(/^(const|createRoot|\s+return)/gm, '\n\n$1');
};

const packagesPath = path.join(import.meta.dirname, '../packages');
const templatePath = path.join(import.meta.dirname, '_template');
const templateDotCS = path.join(templatePath, '.codesandbox');
const templateDotDC = path.join(templatePath, '.devcontainer');
const templateSrc = path.join(templatePath, 'src');
const templateDotCSTemplateJSON = await Bun.file(path.join(templateDotCS, 'template.json')).json();
const templateIndexHTML = await Bun.file(path.join(templatePath, 'index.html')).text();
const templateIndexTSX = await Bun.file(path.join(templateSrc, 'index.tsx')).text();
const templateAppTSX = await Bun.file(path.join(templateSrc, 'App.tsx')).text();
const templateStylesCSS = await Bun.file(path.join(templateSrc, 'styles.css')).text();
const templateREADMEmd = await Bun.file(path.join(templatePath, 'README.md')).text();

const templatePkgJsonNewTextRaw = await Bun.file(path.join(templatePath, 'package.json')).text();
const templatePkgJsonNewText = templatePkgJsonNewTextRaw.replaceAll(
  /("@?react-querybuilder(?:\/\w+)?": ").*?"/g,
  `$1${version}"`
);
await Bun.write(path.join(templatePath, 'package.json'), templatePkgJsonNewText);
const templatePkgJSON: PackageJSON = await Bun.file(path.join(templatePath, 'package.json')).json();

const formatAndWrite = async (filepath: string, fileContents: string) => {
  const formatted = await format(filepath, fileContents, oxfmtConfig);
  if (formatted.errors?.length > 0) {
    console.error(`Errors found in "${filepath}":`);
    formatted.errors.forEach(error => console.error(error));
  }
  return Bun.write(filepath, formatted.code);
};

const generateExampleFromTemplate = async (exampleID: string) => {
  const exampleConfig = configs[exampleID];
  const examplePath = path.join(import.meta.dirname, exampleID);
  const exampleDotCS = path.join(examplePath, '.codesandbox');
  const exampleDotDC = path.join(examplePath, '.devcontainer');
  const exampleSrc = path.join(examplePath, 'src');
  const exampleBaseTitle = `React Query Builder ${exampleConfig.name}`;
  const exampleTitle = `${exampleBaseTitle} Example`;
  const exampleTemplateName = `${exampleBaseTitle} Template`;
  await rm(examplePath, { recursive: true, force: true });
  await mkdir(examplePath);
  await Promise.all([mkdir(exampleDotCS), mkdir(exampleSrc)]);

  // Array of Bun.write promises
  const toWrite: ReturnType<typeof Bun.write>[] = [];

  // #region Straight copies
  toWrite.push(
    Bun.write(
      path.join(exampleDotCS, 'tasks.json'),
      Bun.file(path.join(templateDotCS, 'tasks.json'))
    ),
    Bun.write(
      path.join(exampleDotCS, 'workspace.json'),
      Bun.file(path.join(templateDotCS, 'workspace.json'))
    ),
    Bun.write(
      path.join(exampleDotDC, 'devcontainer.json'),
      Bun.file(path.join(templateDotDC, 'devcontainer.json'))
    ),
    Bun.write(
      path.join(examplePath, '.gitignore'),
      Bun.file(path.join(templatePath, '.gitignore'))
    ),
    Bun.write(
      path.join(examplePath, `vite.config.${exampleConfig.compileToJS ? 'j' : 't'}s`),
      Bun.file(path.join(templatePath, 'vite.config.ts'))
    ),
    ...(exampleConfig.compileToJS
      ? []
      : [
          Bun.write(
            path.join(exampleSrc, 'vite-env.d.ts'),
            Bun.file(path.join(templateSrc, 'vite-env.d.ts'))
          ),
          Bun.write(
            path.join(examplePath, 'tsconfig.json'),
            Bun.file(path.join(templatePath, 'tsconfig.json'))
          ),
        ])
  );
  // #endregion

  // #region /index.html
  const exampleIndexHTML = templateIndexHTML
    .replace('__TITLE__', exampleTitle)
    .replace('index.tsx', exampleConfig.compileToJS ? 'index.jsx' : 'index.tsx');
  toWrite.push(formatAndWrite(path.join(examplePath, 'index.html'), exampleIndexHTML));
  // #endregion

  // #region src/styles.css
  const processedTemplateCSS = templateStylesCSS
    .replace('/* __CSS_PRE__ */', exampleConfig.cssPre.join('\n'))
    .replace('/* __CSS_POST__ */', exampleConfig.cssPost.join('\n'));
  toWrite.push(formatAndWrite(path.join(exampleSrc, `styles.css`), processedTemplateCSS));
  // #endregion

  // #region src/index.tsx
  const exampleIndexSourceCode = exampleConfig.compileToJS
    ? await compileToJS(templateIndexTSX, 'index.tsx')
    : templateIndexTSX;
  toWrite.push(
    formatAndWrite(
      path.join(exampleSrc, `index.${exampleConfig.compileToJS ? 'j' : 't'}sx`),
      exampleIndexSourceCode
    )
  );
  // #endregion

  // #region src/App.tsx
  const processedTemplateAppTSX = templateAppTSX
    .replace('// __IMPORTS__', exampleConfig.tsxImports.join('\n'))
    .replace('// __ADDITIONAL_DECLARATIONS__', exampleConfig.additionalDeclarations.join('\n'))
    .replace('{/* __WRAPPER_OPEN__ */}', exampleConfig.wrapper?.[0] ?? '')
    .replace('{/* __WRAPPER_CLOSE__ */}', exampleConfig.wrapper?.[1] ?? '')
    .replace('// __RQB_PROPS__', exampleConfig.props.join('\n'));
  const exampleAppSourceCode = exampleConfig.compileToJS
    ? await compileToJS(processedTemplateAppTSX, 'App.tsx')
    : processedTemplateAppTSX;
  toWrite.push(
    formatAndWrite(
      path.join(exampleSrc, `App.${exampleConfig.compileToJS ? 'j' : 't'}sx`),
      exampleAppSourceCode
    )
  );
  // #endregion

  // #region package.json
  const examplePkgJSON = structuredClone(templatePkgJSON);
  examplePkgJSON.name = `react-querybuilder-${exampleID}-example`;
  examplePkgJSON.description = exampleTitle;
  if (exampleConfig.isCompatPackage || exampleConfig.enableDnD) {
    examplePkgJSON.dependencies[`@react-querybuilder/${exampleID}`] =
      templatePkgJSON.dependencies['react-querybuilder'];
  }
  if (exampleConfig.compileToJS) {
    delete examplePkgJSON.devDependencies['sass'];
    delete examplePkgJSON.devDependencies['typescript'];
    for (const devDep of Object.keys(examplePkgJSON.devDependencies)) {
      if (devDep.startsWith('@types/')) {
        delete examplePkgJSON.devDependencies[devDep];
      }
    }
  }
  if (exampleConfig.dependencyKeys.length > 0) {
    const compatPkgJson: PackageJSON = await Bun.file(
      path.join(packagesPath, `${exampleID}/package.json`)
    ).json();
    for (const depKey of exampleConfig.dependencyKeys) {
      if (Array.isArray(depKey)) {
        examplePkgJSON.dependencies[depKey[0]] = depKey[1];
      } else {
        let peerDepVersion = compatPkgJson.peerDependencies[depKey];
        if (peerDepVersion === 'catalog:peer') {
          peerDepVersion = catalogs.peer[depKey as keyof typeof catalogs.peer];
        }
        examplePkgJSON.dependencies[depKey] = peerDepVersion;
      }
    }
  }
  toWrite.push(
    formatAndWrite(path.join(examplePath, 'package.json'), stableStringify(examplePkgJSON))
  );
  // #endregion

  // #region .codesandbox/template.json
  const exampleDotCSTemplateJSON = {
    ...templateDotCSTemplateJSON,
    title: exampleTemplateName,
    description: exampleTemplateName,
  };
  toWrite.push(
    formatAndWrite(
      path.join(exampleDotCS, 'template.json'),
      stableStringify(exampleDotCSTemplateJSON)
    )
  );
  // #endregion

  // #region README.md
  const exampleREADMEmd =
    `## ${exampleTitle}` +
    '\n\n' +
    templateREADMEmd
      .replaceAll(/(\/?examples?[/-])?_template/g, `$1${exampleID}`)
      .replaceAll('App.tsx', exampleConfig.compileToJS ? 'App.jsx' : '$&') +
    '\n\n' +
    '> _Development note: Do not modify the files in this folder directly. Edit corresponding ' +
    'files in the [_template](../_template) folder and/or [exampleConfigs.ts](../exampleConfigs.ts), ' +
    'then run `bun generate-examples` from the repository root directory (requires [Bun](https://bun.sh/))._';
  toWrite.push(formatAndWrite(path.join(examplePath, 'README.md'), exampleREADMEmd));
  // #endregion

  console.log(`Generated "${exampleConfig.name}" example code (${exampleID})`);

  return Promise.all(toWrite);
};

// #region Other examples' package.json
const otherExamples = [
  'base-ui',
  'ci',
  'native',
  'native-expo',
  'next',
  'preact',
  'rules-engine',
  'tremor',
] as const;

const updateOtherExample = async (otherExampleName: string) => {
  const otherExamplePkgJSON: PackageJSON = await Bun.file(
    path.join(import.meta.dirname, `${otherExampleName}/package.json`)
  ).json();
  for (const dep of Object.keys(otherExamplePkgJSON.dependencies)) {
    if (/^@?react-querybuilder(\/[a-z]+)?/.test(dep)) {
      otherExamplePkgJSON.dependencies[dep] = templatePkgJSON.dependencies['react-querybuilder'];
    }
  }
  const otherExamplePkgJsonPath = path.join(
    import.meta.dirname,
    `${otherExampleName}/package.json`
  );
  const otherExamplePkgJsonFormatted = await format(
    otherExamplePkgJsonPath,
    stableStringify(otherExamplePkgJSON),
    oxfmtConfig
  );
  if (otherExamplePkgJsonFormatted.errors?.length > 0) {
    console.error(`Errors found in "${otherExampleName}" example's package.json:`);
    otherExamplePkgJsonFormatted.errors.forEach(error => console.error(error));
  }
  console.log(`Updated package.json for "${otherExampleName}" example`);
  return Bun.write(otherExamplePkgJsonPath, otherExamplePkgJsonFormatted.code);
};
// #endregion

const templateExamples = Object.keys(configs);

const results = await Promise.allSettled([
  ...templateExamples.map(v => generateExampleFromTemplate(v)),
  ...otherExamples.map(v => updateOtherExample(v)),
]);

for (const [idx, result] of results.entries()) {
  if (result.status === 'rejected') {
    const exampleName =
      idx >= templateExamples.length
        ? otherExamples[idx - templateExamples.length]
        : templateExamples[idx];
    console.log(
      `Failed to generate or format "${exampleName}" example. Reason: "${result.reason}"`
    );
  }
}

console.log('Finished generating/updating examples');