// @flow strict-local

import type {Bundle, FilePath} from '@parcel/types';

import {Namer} from '@parcel/plugin';
import ThrowableDiagnostic, {
  convertSourceLocationToHighlight,
  md,
} from '@parcel/diagnostic';
import assert from 'assert';
import path from 'path';
import nullthrows from 'nullthrows';

const COMMON_NAMES = new Set(['index', 'src', 'lib']);
const ALLOWED_EXTENSIONS = {
  js: ['js', 'mjs', 'cjs'],
};

export default (new Namer({
  name({bundle, bundleGraph}) {
    let bundleGroup = bundleGraph.getBundleGroupsContainingBundle(bundle)[0];
    let bundleGroupBundles = bundleGraph.getBundlesInBundleGroup(bundleGroup, {
      includeInline: true,
    });
    let isEntry = bundleGraph.isEntryBundleGroup(bundleGroup);

    if (bundle.needsStableName) {
      let entryBundlesOfType = bundleGroupBundles.filter(
        b => b.needsStableName && b.type === bundle.type,
      );
      assert(
        entryBundlesOfType.length === 1,
        // Otherwise, we'd end up naming two bundles the same thing.
        `Bundle group cannot have more than one entry bundle of the same type. The offending bundle type is ${entryBundlesOfType[0].type}`,
      );
    }

    let mainBundle = nullthrows(
      bundleGroupBundles.find(b =>
        b.getEntryAssets().some(a => a.id === bundleGroup.entryAssetId),
      ),
    );

    if (
      bundle.id === mainBundle.id &&
      isEntry &&
      bundle.target &&
      bundle.target.distEntry != null
    ) {
      let loc = bundle.target.loc;
      let distEntry = bundle.target.distEntry;
      let distExtension = path.extname(bundle.target.distEntry).slice(1);
      let allowedExtensions = ALLOWED_EXTENSIONS[bundle.type] || [bundle.type];
      if (!allowedExtensions.includes(distExtension) && loc) {
        let fullName = path.relative(
          path.dirname(loc.filePath),
          path.join(bundle.target.distDir, distEntry),
        );
        let err = new ThrowableDiagnostic({
          diagnostic: {
            message: md`Target "${bundle.target.name}" declares an output file path of "${fullName}" which does not match the compiled bundle type "${bundle.type}".`,
            codeFrames: [
              {
                filePath: loc.filePath,
                codeHighlights: [
                  convertSourceLocationToHighlight(
                    loc,
                    md`Did you mean "${
                      fullName.slice(0, -path.extname(fullName).length) +
                      '.' +
                      bundle.type
                    }"?`,
                  ),
                ],
              },
            ],
            hints: [
              `Try changing the file extension of "${
                bundle.target.name
              }" in ${path.relative(process.cwd(), loc.filePath)}.`,
            ],
          },
        });
        throw err;
      }

      return bundle.target.distEntry;
    }

    // Base split bundle names on the first bundle in their group.
    // e.g. if `index.js` imports `foo.css`, the css bundle should be called
    //      `index.css`.
    let name = nameFromContent(
      mainBundle,
      bundle,
      isEntry,
      bundleGroup.entryAssetId,
      bundleGraph.getEntryRoot(bundle.target),
    );
    if (!bundle.needsStableName) {
      name += '.' + bundle.hashReference;
    }

    // Allow bundle extension to be overridden.
    let extension = bundle.type;
    let entry = bundle.getMainEntry();
    if (entry && typeof entry.meta.bundleExtension === 'string') {
      extension = entry.meta.bundleExtension;
    }

    // Group server and client bundles into separate folders.
    // This allows users to easily upload to different places, and avoid exposing
    // server code on public servers.
    // If bundleExtension is set, assume everything is static (rendering at build time).
    if (
      (bundle.env.context === 'react-server' &&
        entry?.meta?.bundleExtension == null) ||
      (bundle.env.context === 'react-client' &&
        hasReactServerEntries(bundleGraph))
    ) {
      name = bundle.env.context.slice(6) + '/' + name;
    }

    return name + '.' + extension;
  },
}): Namer);

function nameFromContent(
  mainBundle: Bundle,
  bundle: Bundle,
  isEntry: boolean,
  entryAssetId: string,
  entryRoot: FilePath,
): string {
  let entryFilePath = nullthrows(
    mainBundle.getEntryAssets().find(a => a.id === entryAssetId),
  ).filePath;
  let name = basenameWithoutExtension(entryFilePath);

  // If this is an entry bundle, use the original relative path.
  if (bundle.needsStableName) {
    // Match name of target entry if possible, but with a different extension.
    if (isEntry && bundle.target.distEntry != null) {
      return basenameWithoutExtension(bundle.target.distEntry);
    }

    return path
      .join(path.relative(entryRoot, path.dirname(entryFilePath)), name)
      .replace(/\.\.(\/|\\)/g, 'up_$1');
  } else {
    // If this is an index file or common directory name, use the parent
    // directory name instead, which is probably more descriptive.
    while (COMMON_NAMES.has(name)) {
      entryFilePath = path.dirname(entryFilePath);
      name = path.basename(entryFilePath);
      if (name.startsWith('.')) {
        name = name.replace('.', '');
      }
    }

    return name || 'bundle';
  }
}

function basenameWithoutExtension(file) {
  return path.basename(file, path.extname(file));
}

let rscEntryCache = new WeakMap();
function hasReactServerEntries(bundleGraph) {
  let res = rscEntryCache.get(bundleGraph);
  if (res == null) {
    res = bundleGraph
      .getEntryBundles()
      .some(
        b =>
          b.env.context === 'react-server' &&
          b.getMainEntry()?.meta?.bundleExtension == null,
      );
    rscEntryCache.set(bundleGraph, res);
  }
  return res;
}
