"use strict";
/**
 * Binary Downloader
 * Handles downloading files with retry logic, progress tracking, and redirect following.
 * Robust handling for transient network errors (socket hang up, ECONNRESET, etc.)
 * Respects http_proxy, https_proxy, and all_proxy environment variables.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.__testExports = exports.fetchJson = exports.fetchText = exports.downloadWithRetry = exports.downloadFile = exports.isRetryableError = exports.getErrorMessage = exports.categorizeError = void 0;
const fs = __importStar(require("fs"));
const https = __importStar(require("https"));
const http = __importStar(require("http"));
const https_proxy_agent_1 = require("https-proxy-agent");
const http_proxy_agent_1 = require("http-proxy-agent");
const proxy_env_1 = require("../../utils/proxy-env");
/**
 * Extract hostname from URL.
 * @param url The URL to parse
 * @returns Hostname or empty string if invalid
 */
function getHostname(url) {
    try {
        return new URL(url).hostname;
    }
    catch {
        return '';
    }
}
/**
 * Create appropriate proxy agent based on URL protocol.
 * Respects NO_PROXY/no_proxy for bypassing specific hosts.
 * @param url Target URL to determine protocol
 * @returns Proxy agent or false (no agent/pooling disabled)
 */
function getProxyAgent(url) {
    const isHttps = url.startsWith('https');
    const proxyUrl = (0, proxy_env_1.getProxyUrl)(isHttps);
    if (!proxyUrl) {
        return false; // No proxy configured, disable connection pooling for clean exit
    }
    // Check if this host should bypass the proxy
    const hostname = getHostname(url);
    if (hostname && (0, proxy_env_1.shouldBypassProxy)(hostname)) {
        return false; // Bypass proxy for this host
    }
    // Create proxy agent with error handling for malformed URLs
    try {
        if (isHttps) {
            return new https_proxy_agent_1.HttpsProxyAgent(proxyUrl);
        }
        return new http_proxy_agent_1.HttpProxyAgent(proxyUrl);
    }
    catch {
        // Invalid proxy URL, fall back to direct connection
        console.error(`[cliproxy] Invalid proxy URL: ${proxyUrl}`);
        return false;
    }
}
const DEFAULT_CONFIG = {
    maxRetries: 5,
    verbose: false,
    timeout: 120000, // 2 minutes for large binaries
};
/** Categorize error for appropriate retry/reporting */
function categorizeError(error) {
    const msg = error.message.toLowerCase();
    if (msg.includes('socket hang up') || msg.includes('econnreset') || msg.includes('epipe')) {
        return 'socket';
    }
    if (msg.includes('timeout') || msg.includes('etimedout')) {
        return 'timeout';
    }
    if (msg.includes('http ')) {
        return 'http';
    }
    if (msg.includes('redirect')) {
        return 'redirect';
    }
    return 'unknown';
}
exports.categorizeError = categorizeError;
/** Get user-friendly error message */
function getErrorMessage(error, attempt, maxAttempts) {
    const type = categorizeError(error);
    const prefix = `[Attempt ${attempt}/${maxAttempts}]`;
    switch (type) {
        case 'socket':
            return `${prefix} Connection dropped (socket hang up) - retrying with fresh connection...`;
        case 'timeout':
            return `${prefix} Download timed out - retrying with extended timeout...`;
        case 'http':
            return `${prefix} Server error: ${error.message}`;
        case 'redirect':
            return `${prefix} Redirect failed: ${error.message}`;
        default:
            return `${prefix} Network error: ${error.message}`;
    }
}
exports.getErrorMessage = getErrorMessage;
/** Check if error is retryable */
function isRetryableError(error) {
    const type = categorizeError(error);
    // Retry socket errors, timeouts, and unknown errors
    if (type === 'socket' || type === 'timeout' || type === 'unknown') {
        return true;
    }
    if (type === 'http') {
        const msg = error.message;
        // Retry 5xx server errors and 429 rate limit (HTTP 5xx matches 500, 502, 503, 504, etc.)
        return msg.includes('HTTP 5') || msg.includes('HTTP 429');
    }
    return false;
}
exports.isRetryableError = isRetryableError;
/**
 * Download file from URL with progress tracking
 * @param timeout Timeout in ms (default 120000 for large files)
 * @param maxRedirects Maximum number of redirects to follow (default 10)
 */
function downloadFile(url, destPath, onProgress, verbose = false, timeout = 120000, maxRedirects = 10) {
    return new Promise((resolve, reject) => {
        let resolved = false;
        const cleanup = (err) => {
            if (resolved)
                return;
            resolved = true;
            fs.unlink(destPath, () => { }); // Cleanup partial file
            reject(err || new Error('Download aborted'));
        };
        const handleResponse = (res) => {
            // Handle redirects (GitHub releases use 302)
            if (res.statusCode === 301 || res.statusCode === 302) {
                const redirectUrl = res.headers.location;
                if (!redirectUrl) {
                    cleanup(new Error('Redirect without location header'));
                    return;
                }
                if (maxRedirects <= 0) {
                    cleanup(new Error('Too many redirects'));
                    return;
                }
                if (verbose) {
                    console.error(`[cliproxy] Following redirect: ${redirectUrl}`);
                }
                downloadFile(redirectUrl, destPath, onProgress, verbose, timeout, maxRedirects - 1)
                    .then(resolve)
                    .catch(reject);
                return;
            }
            if (res.statusCode !== 200) {
                cleanup(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
                return;
            }
            const totalBytes = parseInt(res.headers['content-length'] || '0', 10);
            let downloadedBytes = 0;
            const fileStream = fs.createWriteStream(destPath);
            res.on('data', (chunk) => {
                downloadedBytes += chunk.length;
                if (onProgress && totalBytes > 0) {
                    onProgress({
                        total: totalBytes,
                        downloaded: downloadedBytes,
                        percentage: Math.round((downloadedBytes / totalBytes) * 100),
                    });
                }
            });
            res.pipe(fileStream);
            fileStream.on('finish', () => {
                if (resolved)
                    return;
                resolved = true;
                fileStream.close();
                resolve();
            });
            fileStream.on('error', cleanup);
            res.on('error', cleanup);
        };
        const protocol = url.startsWith('https') ? https : http;
        // Use proxy agent if configured, otherwise disable connection pooling for clean exit
        const options = {
            headers: {
                'User-Agent': 'CCS-CLIProxyPlus-Downloader/1.0',
            },
            agent: getProxyAgent(url),
        };
        const req = protocol.get(url, options, handleResponse);
        req.on('error', (err) => {
            if (!resolved) {
                cleanup(err);
            }
        });
        req.setTimeout(timeout, () => {
            req.destroy();
            if (!resolved) {
                cleanup(new Error(`Download timeout (${timeout / 1000}s)`));
            }
        });
    });
}
exports.downloadFile = downloadFile;
/**
 * Download file with retry logic and exponential backoff
 * Uses smarter backoff for socket errors (longer delays)
 */
async function downloadWithRetry(url, destPath, config = {}) {
    const { maxRetries, verbose, timeout } = { ...DEFAULT_CONFIG, ...config };
    let lastError = null;
    let retries = 0;
    let currentTimeout = timeout || 120000;
    while (retries < maxRetries) {
        try {
            await downloadFile(url, destPath, undefined, verbose, currentTimeout);
            return { success: true, filePath: destPath, retries };
        }
        catch (error) {
            const err = error;
            lastError = err;
            retries++;
            // Check if error is retryable
            if (!isRetryableError(err)) {
                if (verbose) {
                    console.error(`[cliproxy] Non-retryable error: ${err.message}`);
                }
                break;
            }
            if (retries < maxRetries) {
                const errorType = categorizeError(err);
                // Socket errors: longer backoff (2s, 4s, 8s, 16s, 32s)
                // Timeout errors: increase timeout and use standard backoff
                // Other errors: standard exponential backoff (1s, 2s, 4s...)
                let delay;
                if (errorType === 'socket') {
                    delay = Math.pow(2, retries) * 1000; // 2s, 4s, 8s, 16s, 32s
                }
                else if (errorType === 'timeout') {
                    delay = Math.pow(2, retries - 1) * 1000;
                    currentTimeout = Math.min(currentTimeout * 1.5, 300000); // Increase timeout up to 5 min
                }
                else {
                    delay = Math.pow(2, retries - 1) * 1000;
                }
                // Log with user-friendly message
                console.error(`[cliproxy] ${getErrorMessage(err, retries, maxRetries)}`);
                if (verbose) {
                    console.error(`[cliproxy] Waiting ${delay}ms before retry...`);
                }
                await sleep(delay);
            }
        }
    }
    return {
        success: false,
        error: `Download failed after ${retries} attempts: ${lastError?.message || 'Unknown error'}`,
        retries,
    };
}
exports.downloadWithRetry = downloadWithRetry;
/**
 * Fetch text content from URL (single attempt)
 */
function fetchTextOnce(url, verbose = false, timeout = 30000, maxRedirects = 10) {
    return new Promise((resolve, reject) => {
        let resolved = false;
        const handleResponse = (res) => {
            // Handle redirects
            if (res.statusCode === 301 || res.statusCode === 302) {
                const redirectUrl = res.headers.location;
                if (!redirectUrl) {
                    reject(new Error('Redirect without location header'));
                    return;
                }
                if (maxRedirects <= 0) {
                    reject(new Error('Too many redirects'));
                    return;
                }
                fetchTextOnce(redirectUrl, verbose, timeout, maxRedirects - 1)
                    .then(resolve)
                    .catch(reject);
                return;
            }
            if (res.statusCode !== 200) {
                reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
                return;
            }
            let data = '';
            res.on('data', (chunk) => (data += chunk));
            res.on('end', () => {
                if (!resolved) {
                    resolved = true;
                    resolve(data);
                }
            });
            res.on('error', (err) => {
                if (!resolved) {
                    resolved = true;
                    reject(err);
                }
            });
        };
        const protocol = url.startsWith('https') ? https : http;
        const options = {
            headers: {
                'User-Agent': 'CCS-CLIProxyPlus-Downloader/1.0',
            },
            agent: getProxyAgent(url),
        };
        const req = protocol.get(url, options, handleResponse);
        req.on('error', (err) => {
            if (!resolved) {
                resolved = true;
                reject(err);
            }
        });
        req.setTimeout(timeout, () => {
            req.destroy();
            if (!resolved) {
                resolved = true;
                reject(new Error(`Request timeout (${timeout / 1000}s)`));
            }
        });
    });
}
/**
 * Fetch text content from URL with retry logic
 */
async function fetchText(url, verbose = false, maxRetries = 3) {
    let lastError = null;
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            return await fetchTextOnce(url, verbose);
        }
        catch (error) {
            const err = error;
            lastError = err;
            if (!isRetryableError(err) || attempt === maxRetries) {
                break;
            }
            const delay = Math.pow(2, attempt - 1) * 1000;
            if (verbose) {
                console.error(`[cliproxy] fetchText retry ${attempt}/${maxRetries}: ${err.message}`);
            }
            await sleep(delay);
        }
    }
    throw lastError || new Error('fetchText failed');
}
exports.fetchText = fetchText;
/**
 * Fetch JSON from URL (single attempt, for GitHub API)
 */
function fetchJsonOnce(url, verbose = false, timeout = 15000, maxRedirects = 10) {
    return new Promise((resolve, reject) => {
        let resolved = false;
        const options = {
            headers: {
                'User-Agent': 'CCS-CLIProxyPlus-Updater/1.0',
                Accept: 'application/vnd.github.v3+json',
            },
            agent: getProxyAgent(url),
        };
        const handleResponse = (res) => {
            if (res.statusCode === 301 || res.statusCode === 302) {
                const redirectUrl = res.headers.location;
                if (!redirectUrl) {
                    reject(new Error('Redirect without location header'));
                    return;
                }
                if (maxRedirects <= 0) {
                    reject(new Error('Too many redirects'));
                    return;
                }
                fetchJsonOnce(redirectUrl, verbose, timeout, maxRedirects - 1)
                    .then(resolve)
                    .catch(reject);
                return;
            }
            if (res.statusCode !== 200) {
                reject(new Error(`GitHub API error: HTTP ${res.statusCode}`));
                return;
            }
            let data = '';
            res.on('data', (chunk) => (data += chunk));
            res.on('end', () => {
                if (resolved)
                    return;
                resolved = true;
                try {
                    resolve(JSON.parse(data));
                }
                catch {
                    reject(new Error('Invalid JSON from GitHub API'));
                }
            });
            res.on('error', (err) => {
                if (!resolved) {
                    resolved = true;
                    reject(err);
                }
            });
        };
        const req = https.get(url, options, handleResponse);
        req.on('error', (err) => {
            if (!resolved) {
                resolved = true;
                reject(err);
            }
        });
        req.setTimeout(timeout, () => {
            req.destroy();
            if (!resolved) {
                resolved = true;
                reject(new Error(`GitHub API timeout (${timeout / 1000}s)`));
            }
        });
    });
}
/**
 * Fetch JSON from URL (for GitHub API) with retry logic
 */
async function fetchJson(url, verbose = false, maxRetries = 3) {
    let lastError = null;
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            return await fetchJsonOnce(url, verbose);
        }
        catch (error) {
            const err = error;
            lastError = err;
            if (!isRetryableError(err) || attempt === maxRetries) {
                break;
            }
            const delay = Math.pow(2, attempt - 1) * 1000;
            if (verbose) {
                console.error(`[cliproxy] GitHub API retry ${attempt}/${maxRetries}: ${err.message}`);
            }
            await sleep(delay);
        }
    }
    throw lastError || new Error('fetchJson failed');
}
exports.fetchJson = fetchJson;
/**
 * Sleep helper
 */
function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
// Export internal functions for testing
exports.__testExports = {
    getProxyUrl: proxy_env_1.getProxyUrl,
    shouldBypassProxy: proxy_env_1.shouldBypassProxy,
    getHostname,
    getProxyAgent,
};
//# sourceMappingURL=downloader.js.map