"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const logger_1 = require("@pnpm/logger");
const getRegistryName = require("encode-registry");
const loadJsonFile = require("load-json-file");
const pLimit = require("p-limit");
const path = require("path");
const url = require("url");
const writeJsonFile = require("write-json-file");
const pickPackageFromMeta_1 = require("./pickPackageFromMeta");
const toRaw_1 = require("./toRaw");
class PnpmError extends Error {
    constructor(code, message) {
        super(message);
        this.code = code;
    }
}
// prevents simultainous operations on the meta.json
// otherwise it would cause EPERM exceptions
const metafileOperationLimits = {};
exports.default = (ctx, spec, opts) => __awaiter(this, void 0, void 0, function* () {
    opts = opts || {};
    if (ctx.metaCache.has(spec.name)) {
        const meta = ctx.metaCache.get(spec.name);
        return {
            meta,
            pickedPackage: pickPackageFromMeta_1.default(spec, opts.preferredVersionSelector, meta),
        };
    }
    const registryName = getRegistryName(opts.registry);
    const pkgMirror = path.join(ctx.storePath, registryName, spec.name);
    const limit = metafileOperationLimits[pkgMirror] = metafileOperationLimits[pkgMirror] || pLimit(1);
    let metaCachedInStore;
    if (ctx.offline || ctx.preferOffline) {
        metaCachedInStore = yield limit(() => loadMeta(pkgMirror, ctx.metaFileName));
        if (ctx.offline) {
            if (metaCachedInStore)
                return {
                    meta: metaCachedInStore,
                    pickedPackage: pickPackageFromMeta_1.default(spec, opts.preferredVersionSelector, metaCachedInStore),
                };
            throw new PnpmError('NO_OFFLINE_META', `Failed to resolve ${toRaw_1.default(spec)} in package mirror ${pkgMirror}`);
        }
        if (metaCachedInStore) {
            const pickedPackage = pickPackageFromMeta_1.default(spec, opts.preferredVersionSelector, metaCachedInStore);
            if (pickedPackage) {
                return {
                    meta: metaCachedInStore,
                    pickedPackage,
                };
            }
        }
    }
    if (spec.type === 'version') {
        metaCachedInStore = metaCachedInStore || (yield limit(() => loadMeta(pkgMirror, ctx.metaFileName)));
        // use the cached meta only if it has the required package version
        // otherwise it is probably out of date
        if (metaCachedInStore && metaCachedInStore.versions && metaCachedInStore.versions[spec.fetchSpec]) {
            return {
                meta: metaCachedInStore,
                pickedPackage: metaCachedInStore.versions[spec.fetchSpec],
            };
        }
    }
    try {
        const meta = yield fromRegistry(ctx.fetch, spec.name, opts.registry, opts.auth);
        meta.cachedAt = Date.now();
        // only save meta to cache, when it is fresh
        ctx.metaCache.set(spec.name, meta);
        if (!opts.dryRun) {
            limit(() => saveMeta(pkgMirror, meta, ctx.metaFileName));
        }
        return {
            meta,
            pickedPackage: pickPackageFromMeta_1.default(spec, opts.preferredVersionSelector, meta),
        };
    }
    catch (err) {
        const meta = yield loadMeta(pkgMirror, ctx.metaFileName); // TODO: add test for this usecase
        if (!meta)
            throw err;
        logger_1.default.error(err);
        logger_1.default.info(`Using cached meta from ${pkgMirror}`);
        return {
            meta,
            pickedPackage: pickPackageFromMeta_1.default(spec, opts.preferredVersionSelector, meta),
        };
    }
});
function fromRegistry(fetch, pkgName, registry, auth) {
    return __awaiter(this, void 0, void 0, function* () {
        const uri = toUri(pkgName, registry);
        const res = yield fetch(uri, { auth });
        if (res.status > 400) {
            const err = new Error(`${res.status} ${res.statusText}: ${pkgName} (via ${uri})`);
            // tslint:disable
            err['code'] = `ERR_PNPM_REGISTRY_META_RESPONSE_${res.status}`;
            err['uri'] = uri;
            err['response'] = res;
            err['package'] = pkgName;
            // tslint:enable
            throw err;
        }
        return yield res.json();
    });
}
function loadMeta(pkgMirror, metaFileName) {
    return __awaiter(this, void 0, void 0, function* () {
        try {
            return yield loadJsonFile(path.join(pkgMirror, metaFileName));
        }
        catch (err) {
            return null;
        }
    });
}
function saveMeta(pkgMirror, meta, metaFileName) {
    return writeJsonFile(path.join(pkgMirror, metaFileName), meta);
}
function toUri(pkgName, registry) {
    let encodedName;
    if (pkgName[0] === '@') {
        encodedName = `@${encodeURIComponent(pkgName.substr(1))}`;
    }
    else {
        encodedName = encodeURIComponent(pkgName);
    }
    return url.resolve(registry, encodedName);
}
//# sourceMappingURL=pickPackage.js.map