/**
 * @license
 * Copyright 2018 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import log from 'lighthouse-logger';

import {ArbitraryEqualityMap} from '../lib/arbitrary-equality-map.js';
import {isUnderTest} from '../lib/lh-env.js';

/**
 * Decorate computableArtifact with a caching `request()` method which will
 * automatically call `computableArtifact.compute_()` under the hood.
 * @template {{name: string, compute_(dependencies: unknown, context: LH.Artifacts.ComputedContext): Promise<unknown>}} C
 * @template {Array<keyof LH.Util.FirstParamType<C['compute_']>>} K
 * @param {C} computableArtifact
 * @param {(K & ([keyof LH.Util.FirstParamType<C['compute_']>] extends [K[number]] ? unknown : never)) | null} keys
 *        List of properties of `dependencies` used by `compute_`; other properties are filtered out.
 *        Use `null` to allow all properties. Ensures that only required properties are used for caching result.
 *        For optional properties of `dependencies`, undefined cannot be used and if found is treated as an error.
 *        This is to guard against developer mistakes. For optional properties, make it nullable instead.
 */
function makeComputedArtifact(computableArtifact, keys) {
  // tsc (3.1) has more difficulty with template inter-references in jsdoc, so
  // give types to params and return value the long way, essentially recreating
  // polymorphic-this behavior for C.
  /**
   * Return an automatically cached result from the computed artifact.
   * @param {LH.Util.FirstParamType<C['compute_']>} dependencies
   * @param {LH.Artifacts.ComputedContext} context
   * @return {ReturnType<C['compute_']>}
   */
  const request = (dependencies, context) => {
    const computedName = computableArtifact.name;

    // Guard against missing properties. Optional properties must be passed as null - if missing or
    // undefined, throw an error.
    for (const key of keys || []) {
      if (dependencies && typeof dependencies === 'object' && dependencies[key] === undefined) {
        // eslint-disable-next-line max-len
        const err = new Error(`missing required key "${String(key)}" for computed artifact ${computableArtifact.name}`);
        if (isUnderTest) {
          throw err;
        } else {
          // For now, simply log in production.
          log.error(`lh:computed:${computedName}`, err);
        }
      }
    }

    const pickedDependencies = keys ?
      Object.fromEntries(keys.map(key => [key, dependencies[key]])) :
      dependencies;

    // NOTE: break immutability solely for this caching-controller function.
    const computedCache = /** @type {Map<string, ArbitraryEqualityMap>} */ (context.computedCache);

    const cache = computedCache.get(computedName) || new ArbitraryEqualityMap();
    computedCache.set(computedName, cache);

    /** @type {ReturnType<C['compute_']>|undefined} */
    const computed = cache.get(pickedDependencies);
    if (computed) {
      return computed;
    }

    const status = {msg: `Computing artifact: ${computedName}`, id: `lh:computed:${computedName}`};
    log.time(status, 'verbose');

    const artifactPromise = /** @type {ReturnType<C['compute_']>} */
        (computableArtifact.compute_(pickedDependencies, context));
    cache.set(pickedDependencies, artifactPromise);

    artifactPromise.then(() => log.timeEnd(status)).catch(() => log.timeEnd(status));

    return artifactPromise;
  };

  return Object.assign(computableArtifact, {request});
}

export {makeComputedArtifact};
