import zlib from 'node:zlib';
import { buffer as stream_to_buffer, text as stream_to_text } from 'node:stream/consumers';
import { pipeline, isReadable } from 'node:stream';
import { noop } from "./prototype.js";
import { inspect, assert, delay, map_values, unique, timeout, check, colored, encode, TimeoutError } from "./utils.js";
import { drop_request_headers } from "./net.common.js";
export * from "./net.common.js";
export var MyProxy;
(function (MyProxy) {
    MyProxy["socks5"] = "http://127.0.0.1:10080";
    MyProxy["whistle"] = "http://localhost:8899";
    MyProxy["work"] = "http://localhost:10090";
    /** 需要先启动 server.start_tunnel_server('ddb.test.proxy') */
    MyProxy["test"] = "http://localhost:10091";
})(MyProxy || (MyProxy = {}));
export const byproxy = { proxy: true };
// ------------------------------------ fetch, request
export const cookies = {
    store: null,
    jar: null,
    async init() {
        if (this.jar)
            return;
        const { MemoryCookieStore, CookieJar } = await import('tough-cookie');
        this.jar = new CookieJar(this.store = new MemoryCookieStore());
    },
};
let undici;
let dispatchers = {};
async function request_retry(url, options, _timeout, retries = 0, count = 0, print) {
    try {
        if (_timeout > 0) {
            // 设置给 undici 设置 timeout, signal 不一定管用，还是得自己兜底
            options.signal = AbortSignal.timeout(_timeout);
            return await timeout(_timeout + 300, // 为 undici 兜底
            undici.request(url, options), undefined, print.timeout && count >= retries); // 只打印最后一次超时的错误，避免太多冗余输出
        }
        else
            return await undici.request(url, options);
    }
    catch (error) {
        if (error.name !== 'TimeoutError')
            throw error;
        // 重试次数内
        if (count < retries) {
            const duration = 2 ** count;
            if (print.retry)
                console.log(`等待 ${duration} 秒后重试请求 (已尝试 ${count + 1} 次) ··`.yellow + '  ' +
                    url.toString().blue.underline);
            await delay(1000 * duration);
            return request_retry(url, options, _timeout, retries, count + 1, print);
        }
        const seconds = _timeout / 1000;
        throw new TimeoutError(`请求超过 ${seconds.toFixed(seconds < 1 ? 1 : 0)} 秒等待时间: ${url.toString()}`);
    }
}
export class StatusCodeError extends Error {
    name = 'StatusCodeError';
    constructor(status, url) {
        super(`状态码 ${status}: ${url}`);
        Object.defineProperties(this, {
            status: { value: status }
        });
    }
}
export async function request(url, options = {}) {
    undici ??= (await import('undici')).default;
    const { Cookie } = await import('tough-cookie');
    await cookies.init();
    const { queries, headers: _headers, body, type = 'application/json', timeout = 5 * 1000, auth, cookies: _cookies, raw = false, full = false, redirect = 'follow', decode = true, print = {
        timeout: true,
        retry: true
    } } = options;
    let { method, retries, encoding, proxy, } = options;
    url = new URL(url);
    if (queries)
        for (const key in queries) {
            let value = queries[key];
            if (typeof value === 'boolean')
                value = value ? '1' : '0';
            url.searchParams.append(key, value);
        }
    const urlstr = url.toString();
    if (body !== undefined && !method)
        method = 'POST';
    if (retries === true)
        retries = 2;
    // --- headers, http/2 开始都用小写的 headers
    let headers = {
        'accept-language': 'zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7,ja-JP;q=0.6,ja;q=0.5',
        'accept-encoding': 'gzip, deflate, br',
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36',
        'sec-ch-ua-platform': '"Windows"',
        'sec-ch-ua-platform-version': '"19.0.0"',
    };
    // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest_API/Using_FormData_Objects
    // When using FormData to submit POST requests using XMLHttpRequest or the Fetch API with the multipart/form-data content type (e.g., when uploading files and blobs to the server), 
    // do not explicitly set the Content-Type header on the request. 
    // Doing so will prevent the browser from being able to set the Content-Type header with the boundary expression it will use to delimit form fields in the request body.
    if (body !== undefined && type !== 'multipart/form-data')
        headers['content-type'] = type;
    if (auth)
        headers.authorization = auth.type === 'basic' ?
            `Basic ${`${auth.username}:${auth.password}`.to_base64()}`
            :
                `Bearer ${auth.token}`;
    const request_cookies = unique([
        ...cookies.jar.getCookiesSync(urlstr),
        ..._cookies ?
            Object.entries(_cookies).map(([key, value]) => new Cookie({ key, value }))
            :
                []
    ], 'key');
    if (request_cookies.length)
        headers.cookie = request_cookies.map(cookie => cookie.cookieString())
            .join('; ');
    if (_headers) {
        check(Object.getPrototypeOf(_headers)?.constructor.name !== 'Headers');
        for (const key in _headers)
            // 可能在 http/2 的 response 中会有这样开头的保留 headers, 在透传时忽略比较好
            if (!key.startsWith(':') && !key.startsWith('sec-') && !drop_request_headers.has(key)) {
                check(key === key.toLowerCase(), `传入 request 的 headers 参数中 key 应该都是小写的，实际为 ${key}`);
                headers[key] = _headers[key];
            }
    }
    if (proxy === true)
        proxy = MyProxy.socks5;
    let undici_options = {
        ...method ? { method } : {},
        dispatcher: get_dispatcher(proxy, redirect),
        // 下面这些 timeout 都不是总的时间
        headersTimeout: timeout,
        // 从收完 headers 开始算
        bodyTimeout: timeout,
        // @ts-ignore 没有类型声明，实际可用
        connectTimeout: timeout,
        headers,
        body: get_request_body(body, type)
    };
    let response;
    try {
        const { statusCode: status, headers: _headers, body: _body } = await request_retry(url, undici_options, timeout, retries, 0, print);
        // 处理 cookie，自动保存到 cookie jar
        let _cookies = _headers['set-cookie'] || [];
        if (typeof _cookies === 'string')
            _cookies = [_cookies];
        _cookies.map(cookie => cookies.jar.setCookieSync(cookie, urlstr, { ignoreError: true }));
        const headers = map_values(_headers, value => Array.isArray(value) ? value.join(', ') : (value || ''));
        // UndiciResponse.body 没有自动根据 content-encoding 来解码，这里手动处理并替换 body 为解码后的 stream
        let body = _body;
        const content_encoding = headers['content-encoding']?.trim().toLowerCase();
        if (decode && content_encoding) {
            assert(!content_encoding.includes(','));
            switch (content_encoding) {
                case 'gzip':
                case 'x-gzip':
                    body = pipeline(_body, zlib.createGunzip({
                        // Be less strict when decoding compressed responses, since sometimes
                        // servers send slightly invalid responses that are still accepted
                        // by common browsers.
                        // Always using Z_SYNC_FLUSH is what cURL does.
                        flush: zlib.constants.Z_SYNC_FLUSH,
                        finishFlush: zlib.constants.Z_SYNC_FLUSH
                    }), noop);
                    break;
                case 'deflate':
                    body = pipeline(_body, zlib.createInflate(), noop);
                    break;
                case 'br':
                    body = pipeline(_body, zlib.createBrotliDecompress(), noop);
                    break;
                default:
                    throw new Error(`不支持 content-encoding: ${content_encoding.quote()} 的 http 请求`);
            }
        }
        response = { status, headers, body };
        if (!((200 <= status && status <= 299) || status === 304 || (redirect === 'manual' && 300 <= status && status < 400)))
            throw new StatusCodeError(status, url.toString());
    }
    catch (error) {
        response ??= error.response;
        throw Object.defineProperties(error, {
            url: { value: url },
            options: { value: options },
            ...response ? {
                response: {
                    value: {
                        status: response.status,
                        headers: response.headers,
                        body: decode
                            ? await stream_to_text(response.body)
                            : await stream_to_buffer(response.body),
                    }
                }
            } : {},
            [inspect.custom]: {
                value(depth, options, inspect) {
                    const { colors } = options;
                    const c = (str, color) => colored(str, color, colors);
                    let s = '\n' +
                        `${c(method || 'GET', 'red')} ${c(c(urlstr, 'blue'), 'underline')}\n`;
                    if (queries && Object.keys(queries).length)
                        s += c('请求参数:\n', 'yellow') +
                            inspect(queries, options).ensure_end();
                    if (body !== undefined)
                        s += c('请求体:\n', 'yellow') +
                            inspect(body, options).ensure_end();
                    if (response) {
                        s += c('响应头:\n', 'yellow');
                        for (const key in response.headers) {
                            const value = response.headers[key];
                            s += `${key}: ${value}\n`;
                        }
                        if (error.response.body)
                            s += c('响应体:\n', 'yellow') +
                                error.response.body.toString().ensure_end();
                    }
                    s += inspect(error, { ...options, customInspect: false });
                    return s;
                }
            }
        });
    }
    if (raw)
        return response;
    const body_ = await get_response_body(response.body, encoding, response.headers);
    return full ?
        { status: response.status, headers: response.headers, body: body_ }
        :
            body_;
}
function get_dispatcher(proxy, redirect) {
    const key = `${proxy || 'direct'}.${redirect}`;
    let dispatcher = dispatchers[key];
    if (dispatcher)
        return dispatcher;
    dispatcher = proxy ?
        // @ts-ignore
        new undici.ProxyAgent({ uri: proxy })
        :
            new undici.Agent({ allowH2: true });
    if (redirect === 'follow')
        dispatcher = dispatcher.compose(
        // todo: 强制手动处理重定向，来正确处理 cookie ?
        undici.interceptors.redirect({ maxRedirections: 5 }));
    return dispatchers[key] = dispatcher;
}
function get_request_body(body, type) {
    if (body === undefined)
        return undefined;
    if (typeof body?.read === 'function' && isReadable(body))
        return body;
    if (type === 'application/json') {
        // 可能的类型 string | Record<string, any> | Uint8Array
        if (typeof body === 'string')
            return encode(body);
        if (body instanceof Uint8Array)
            return body;
        check(!(body instanceof ArrayBuffer || ArrayBuffer.isView(body)));
        return JSON.stringify(body);
    }
    if (type === 'application/x-www-form-urlencoded')
        return encode((body instanceof URLSearchParams ? body : new URLSearchParams(body)).toString());
    check(type === 'multipart/form-data');
    // undici.FormData 会和 globalThis.FormData 不一致，禁止传入 FormData 类型
    let form = new undici.FormData();
    for (const key in body)
        form.set(key, body[key]);
    return form;
}
async function get_response_body(body, encoding, headers) {
    if (!body)
        return encoding === 'binary' ? Buffer.from([]) : '';
    if (encoding === 'binary')
        return stream_to_buffer(body);
    encoding ||= /charset=(.*)/.exec(headers['content-type'])?.[1] || 'utf-8';
    if (/utf-?8/i.test(encoding))
        return stream_to_text(body);
    return new TextDecoder(encoding)
        .decode(await stream_to_buffer(body));
}
/** 发起 http 请求并将响应体作为 json 解析 */
export async function request_json(url, options) {
    const body = await request(url, options);
    if (!body)
        return;
    try {
        return JSON.parse(body);
    }
    catch (error) {
        console.error(body);
        throw error;
    }
}
export function get_socket_ip(socket) {
    return socket.remoteAddress.strip_if_start('::ffff:');
}
//# sourceMappingURL=net.js.map