import { getDatabase, } from "./schema.js";
import { upsertTransactionRecord, insertTransactionScript, } from "./transactions.js";
import { upsertInputNote, upsertOutputNote } from "./notes.js";
import { applyFullAccountState } from "./accounts.js";
import { logWebStoreError, uint8ArrayToBase64 } from "./utils.js";
export async function getNoteTags(dbId) {
    try {
        const db = getDatabase(dbId);
        let records = await db.tags.toArray();
        let processedRecords = records.map((record) => {
            record.sourceNoteId =
                record.sourceNoteId == "" ? undefined : record.sourceNoteId;
            record.sourceAccountId =
                record.sourceAccountId == "" ? undefined : record.sourceAccountId;
            record.sourceSubscriptionKey =
                record.sourceSubscriptionKey == ""
                    ? undefined
                    : record.sourceSubscriptionKey;
            return record;
        });
        return processedRecords;
    }
    catch (error) {
        logWebStoreError(error, "Error fetch tag record");
    }
}
export async function getSyncHeight(dbId) {
    try {
        const db = getDatabase(dbId);
        const record = await db.stateSync.get(1);
        if (record) {
            let data = {
                blockNum: record.blockNum,
            };
            return data;
        }
        else {
            return null;
        }
    }
    catch (error) {
        logWebStoreError(error, "Error fetching sync height");
    }
}
export async function addNoteTag(dbId, tag, sourceNoteId, sourceAccountId, sourceSubscriptionKey) {
    try {
        const db = getDatabase(dbId);
        let tagArray = new Uint8Array(tag);
        let tagBase64 = uint8ArrayToBase64(tagArray);
        await db.tags.add({
            tag: tagBase64,
            sourceNoteId: sourceNoteId ? sourceNoteId : "",
            sourceAccountId: sourceAccountId ? sourceAccountId : "",
            sourceSubscriptionKey: sourceSubscriptionKey ? sourceSubscriptionKey : "",
        });
    }
    catch (error) {
        logWebStoreError(error, "Failed to add note tag");
    }
}
export async function removeNoteTag(dbId, tag, sourceNoteId, sourceAccountId, sourceSubscriptionKey) {
    try {
        const db = getDatabase(dbId);
        let tagArray = new Uint8Array(tag);
        let tagBase64 = uint8ArrayToBase64(tagArray);
        const subscriptionKey = sourceSubscriptionKey ? sourceSubscriptionKey : "";
        return await db.tags
            .where({
            tag: tagBase64,
            sourceNoteId: sourceNoteId ? sourceNoteId : "",
            sourceAccountId: sourceAccountId ? sourceAccountId : "",
        })
            // Filtered in JS rather than via the `where` clause: rows written
            // before the column existed lack the property entirely, and a
            // `where` equality on `""` would never match them.
            .and((record) => (record.sourceSubscriptionKey ?? "") == subscriptionKey)
            .delete();
    }
    catch (error) {
        logWebStoreError(error, "Failed to remove note tag");
    }
}
export async function applyStateSync(dbId, stateUpdate) {
    const db = getDatabase(dbId);
    const { blockNum, flattenedNewBlockHeaders, partialBlockchainPeaks, newBlockNums, blockHasRelevantNotes, serializedNodeIds, serializedNodes, committedNoteTagSources, serializedInputNotes, serializedOutputNotes, accountUpdates, transactionUpdates, } = stateUpdate;
    const newBlockHeaders = reconstructFlattenedVec(flattenedNewBlockHeaders);
    const tablesToAccess = [
        db.stateSync,
        db.inputNotes,
        db.outputNotes,
        db.notesScripts,
        db.transactions,
        db.transactionScripts,
        db.blockHeaders,
        db.partialBlockchainNodes,
        db.tags,
        db.latestAccountHeaders,
        db.historicalAccountHeaders,
        db.latestAccountStorages,
        db.historicalAccountStorages,
        db.latestStorageMapEntries,
        db.historicalStorageMapEntries,
        db.latestAccountAssets,
        db.historicalAccountAssets,
    ];
    return await db.dexie.transaction("rw", tablesToAccess, async (tx) => {
        await Promise.all([
            Promise.all(serializedInputNotes.map((note) => {
                return upsertInputNote(dbId, note.detailsCommitment, note.noteId, note.noteAssets, note.attachments, note.serialNumber, note.inputs, note.noteScriptRoot, note.noteScript, note.nullifier, note.createdAt, note.stateDiscriminant, note.state, note.consumedBlockHeight, note.consumedTxOrder, note.consumerAccountId);
            })),
            Promise.all(serializedOutputNotes.map((note) => {
                return upsertOutputNote(dbId, note.detailsCommitment, note.noteId, note.noteAssets, note.attachments, note.recipientDigest, note.metadata, note.nullifier, note.expectedHeight, note.stateDiscriminant, note.state);
            })),
            Promise.all(transactionUpdates.map((transactionRecord) => {
                let promises = [
                    upsertTransactionRecord(dbId, transactionRecord.id, transactionRecord.details, transactionRecord.blockNum, transactionRecord.statusVariant, transactionRecord.status, transactionRecord.scriptRoot),
                ];
                if (transactionRecord.scriptRoot && transactionRecord.txScript) {
                    promises.push(insertTransactionScript(dbId, transactionRecord.scriptRoot, transactionRecord.txScript));
                }
                return Promise.all(promises);
            })),
            Promise.all(accountUpdates.map((accountUpdate) => applyFullAccountState(dbId, {
                accountId: accountUpdate.accountId,
                nonce: accountUpdate.nonce,
                storageSlots: accountUpdate.storageSlots,
                storageMapEntries: accountUpdate.storageMapEntries,
                assets: accountUpdate.assets,
                codeRoot: accountUpdate.codeRoot,
                storageRoot: accountUpdate.storageRoot,
                vaultRoot: accountUpdate.vaultRoot,
                committed: accountUpdate.committed,
                accountCommitment: accountUpdate.accountCommitment,
                accountSeed: accountUpdate.accountSeed,
            }))),
            updateSyncHeight(tx, blockNum),
            updatePartialBlockchainNodes(tx, serializedNodeIds, serializedNodes),
            updateCommittedNoteTags(tx, committedNoteTagSources),
            Promise.all(newBlockHeaders.map((newBlockHeader, i) => {
                // Peaks are attached only to the chain-tip block (the one whose
                // blockNum matches the new sync height). That row is always
                // present in this iteration because `partial_blockchain_updates`
                // includes the chain tip header by construction.
                // TODO: potentially move this to be under the sync state info table
                // as currently done in SQLite
                const peaks = newBlockNums[i] === blockNum ? partialBlockchainPeaks : undefined;
                return updateBlockHeader(tx, newBlockNums[i], newBlockHeader, blockHasRelevantNotes[i] == 1, peaks);
            })),
        ]);
    });
}
/**
 * Advances `stateSync.blockNum` only when moving forward. Mirrors SQLite's
 * `UPDATE blockchain_checkpoint ... WHERE block_num < ?`.
 */
async function updateSyncHeight(tx, blockNum) {
    try {
        const current = await tx.stateSync.get(1);
        if (!current || current.blockNum < blockNum) {
            await tx.stateSync.update(1, {
                blockNum: blockNum,
            });
        }
    }
    catch (error) {
        logWebStoreError(error, "Failed to update sync height");
    }
}
async function updateBlockHeader(tx, blockNum, blockHeader, hasClientNotes, partialBlockchainPeaks) {
    try {
        const data = {
            blockNum: blockNum,
            header: blockHeader,
            hasClientNotes: hasClientNotes.toString(),
            ...(partialBlockchainPeaks !== undefined && { partialBlockchainPeaks }),
        };
        const existingBlockHeader = await tx.blockHeaders.get(blockNum);
        if (!existingBlockHeader) {
            await tx.blockHeaders.add(data);
        }
    }
    catch (err) {
        /* v8 ignore next 2 -- IDB errors inside a Dexie tx abort the whole tx; this catch is unreachable */
        logWebStoreError(err, "Failed to insert block header");
    }
}
async function updatePartialBlockchainNodes(tx, nodeIndexes, nodes) {
    try {
        if (nodeIndexes.length !== nodes.length) {
            throw new Error("nodeIndexes and nodes arrays must be of the same length");
        }
        if (nodeIndexes.length === 0) {
            return;
        }
        const data = nodes.map((node, index) => ({
            id: Number(nodeIndexes[index]),
            node: node,
        }));
        // Use bulkPut to add/overwrite the entries
        await tx.partialBlockchainNodes.bulkPut(data);
    }
    catch (err) {
        logWebStoreError(err, "Failed to update partial blockchain nodes");
    }
}
async function updateCommittedNoteTags(tx, committedNoteTagSources) {
    try {
        for (let i = 0; i < committedNoteTagSources.length; i++) {
            const tagSource = committedNoteTagSources[i];
            await tx.tags
                .where("sourceNoteId")
                .equals(tagSource)
                .delete();
        }
    }
    catch (error) {
        /* v8 ignore next 2 -- IDB errors inside a Dexie tx abort the whole tx; this catch is unreachable */
        logWebStoreError(error, "Failed to update committed note tags");
    }
}
function reconstructFlattenedVec(flattenedVec) {
    const data = flattenedVec.data();
    const lengths = flattenedVec.lengths();
    let index = 0;
    const result = [];
    lengths.forEach((length) => {
        result.push(data.slice(index, index + length));
        index += length;
    });
    return result;
}
export async function discardTransactions(dbId, transactions) {
    try {
        const db = getDatabase(dbId);
        await db.transactions.where("id").anyOf(transactions).delete();
    }
    catch (err) {
        logWebStoreError(err, "Failed to discard transactions");
    }
}
