import { findEntrypoints } from './find-entrypoints';
import { BuildOutput, Entrypoint } from '../../../types';
import { mkdir, rm } from 'node:fs/promises';
import { groupEntrypoints } from './group-entrypoints';
import { formatDuration } from '../time';
import { printBuildSummary } from '../log';
import { glob } from 'tinyglobby';
import { unnormalizePath } from '../paths';
import { rebuild } from './rebuild';
import { relative } from 'node:path';
import {
  ValidationError,
  ValidationResult,
  ValidationResults,
  validateEntrypoints,
} from '../validation';
import { wxt } from '../../wxt';
import { mergeJsonOutputs } from '@aklinker1/rollup-plugin-visualizer';
import { isCI } from '../env';
import { styleText } from 'node:util';
import open from 'tiny-open';

/**
 * Builds the extension based on an internal config. No more config discovery is
 * performed, the build is based on exactly what is passed in.
 *
 * This function:
 *
 * 1. Cleans the output directory
 * 2. Executes the rebuild function with a blank previous output so everything is
 *    built (see `rebuild` for more details)
 * 3. Prints the summary
 */
export async function internalBuild(): Promise<BuildOutput> {
  await wxt.hooks.callHook('build:before', wxt);

  const verb = wxt.config.command === 'serve' ? 'Pre-rendering' : 'Building';
  const target = `${wxt.config.browser}-mv${wxt.config.manifestVersion}`;
  wxt.logger.info(
    `${verb} ${styleText('cyan', target)} for ${styleText('cyan', wxt.config.mode)} with ${styleText(
      'green',
      `${wxt.builder.name} ${wxt.builder.version}`,
    )}`,
  );
  const startTime = Date.now();

  // Cleanup
  await rm(wxt.config.outDir, { recursive: true, force: true });
  await mkdir(wxt.config.outDir, { recursive: true });

  const entrypoints = await findEntrypoints();
  wxt.logger.debug('Detected entrypoints:', entrypoints);

  const validationResults = validateEntrypoints(entrypoints);
  if (validationResults.errorCount + validationResults.warningCount > 0) {
    printValidationResults(validationResults);
  }
  if (validationResults.errorCount > 0) {
    throw new ValidationError(`Entrypoint validation failed`, {
      cause: validationResults,
    });
  }

  const groups = groupEntrypoints(entrypoints);
  await wxt.hooks.callHook('entrypoints:grouped', wxt, groups);

  const { output, warnings } = await rebuild(entrypoints, groups, undefined);
  await wxt.hooks.callHook('build:done', wxt, output);

  // Post-build
  await printBuildSummary(
    wxt.logger.success,
    `Built extension in ${formatDuration(Date.now() - startTime)}`,
    output,
  );

  for (const warning of warnings) {
    wxt.logger.warn(...warning);
  }

  if (wxt.config.analysis.enabled) {
    await combineAnalysisStats();
    const statsPath = relative(wxt.config.root, wxt.config.analysis.outputFile);
    wxt.logger.info(
      `Analysis complete:\n  ${styleText('gray', '└─')} ${styleText('yellow', statsPath)}`,
    );
    if (wxt.config.analysis.open) {
      if (isCI()) {
        wxt.logger.debug(
          `Skipped opening ${styleText('yellow', statsPath)} in CI`,
        );
      } else {
        wxt.logger.info(
          `Opening ${styleText('yellow', statsPath)} in browser...`,
        );
        await open(wxt.config.analysis.outputFile);
      }
    }
  }

  return output;
}

async function combineAnalysisStats(): Promise<void> {
  const unixFiles = await glob(`${wxt.config.analysis.outputName}-*.json`, {
    cwd: wxt.config.analysis.outputDir,
    absolute: true,
    expandDirectories: false,
  });
  const absolutePaths = unixFiles.map(unnormalizePath);

  await mergeJsonOutputs({
    inputs: absolutePaths,
    template: wxt.config.analysis.template,
    filename: wxt.config.analysis.outputFile,
  });

  if (!wxt.config.analysis.keepArtifacts) {
    await Promise.all(
      absolutePaths.map((statsFile) => rm(statsFile, { force: true })),
    );
  }
}

function printValidationResults({
  errorCount,
  errors,
  warningCount,
}: ValidationResults) {
  (errorCount > 0 ? wxt.logger.error : wxt.logger.warn)(
    `Entrypoint validation failed: ${errorCount} error${
      errorCount === 1 ? '' : 's'
    }, ${warningCount} warning${warningCount === 1 ? '' : 's'}`,
  );

  const cwd = process.cwd();
  const entrypointErrors = errors.reduce((map, error) => {
    const entryErrors = map.get(error.entrypoint) ?? [];
    entryErrors.push(error);
    map.set(error.entrypoint, entryErrors);
    return map;
  }, new Map<Entrypoint, ValidationResult[]>());

  Array.from(entrypointErrors.entries()).forEach(([entrypoint, errors]) => {
    wxt.logger.log(relative(cwd, entrypoint.inputPath) + '\n');

    errors.forEach((err) => {
      const type =
        err.type === 'error'
          ? styleText('red', 'ERROR')
          : styleText('yellow', 'WARN');
      const received = styleText(
        'dim',
        `(received: ${JSON.stringify(err.value)})`,
      );
      wxt.logger.log(`  - ${type} ${err.message} ${received}`);
    });
    console.log();
  });
}
