"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ServiceName = exports.KodoHttpClient = void 0;
const async_lock_1 = __importDefault(require("async-lock"));
const urllib_1 = require("urllib");
const url_1 = require("url");
const region_1 = require("./region");
const region_service_1 = require("./region_service");
const kodo_auth_1 = require("./kodo-auth");
const stream_buffers_1 = require("stream-buffers");
class KodoHttpClient {
    constructor(sharedOptions) {
        this.sharedOptions = sharedOptions;
        this.regionsCache = {};
        this.regionsCacheLock = new async_lock_1.default();
        this.regionService = new region_service_1.RegionService(sharedOptions);
    }
    call(options) {
        return new Promise((resolve, reject) => {
            this.getServiceUrls(options.serviceName, options.bucketName, options.s3RegionId).then((urls) => {
                this.callForOneUrl(urls, options, resolve, reject);
            }).catch(reject);
        });
    }
    clearCache() {
        Object.keys(this.regionsCache).forEach((key) => { delete this.regionsCache[key]; });
        this.regionService.clearCache();
    }
    callForOneUrl(urls, options, resolve, reject) {
        var _a, _b;
        const urlString = urls.shift();
        if (!urlString) {
            reject(new Error('urls is empty'));
            return;
        }
        const url = this.makeUrl(urlString, options);
        const headers = {
            'authorization': this.makeAuthorization(url, options),
            'user-agent': this.sharedOptions.userAgent,
        };
        if (options.contentType !== 'json') {
            headers['content-type'] = options.contentType;
        }
        if (options.headers) {
            for (const [headerName, headerValue] of Object.entries(options.headers)) {
                headers[headerName] = headerValue;
            }
        }
        let requestInfo = undefined;
        const beginTime = new Date().getTime();
        const requestOption = {
            method: options.method,
            dataType: options.dataType,
            contentType: options.contentType,
            headers: headers,
            timeout: this.sharedOptions.timeout,
            followRedirect: true,
            retry: this.sharedOptions.retry,
            retryDelay: this.sharedOptions.retryDelay,
            isRetry: this.isRetry,
            beforeRequest: (info) => {
                requestInfo = {
                    url: url,
                    method: info.method,
                    headers: info.headers,
                };
                if (this.sharedOptions.requestCallback) {
                    this.sharedOptions.requestCallback(requestInfo);
                }
            },
        };
        const data = (_a = options.data) !== null && _a !== void 0 ? _a : (_b = options.form) === null || _b === void 0 ? void 0 : _b.getBuffer();
        let callbackError = undefined;
        if (data) {
            if (options.uploadProgress) {
                const stream = new stream_buffers_1.ReadableStreamBuffer({ initialSize: data.length, chunkSize: 1 << 20 });
                stream.put(data);
                stream.stop();
                let uploaded = 0;
                let total = data.length;
                stream.on('data', (chunk) => {
                    uploaded += chunk.length;
                    try {
                        options.uploadProgress(uploaded, total);
                    }
                    catch (err) {
                        if (!stream.destroyed) {
                            stream.destroy(err);
                        }
                        callbackError = err;
                        reject(err);
                    }
                });
                if (options.uploadThrottle) {
                    requestOption.stream = stream.pipe(options.uploadThrottle);
                }
                else {
                    requestOption.stream = stream;
                }
            }
            else if (options.uploadThrottle) {
                const stream = new stream_buffers_1.ReadableStreamBuffer({ initialSize: data.length, chunkSize: 1 << 20 });
                stream.put(data);
                stream.stop();
                requestOption.stream = stream.pipe(options.uploadThrottle);
            }
            else {
                requestOption.data = data;
            }
        }
        KodoHttpClient.httpClient.request(url, requestOption).then((response) => {
            const responseInfo = {
                request: requestInfo,
                statusCode: response.status,
                headers: response.headers,
                data: response.data,
                interval: new Date().getTime() - beginTime,
            };
            try {
                if (callbackError) {
                    return;
                }
                else if (response.status >= 200 && response.status < 400) {
                    resolve(response);
                }
                else if (urls.length > 0) {
                    this.callForOneUrl(urls, options, resolve, reject);
                }
                else if (response.data.error) {
                    const error = new Error(response.data.error);
                    responseInfo.error = error;
                    reject(error);
                }
                else {
                    try {
                        const data = JSON.parse(response.data);
                        if (data.error) {
                            const error = new Error(data.error);
                            responseInfo.error = error;
                            reject(error);
                        }
                        else {
                            const error = new Error(response.res.statusMessage);
                            responseInfo.error = error;
                            reject(error);
                        }
                    }
                    catch (_a) {
                        const error = new Error(response.res.statusMessage);
                        responseInfo.error = error;
                        reject(error);
                    }
                }
            }
            finally {
                if (this.sharedOptions.responseCallback) {
                    this.sharedOptions.responseCallback(responseInfo);
                }
            }
        }).catch((err) => {
            const responseInfo = {
                request: requestInfo,
                interval: new Date().getTime() - beginTime,
                error: err,
            };
            if (this.sharedOptions.responseCallback) {
                this.sharedOptions.responseCallback(responseInfo);
            }
            if (callbackError) {
                return;
            }
            else if (urls.length > 0) {
                this.callForOneUrl(urls, options, resolve, reject);
            }
            else {
                reject(err);
            }
        });
    }
    isRetry(response) {
        const dontRetryStatusCodes = [501, 579, 599, 608, 612, 614, 616,
            618, 630, 631, 632, 640, 701];
        return !response.headers['x-reqid'] ||
            response.status >= 500 && !dontRetryStatusCodes.find((status) => status === response.status);
    }
    makeUrl(base, options) {
        const url = new url_1.URL(base);
        if (options.path) {
            url.pathname = options.path;
        }
        let protocol = this.sharedOptions.protocol;
        if (protocol) {
            switch (protocol) {
                case "http":
                    url.protocol = "http";
                    break;
                case "https":
                    url.protocol = "https";
                    break;
            }
        }
        if (options.query) {
            options.query.forEach((value, name) => {
                url.searchParams.append(name, value);
            });
        }
        return url.toString();
    }
    makeAuthorization(url, options) {
        var _a;
        let data = undefined;
        if (options.data) {
            if (options.dataType === 'json') {
                data = JSON.stringify(options.data);
            }
            data = options.data.toString();
        }
        return kodo_auth_1.generateAccessTokenV2(this.sharedOptions.accessKey, this.sharedOptions.secretKey, url.toString(), (_a = options.method) !== null && _a !== void 0 ? _a : 'GET', options.contentType, data);
    }
    getServiceUrls(serviceName, bucketName, s3RegionId) {
        return new Promise((resolve, reject) => {
            let key;
            if (s3RegionId) {
                key = `${this.sharedOptions.ucUrl}/${s3RegionId}`;
            }
            else {
                key = `${this.sharedOptions.ucUrl}/${this.sharedOptions.accessKey}/${bucketName}`;
            }
            if (this.regionsCache[key]) {
                resolve(this.getUrlsFromRegion(serviceName, this.regionsCache[key]));
                return;
            }
            this.regionsCacheLock.acquire(key, () => {
                if (this.regionsCache[key]) {
                    return Promise.resolve(this.regionsCache[key]);
                }
                else if (bucketName) {
                    return region_1.Region.query({
                        bucketName,
                        accessKey: this.sharedOptions.accessKey,
                        ucUrl: this.sharedOptions.ucUrl,
                    });
                }
                else {
                    return new Promise((resolve, reject) => {
                        this.regionService.getAllRegions().then((regions) => {
                            if (regions.length == 0) {
                                reject(Error('regions is empty'));
                                return;
                            }
                            if (s3RegionId) {
                                const region = regions.find((region) => region.s3Id === s3RegionId);
                                if (!region) {
                                    reject(new Error(`Cannot find region of ${s3RegionId}`));
                                    return;
                                }
                                resolve(region);
                            }
                            else {
                                resolve(regions[0]);
                            }
                        }).catch(reject);
                    });
                }
            }).then((region) => {
                this.regionsCache[key] = region;
                resolve(this.getUrlsFromRegion(serviceName, region));
            }).catch(reject);
        });
    }
    getUrlsFromRegion(serviceName, region) {
        switch (serviceName) {
            case ServiceName.Up:
                return [...region.upUrls];
            case ServiceName.Uc:
                return [...region.ucUrls];
            case ServiceName.Rs:
                return [...region.rsUrls];
            case ServiceName.Rsf:
                return [...region.rsfUrls];
            case ServiceName.Api:
                return [...region.apiUrls];
            case ServiceName.S3:
                return [...region.s3Urls];
            case ServiceName.Qcdn:
                return ['https://api.qiniu.com'];
            case ServiceName.Portal:
                return ['https://portal.qiniu.com'];
        }
    }
}
exports.KodoHttpClient = KodoHttpClient;
KodoHttpClient.httpClient = new urllib_1.HttpClient2();
var ServiceName;
(function (ServiceName) {
    ServiceName[ServiceName["Up"] = 0] = "Up";
    ServiceName[ServiceName["Uc"] = 1] = "Uc";
    ServiceName[ServiceName["Rs"] = 2] = "Rs";
    ServiceName[ServiceName["Rsf"] = 3] = "Rsf";
    ServiceName[ServiceName["Api"] = 4] = "Api";
    ServiceName[ServiceName["S3"] = 5] = "S3";
    ServiceName[ServiceName["Qcdn"] = 6] = "Qcdn";
    ServiceName[ServiceName["Portal"] = 7] = "Portal";
})(ServiceName = exports.ServiceName || (exports.ServiceName = {}));
//# sourceMappingURL=kodo-http-client.js.map