"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MinecraftAssetsDownloader = 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 events_1 = __importDefault(require("events"));
const crypto_1 = __importDefault(require("crypto"));
const p_limit_1 = __importDefault(require("p-limit"));
class MinecraftAssetsDownloader extends events_1.default {
    #version;
    #assetsDir;
    #objectsDir;
    #indexesDir;
    #legacyVirtualDir;
    #resourcesDir;
    #concurrency;
    #maxRetries;
    #overwrite;
    #downloadLegacyResources;
    constructor(root, version, concurrency = 20, maxRetries = 10, overwrite = true, downloadLegacyResources = false) {
        super();
        this.#version = version;
        this.#concurrency = concurrency;
        this.#maxRetries = maxRetries;
        this.#overwrite = overwrite;
        this.#downloadLegacyResources = downloadLegacyResources;
        this.#assetsDir = path_1.default.join(root, "assets");
        this.#objectsDir = path_1.default.join(this.#assetsDir, "objects");
        this.#indexesDir = path_1.default.join(this.#assetsDir, "indexes");
        this.#legacyVirtualDir = path_1.default.join(this.#assetsDir, "legacy", "virtual");
        this.#resourcesDir = path_1.default.join(root, "resources");
    }
    async start() {
        try {
            await this.#ensureDirs();
            const versionManifest = await this.#fetchJSON("https://piston-meta.mojang.com/mc/game/version_manifest_v2.json");
            const versionMeta = versionManifest?.versions?.find((v) => v.id === this.#version);
            if (!versionMeta)
                throw new Error(`La versión ${this.#version} no se encontró.`);
            const versionJSON = await this.#fetchJSON(versionMeta.url);
            const assetIndexURL = versionJSON?.assetIndex?.url;
            if (!assetIndexURL)
                throw new Error(`No se encontró el assetIndex para la versión ${this.#version}`);
            const assetIndex = await this.#fetchJSON(assetIndexURL);
            const assetIndexID = versionJSON.assetIndex.id;
            const indexPath = path_1.default.join(this.#indexesDir, `${assetIndexID}.json`);
            await fs_1.default.promises.writeFile(indexPath, JSON.stringify(assetIndex, null, 2));
            const assets = Object.entries(assetIndex.objects || {});
            let downloaded = 0;
            const limit = (0, p_limit_1.default)(this.#concurrency);
            const tasks = assets.map(([logicalPath, { hash }]) => limit(async () => {
                try {
                    await this.#handleAsset(logicalPath, hash, versionJSON.assets);
                    downloaded++;
                    this.#emitProgress(downloaded, assets.length);
                }
                catch (err) {
                    this.emit("log", `❌ Error descargando ${logicalPath}: ${err.message}`);
                }
            }));
            await Promise.allSettled(tasks);
            this.emit("done", { totalAssets: assets.length, downloaded });
        }
        catch (err) {
            this.emit("error", err);
        }
    }
    async #handleAsset(logicalPath, hash, assetsType) {
        const subDir = hash.slice(0, 2);
        const objectPath = path_1.default.join(this.#objectsDir, subDir, hash);
        if (!fs_1.default.existsSync(objectPath) || this.#overwrite) {
            if (!fs_1.default.existsSync(objectPath) || !(await this.#verifyFile(objectPath, hash))) {
                const url = `https://resources.download.minecraft.net/${subDir}/${hash}`;
                await this.#downloadFileWithRetries(url, objectPath, hash);
            }
        }
        if (this.#downloadLegacyResources && ["legacy", "pre-1.6"].includes(assetsType)) {
            await this.#safeCopy(objectPath, path_1.default.join(this.#resourcesDir, logicalPath), this.#overwrite);
            await this.#safeCopy(objectPath, path_1.default.join(this.#legacyVirtualDir, logicalPath), this.#overwrite);
        }
    }
    async #safeCopy(src, dest, overwrite) {
        if (!fs_1.default.existsSync(src))
            return;
        if (!overwrite && fs_1.default.existsSync(dest))
            return;
        await fs_1.default.promises.mkdir(path_1.default.dirname(dest), { recursive: true });
        await fs_1.default.promises.copyFile(src, dest);
    }
    async #verifyFile(filePath, expectedHash) {
        if (!fs_1.default.existsSync(filePath))
            return false;
        const actualHash = await this.#calcSHA1(filePath);
        return actualHash === expectedHash;
    }
    async #calcSHA1(filePath) {
        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")));
            stream.on("error", reject);
        });
    }
    async #downloadFileWithRetries(url, dest, expectedHash, retries = 0) {
        try {
            await this.#downloadFile(url, dest);
            if (!(await this.#verifyFile(dest, expectedHash))) {
                throw new Error(`SHA1 mismatch en ${dest}`);
            }
        }
        catch (err) {
            if (retries < this.#maxRetries) {
                const delay = 1000 * Math.pow(2, retries);
                this.emit("log", `🔄 Reintentando (${retries + 1}) -> ${url} en ${delay}ms`);
                await new Promise(r => setTimeout(r, delay));
                return this.#downloadFileWithRetries(url, dest, expectedHash, 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) {
                    file.close(() => fs_1.default.unlink(dest, () => void 0));
                    return reject(new Error(`Fallo al descargar: ${url} (status ${res.statusCode})`));
                }
                res.pipe(file);
                file.on("finish", () => file.close(err => (err ? reject(err) : resolve())));
                file.on("error", err => {
                    file.close(() => fs_1.default.unlink(dest, () => void 0));
                    reject(err);
                });
            })
                .on("error", err => {
                file.close(() => fs_1.default.unlink(dest, () => void 0));
                reject(err);
            });
        });
    }
    async #ensureDirs() {
        await Promise.all([
            fs_1.default.promises.mkdir(this.#objectsDir, { recursive: true }),
            fs_1.default.promises.mkdir(this.#indexesDir, { recursive: true }),
            fs_1.default.promises.mkdir(this.#resourcesDir, { recursive: true }),
            fs_1.default.promises.mkdir(this.#legacyVirtualDir, { recursive: true }),
        ]);
    }
    #emitProgress(current, total) {
        const stepPercent = total > 0 ? parseFloat(((current / total) * 100).toFixed(2)) : 100;
        this.emit("progress", { current, total, stepPercent });
    }
    async #fetchJSON(url) {
        return new Promise((resolve, reject) => {
            https_1.default
                .get(url, res => {
                if (res.statusCode !== 200)
                    return reject(new Error(`Error JSON desde ${url} (status ${res.statusCode})`));
                let data = "";
                res.on("data", chunk => (data += chunk));
                res.on("end", () => {
                    try {
                        resolve(JSON.parse(data));
                    }
                    catch (err) {
                        reject(new Error(`Error parseando JSON: ${err.message}`));
                    }
                });
            })
                .on("error", reject);
        });
    }
}
exports.MinecraftAssetsDownloader = MinecraftAssetsDownloader;
