"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MinecraftNativesDownloader = 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 os_1 = __importDefault(require("os"));
const https_1 = __importDefault(require("https"));
const events_1 = __importDefault(require("events"));
const unzipper_1 = __importDefault(require("unzipper"));
const crypto_1 = __importDefault(require("crypto"));
const p_limit_1 = __importDefault(require("p-limit"));
class MinecraftNativesDownloader extends events_1.default {
    #version;
    #destDir;
    #concurrency;
    #overwrite;
    #currentOS;
    #currentArch;
    constructor(root, version, concurrency = 5, overwrite = true) {
        super();
        this.#version = version;
        this.#concurrency = concurrency;
        this.#overwrite = overwrite;
        this.#destDir = path_1.default.join(root, "versions", version, "natives");
        this.#currentOS = this.#mapOS(os_1.default.platform());
        this.#currentArch = os_1.default.arch();
    }
    async start() {
        try {
            await fs_1.default.promises.mkdir(this.#destDir, { recursive: true });
            const versionJSON = await this.#fetchVersionJSON(this.#version);
            const libraries = versionJSON.libraries || [];
            const nativeLibs = libraries.filter((lib) => this.#isNativeLibrary(lib));
            const limit = (0, p_limit_1.default)(this.#concurrency);
            let completed = 0;
            await Promise.all(nativeLibs.map((lib) => limit(async () => {
                await this.#handleNative(lib);
                completed++;
                this.emit("progress", {
                    current: completed,
                    total: nativeLibs.length,
                    stepPercent: parseFloat(((completed / nativeLibs.length) * 100).toFixed(2)),
                });
            })));
            this.emit("done", { total: nativeLibs.length, downloaded: completed });
        }
        catch (err) {
            this.emit("error", err);
        }
    }
    async #handleNative(lib) {
        const urlInfo = this.#getNativeURL(lib);
        if (!urlInfo)
            return;
        const jarPath = path_1.default.join(this.#destDir, path_1.default.basename(urlInfo.url));
        const needsDownload = this.#overwrite || !(await this.#verifyFile(jarPath, urlInfo.sha1));
        if (needsDownload) {
            await this.#downloadFileWithRetries(urlInfo.url, jarPath, urlInfo.sha1);
        }
        await this.#extractAndCleanup(jarPath, this.#destDir);
    }
    async #extractAndCleanup(jarPath, destDir) {
        const exts = { windows: [".dll"], linux: [".so"], osx: [".dylib", ".jnilib"] };
        const validExts = exts[this.#currentOS] || [];
        await new Promise((resolve, reject) => {
            fs_1.default.createReadStream(jarPath)
                .pipe(unzipper_1.default.Parse())
                .on("entry", (entry) => {
                const ext = path_1.default.extname(entry.path).toLowerCase();
                if (!validExts.includes(ext) || !this.#isValidForCurrentArch(entry.path))
                    return entry.autodrain();
                const outPath = path_1.default.join(destDir, path_1.default.basename(entry.path));
                entry.pipe(fs_1.default.createWriteStream(outPath));
            })
                .on("close", resolve)
                .on("error", reject);
        });
        await fs_1.default.promises.unlink(jarPath).catch(() => { });
    }
    #isValidForCurrentArch(fileName) {
        fileName = fileName.toLowerCase();
        if (this.#currentOS === "windows") {
            if (this.#currentArch === "x64" && (fileName.includes("arm64") || fileName.includes("x86") || fileName.includes("windows-32")))
                return false;
            if (this.#currentArch === "ia32" && !fileName.includes("windows-32"))
                return false;
        }
        if (this.#currentOS === "linux" || this.#currentOS === "osx") {
            if (this.#currentArch === "x64" && fileName.includes("arm64"))
                return false;
            if (this.#currentArch === "arm64" && (fileName.includes("x86") || fileName.includes("x64")))
                return false;
        }
        return true;
    }
    #isNativeLibrary(lib) {
        if (!this.#matchesRules(lib.rules))
            return false;
        if (lib.name.includes(":natives"))
            return true;
        if (lib.downloads?.classifiers)
            return Object.keys(lib.downloads.classifiers).some((k) => k.startsWith("natives-"));
        return false;
    }
    #getNativeURL(lib) {
        if (!this.#matchesRules(lib.rules))
            return null;
        // Primero intentamos classifiers
        if (lib.downloads?.classifiers) {
            const keysToTry = [`natives-${this.#currentOS}-${this.#mapArchForURL()}`, `natives-${this.#currentOS}`];
            for (const key of keysToTry) {
                const classifier = lib.downloads.classifiers[key];
                if (classifier)
                    return { url: classifier.url, sha1: classifier.sha1 };
            }
        }
        // Fallback a artifact si es un native
        if (lib.downloads?.artifact && lib.name.includes(":natives")) {
            return { url: lib.downloads.artifact.url, sha1: lib.downloads.artifact.sha1 };
        }
        return null;
    }
    #matchesRules(rules) {
        if (!rules)
            return true;
        let allowed = null;
        for (const rule of rules) {
            if (!rule.os) {
                allowed = rule.action === "allow";
                continue;
            }
            if (rule.os.name && rule.os.name !== this.#currentOS)
                continue;
            allowed = rule.action === "allow";
        }
        return allowed ?? true;
    }
    #mapOS(platform) {
        switch (platform) {
            case "win32": return "windows";
            case "darwin": return "osx";
            case "linux": return "linux";
            default: return platform;
        }
    }
    #mapArchForURL() {
        switch (this.#currentOS) {
            case "windows": return this.#currentArch === "x64" ? "x64" : "x86";
            case "linux": return this.#currentArch === "x64" ? "x64" : this.#currentArch === "arm64" ? "arm64" : this.#currentArch;
            case "osx": return this.#currentArch === "x64" ? "x64" : "arm64";
            default: return this.#currentArch;
        }
    }
    async #verifyFile(filePath, sha1) {
        if (!sha1 || !fs_1.default.existsSync(filePath))
            return false;
        const hash = crypto_1.default.createHash("sha1");
        const data = await fs_1.default.promises.readFile(filePath);
        hash.update(data);
        return hash.digest("hex") === sha1;
    }
    async #downloadFileWithRetries(url, dest, sha1, retries = 0) {
        try {
            await this.#downloadFile(url, dest);
            if (sha1) {
                const ok = await this.#verifyFile(dest, sha1);
                if (!ok)
                    throw new Error(`SHA1 mismatch en ${dest}`);
            }
        }
        catch (err) {
            if (retries < 5) {
                this.emit("log", `Retry ${retries + 1} for ${url}`);
                await new Promise((r) => setTimeout(r, 1500));
                return this.#downloadFileWithRetries(url, dest, sha1, retries + 1);
            }
            throw err;
        }
    }
    async #downloadFile(url, dest) {
        await fs_1.default.promises.mkdir(path_1.default.dirname(dest), { recursive: true });
        return new Promise((resolve, reject) => {
            const file = fs_1.default.createWriteStream(dest);
            https_1.default.get(url, (res) => {
                if (res.statusCode !== 200)
                    return reject(new Error(`HTTP ${res.statusCode}: ${url}`));
                res.pipe(file);
                file.on("finish", () => file.close(err => err ? reject(err) : resolve()));
                file.on("error", err => { file.close(() => fs_1.default.unlink(dest, () => { })); reject(err); });
            }).on("error", err => { file.close(() => fs_1.default.unlink(dest, () => { })); reject(err); });
        });
    }
    async #fetchVersionJSON(version) {
        const manifest = await this.#fetchJSON("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json");
        const versionMeta = manifest.versions.find((v) => v.id === version);
        if (!versionMeta)
            throw new Error(`Versión ${version} no encontrada`);
        return this.#fetchJSON(versionMeta.url);
    }
    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 (e) {
                        reject(e);
                    }
                });
            }).on("error", reject);
        });
    }
}
exports.MinecraftNativesDownloader = MinecraftNativesDownloader;
