"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.Indexer = void 0;
const open_jsonrpc_provider_1 = require("open-jsonrpc-provider");
const index_js_1 = require("../common/index.js");
const index_js_2 = require("../transfer/index.js");
const index_js_3 = require("../node/index.js");
const utils_js_1 = require("../utils.js");
const encryption_js_1 = require("../common/encryption.js");
const decryption_js_1 = require("./decryption.js");
function hexStringToBytes(h) {
    const clean = h.startsWith('0x') ? h.slice(2) : h;
    const out = new Uint8Array(clean.length / 2);
    for (let i = 0; i < out.length; i++) {
        out[i] = parseInt(clean.substr(i * 2, 2), 16);
    }
    return out;
}
// NOTE: `fs` is intentionally NOT imported at the top level so that this
// module is safe to bundle for browser environments.  The two methods that
// actually need `fs` (`downloadFragments`, `downloadSingle`) import it
// dynamically at call time.
class Indexer extends open_jsonrpc_provider_1.HttpProvider {
    constructor(url) {
        super({ url });
    }
    async getShardedNodes() {
        const res = await super.request({
            method: 'indexer_getShardedNodes',
        });
        return res;
    }
    async getNodeLocations() {
        const res = await super.request({
            method: 'indexer_getNodeLocations',
        });
        return res;
    }
    async getFileLocations(rootHash) {
        const res = await super.request({
            method: 'indexer_getFileLocations',
            params: [rootHash],
        });
        // The indexer may legitimately return null (e.g. for a root hash that
        // has no known locations yet). Normalize to [] so callers can treat
        // "no results" uniformly and don't crash reading `.length` / `.map`.
        return (res ?? []);
    }
    // ─── Node selection ───────────────────────────────────────────────────
    /**
     * Select `expectedReplica` complete sharding sets from the indexer's
     * trusted nodes and return them as StorageNode clients.
     *
     * @param expectedReplica  Number of full replicas required.
     * @param method           Node ordering before selection (default 'min').
     */
    async selectNodes(expectedReplica, method = 'min') {
        const nodes = await this.getShardedNodes();
        const [trusted, ok] = (0, index_js_1.selectNodes)(nodes.trusted, expectedReplica, method);
        if (!ok) {
            return [
                [],
                new Error('cannot select a subset from the returned nodes that meets the replication requirement'),
            ];
        }
        const clients = trusted.map((node) => new index_js_3.StorageNode(node.url));
        return [clients, null];
    }
    // ─── Upload ───────────────────────────────────────────────────────────
    async newUploaderFromIndexerNodes(blockchain_rpc, signer, expectedReplica, opts) {
        const [clients, err] = await this.selectNodes(expectedReplica, 'min');
        if (err != null) {
            return [null, err];
        }
        const status = await clients[0].getStatus();
        if (status == null) {
            return [
                null,
                new Error('failed to get status from the selected node'),
            ];
        }
        console.log('First selected node status :', status);
        const flow = (0, utils_js_1.getFlowContract)(status.networkIdentity.flowAddress, signer);
        console.log('Selected nodes:', clients);
        const uploader = new index_js_2.Uploader(clients, blockchain_rpc, flow, opts?.gasPrice, opts?.gasLimit);
        return [uploader, null];
    }
    async upload(file, blockchain_rpc, signer, uploadOpts, retryOpts, opts) {
        console.log(`Starting upload for file of size: ${file.size()} bytes`);
        const mergedOpts = (0, index_js_2.mergeUploadOptions)(uploadOpts);
        console.log(`Upload options:`, mergedOpts);
        const [uploader, err] = await this.newUploaderFromIndexerNodes(blockchain_rpc, signer, mergedOpts.expectedReplica, opts);
        if (err != null || uploader == null) {
            console.error(`Failed to create uploader: ${err?.message}`);
            return [{ txHash: '', rootHash: '', txSeq: 0 }, err];
        }
        console.log(`Using splitable upload (handles both single and fragment cases)`);
        console.log(`File details - size: ${file.size()}, numSegments: ${file.numSegments()}, numChunks: ${file.numChunks()}`);
        const [result, uploadErr] = await uploader.splitableUpload(file, mergedOpts, retryOpts);
        if (uploadErr != null) {
            console.error(`Upload failed with error:`, uploadErr.message);
            console.error(`Error stack:`, uploadErr.stack);
            return [{ txHash: '', rootHash: '', txSeq: 0 }, uploadErr];
        }
        if (result.txHashes.length === 1 && result.rootHashes.length === 1) {
            console.log(`Single file upload completed - returning single result`);
            return [
                {
                    txHash: result.txHashes[0],
                    rootHash: result.rootHashes[0],
                    txSeq: result.txSeqs[0],
                },
                null,
            ];
        }
        else {
            console.log(`Fragment upload completed - returning ${result.txHashes.length} fragments`);
            return [result, null];
        }
    }
    async download(rootHashOrHashes, filePath, proof = false) {
        console.log(`Starting download to: ${filePath}, proof: ${proof}`);
        if (Array.isArray(rootHashOrHashes)) {
            console.log(`Downloading ${rootHashOrHashes.length} fragments:`, rootHashOrHashes);
            return await this.downloadFragments(rootHashOrHashes, filePath, proof);
        }
        else {
            console.log(`Downloading single file with root hash: ${rootHashOrHashes}`);
            return await this.downloadSingle(rootHashOrHashes, filePath, proof);
        }
    }
    async downloadFragments(rootHashes, filePath, proof) {
        // Dynamic import — keeps `fs` out of browser bundles
        const fs = await Promise.resolve().then(() => __importStar(require(/* webpackIgnore: true */ 'fs')));
        let outFile;
        try {
            outFile = fs.createWriteStream(filePath);
        }
        catch (err) {
            return new Error(`Failed to create output file: ${err instanceof Error ? err.message : String(err)}`);
        }
        try {
            for (const rootHash of rootHashes) {
                console.log(`Processing fragment: ${rootHash}`);
                const tempFile = `${rootHash}.temp`;
                const [downloader, err] = await this.newDownloaderFromIndexerNodes(rootHash);
                if (err !== null || downloader === null) {
                    outFile.destroy();
                    return new Error(`Failed to create downloader for ${rootHash}: ${err?.message}`);
                }
                const downloadErr = await downloader.download(rootHash, tempFile, proof);
                if (downloadErr !== null) {
                    outFile.destroy();
                    return new Error(`Failed to download fragment ${rootHash}: ${downloadErr.message}`);
                }
                try {
                    const inFile = fs.createReadStream(tempFile);
                    await new Promise((resolve, reject) => {
                        inFile.pipe(outFile, { end: false });
                        inFile.on('end', resolve);
                        inFile.on('error', reject);
                    });
                }
                catch (err) {
                    outFile.destroy();
                    return new Error(`Failed to copy content from temp file ${tempFile}: ${err instanceof Error ? err.message : String(err)}`);
                }
                try {
                    fs.unlinkSync(tempFile);
                }
                catch (err) {
                    console.warn(`Failed to delete temp file ${tempFile}: ${err instanceof Error ? err.message : String(err)}`);
                }
            }
            outFile.end();
            return null;
        }
        catch (err) {
            outFile.destroy();
            return new Error(`Fragment download failed: ${err instanceof Error ? err.message : String(err)}`);
        }
    }
    async downloadSingle(rootHash, filePath, proof) {
        const [downloader, err] = await this.newDownloaderFromIndexerNodes(rootHash);
        if (err !== null || downloader === null) {
            return new Error(`Failed to create downloader: ${err?.message}`);
        }
        return await downloader.download(rootHash, filePath, proof);
    }
    async downloadToBlob(rootHashOrHashes, opts = {}) {
        if (Array.isArray(rootHashOrHashes)) {
            return this.downloadFragmentsToBlob(rootHashOrHashes, opts);
        }
        else {
            return this.downloadSingleToBlob(rootHashOrHashes, opts);
        }
    }
    // Downloads each root raw, then (if `decryption` is supplied) coordinates
    // a single cross-fragment AES-CTR decrypt using the header from root[0].
    // Mirrors 0g-storage-client indexer/client.go:390. Best-effort: on any
    // decrypt-related failure (missing header, wrong key, non-curve ephemeral
    // pubkey), falls back to the raw-concat path silently.
    async downloadFragmentsToBlob(rootHashes, opts) {
        // Step 1 — download all fragments raw.
        const rawParts = [];
        for (const rootHash of rootHashes) {
            const [blob, err] = await this.downloadSingleToBlob(rootHash, {
                proof: opts.proof,
                // force raw: ignore decryption on per-fragment download; we
                // coordinate the decrypt across fragments below.
                decryption: undefined,
            });
            if (err !== null)
                return [new Blob(), err];
            rawParts.push(new Uint8Array(await blob.arrayBuffer()));
        }
        if (!opts.decryption) {
            return [new Blob(rawParts), null];
        }
        // Step 2 — parse header from fragment 0, resolve key, decrypt each
        // fragment with the correct CTR offset.
        const symmetricKey = typeof opts.decryption.symmetricKey === 'string'
            ? hexStringToBytes(opts.decryption.symmetricKey)
            : opts.decryption.symmetricKey;
        const privateKey = opts.decryption.privateKey;
        const plaintexts = (0, decryption_js_1.tryDecryptFragments)(rawParts, symmetricKey, privateKey);
        if (plaintexts === null) {
            // Fall back: not encrypted, wrong key, or malformed — return raw.
            return [new Blob(rawParts), null];
        }
        return [new Blob(plaintexts), null];
    }
    async downloadSingleToBlob(rootHash, opts) {
        const [downloader, err] = await this.newDownloaderFromIndexerNodes(rootHash);
        if (err !== null || downloader === null) {
            return [
                new Blob(),
                new Error(`Failed to create downloader: ${err?.message}`),
            ];
        }
        const [rawBlob, dlErr] = await downloader.downloadToBlob(rootHash, opts.proof ?? false);
        if (dlErr !== null)
            return [new Blob(), dlErr];
        if (!opts.decryption)
            return [rawBlob, null];
        // Best-effort decrypt: if the file is encrypted and the caller
        // supplied the matching key, return plaintext; otherwise return the
        // raw bytes unchanged (see DownloadOption.decryption).
        const encrypted = new Uint8Array(await rawBlob.arrayBuffer());
        const symmetricKey = typeof opts.decryption.symmetricKey === 'string'
            ? hexStringToBytes(opts.decryption.symmetricKey)
            : opts.decryption.symmetricKey;
        const { bytes, decrypted } = (0, decryption_js_1.tryDecrypt)(encrypted, {
            symmetricKey,
            privateKey: opts.decryption.privateKey,
        });
        if (!decrypted)
            return [rawBlob, null];
        // Cast: Uint8Array<ArrayBufferLike> vs strict BlobPart
        return [new Blob([bytes]), null];
    }
    /**
     * Peek at a file's first bytes to see whether it carries a v1 / v2
     * encryption header, without downloading the whole file.
     *
     * Returns the parsed EncryptionHeader on match, or null if the file is
     * not encrypted (or the header is malformed). Use this to render
     * decryption UI (e.g. a key-input field) before the full download.
     */
    async peekHeader(rootHash) {
        const [downloader, err] = await this.newDownloaderFromIndexerNodes(rootHash);
        if (err !== null || downloader === null) {
            return [
                null,
                new Error(`Failed to create downloader: ${err?.message}`),
            ];
        }
        const [rawBlob, dlErr] = await downloader.downloadToBlob(rootHash, false);
        if (dlErr !== null)
            return [null, dlErr];
        // We only need the first 50 bytes for a v2 header (or 17 for v1).
        // The downloader currently always returns the full file; slicing
        // avoids copying the whole body into Uint8Array.
        const prefixSize = Math.min(50, rawBlob.size);
        const prefix = new Uint8Array(await rawBlob.slice(0, prefixSize).arrayBuffer());
        try {
            return [(0, encryption_js_1.parseEncryptionHeader)(prefix), null];
        }
        catch {
            return [null, null];
        }
    }
    // ─── Internal helpers ─────────────────────────────────────────────────
    /**
     * Creates a Downloader whose node list is the minimal covering set for
     * `rootHash`, selected with the 'random' method for load balancing.
     */
    async newDownloaderFromIndexerNodes(rootHash) {
        console.log(`Getting file locations for root hash: ${rootHash}`);
        const locations = await this.getFileLocations(rootHash);
        console.log(`Found ${locations.length} locations for ${rootHash}:`, locations.map((l) => l.url));
        if (locations.length === 0) {
            return [
                null,
                new Error(`No locations found for root hash: ${rootHash}`),
            ];
        }
        // Pick one complete covering set, shuffled for load balancing
        const [selected, ok] = (0, index_js_1.selectNodes)(locations, 1, 'random');
        if (!ok) {
            return [
                null,
                new Error(`Cannot form a complete shard covering set for ${rootHash}`),
            ];
        }
        console.log(`Selected ${selected.length} of ${locations.length} nodes for ${rootHash}`);
        const clients = selected.map((node) => new index_js_3.StorageNode(node.url));
        return [new index_js_2.Downloader(clients), null];
    }
}
exports.Indexer = Indexer;
//# sourceMappingURL=Indexer.js.map