import { join, dirname, sep } from "node:path";
import { readFileSync } from "node:fs";
import memoize, { memoizeClear } from "memoize";
import mergePackages from "./merge-manifests.mjs";

/**
 * return the contents of the package manifest ('package.json' closest to
 * the passed folder (or null if there's no such package.json/ that
 * package.json is invalid).
 *
 * This behavior is consistent with node's lookup mechanism
 *
 * @param {string} pFileDirectory the folder relative to which to find
 *                          the package.json
 * @return {any} the contents of the package.json as a javascript
 *               object or null if the package.json could not be
 *               found or is invalid
 */
const getSingleManifest = memoize((pFileDirectory) => {
  let lReturnValue = null;

  try {
    // find the closest package.json from pFileDirectory
    const lPackageContent = readFileSync(
      join(pFileDirectory, "package.json"),
      "utf8",
    );

    try {
      lReturnValue = JSON.parse(lPackageContent);
    } catch (pError) {
      // left empty on purpose
    }
  } catch (pError) {
    const lNextDirectory = dirname(pFileDirectory);

    if (lNextDirectory !== pFileDirectory) {
      // not yet reached root directory
      lReturnValue = getSingleManifest(lNextDirectory);
    }
  }
  return lReturnValue;
});

function maybeReadPackage(pFileDirectory) {
  let lReturnValue = {};

  try {
    const lPackageContent = readFileSync(
      join(pFileDirectory, "package.json"),
      "utf8",
    );

    try {
      lReturnValue = JSON.parse(lPackageContent);
    } catch (pError) {
      // left empty on purpose
    }
  } catch (pError) {
    // left empty on purpose
  }
  return lReturnValue;
}

function getIntermediatePaths(pFileDirectory, pBaseDirectory) {
  let lReturnValue = [];
  let lIntermediate = pFileDirectory;

  while (
    lIntermediate !== pBaseDirectory &&
    // safety hatch in case pBaseDirectory is either not a part of
    // pFileDirectory or not something uniquely comparable to a
    // dirname
    lIntermediate !== dirname(lIntermediate)
  ) {
    lReturnValue.push(lIntermediate);
    lIntermediate = dirname(lIntermediate);
  }
  lReturnValue.push(pBaseDirectory);
  return lReturnValue;
}

// despite the two parameters there's no resolver function provided
// to memoize. This is deliberate - the pBaseDirectory will typically
// be the same for each call in a typical cruise, so the default
// memoize resolver (the first param) will suffice.
const getCombinedManifests = memoize((pFileDirectory, pBaseDirectory) => {
  // The way this is called, this shouldn't happen. If it is, there's
  // something gone terribly awry
  if (
    !pFileDirectory.startsWith(pBaseDirectory) ||
    pBaseDirectory.endsWith(sep)
  ) {
    throw new Error(
      `Unexpected Error: Unusual baseDir passed to package reading function: '${pBaseDirectory}'\n` +
        `Please file a bug: https://github.com/sverweij/dependency-cruiser/issues/new?template=bug-report.md` +
        `&title=Unexpected Error: Unusual baseDir passed to package reading function: '${pBaseDirectory}'`,
    );
  }

  const lReturnValue = getIntermediatePaths(
    pFileDirectory,
    pBaseDirectory,
  ).reduce(
    (pAll, pCurrent) => mergePackages(pAll, maybeReadPackage(pCurrent)),
    {},
  );

  return Object.keys(lReturnValue).length > 0 ? lReturnValue : null;
});

/**
 * return
 * - the contents of the package manifest ('package.json') closest to the passed
 *   folder (see read-package-deps above) when pCombinedDependencies === false
 * - the 'combined' contents of all manifests between the passed folder
 *   and the 'root' folder (the folder the cruise was run from) in
 *   all other cases
 * @param  {string}  pFileDirectory              the folder relative to which to find
 *                                         the (closest) package.json
 * @param  {string}  pBaseDirectory              the directory to consider as base (or 'root')
 * @param  {Boolean} pCombinedDependencies whether to stop (false) or continue
 *                                         searching until the 'root'
 * @return {any}                           the contents of a package.json as a javascript
 *                                         object or null if a package.json could not be
 *                                        found or is invalid
 */
export function getManifest(
  pFileDirectory,
  pBaseDirectory,
  pCombinedDependencies = false,
) {
  if (pCombinedDependencies) {
    return getCombinedManifests(pFileDirectory, pBaseDirectory);
  } else {
    return getSingleManifest(pFileDirectory);
  }
}

export function clearCache() {
  memoizeClear(getCombinedManifests);
  memoizeClear(getSingleManifest);
}
