"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MinecraftLibrariesDownloader = void 0;
/**
 * @author NovaStepStudios
 * @alias StepnickaSantiago
 * @license Apache-2.0
 * @link https://www.apache.org/licenses/LICENSE-2.0
 */
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const https_1 = __importDefault(require("https"));
const os_1 = __importDefault(require("os"));
const crypto_1 = __importDefault(require("crypto"));
const events_1 = require("events");
class MinecraftLibrariesDownloader extends events_1.EventEmitter {
    #version;
    #libsDir;
    #currentOS;
    #maxRetries = 5;
    #concurrency;
    constructor(root, version, concurrency = 20) {
        super();
        this.#version = version;
        this.#libsDir = path_1.default.join(root, "libraries");
        this.#currentOS = this.#mapOS(os_1.default.platform());
        this.#concurrency = concurrency;
    }
    async start() {
        await this.#ensureDir(this.#libsDir);
        const manifest = await this.#fetchJSON("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json");
        const versionMeta = manifest.versions.find((v) => v.id === this.#version);
        if (!versionMeta)
            throw new Error(`[Downloader] Versión ${this.#version} no encontrada.`);
        const versionJSON = await this.#fetchJSON(versionMeta.url);
        const libraries = versionJSON.libraries || [];
        const filtered = libraries.filter(lib => this.#matchesRules(lib.rules));
        const officialLibs = filtered
            .map(lib => lib.downloads?.artifact)
            .filter((a) => !!a && !!a.url && !!a.path);
        const extraLibs = await this.#loadExtraLibs();
        const allLibs = [...officialLibs, ...extraLibs];
        let downloaded = 0;
        await this.#runConcurrent(allLibs, this.#concurrency, async (lib) => {
            const fullPath = path_1.default.join(this.#libsDir, lib.path);
            const needsDownload = !fs_1.default.existsSync(fullPath) ||
                (lib.sha1 && !(await this.#verifySHA1(fullPath, lib.sha1)));
            if (needsDownload) {
                await this.#downloadFileWithRetries(lib.url, fullPath, lib.sha1);
            }
            downloaded++;
            this.#emitProgress(downloaded, allLibs.length);
        });
        this.emit("done", { current: downloaded, total: allLibs.length });
    }
    #mapOS(platform) {
        switch (platform) {
            case "win32": return "windows";
            case "darwin": return "osx";
            case "linux": return "linux";
            default: return platform;
        }
    }
    #matchesRules(rules) {
        if (!rules || rules.length === 0)
            return true;
        let allowed = null;
        for (const rule of rules) {
            const osRule = rule.os?.name;
            const matches = osRule === this.#currentOS;
            if (rule.action === "disallow" && matches)
                return false;
            if (rule.action === "allow" && matches)
                allowed = true;
        }
        return allowed ?? true;
    }
    async #loadExtraLibs() {
        const jsonPath = path_1.default.join(__dirname, "../../resources/libs/extralibs.json");
        let urls = [];
        if (fs_1.default.existsSync(jsonPath)) {
            try {
                const content = await fs_1.default.promises.readFile(jsonPath, "utf-8");
                urls = JSON.parse(content);
                if (!Array.isArray(urls))
                    throw new Error("extralibs.json local no es un array de strings");
            }
            catch (err) {
                console.warn(`[Downloader] Error leyendo el extralibs.json local:`, err);
                urls = [];
            }
        }
        else {
            console.warn(`[Downloader] extralibs.json no encontrado en ${jsonPath}, usando URL remota`);
        }
        if (urls.length === 0) {
            try {
                const remoteContent = await this.#fetchRemoteJSON("https://raw.githubusercontent.com/NovaStepStudios/Mojang-Api-Meta/refs/heads/minecraft-core-master/extraLibs.json");
                if (Array.isArray(remoteContent))
                    urls = remoteContent;
            }
            catch (err) {
                console.warn(`[Downloader] No se pudo cargar el extralibs.json remoto:`, err);
                urls = [];
            }
        }
        return urls.map(url => {
            const u = new URL(url);
            const libPath = u.pathname.replace(/^\/+/, '');
            return { url, path: libPath };
        });
    }
    async #fetchRemoteJSON(url) {
        return new Promise((resolve, reject) => {
            https_1.default.get(url, res => {
                if (res.statusCode !== 200)
                    return reject(new Error(`HTTP ${res.statusCode} - ${url}`));
                let data = "";
                res.on("data", chunk => data += chunk);
                res.on("end", () => {
                    try {
                        resolve(JSON.parse(data));
                    }
                    catch (err) {
                        reject(new Error(`Error parseando JSON remoto de ${url}: ${err.message}`));
                    }
                });
            }).on("error", reject);
        });
    }
    async #verifySHA1(filePath, expected) {
        return new Promise((resolve, reject) => {
            const hash = crypto_1.default.createHash("sha1");
            const stream = fs_1.default.createReadStream(filePath);
            stream.on("data", d => hash.update(d));
            stream.on("end", () => resolve(hash.digest("hex") === expected));
            stream.on("error", reject);
        });
    }
    async #downloadFileWithRetries(url, dest, sha1, retries = 0) {
        try {
            await this.#downloadFile(url, dest);
            if (sha1 && !(await this.#verifySHA1(dest, sha1)))
                throw new Error(`SHA1 mismatch en ${dest}`);
        }
        catch (err) {
            if (retries < this.#maxRetries) {
                await new Promise(r => setTimeout(r, 1500));
                return this.#downloadFileWithRetries(url, dest, sha1, retries + 1);
            }
            throw err;
        }
    }
    async #downloadFile(url, dest) {
        await this.#ensureDir(path_1.default.dirname(dest));
        return new Promise((resolve, reject) => {
            const file = fs_1.default.createWriteStream(dest);
            https_1.default.get(url, res => {
                if (res.statusCode !== 200) {
                    file.close();
                    fs_1.default.unlink(dest, () => { });
                    return reject(new Error(`HTTP ${res.statusCode} en ${url}`));
                }
                res.pipe(file);
                file.on("finish", () => file.close(err => (err ? reject(err) : resolve())));
            }).on("error", err => {
                file.close();
                fs_1.default.unlink(dest, () => { });
                reject(err);
            });
        });
    }
    async #fetchJSON(url) {
        return new Promise((resolve, reject) => {
            https_1.default.get(url, res => {
                if (res.statusCode !== 200)
                    return reject(new Error(`HTTP ${res.statusCode} - ${url}`));
                let data = "";
                res.on("data", chunk => data += chunk);
                res.on("end", () => {
                    try {
                        resolve(JSON.parse(data));
                    }
                    catch (err) {
                        reject(new Error(`Error parseando JSON de ${url}: ${err.message}`));
                    }
                });
            }).on("error", reject);
        });
    }
    async #ensureDir(dir) { await fs_1.default.promises.mkdir(dir, { recursive: true }); }
    async #runConcurrent(items, limit, worker) {
        const queue = [...items];
        const workers = new Array(limit).fill(null).map(async () => {
            while (queue.length > 0) {
                const item = queue.shift();
                if (item)
                    await worker(item);
            }
        });
        await Promise.all(workers);
    }
    #emitProgress(current, total) {
        const stepPercent = total > 0 ? (current / total) * 100 : 100;
        this.emit("progress", { current, total, stepPercent });
    }
}
exports.MinecraftLibrariesDownloader = MinecraftLibrariesDownloader;
