/**
 * @license MIT
 * @copyright Copyright (c) 2026, GoldFrite
 */
import fsSync from 'node:fs';
import fs from 'node:fs/promises';
import path_ from 'node:path';
import EventEmitter from '../utils/events.js';
import utils from './utils.js';
import { EMLLibError, ErrorType } from '../../types/errors.js';
import { Readable } from 'node:stream';
export default class Downloader extends EventEmitter {
    /**
     * @param dest Destination folder.
     */
    constructor(dest) {
        super();
        this.CONCURRENCY_LIMIT = 8;
        this.size = 0;
        this.amount = 0;
        this.downloaded = { amount: 0, size: 0 };
        this.speed = 0;
        this.lastTime = 0;
        this.lastSize = 0;
        this.dest = path_.join(dest);
    }
    /**
     * Download files from the list.
     * @param files List of files to download. This list must include folders.
     * @param skipCheck [Optional: defaults to `false`] Skip files that already exist in the
     * destination folder (force to download all files).
     */
    async download(files, skipCheck = false) {
        const filesToDownload = !skipCheck ? await this.getFilesToDownload(files) : files;
        this.size = filesToDownload.reduce((acc, curr) => acc + (curr.size ?? 0), 0);
        this.amount = filesToDownload.length;
        this.downloaded = { amount: 0, size: 0 };
        this.speed = 0;
        this.lastTime = Date.now();
        this.lastSize = 0;
        if (this.size === 0 || filesToDownload.length === 0) {
            this.emit('download_end', { downloaded: this.downloaded });
            return;
        }
        const queue = [...filesToDownload];
        const workers = Array(this.CONCURRENCY_LIMIT)
            .fill(null)
            .map(async () => {
            while (queue.length > 0) {
                const file = queue.shift();
                if (file)
                    await this.downloadFileWithRetry(file);
            }
        });
        await Promise.all(workers);
        this.emit('download_end', { downloaded: this.downloaded });
    }
    /**
     * Get files that need to be downloaded (files that don't exist or have different hash).
     * @param files List of files to check.
     * @returns List of files to download.
     */
    async getFilesToDownload(files) {
        let filesToDownload = [];
        const promises = files.map(async (file) => {
            const filePath = path_.join(this.dest, file.path, file.name);
            const relative = path_.relative(this.dest, filePath);
            const isSafe = relative && !relative.startsWith('..') && !path_.isAbsolute(relative);
            if (!isSafe) {
                this.emit('download_error', { filename: file.name, type: file.type, message: 'Unsafe file path detected, skipping.' });
                return;
            }
            if (file.type === 'FOLDER') {
                try {
                    await fs.access(filePath);
                }
                catch {
                    await fs.mkdir(filePath, { recursive: true });
                }
            }
            let needsDownload = false;
            try {
                await fs.access(filePath);
                if (file.sha1 && file.sha1 !== (await utils.getFileHash(filePath))) {
                    needsDownload = true;
                }
            }
            catch {
                needsDownload = true;
            }
            if (needsDownload && file.url) {
                filesToDownload.push(file);
            }
        });
        await Promise.all(promises);
        return filesToDownload;
    }
    async downloadFileWithRetry(file, attempt = 0) {
        try {
            await this.downloadFile(file);
            this.downloaded.amount++;
        }
        catch (err) {
            if (attempt < 5) {
                await new Promise((r) => setTimeout(r, 1000 * (attempt + 1)));
                return this.downloadFileWithRetry(file, attempt + 1);
            }
            else {
                this.emit('download_error', {
                    filename: file.name,
                    type: file.type,
                    message: err.message ?? err
                });
                throw new EMLLibError(ErrorType.DOWNLOAD_ERROR, `Failed to download ${file.name} after 5 attempts`);
            }
        }
    }
    async downloadFile(file) {
        const dirPath = path_.join(this.dest, file.path);
        const filePath = path_.join(dirPath, file.name);
        let bytesDownloadedThisAttempt = 0;
        await fs.mkdir(dirPath, { recursive: true });
        const req = await fetch(file.url, {
            headers: { Accept: 'application/octet-stream' }
        });
        if (!req.ok || !req.body) {
            const errorText = await req.text();
            throw new EMLLibError(ErrorType.FETCH_ERROR, `Error while fetching ${file.name}: HTTP ${req.status} ${errorText}`);
        }
        const fileStream = fsSync.createWriteStream(filePath);
        const nodeStream = Readable.fromWeb(req.body);
        return new Promise((resolve, reject) => {
            const cleanup = () => {
                nodeStream.removeAllListeners();
                fileStream.removeAllListeners();
                fileStream.destroy();
            };
            nodeStream.on('data', (chunk) => {
                bytesDownloadedThisAttempt += chunk.length;
                this.downloaded.size += chunk.length;
                const now = Date.now();
                const diffTime = now - this.lastTime;
                if (diffTime > 500) {
                    const diffSize = this.downloaded.size - this.lastSize;
                    this.speed = diffSize / (diffTime / 1000);
                    this.lastTime = now;
                    this.lastSize = this.downloaded.size;
                }
                this.emit('download_progress', {
                    total: { amount: this.amount, size: this.size },
                    downloaded: this.downloaded,
                    speed: Math.floor(this.speed),
                    type: file.type
                });
            });
            nodeStream.on('error', (err) => {
                this.downloaded.size -= bytesDownloadedThisAttempt;
                this.lastSize = Math.min(this.lastSize, this.downloaded.size);
                cleanup();
                reject(err);
            });
            fileStream.on('finish', async () => {
                cleanup();
                try {
                    await this.chmodJavaFiles(filePath, file);
                    resolve();
                }
                catch (err) {
                    reject(err);
                }
            });
            fileStream.on('error', (err) => {
                this.downloaded.size -= bytesDownloadedThisAttempt;
                cleanup();
                reject(err);
            });
            nodeStream.pipe(fileStream);
        });
    }
    async chmodJavaFiles(filePath, file) {
        if (process.platform !== 'win32' && file.executable) {
            await fs.chmod(filePath, 0o755);
        }
    }
}
//# sourceMappingURL=downloader.js.map