import { InlineConfig } from '../types';
import path from 'node:path';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { safeFilename } from './utils/strings';
import { getPackageJson } from './utils/package';
import { formatDuration } from './utils/time';
import { printFileList } from './utils/log';
import { findEntrypoints, internalBuild } from './utils/building';
import { registerWxt, wxt } from './wxt';
import { createZip, type Zip } from '@aklinker1/zero-zip';
import { glob } from 'tinyglobby';
import { normalizePath } from './utils';

/**
 * Build and zip the extension for distribution.
 *
 * @param config Optional config that will override your `<root>/wxt.config.ts`.
 * @returns A list of all files included in the ZIP.
 */
export async function zip(config?: InlineConfig): Promise<string[]> {
  await registerWxt('build', config);
  const output = await internalBuild();
  await wxt.hooks.callHook('zip:start', wxt);

  const start = Date.now();
  wxt.logger.info('Zipping extension...');
  const zipFiles: string[] = [];

  const packageJson = await getPackageJson();
  const projectName =
    wxt.config.zip.name ??
    safeFilename(packageJson?.name || path.basename(process.cwd()));
  const modeSuffixes: Record<string, string | undefined> = {
    production: '',
    development: '-dev',
  };
  const modeSuffix = modeSuffixes[wxt.config.mode] ?? `-${wxt.config.mode}`;
  const applyTemplate = (template: string): string =>
    template
      .replaceAll('{{name}}', projectName)
      .replaceAll('{{browser}}', wxt.config.browser)
      .replaceAll('{{version}}', output.manifest.version)
      .replaceAll(
        '{{versionName}}',
        output.manifest.version_name ?? output.manifest.version,
      )
      .replaceAll('{{packageVersion}}', packageJson?.version)
      .replaceAll('{{modeSuffix}}', modeSuffix)
      .replaceAll('{{mode}}', wxt.config.mode)
      .replaceAll('{{manifestVersion}}', `mv${wxt.config.manifestVersion}`);

  await mkdir(wxt.config.outBaseDir, { recursive: true });

  // ZIP output directory
  await wxt.hooks.callHook('zip:extension:start', wxt);
  const outZipFilename = applyTemplate(wxt.config.zip.artifactTemplate);
  const outZipPath = path.resolve(wxt.config.outBaseDir, outZipFilename);
  await zipDir(wxt.config.outDir, outZipPath, {
    exclude: wxt.config.zip.exclude,
  });
  zipFiles.push(outZipPath);
  await wxt.hooks.callHook('zip:extension:done', wxt, outZipPath);

  if (wxt.config.zip.zipSources) {
    const entrypoints = await findEntrypoints();
    const skippedEntrypoints = entrypoints.filter((entry) => entry.skipped);
    const excludeSources = [
      ...wxt.config.zip.excludeSources,
      ...skippedEntrypoints.map((entry) =>
        path.relative(wxt.config.zip.sourcesRoot, entry.inputPath),
      ),
    ].map((paths) => paths.replaceAll('\\', '/')); // TODO: Use normalizePath?
    await wxt.hooks.callHook('zip:sources:start', wxt);
    const { overrides, files: downloadedPackages } =
      await downloadPrivatePackages();
    const sourcesZipFilename = applyTemplate(wxt.config.zip.sourcesTemplate);
    const sourcesZipPath = path.resolve(
      wxt.config.outBaseDir,
      sourcesZipFilename,
    );
    const files = await zipDir(wxt.config.zip.sourcesRoot, sourcesZipPath, {
      include: wxt.config.zip.includeSources,
      exclude: excludeSources,
      transform(absolutePath, zipPath, content) {
        if (zipPath.endsWith('package.json')) {
          return addOverridesToPackageJson(absolutePath, content, overrides);
        }
      },
      additionalFiles: downloadedPackages,
      dot: wxt.config.zip.dotSources,
    });
    zipFiles.push(sourcesZipPath);
    await wxt.hooks.callHook('zip:sources:done', wxt, sourcesZipPath);

    await printFileList(
      wxt.logger.info,
      `Sources included in \`${sourcesZipFilename}\``,
      wxt.config.zip.sourcesRoot,
      files,
    );
  }

  await printFileList(
    wxt.logger.success,
    `Zipped extension in ${formatDuration(Date.now() - start)}`,
    wxt.config.outBaseDir,
    zipFiles,
  );

  await wxt.hooks.callHook('zip:done', wxt, zipFiles);

  return zipFiles;
}

async function zipDir(
  directory: string,
  outputPath: string,
  options?: {
    include?: string[];
    exclude?: string[];
    transform?: (
      absolutePath: string,
      zipPath: string,
      content: string,
    ) => Promise<string | undefined | void> | string | undefined | void;
    additionalWork?: (archive: Zip) => Promise<void> | void;
    additionalFiles?: string[];
    dot?: boolean;
  },
): Promise<string[]> {
  const archive = createZip({ level: wxt.config.zip.compressionLevel });
  // includeSources patterns are used directly (defaults to ['**/*'] from config)
  // excludeSources patterns are passed to glob's ignore option for efficient filtering
  const files = await glob(options?.include ?? ['**/*'], {
    cwd: directory,
    ignore: options?.exclude ?? [],
    onlyFiles: true,
    dot: options?.dot,
  });
  const filesToZip = [
    ...files,
    ...(options?.additionalFiles ?? []).map((file) =>
      path.relative(directory, file),
    ),
  ].sort();
  for (const file of filesToZip) {
    const absolutePath = path.resolve(directory, file);
    if (file.endsWith('.json')) {
      const content = await readFile(absolutePath, 'utf-8');
      archive.addFile(
        file,
        (await options?.transform?.(absolutePath, file, content)) || content,
      );
    } else {
      const content = await readFile(absolutePath);
      archive.addFile(file, content);
    }
  }
  await options?.additionalWork?.(archive);

  const zipBuffer = await archive.toBuffer();
  await writeFile(outputPath, zipBuffer);

  return filesToZip;
}

async function downloadPrivatePackages() {
  const overrides: Record<string, string> = {};
  const files: string[] = [];

  if (wxt.config.zip.downloadPackages.length > 0) {
    const _downloadPackages = new Set(wxt.config.zip.downloadPackages);
    const allPackages = await wxt.pm.listDependencies({
      all: true,
      cwd: wxt.config.root,
    });
    const downloadPackages = allPackages.filter((pkg) =>
      _downloadPackages.has(pkg.name),
    );

    for (const pkg of downloadPackages) {
      wxt.logger.info(`Downloading package: ${pkg.name}@${pkg.version}`);
      const id = `${pkg.name}@${pkg.version}`;
      const tgzPath = await wxt.pm.downloadDependency(
        id,
        wxt.config.zip.downloadedPackagesDir,
      );
      files.push(tgzPath);
      overrides[id] = tgzPath;
    }
  }

  return { overrides, files };
}

function addOverridesToPackageJson(
  absolutePackageJsonPath: string,
  content: string,
  overrides: Record<string, string>,
): string {
  if (Object.keys(overrides).length === 0) return content;

  const packageJsonDir = path.dirname(absolutePackageJsonPath);
  const oldPackage = JSON.parse(content);
  const newPackage = {
    ...oldPackage,
    [wxt.pm.overridesKey]: { ...oldPackage[wxt.pm.overridesKey] },
  };
  Object.entries(overrides).forEach(([key, absolutePath]) => {
    newPackage[wxt.pm.overridesKey][key] =
      'file://./' + normalizePath(path.relative(packageJsonDir, absolutePath));
  });
  return JSON.stringify(newPackage, null, 2);
}
