"use strict";
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.Downloader = void 0;
const constant_js_1 = require("../constant.js");
const utils_js_1 = require("../utils.js");
const ethers_1 = require("ethers");
const utils_js_2 = require("./utils.js");
const encryption_js_1 = require("../common/encryption.js");
class Downloader {
    nodes;
    shardConfigs;
    startSegmentIndex;
    endSegmentIndex;
    symmetricKey;
    privateKey;
    constructor(nodes) {
        this.nodes = nodes;
        this.shardConfigs = [];
        this.startSegmentIndex = 0;
        this.endSegmentIndex = 0;
    }
    // Set the v1 symmetric AES-256 key (32 bytes) used when the downloaded
    // file's encryption header has version=0x01.
    withSymmetricKey(key) {
        let bytes;
        if (typeof key === 'string') {
            const clean = key.startsWith('0x') ? key.slice(2) : key;
            bytes = new Uint8Array(clean.length / 2);
            for (let i = 0; i < bytes.length; i++) {
                bytes[i] = parseInt(clean.substr(i * 2, 2), 16);
            }
        }
        else {
            bytes = key;
        }
        if (bytes.length !== 32) {
            throw new Error(`symmetric key must be 32 bytes, got ${bytes.length}`);
        }
        this.symmetricKey = bytes;
        return this;
    }
    // Set the secp256k1 private key used when the downloaded file's
    // encryption header has version=0x02 (ECIES).
    withPrivateKey(key) {
        this.privateKey = (0, encryption_js_1.normalizePrivKey)(key);
        return this;
    }
    hasDecryptionKey() {
        return this.symmetricKey !== undefined || this.privateKey !== undefined;
    }
    async download(rootOrRoots, filePath, proof = false) {
        if (Array.isArray(rootOrRoots)) {
            return this.downloadFragments(rootOrRoots, filePath, proof);
        }
        else {
            return this.downloadFile(rootOrRoots, filePath, proof);
        }
    }
    async downloadFile(root, filePath, proof) {
        const rawErr = await this.downloadFileRaw(root, filePath, proof);
        if (rawErr != null)
            return rawErr;
        if (this.hasDecryptionKey()) {
            const fs = await Promise.resolve().then(() => __importStar(require(/* webpackIgnore: true */ 'fs')));
            const encrypted = new Uint8Array(fs.readFileSync(filePath));
            const header = (0, encryption_js_1.parseEncryptionHeader)(encrypted);
            const aesKey = (0, encryption_js_1.resolveDecryptionKey)(this.symmetricKey, this.privateKey, header);
            const plaintext = (0, encryption_js_1.decryptFile)(aesKey, encrypted);
            fs.writeFileSync(filePath, plaintext);
        }
        return null;
    }
    // Download a single root to `filePath` without running the post-download
    // decrypt step — used by the multi-root fragment path, which coordinates
    // its own cross-fragment decrypt.
    async downloadFileRaw(root, filePath, proof) {
        const [info, err] = await this.queryFile(root);
        if (err != null || info === null) {
            return new Error(err?.message);
        }
        if (!info.finalized) {
            return new Error('File not finalized');
        }
        if (await (0, utils_js_1.checkExist)(filePath)) {
            return new Error('Wrong path, provide a file path which does not exist.');
        }
        const shardConfigs = await (0, utils_js_2.getShardConfigs)(this.nodes);
        if (shardConfigs === null) {
            return new Error('Failed to get shard configs');
        }
        this.shardConfigs = shardConfigs;
        return this.downloadFileHelper(filePath, info, proof);
    }
    async downloadFragments(roots, filename, withProof = false) {
        // Dynamic import keeps `fs` out of browser bundles
        const fs = await Promise.resolve().then(() => __importStar(require(/* webpackIgnore: true */ 'fs')));
        const path = await Promise.resolve().then(() => __importStar(require(/* webpackIgnore: true */ 'path')));
        if (await (0, utils_js_1.checkExist)(filename)) {
            return new Error('Output file already exists. Provide a file path which does not exist.');
        }
        const outputDir = path.dirname(filename);
        if (!fs.existsSync(outputDir)) {
            try {
                fs.mkdirSync(outputDir, { recursive: true });
            }
            catch (err) {
                return new Error(`Failed to create output directory: ${err}`);
            }
        }
        let outFileHandle;
        try {
            outFileHandle = fs.openSync(filename, 'w');
        }
        catch (err) {
            return new Error(`Failed to create output file: ${err}`);
        }
        const tempFiles = [];
        const decrypting = this.hasDecryptionKey();
        let header;
        let aesKey;
        let cumulativeDataOffset = 0;
        try {
            for (let i = 0; i < roots.length; i++) {
                const root = roots[i];
                const tempFile = path.join(outputDir, `${root}.temp`);
                tempFiles.push(tempFile);
                const downloadErr = await this.downloadFileRaw(root, tempFile, withProof);
                if (downloadErr != null) {
                    return new Error(`Failed to download file with root ${root}: ${downloadErr.message}`);
                }
                try {
                    const data = new Uint8Array(fs.readFileSync(tempFile));
                    let toWrite = data;
                    if (decrypting) {
                        if (i === 0) {
                            header = (0, encryption_js_1.parseEncryptionHeader)(data);
                            aesKey = (0, encryption_js_1.resolveDecryptionKey)(this.symmetricKey, this.privateKey, header);
                            const res = (0, encryption_js_1.decryptFragmentData)(aesKey, header, data, true, 0);
                            toWrite = res.plaintext;
                            cumulativeDataOffset = res.newOffset;
                        }
                        else {
                            const res = (0, encryption_js_1.decryptFragmentData)(aesKey, header, data, false, cumulativeDataOffset);
                            toWrite = res.plaintext;
                            cumulativeDataOffset = res.newOffset;
                        }
                    }
                    fs.writeSync(outFileHandle, toWrite);
                }
                catch (err) {
                    return new Error(`Failed to copy content from temp file ${tempFile}: ${err}`);
                }
                try {
                    fs.unlinkSync(tempFile);
                }
                catch (err) {
                    console.warn(`Warning: failed to delete temp file ${tempFile}: ${err}`);
                }
            }
            return null;
        }
        catch (err) {
            return new Error(`Unexpected error during download fragments: ${err}`);
        }
        finally {
            try {
                fs.closeSync(outFileHandle);
            }
            catch (err) {
                console.warn(`Warning: failed to close output file: ${err}`);
            }
            for (const tempFile of tempFiles) {
                try {
                    if (fs.existsSync(tempFile))
                        fs.unlinkSync(tempFile);
                }
                catch (err) {
                    console.warn(`Warning: failed to clean up temp file ${tempFile}: ${err}`);
                }
            }
        }
    }
    async downloadToBlob(rootOrRoots, proof = false) {
        if (Array.isArray(rootOrRoots)) {
            return this.downloadFragmentsToBlob(rootOrRoots, proof);
        }
        else {
            return this.downloadFileToBlob(rootOrRoots, proof);
        }
    }
    async downloadFileToBlob(root, proof) {
        const [rawBlob, err] = await this.downloadFileRawToBlob(root, proof);
        if (err != null)
            return [new Blob(), err];
        if (!this.hasDecryptionKey())
            return [rawBlob, null];
        const encrypted = new Uint8Array(await rawBlob.arrayBuffer());
        const header = (0, encryption_js_1.parseEncryptionHeader)(encrypted);
        const aesKey = (0, encryption_js_1.resolveDecryptionKey)(this.symmetricKey, this.privateKey, header);
        const plaintext = (0, encryption_js_1.decryptFile)(aesKey, encrypted);
        // Cast required: Uint8Array<ArrayBufferLike> doesn't line up with
        // BlobPart in TypeScript's stricter lib.dom.
        return [new Blob([plaintext]), null];
    }
    // Download a single root to a Blob without the post-download decrypt
    // step — used by the multi-root fragment path.
    async downloadFileRawToBlob(root, proof) {
        const [info, err] = await this.queryFile(root);
        if (err != null || info === null) {
            return [
                new Blob(),
                new Error(err?.message ?? 'Failed to query file'),
            ];
        }
        if (!info.finalized) {
            return [new Blob(), new Error('File not finalized')];
        }
        const shardConfigs = await (0, utils_js_2.getShardConfigs)(this.nodes);
        if (shardConfigs === null) {
            return [new Blob(), new Error('Failed to get shard configs')];
        }
        this.shardConfigs = shardConfigs;
        return this.downloadFileHelperToBlob(info, proof);
    }
    async downloadFileHelperToBlob(info, proof) {
        const numChunks = (0, utils_js_1.GetSplitNum)(info.tx.size, constant_js_1.DEFAULT_CHUNK_SIZE);
        this.startSegmentIndex = Math.floor(info.tx.startEntryIndex / constant_js_1.DEFAULT_SEGMENT_MAX_CHUNKS);
        this.endSegmentIndex = Math.floor((info.tx.startEntryIndex +
            (0, utils_js_1.GetSplitNum)(info.tx.size, constant_js_1.DEFAULT_CHUNK_SIZE) -
            1) /
            constant_js_1.DEFAULT_SEGMENT_MAX_CHUNKS);
        const numTasks = this.endSegmentIndex - this.startSegmentIndex + 1;
        const chunks = [];
        for (let taskInd = 0; taskInd < numTasks; taskInd++) {
            const [segArray, err] = await this.downloadTask(info, 0, taskInd, numChunks, proof);
            if (err != null) {
                return [new Blob(), err];
            }
            chunks.push(segArray);
        }
        // Cast required: ethers decodeBase64 returns Uint8Array<ArrayBufferLike>
        // which TypeScript won't accept directly as BlobPart[].
        return [new Blob(chunks), null];
    }
    async downloadFragmentsToBlob(roots, proof) {
        const decrypting = this.hasDecryptionKey();
        const parts = [];
        const rawBlobs = [];
        let header;
        let aesKey;
        let cumulativeDataOffset = 0;
        for (let i = 0; i < roots.length; i++) {
            const [blob, err] = await this.downloadFileRawToBlob(roots[i], proof);
            if (err != null)
                return [new Blob(), err];
            if (!decrypting) {
                rawBlobs.push(blob);
                continue;
            }
            const data = new Uint8Array(await blob.arrayBuffer());
            if (i === 0) {
                header = (0, encryption_js_1.parseEncryptionHeader)(data);
                aesKey = (0, encryption_js_1.resolveDecryptionKey)(this.symmetricKey, this.privateKey, header);
                const res = (0, encryption_js_1.decryptFragmentData)(aesKey, header, data, true, 0);
                parts.push(res.plaintext);
                cumulativeDataOffset = res.newOffset;
            }
            else {
                const res = (0, encryption_js_1.decryptFragmentData)(aesKey, header, data, false, cumulativeDataOffset);
                parts.push(res.plaintext);
                cumulativeDataOffset = res.newOffset;
            }
        }
        if (!decrypting)
            return [new Blob(rawBlobs), null];
        return [new Blob(parts), null];
    }
    // ─── Shared helpers ───────────────────────────────────────────────────
    async queryFile(root) {
        let fileInfo = null;
        for (const node of this.nodes) {
            const currInfo = await node.getFileInfo(root, true);
            if (currInfo === null) {
                return [null, new Error('File not found on node ' + node.url)];
            }
            else if (fileInfo === null) {
                fileInfo = currInfo;
            }
        }
        return [fileInfo, null];
    }
    // TODO: add proof check
    async downloadTask(info, segmentOffset, taskInd, numChunks, _proof) {
        const segmentIndex = segmentOffset + taskInd;
        const startIndex = segmentIndex * constant_js_1.DEFAULT_SEGMENT_MAX_CHUNKS;
        let endIndex = startIndex + constant_js_1.DEFAULT_SEGMENT_MAX_CHUNKS;
        if (endIndex > numChunks) {
            endIndex = numChunks;
        }
        let segment = null;
        for (let i = 0; i < this.shardConfigs.length; i++) {
            const nodeIndex = (taskInd + i) % this.shardConfigs.length;
            if ((this.startSegmentIndex + segmentIndex) %
                this.shardConfigs[nodeIndex].numShard !==
                this.shardConfigs[nodeIndex].shardId) {
                continue;
            }
            segment = await this.nodes[nodeIndex].downloadSegmentByTxSeq(info.tx.seq, startIndex, endIndex);
            if (segment === null) {
                continue;
            }
            let segArray = (0, ethers_1.decodeBase64)(segment);
            if (this.startSegmentIndex + segmentIndex ===
                this.endSegmentIndex) {
                const lastChunkSize = info.tx.size % constant_js_1.DEFAULT_CHUNK_SIZE;
                if (lastChunkSize > 0) {
                    const paddings = constant_js_1.DEFAULT_CHUNK_SIZE - lastChunkSize;
                    segArray = segArray.slice(0, segArray.length - paddings);
                }
            }
            return [segArray, null];
        }
        return [
            new Uint8Array(),
            new Error('No storage node holds segment with index ' + segmentIndex),
        ];
    }
    async downloadFileHelper(filePath, info, proof) {
        // Dynamic import keeps `fs` out of browser bundles
        const fs = await Promise.resolve().then(() => __importStar(require(/* webpackIgnore: true */ 'fs')));
        const numChunks = (0, utils_js_1.GetSplitNum)(info.tx.size, constant_js_1.DEFAULT_CHUNK_SIZE);
        this.startSegmentIndex = Math.floor(info.tx.startEntryIndex / constant_js_1.DEFAULT_SEGMENT_MAX_CHUNKS);
        this.endSegmentIndex = Math.floor((info.tx.startEntryIndex +
            (0, utils_js_1.GetSplitNum)(info.tx.size, constant_js_1.DEFAULT_CHUNK_SIZE) -
            1) /
            constant_js_1.DEFAULT_SEGMENT_MAX_CHUNKS);
        const numTasks = this.endSegmentIndex - this.startSegmentIndex + 1;
        for (let taskInd = 0; taskInd < numTasks; taskInd++) {
            const [segArray, err] = await this.downloadTask(info, 0, taskInd, numChunks, proof);
            if (err != null) {
                return err;
            }
            fs.appendFileSync(filePath, segArray);
        }
        return null;
    }
}
exports.Downloader = Downloader;
//# sourceMappingURL=Downloader.js.map