import { KeetaAnchorQueueRunner } from "./queue/index.js";
import { KeetaNet } from "../client/index.js";
import { ConvertStringToRequestID } from "./queue/internal.js";
export class BlockListener {
    #config;
    #blockListeners = [];
    constructor(config) {
        this.#config = config;
    }
    get #client() {
        return (this.#config.client);
    }
    get logger() {
        return (this.#config.logger);
    }
    async #runWithLog(label, func) {
        try {
            await func();
        }
        catch (error) {
            this.logger?.error(`BlockListener::${label}`, `Received Error running`, error);
        }
    }
    async #processBlockListeners(block) {
        let listenersHaveWork = false;
        const promises = [];
        for (const listener of this.#blockListeners) {
            promises.push(this.#runWithLog('block callback', async function () {
                const response = await listener.callback({ block });
                if (response.requiresWork) {
                    listenersHaveWork = true;
                }
            }));
        }
        await Promise.allSettled(promises);
        return ({ listenersHaveWork });
    }
    async scan(options) {
        let listenersHaveWork = false;
        try {
            let reachedEndOfTime = false;
            let startBlocksHash;
            let pageCount = 0;
            /*
             * Either perform an extended
             * scan (30 days) or a short
             * scan (4 hours) to find
             * relevant operations
             */
            const now = Date.now();
            let oldestVoteStapleToCheck = now - (4 * 60 * 60 * 1000); // 4 hours ago
            if (options?.searchTo) {
                if (typeof options.searchTo === 'number') {
                    if (options.searchTo >= now || options.searchTo <= 0) {
                        throw (new Error('BlockListener::scan: When providing searchTo as a number, it must be a timestamp in the past'));
                    }
                    oldestVoteStapleToCheck = options.searchTo;
                }
                else if ('extended' in options.searchTo) {
                    oldestVoteStapleToCheck = now - (30 * 24 * 60 * 60 * 1000); // 30 days ago
                }
                else {
                    throw (new Error('BlockListener::scan: Invalid searchTo option provided'));
                }
            }
            while (!reachedEndOfTime) {
                pageCount++;
                const historyOptions = { depth: 20, ...(startBlocksHash ? { startBlocksHash } : {}) };
                this.logger?.debug('BlockListener::poll', `Fetching history page ${pageCount}`, { ...historyOptions });
                const history = await this.#client.getHistory(null, historyOptions);
                if (history.length === 0) {
                    this.logger?.debug('BlockListener::poll', `No more history found, ending discovery`);
                    break;
                }
                const processBlocksPromises = [];
                for (const { voteStaple } of history) {
                    if (voteStaple.timestamp().valueOf() < oldestVoteStapleToCheck) {
                        this.logger?.debug('BlockListener::poll', 'Reached vote staple older than scanning time, ending discovery for this run', { voteStapleTime: voteStaple.timestamp(), oldestVoteStapleToCheck: new Date(oldestVoteStapleToCheck) });
                        reachedEndOfTime = true;
                        break;
                    }
                    for (const block of voteStaple.blocks) {
                        processBlocksPromises.push(this.#processBlockListeners(block));
                    }
                }
                const processBlockPromiseResults = await Promise.all(processBlocksPromises);
                if (processBlockPromiseResults.some(result => result.listenersHaveWork)) {
                    listenersHaveWork = true;
                }
                // Set up for next page
                const lastVoteStaple = history[history.length - 1]?.voteStaple;
                if (lastVoteStaple) {
                    startBlocksHash = lastVoteStaple.blocksHash;
                }
                else {
                    break;
                }
            }
        }
        catch (error) {
            this.logger?.error('BlockListener::poll', 'Error during discovery:', error);
        }
        return ({ listenersHaveWork });
    }
    on(_ignore_type, args) {
        const id = String(Math.random());
        this.#blockListeners.push({ ...args, id });
        return ({
            remove: () => {
                const index = this.#blockListeners.findIndex(listener => listener.id === id);
                if (index !== -1) {
                    this.#blockListeners.splice(index, 1);
                }
            }
        });
    }
}
class BaseBlockOperationQueueRunner extends KeetaAnchorQueueRunner {
    #listener;
    #lastExtendedScanTime = null;
    #lastScanTime = null;
    #scanOptions;
    #removeListenerCallback;
    constructor(config) {
        const { listener, ...restConfig } = config;
        super(restConfig);
        if (listener instanceof BlockListener) {
            this.#listener = listener;
        }
        else {
            this.#listener = new BlockListener(listener);
        }
        const addedListener = this.#listener.on('block', {
            callback: async ({ block }) => {
                return ({ requiresWork: await this.onBlockSeen(block) });
            }
        });
        this.#removeListenerCallback = addedListener.remove;
        if (!config.scanOptions || config.scanOptions?.scanWhenRunning || config.scanOptions?.scanWhenRunning === undefined) {
            this.#scanOptions = {
                scanWhenRunning: true,
                extendedScanIntervalMs: 60 * 60 * 1000, // 1 hour
                regularScanIntervalMs: 5 * 60 * 1000, // 5 minutes
                ...config.scanOptions
            };
        }
        else {
            this.#scanOptions = { scanWhenRunning: false };
        }
    }
    decodeResponse(response) {
        if (response === null) {
            return (null);
        }
        else {
            throw (new Error('BlockQueueRunner::decodeResponse: Not implemented when response is not null, you must override this method in your subclass'));
        }
    }
    encodeResponse(response) {
        if (response === null) {
            // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
            return null;
        }
        else {
            throw (new Error('BlockQueueRunner::encodeResponse: Not implemented when response is not null, you must override this method in your subclass'));
        }
    }
    async run(options) {
        let scanYieldedWork = false;
        if (this.#scanOptions.scanWhenRunning) {
            const now = Date.now();
            let extendedScanDue = false;
            let dueForScan = false;
            if (!this.#lastScanTime || (now - this.#lastScanTime > this.#scanOptions.regularScanIntervalMs)) {
                dueForScan = true;
            }
            if (!this.#lastExtendedScanTime || (now - this.#lastExtendedScanTime > this.#scanOptions.extendedScanIntervalMs)) {
                extendedScanDue = true;
            }
            if (extendedScanDue || dueForScan) {
                let searchTo;
                if (extendedScanDue) {
                    searchTo = { extended: true };
                }
                else if (this.#lastScanTime) {
                    searchTo = this.#lastScanTime;
                }
                const scanResult = await this.#listener.scan({ searchTo });
                this.#lastScanTime = now;
                if (extendedScanDue) {
                    this.#lastExtendedScanTime = now;
                }
                scanYieldedWork = scanResult.listenersHaveWork;
            }
        }
        const moreRunWork = await super.run(options);
        return (moreRunWork || scanYieldedWork);
    }
    [Symbol.dispose]() {
        this.#removeListenerCallback();
    }
}
export class BlockQueueRunner extends BaseBlockOperationQueueRunner {
    decodeRequest(request) {
        return ({ blockHash: new KeetaNet.lib.Block.Hash(request.blockHash) });
    }
    encodeRequest(request) {
        return ({ blockHash: request.blockHash.toString() });
    }
    async onBlockSeen(block) {
        const shouldInclude = await this.filterBlock(block);
        if (shouldInclude) {
            await this.add({ blockHash: block.hash });
        }
        return (shouldInclude);
    }
    add(request, info) {
        const requestIdString = ConvertStringToRequestID(request.blockHash.toString().toLowerCase());
        if (info?.id && String(info.id) !== String(requestIdString)) {
            throw (new Error(`OperationQueueRunner::add: When providing an "id" in info, it must match the blockHash of the request. Expected ${String(requestIdString)}, got ${String(info.id)}`));
        }
        return (super.add(request, { ...info, id: requestIdString }));
    }
}
export class OperationQueueRunner extends BaseBlockOperationQueueRunner {
    decodeRequest(request) {
        return ({ blockHash: new KeetaNet.lib.Block.Hash(request.blockHash), operationIndex: request.operationIndex });
    }
    encodeRequest(request) {
        return ({ blockHash: request.blockHash.toString(), operationIndex: Number(request.operationIndex) });
    }
    async onBlockSeen(block) {
        const addPromises = [];
        let anyShouldInclude = false;
        for (let operationIndex = 0; operationIndex < block.operations.length; operationIndex++) {
            const operation = block.operations[operationIndex];
            if (!operation) {
                continue;
            }
            addPromises.push((async () => {
                const shouldInclude = await this.filterOperation(operation, { block, operationIndex });
                if (shouldInclude) {
                    anyShouldInclude = true;
                    await this.add({ blockHash: block.hash, operationIndex });
                }
            })());
        }
        await Promise.all(addPromises);
        return (anyShouldInclude);
    }
    add(request, info) {
        const requestIdString = ConvertStringToRequestID(`${request.blockHash.toString()}-${String(request.operationIndex)}`.toLowerCase());
        if (info?.id && String(info.id) !== String(requestIdString)) {
            throw (new Error(`OperationQueueRunner::add: When providing an "id" in info, it must match the blockHash and operationIndex of the request. Expected ${String(requestIdString)}, got ${String(info.id)}`));
        }
        return (super.add(request, { ...info, id: requestIdString }));
    }
}
//# sourceMappingURL=block-listener.js.map