import { logger } from '../../../../../../logger';
import type {
  GetPkgReleasesConfig,
  ReleaseResult,
} from '../../../../../datasource';
import { getPkgReleases } from '../../../../../datasource';
import { api as semver } from '../../../../../versioning/npm';

const pkgCache = new Map<string, Promise<ReleaseResult | null>>();

function getPkgReleasesCached(
  packageName: string,
): Promise<ReleaseResult | null> {
  let cachedResult = pkgCache.get(packageName);
  if (!cachedResult) {
    const lookupConfig: GetPkgReleasesConfig = {
      datasource: 'npm',
      packageName,
    };
    cachedResult = getPkgReleases(lookupConfig);
    pkgCache.set(packageName, cachedResult);
  }
  return cachedResult;
}

/**
 * Finds the first stable version of parentName after parentStartingVersion which either:
 * - depends on targetDepName@targetVersion or a range which it satisfies, OR
 * - removes the dependency targetDepName altogether, OR
 * - depends on any version of targetDepName higher than targetVersion
 */
export async function findFirstParentVersion(
  parentName: string,
  parentStartingVersion: string,
  targetDepName: string,
  targetVersion: string,
): Promise<string | null> {
  /* v8 ignore start -- needs test */
  if (!semver.isVersion(parentStartingVersion)) {
    logger.debug('parentStartingVersion is not a version - cannot remediate');
    return null;
  } /* v8 ignore stop -- needs test */
  logger.debug(
    `Finding first version of ${parentName} starting with ${parentStartingVersion} which supports >= ${targetDepName}@${targetVersion}`,
  );
  try {
    const targetDep = await getPkgReleasesCached(targetDepName);
    /* v8 ignore start -- needs test */
    if (!targetDep) {
      logger.info(
        { targetDepName },
        'Could not look up target dependency for remediation',
      );
      return null;
    } /* v8 ignore stop -- needs test */
    const targetVersions = targetDep.releases
      .map((release) => release.version)
      .filter(
        (version) =>
          semver.isVersion(version) &&
          semver.isStable(version) &&
          (version === targetVersion ||
            semver.isGreaterThan(version, targetVersion)),
      );
    const parentDep = await getPkgReleasesCached(parentName);
    /* v8 ignore start -- needs test */
    if (!parentDep) {
      logger.info(
        { parentName },
        'Could not look up parent dependency for remediation',
      );
      return null;
    } /* v8 ignore stop -- needs test */
    const parentVersions = parentDep.releases
      .map((release) => release.version)
      .filter(
        (version) =>
          semver.isVersion(version) &&
          semver.isStable(version) &&
          (version === parentStartingVersion ||
            semver.isGreaterThan(version, parentStartingVersion)),
      )
      .sort((v1, v2) => semver.sortVersions(v1, v2));
    // iterate through parentVersions in sorted order
    for (const parentVersion of parentVersions) {
      const constraint = parentDep.releases.find(
        (release) => release.version === parentVersion,
      )?.dependencies?.[targetDepName];
      if (!constraint) {
        logger.debug(
          `${targetDepName} has been removed from ${parentName}@${parentVersion}`,
        );
        return parentVersion;
      }
      if (semver.matches(targetVersion, constraint)) {
        // could be version or range
        logger.debug(
          `${targetDepName} needs ${parentName}@${parentVersion} which uses constraint "${constraint}" in order to update to ${targetVersion}`,
        );
        return parentVersion;
      }
      if (semver.isVersion(constraint)) {
        if (semver.isGreaterThan(constraint, targetVersion)) {
          // it's not the version we were after - the parent skipped to a higher version
          logger.debug(
            `${targetDepName} needs ${parentName}@${parentVersion} which uses version "${constraint}" in order to update to greater than ${targetVersion}`,
          );
          return parentVersion;
        }
      } else if (
        // check the range against all versions
        targetVersions.some((version) => semver.matches(version, constraint))
      ) {
        // the constraint didn't match the version we wanted, but it matches one of the versions higher
        logger.debug(
          `${targetDepName} needs ${parentName}@${parentVersion} which uses constraint "${constraint}" in order to update to greater than ${targetVersion}`,
        );
        return parentVersion;
      }
    }
    /* v8 ignore start -- needs test */
  } catch (err) {
    logger.warn(
      { parentName, parentStartingVersion, targetDepName, targetVersion, err },
      'findFirstParentVersion error',
    );
    return null;
  } /* v8 ignore stop -- needs test */
  logger.debug(`Could not find a matching version`);
  return null;
}
