"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const url_1 = require("url");
const util_1 = require("util");
const CacheableRequest = require("cacheable-request");
const http = require("http");
const https = require("https");
const lowercaseKeys = require("lowercase-keys");
const toReadableStream = require("to-readable-stream");
const is_1 = require("@sindresorhus/is");
const cacheable_lookup_1 = require("cacheable-lookup");
const errors_1 = require("./errors");
const known_hook_events_1 = require("./known-hook-events");
const dynamic_require_1 = require("./utils/dynamic-require");
const get_body_size_1 = require("./utils/get-body-size");
const is_form_data_1 = require("./utils/is-form-data");
const merge_1 = require("./utils/merge");
const options_to_url_1 = require("./utils/options-to-url");
const supports_brotli_1 = require("./utils/supports-brotli");
const types_1 = require("./types");
const nonEnumerableProperties = [
    'context',
    'body',
    'json',
    'form'
];
const isAgentByProtocol = (agent) => is_1.default.object(agent);
// TODO: `preNormalizeArguments` should merge `options` & `defaults`
exports.preNormalizeArguments = (options, defaults) => {
    var _a, _b, _c, _d, _e, _f;
    // `options.headers`
    if (is_1.default.undefined(options.headers)) {
        options.headers = {};
    }
    else {
        options.headers = lowercaseKeys(options.headers);
    }
    for (const [key, value] of Object.entries(options.headers)) {
        if (is_1.default.null_(value)) {
            throw new TypeError(`Use \`undefined\` instead of \`null\` to delete the \`${key}\` header`);
        }
    }
    // `options.prefixUrl`
    if (is_1.default.urlInstance(options.prefixUrl) || is_1.default.string(options.prefixUrl)) {
        options.prefixUrl = options.prefixUrl.toString();
        if (options.prefixUrl.length !== 0 && !options.prefixUrl.endsWith('/')) {
            options.prefixUrl += '/';
        }
    }
    else {
        options.prefixUrl = defaults ? defaults.prefixUrl : '';
    }
    // `options.hooks`
    if (is_1.default.undefined(options.hooks)) {
        options.hooks = {};
    }
    if (is_1.default.object(options.hooks)) {
        for (const event of known_hook_events_1.default) {
            if (Reflect.has(options.hooks, event)) {
                if (!is_1.default.array(options.hooks[event])) {
                    throw new TypeError(`Parameter \`${event}\` must be an Array, not ${is_1.default(options.hooks[event])}`);
                }
            }
            else {
                options.hooks[event] = [];
            }
        }
    }
    else {
        throw new TypeError(`Parameter \`hooks\` must be an Object, not ${is_1.default(options.hooks)}`);
    }
    if (defaults) {
        for (const event of known_hook_events_1.default) {
            if (!(Reflect.has(options.hooks, event) && is_1.default.undefined(options.hooks[event]))) {
                // @ts-ignore Union type array is not assignable to union array type
                options.hooks[event] = [
                    ...defaults.hooks[event],
                    ...options.hooks[event]
                ];
            }
        }
    }
    // `options.timeout`
    if (is_1.default.number(options.timeout)) {
        options.timeout = { request: options.timeout };
    }
    else if (!is_1.default.object(options.timeout)) {
        options.timeout = {};
    }
    // `options.retry`
    const { retry } = options;
    if (defaults) {
        options.retry = { ...defaults.retry };
    }
    else {
        options.retry = {
            calculateDelay: retryObject => retryObject.computedValue,
            limit: 0,
            methods: [],
            statusCodes: [],
            errorCodes: [],
            maxRetryAfter: undefined
        };
    }
    if (is_1.default.object(retry)) {
        options.retry = {
            ...options.retry,
            ...retry
        };
    }
    else if (is_1.default.number(retry)) {
        options.retry.limit = retry;
    }
    if (options.retry.maxRetryAfter === undefined) {
        options.retry.maxRetryAfter = Math.min(...[options.timeout.request, options.timeout.connect].filter((n) => !is_1.default.nullOrUndefined(n)));
    }
    options.retry.methods = [...new Set(options.retry.methods.map(method => method.toUpperCase()))];
    options.retry.statusCodes = [...new Set(options.retry.statusCodes)];
    options.retry.errorCodes = [...new Set(options.retry.errorCodes)];
    // `options.dnsCache`
    if (options.dnsCache && !(options.dnsCache instanceof cacheable_lookup_1.default)) {
        options.dnsCache = new cacheable_lookup_1.default({ cacheAdapter: options.dnsCache });
    }
    // `options.method`
    if (is_1.default.string(options.method)) {
        options.method = options.method.toUpperCase();
    }
    else {
        options.method = (_b = (_a = defaults) === null || _a === void 0 ? void 0 : _a.method, (_b !== null && _b !== void 0 ? _b : 'GET'));
    }
    // Better memory management, so we don't have to generate a new object every time
    if (options.cache) {
        options.cacheableRequest = new CacheableRequest(
        // @ts-ignore Cannot properly type a function with multiple definitions yet
        (requestOptions, handler) => requestOptions[types_1.requestSymbol](requestOptions, handler), options.cache);
    }
    // `options.cookieJar`
    if (is_1.default.object(options.cookieJar)) {
        let { setCookie, getCookieString } = options.cookieJar;
        // Horrible `tough-cookie` check
        if (setCookie.length === 4 && getCookieString.length === 0) {
            if (!Reflect.has(setCookie, util_1.promisify.custom)) {
                // @ts-ignore TS is dumb - it says `setCookie` is `never`.
                setCookie = util_1.promisify(setCookie.bind(options.cookieJar));
                getCookieString = util_1.promisify(getCookieString.bind(options.cookieJar));
            }
        }
        else if (setCookie.length !== 2) {
            throw new TypeError('`options.cookieJar.setCookie` needs to be an async function with 2 arguments');
        }
        else if (getCookieString.length !== 1) {
            throw new TypeError('`options.cookieJar.getCookieString` needs to be an async function with 1 argument');
        }
        options.cookieJar = { setCookie, getCookieString };
    }
    // `options.encoding`
    if (is_1.default.null_(options.encoding)) {
        throw new TypeError('To get a Buffer, set `options.responseType` to `buffer` instead');
    }
    // `options.maxRedirects`
    if (!Reflect.has(options, 'maxRedirects') && !(defaults && Reflect.has(defaults, 'maxRedirects'))) {
        options.maxRedirects = 0;
    }
    // Merge defaults
    if (defaults) {
        options = merge_1.default({}, defaults, options);
    }
    // `options._pagination`
    if (is_1.default.object(options._pagination)) {
        const { _pagination: pagination } = options;
        if (!is_1.default.function_(pagination.transform)) {
            throw new TypeError('`options._pagination.transform` must be implemented');
        }
        if (!is_1.default.function_(pagination.shouldContinue)) {
            throw new TypeError('`options._pagination.shouldContinue` must be implemented');
        }
        if (!is_1.default.function_(pagination.filter)) {
            throw new TypeError('`options._pagination.filter` must be implemented');
        }
        if (!is_1.default.function_(pagination.paginate)) {
            throw new TypeError('`options._pagination.paginate` must be implemented');
        }
    }
    // Other values
    options.decompress = Boolean(options.decompress);
    options.isStream = Boolean(options.isStream);
    options.throwHttpErrors = Boolean(options.throwHttpErrors);
    options.ignoreInvalidCookies = Boolean(options.ignoreInvalidCookies);
    options.cache = (_c = options.cache, (_c !== null && _c !== void 0 ? _c : false));
    options.responseType = (_d = options.responseType, (_d !== null && _d !== void 0 ? _d : 'text'));
    options.resolveBodyOnly = Boolean(options.resolveBodyOnly);
    options.followRedirect = Boolean(options.followRedirect);
    options.dnsCache = (_e = options.dnsCache, (_e !== null && _e !== void 0 ? _e : false));
    options.useElectronNet = Boolean(options.useElectronNet);
    options.methodRewriting = Boolean(options.methodRewriting);
    options.allowGetBody = Boolean(options.allowGetBody);
    options.context = (_f = options.context, (_f !== null && _f !== void 0 ? _f : {}));
    return options;
};
exports.mergeOptions = (...sources) => {
    let mergedOptions = exports.preNormalizeArguments({});
    // Non enumerable properties shall not be merged
    const properties = {};
    for (const source of sources) {
        mergedOptions = exports.preNormalizeArguments(merge_1.default({}, source), mergedOptions);
        for (const name of nonEnumerableProperties) {
            if (!Reflect.has(source, name)) {
                continue;
            }
            properties[name] = {
                writable: true,
                configurable: true,
                enumerable: false,
                value: source[name]
            };
        }
    }
    Object.defineProperties(mergedOptions, properties);
    return mergedOptions;
};
exports.normalizeArguments = (url, options, defaults) => {
    var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
    // Merge options
    if (typeof url === 'undefined') {
        throw new TypeError('Missing `url` argument');
    }
    const runInitHooks = (hooks, options) => {
        if (hooks && options) {
            for (const hook of hooks) {
                const result = hook(options);
                if (is_1.default.promise(result)) {
                    throw new TypeError('The `init` hook must be a synchronous function');
                }
            }
        }
    };
    const hasUrl = is_1.default.urlInstance(url) || is_1.default.string(url);
    if (hasUrl) {
        if (options) {
            if (Reflect.has(options, 'url')) {
                throw new TypeError('The `url` option cannot be used if the input is a valid URL.');
            }
        }
        else {
            options = {};
        }
        // @ts-ignore URL is not URL
        options.url = url;
        runInitHooks((_a = defaults) === null || _a === void 0 ? void 0 : _a.options.hooks.init, options);
        runInitHooks((_b = options.hooks) === null || _b === void 0 ? void 0 : _b.init, options);
    }
    else if (Reflect.has(url, 'resolve')) {
        throw new Error('The legacy `url.Url` is deprecated. Use `URL` instead.');
    }
    else {
        runInitHooks((_c = defaults) === null || _c === void 0 ? void 0 : _c.options.hooks.init, url);
        runInitHooks((_d = url.hooks) === null || _d === void 0 ? void 0 : _d.init, url);
        if (options) {
            runInitHooks((_e = defaults) === null || _e === void 0 ? void 0 : _e.options.hooks.init, options);
            runInitHooks((_f = options.hooks) === null || _f === void 0 ? void 0 : _f.init, options);
        }
    }
    if (hasUrl) {
        options = exports.mergeOptions((_h = (_g = defaults) === null || _g === void 0 ? void 0 : _g.options, (_h !== null && _h !== void 0 ? _h : {})), (options !== null && options !== void 0 ? options : {}));
    }
    else {
        options = exports.mergeOptions((_k = (_j = defaults) === null || _j === void 0 ? void 0 : _j.options, (_k !== null && _k !== void 0 ? _k : {})), url, (options !== null && options !== void 0 ? options : {}));
    }
    // Normalize URL
    // TODO: drop `optionsToUrl` in Got 12
    if (is_1.default.string(options.url)) {
        options.url = options.prefixUrl + options.url;
        options.url = options.url.replace(/^unix:/, 'http://$&');
        if (options.searchParams || options.search) {
            options.url = options.url.split('?')[0];
        }
        // @ts-ignore URL is not URL
        options.url = options_to_url_1.default({
            origin: options.url,
            ...options
        });
    }
    else if (!is_1.default.urlInstance(options.url)) {
        // @ts-ignore URL is not URL
        options.url = options_to_url_1.default({ origin: options.prefixUrl, ...options });
    }
    const normalizedOptions = options;
    // Make it possible to change `options.prefixUrl`
    let prefixUrl = options.prefixUrl;
    Object.defineProperty(normalizedOptions, 'prefixUrl', {
        set: (value) => {
            if (!normalizedOptions.url.href.startsWith(value)) {
                throw new Error(`Cannot change \`prefixUrl\` from ${prefixUrl} to ${value}: ${normalizedOptions.url.href}`);
            }
            normalizedOptions.url = new url_1.URL(value + normalizedOptions.url.href.slice(prefixUrl.length));
            prefixUrl = value;
        },
        get: () => prefixUrl
    });
    // Make it possible to remove default headers
    for (const [key, value] of Object.entries(normalizedOptions.headers)) {
        if (is_1.default.undefined(value)) {
            // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
            delete normalizedOptions.headers[key];
        }
    }
    return normalizedOptions;
};
const withoutBody = new Set(['HEAD']);
const withoutBodyUnlessSpecified = 'GET';
exports.normalizeRequestArguments = async (options) => {
    var _a, _b, _c;
    options = exports.mergeOptions(options);
    // Serialize body
    const { headers } = options;
    const hasNoContentType = is_1.default.undefined(headers['content-type']);
    {
        // TODO: these checks should be moved to `preNormalizeArguments`
        const isForm = !is_1.default.undefined(options.form);
        const isJson = !is_1.default.undefined(options.json);
        const isBody = !is_1.default.undefined(options.body);
        if ((isBody || isForm || isJson) && withoutBody.has(options.method)) {
            throw new TypeError(`The \`${options.method}\` method cannot be used with a body`);
        }
        if (!options.allowGetBody && (isBody || isForm || isJson) && withoutBodyUnlessSpecified === options.method) {
            throw new TypeError(`The \`${options.method}\` method cannot be used with a body`);
        }
        if ([isBody, isForm, isJson].filter(isTrue => isTrue).length > 1) {
            throw new TypeError('The `body`, `json` and `form` options are mutually exclusive');
        }
        if (isBody &&
            !is_1.default.nodeStream(options.body) &&
            !is_1.default.string(options.body) &&
            !is_1.default.buffer(options.body) &&
            !(is_1.default.object(options.body) && is_form_data_1.default(options.body))) {
            throw new TypeError('The `body` option must be a stream.Readable, string or Buffer');
        }
        if (isForm && !is_1.default.object(options.form)) {
            throw new TypeError('The `form` option must be an Object');
        }
    }
    if (options.body) {
        // Special case for https://github.com/form-data/form-data
        if (is_1.default.object(options.body) && is_form_data_1.default(options.body) && hasNoContentType) {
            headers['content-type'] = `multipart/form-data; boundary=${options.body.getBoundary()}`;
        }
    }
    else if (options.form) {
        if (hasNoContentType) {
            headers['content-type'] = 'application/x-www-form-urlencoded';
        }
        options.body = (new url_1.URLSearchParams(options.form)).toString();
    }
    else if (options.json) {
        if (hasNoContentType) {
            headers['content-type'] = 'application/json';
        }
        options.body = JSON.stringify(options.json);
    }
    const uploadBodySize = await get_body_size_1.default(options);
    if (!is_1.default.nodeStream(options.body)) {
        options.body = toReadableStream(options.body);
    }
    // See https://tools.ietf.org/html/rfc7230#section-3.3.2
    // A user agent SHOULD send a Content-Length in a request message when
    // no Transfer-Encoding is sent and the request method defines a meaning
    // for an enclosed payload body.  For example, a Content-Length header
    // field is normally sent in a POST request even when the value is 0
    // (indicating an empty payload body).  A user agent SHOULD NOT send a
    // Content-Length header field when the request message does not contain
    // a payload body and the method semantics do not anticipate such a
    // body.
    if (is_1.default.undefined(headers['content-length']) && is_1.default.undefined(headers['transfer-encoding'])) {
        if ((options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH' || options.method === 'DELETE' || (options.allowGetBody && options.method === 'GET')) &&
            !is_1.default.undefined(uploadBodySize)) {
            // @ts-ignore We assign if it is undefined, so this IS correct
            headers['content-length'] = String(uploadBodySize);
        }
    }
    if (!options.isStream && options.responseType === 'json' && is_1.default.undefined(headers.accept)) {
        headers.accept = 'application/json';
    }
    if (options.decompress && is_1.default.undefined(headers['accept-encoding'])) {
        headers['accept-encoding'] = supports_brotli_1.default ? 'gzip, deflate, br' : 'gzip, deflate';
    }
    // Validate URL
    if (options.url.protocol !== 'http:' && options.url.protocol !== 'https:') {
        throw new errors_1.UnsupportedProtocolError(options);
    }
    decodeURI(options.url.toString());
    // Normalize request function
    if (is_1.default.function_(options.request)) {
        options[types_1.requestSymbol] = options.request;
        delete options.request;
    }
    else {
        options[types_1.requestSymbol] = options.url.protocol === 'https:' ? https.request : http.request;
    }
    // UNIX sockets
    if (options.url.hostname === 'unix') {
        const matches = /(?<socketPath>.+?):(?<path>.+)/.exec(options.url.pathname);
        if ((_a = matches) === null || _a === void 0 ? void 0 : _a.groups) {
            const { socketPath, path } = matches.groups;
            options = {
                ...options,
                socketPath,
                path,
                host: ''
            };
        }
    }
    if (isAgentByProtocol(options.agent)) {
        options.agent = (_b = options.agent[options.url.protocol.slice(0, -1)], (_b !== null && _b !== void 0 ? _b : options.agent));
    }
    if (options.dnsCache) {
        options.lookup = options.dnsCache.lookup;
    }
    /* istanbul ignore next: electron.net is broken */
    // No point in typing process.versions correctly, as
    // `process.version.electron` is used only once, right here.
    if (options.useElectronNet && process.versions.electron) {
        const electron = dynamic_require_1.default(module, 'electron'); // Trick webpack
        options.request = util_1.deprecate((_c = electron.net.request, (_c !== null && _c !== void 0 ? _c : electron.remote.net.request)), 'Electron support has been deprecated and will be removed in Got 11.\n' +
            'See https://github.com/sindresorhus/got/issues/899 for further information.', 'GOT_ELECTRON');
    }
    // Got's `timeout` is an object, http's `timeout` is a number, so they're not compatible.
    delete options.timeout;
    // Set cookies
    if (options.cookieJar) {
        const cookieString = await options.cookieJar.getCookieString(options.url.toString());
        if (is_1.default.nonEmptyString(cookieString)) {
            options.headers.cookie = cookieString;
        }
        else {
            delete options.headers.cookie;
        }
    }
    // `http-cache-semantics` checks this
    delete options.url;
    return options;
};
