"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Monitor = void 0;
const utilityHelpers_1 = require("../utility/utilityHelpers");
const TaskPurge_1 = require("./tasks/TaskPurge");
const TaskReviewStatus_1 = require("./tasks/TaskReviewStatus");
const TaskFailAbandoned_1 = require("./tasks/TaskFailAbandoned");
const TaskCheckForProofs_1 = require("./tasks/TaskCheckForProofs");
const TaskClock_1 = require("./tasks/TaskClock");
const TaskNewHeader_1 = require("./tasks/TaskNewHeader");
const TaskMonitorCallHistory_1 = require("./tasks/TaskMonitorCallHistory");
const TaskReorg_1 = require("./tasks/TaskReorg");
const TaskArcSSE_1 = require("./tasks/TaskArcSSE");
const TaskMineBlock_1 = require("./tasks/TaskMineBlock");
const TaskSendWaiting_1 = require("./tasks/TaskSendWaiting");
const TaskCheckNoSends_1 = require("./tasks/TaskCheckNoSends");
const TaskUnFail_1 = require("./tasks/TaskUnFail");
const TaskReviewUtxos_1 = require("./tasks/TaskReviewUtxos");
const TaskReviewDoubleSpends_1 = require("./tasks/TaskReviewDoubleSpends");
const TaskReviewProvenTxs_1 = require("./tasks/TaskReviewProvenTxs");
const WERR_errors_1 = require("../sdk/WERR_errors");
const WalletError_1 = require("../sdk/WalletError");
const Services_1 = require("../services/Services");
/**
 * Background task to make sure transactions are processed, transaction proofs are received and propagated,
 * and potentially that reorgs update proofs that were already received.
 */
class Monitor {
    static createDefaultWalletMonitorOptions(chain, storage, services, chaintracks, startupTaskMode = 'none') {
        services || (services = new Services_1.Services(chain));
        if (!services.options.chaintracks)
            throw new WERR_errors_1.WERR_INVALID_PARAMETER('services.options.chaintracks', 'valid');
        const o = {
            chain,
            services,
            storage,
            msecsWaitPerMerkleProofServiceReq: 500,
            taskRunWaitMsecs: 5000,
            abandonedMsecs: 1000 * 60 * 5,
            unprovenAttemptsLimitTest: 10,
            unprovenAttemptsLimitMain: 144,
            chaintracks: services.options.chaintracks,
            chaintracksWithEvents: chaintracks,
            startupTaskMode
        };
        return o;
    }
    constructor(options) {
        /**
         * _tasks are typically run by the scheduler but may also be run by runTask.
         */
        this._tasks = [];
        /**
         * _otherTasks can be run by runTask but not by scheduler.
         */
        this._otherTasks = [];
        this._tasksRunning = false;
        this.defaultPurgeParams = {
            purgeSpent: false,
            purgeCompleted: false,
            purgeFailed: true,
            purgeSpentAge: 2 * Monitor.oneWeek,
            purgeCompletedAge: 2 * Monitor.oneWeek,
            purgeFailedAge: 5 * Monitor.oneDay
        };
        this._runAsyncSetup = true;
        this.resolveCompletion = undefined;
        this.deactivatedHeaders = [];
        this.options = { ...options };
        this.services = options.services;
        this.chain = this.services.chain;
        this.storage = options.storage;
        this.chaintracks = options.chaintracks;
        this.chaintracksWithEvents = options.chaintracksWithEvents;
        this.onTransactionProven = options.onTransactionProven;
        this.onTransactionBroadcasted = options.onTransactionBroadcasted;
        this.onTransactionStatusChanged = options.onTransactionStatusChanged;
        if (this.chaintracksWithEvents) {
            const c = this.chaintracksWithEvents;
            this.reorgSubscriptionPromise = c.subscribeReorgs(this.processReorg.bind(this));
            this.headersSubscriptionPromise = c.subscribeHeaders(this.processHeader.bind(this));
        }
        this.applyStartupTaskMode(options.startupTaskMode || 'none');
    }
    applyStartupTaskMode(mode) {
        switch (mode) {
            case 'default':
                this.addDefaultTasks();
                break;
            case 'multiuser':
                this.addMultiUserTasks();
                break;
            case 'alltoother':
                this.addAllTasksToOther();
                break;
            case 'none':
                break;
            default:
                throw new WERR_errors_1.WERR_INVALID_PARAMETER('startupTaskMode', `'none', 'default', 'multiuser', or 'alltoother'`);
        }
    }
    async destroy() {
        if (this.chaintracksWithEvents) {
            const c = this.chaintracksWithEvents;
            if (this.reorgSubscriptionPromise)
                await c.unsubscribe(await this.reorgSubscriptionPromise);
            if (this.headersSubscriptionPromise)
                await c.unsubscribe(await this.headersSubscriptionPromise);
        }
    }
    addAllTasksToOther() {
        this._otherTasks.push(new TaskClock_1.TaskClock(this));
        this._otherTasks.push(new TaskNewHeader_1.TaskNewHeader(this));
        this._otherTasks.push(new TaskMonitorCallHistory_1.TaskMonitorCallHistory(this));
        this._otherTasks.push(new TaskSendWaiting_1.TaskSendWaiting(this));
        this._otherTasks.push(new TaskCheckForProofs_1.TaskCheckForProofs(this));
        this._otherTasks.push(new TaskCheckNoSends_1.TaskCheckNoSends(this));
        this._otherTasks.push(new TaskFailAbandoned_1.TaskFailAbandoned(this));
        this._otherTasks.push(new TaskUnFail_1.TaskUnFail(this));
        this._otherTasks.push(new TaskReviewStatus_1.TaskReviewStatus(this));
        this._otherTasks.push(new TaskReorg_1.TaskReorg(this));
        this._otherTasks.push(new TaskReviewUtxos_1.TaskReviewUtxos(this));
        this._otherTasks.push(new TaskReviewDoubleSpends_1.TaskReviewDoubleSpends(this));
        this._otherTasks.push(new TaskReviewProvenTxs_1.TaskReviewProvenTxs(this));
        this._otherTasks.push(new TaskPurge_1.TaskPurge(this, this.defaultPurgeParams));
        //this._otherTasks.push(new TaskSyncWhenIdle(this))
        if (this.chain === 'mock') {
            this._otherTasks.push(new TaskMineBlock_1.TaskMineBlock(this));
        }
    }
    /**
     * Default tasks with settings appropriate for a single user storage
     */
    addDefaultTasks() {
        this._tasks.push(new TaskClock_1.TaskClock(this));
        this._tasks.push(new TaskNewHeader_1.TaskNewHeader(this));
        this._tasks.push(new TaskMonitorCallHistory_1.TaskMonitorCallHistory(this));
        this._tasks.push(new TaskSendWaiting_1.TaskSendWaiting(this, 8 * Monitor.oneSecond, 7 * Monitor.oneSecond)); // Check every 8 seconds but must be 7 seconds old
        this._tasks.push(new TaskCheckForProofs_1.TaskCheckForProofs(this, 2 * Monitor.oneHour)); // Every two hours if no block found
        this._tasks.push(new TaskCheckNoSends_1.TaskCheckNoSends(this));
        this._tasks.push(new TaskFailAbandoned_1.TaskFailAbandoned(this, 8 * Monitor.oneMinute));
        this._tasks.push(new TaskUnFail_1.TaskUnFail(this));
        this._tasks.push(new TaskReviewStatus_1.TaskReviewStatus(this));
        this._tasks.push(new TaskReorg_1.TaskReorg(this));
        this._tasks.push(new TaskReviewDoubleSpends_1.TaskReviewDoubleSpends(this));
        this._tasks.push(new TaskReviewProvenTxs_1.TaskReviewProvenTxs(this));
        this._otherTasks.push(new TaskPurge_1.TaskPurge(this, this.defaultPurgeParams, 6 * Monitor.oneHour));
        this._otherTasks.push(new TaskReviewUtxos_1.TaskReviewUtxos(this));
        this._tasks.push(new TaskArcSSE_1.TaskArcadeSSE(this));
        if (this.chain === 'mock') {
            this._tasks.push(new TaskMineBlock_1.TaskMineBlock(this));
        }
    }
    /**
     * Tasks appropriate for multi-user storage
     */
    addMultiUserTasks() {
        this._tasks.push(new TaskClock_1.TaskClock(this));
        this._tasks.push(new TaskNewHeader_1.TaskNewHeader(this));
        this._tasks.push(new TaskMonitorCallHistory_1.TaskMonitorCallHistory(this));
        this._tasks.push(new TaskSendWaiting_1.TaskSendWaiting(this, 8 * Monitor.oneSecond, 7 * Monitor.oneSecond)); // Check every 8 seconds but must be 7 seconds old
        this._tasks.push(new TaskCheckForProofs_1.TaskCheckForProofs(this, 2 * Monitor.oneHour)); // Every two hours if no block found
        this._tasks.push(new TaskCheckNoSends_1.TaskCheckNoSends(this));
        this._tasks.push(new TaskFailAbandoned_1.TaskFailAbandoned(this, 8 * Monitor.oneMinute));
        this._tasks.push(new TaskUnFail_1.TaskUnFail(this));
        this._tasks.push(new TaskReviewStatus_1.TaskReviewStatus(this));
        this._tasks.push(new TaskReorg_1.TaskReorg(this));
        this._tasks.push(new TaskReviewDoubleSpends_1.TaskReviewDoubleSpends(this));
        this._tasks.push(new TaskReviewProvenTxs_1.TaskReviewProvenTxs(this));
        this._otherTasks.push(new TaskPurge_1.TaskPurge(this, this.defaultPurgeParams));
        this._otherTasks.push(new TaskReviewUtxos_1.TaskReviewUtxos(this));
        if (this.chain === 'mock') {
            this._tasks.push(new TaskMineBlock_1.TaskMineBlock(this));
        }
    }
    addTask(task) {
        if (this._tasks.some(t => t.name === task.name))
            throw new WERR_errors_1.WERR_BAD_REQUEST(`task ${task.name} has already been added.`);
        this._tasks.push(task);
    }
    removeTask(name) {
        this._tasks = this._tasks.filter(t => t.name !== name);
    }
    async runTask(name) {
        let task = this._tasks.find(t => t.name === name);
        let log = '';
        if (!task)
            task = this._otherTasks.find(t => t.name === name);
        if (task) {
            await task.asyncSetup();
            log = await task.runTask();
        }
        return log;
    }
    async runOnce() {
        if (this._runAsyncSetup) {
            for (const t of this._tasks) {
                try {
                    await t.asyncSetup();
                }
                catch (eu) {
                    const e = WalletError_1.WalletError.fromUnknown(eu);
                    const details = `monitor task ${t.name} asyncSetup error ${e.code} ${e.description}`;
                    console.log(details);
                    await this.logEvent('error0', details);
                }
                if (!this._tasksRunning)
                    break;
            }
            this._runAsyncSetup = false;
        }
        if (this.storage.getActive().isStorageProvider()) {
            const tasksToRun = [];
            const now = new Date().getTime();
            for (const t of this._tasks) {
                try {
                    if (t.trigger(now).run)
                        tasksToRun.push(t);
                }
                catch (eu) {
                    const e = WalletError_1.WalletError.fromUnknown(eu);
                    const details = `monitor task ${t.name} trigger error ${e.code} ${e.description}`;
                    console.log(details);
                    await this.logEvent('error0', details);
                }
            }
            for (const ttr of tasksToRun) {
                try {
                    if (this.storage.getActive().isStorageProvider()) {
                        const log = await ttr.runTask();
                        if (log && log.length > 0) {
                            console.log(`Task${ttr.name} ${log.slice(0, 1024)}`);
                            await this.logEvent(ttr.name, log);
                        }
                    }
                }
                catch (eu) {
                    const e = WalletError_1.WalletError.fromUnknown(eu);
                    const details = `monitor task ${ttr.name} runTask error ${e.code} ${e.description}\n${e.stack}`;
                    console.log(details);
                    await this.logEvent('error1', details);
                }
                finally {
                    ttr.lastRunMsecsSinceEpoch = new Date().getTime();
                }
            }
        }
    }
    async startTasks() {
        if (this._tasksRunning)
            throw new WERR_errors_1.WERR_BAD_REQUEST('monitor tasks are already runnining.');
        this._tasksRunning = true;
        this._tasksRunningPromise = new Promise(resolve => {
            this.resolveCompletion = resolve;
        });
        for (; this._tasksRunning;) {
            await this.runOnce();
            // console.log(`${new Date().toISOString()} tasks run, waiting...`)
            await (0, utilityHelpers_1.wait)(this.options.taskRunWaitMsecs);
        }
        if (this.resolveCompletion) {
            this.resolveCompletion();
            this.resolveCompletion = undefined;
        }
    }
    async logEvent(event, details) {
        await this.storage.runAsStorageProvider(async (sp) => {
            await sp.insertMonitorEvent({
                created_at: new Date(),
                updated_at: new Date(),
                id: 0,
                event,
                details
            });
        });
    }
    stopTasks() {
        this._tasksRunning = false;
    }
    /**
     * Process new chain header event received from Chaintracks
     *
     * Kicks processing 'unconfirmed' and 'unmined' request processing.
     *
     * @param reqs
     */
    processNewBlockHeader(header) {
        const h = header;
        this.lastNewHeader = h;
        this.lastNewHeaderWhen = new Date();
        // console.log(`WalletMonitor notified of new block header ${h.height}`)
        // Nudge the proof checker to try again.
        TaskCheckForProofs_1.TaskCheckForProofs.checkNow = true;
    }
    /**
     * This is a function run from a TaskSendWaiting Monitor task.
     *
     * This allows the user of wallet-toolbox to 'subscribe' for transaction broadcast updates.
     *
     * @param broadcastResult
     */
    callOnBroadcastedTransaction(broadcastResult) {
        if (this.onTransactionBroadcasted) {
            this.onTransactionBroadcasted(broadcastResult);
        }
    }
    /**
     * This is a function run from a TaskCheckForProofs Monitor task.
     *
     * This allows the user of wallet-toolbox to 'subscribe' for transaction updates.
     *
     * @param txStatus
     */
    callOnProvenTransaction(txStatus) {
        if (this.onTransactionProven) {
            this.onTransactionProven(txStatus);
        }
    }
    /**
     * Called by TaskArcadeSSE when an SSE status event is received from Arcade.
     */
    callOnTransactionStatusChanged(txid, newStatus) {
        if (this.onTransactionStatusChanged) {
            this.onTransactionStatusChanged(txid, newStatus);
        }
    }
    /**
     * Fetch pending transaction status events from Arcade on demand.
     * Call this on app open, balance refresh, transaction list view, etc.
     */
    async fetchSSEEvents() {
        var _a;
        const sseTask = this._tasks.find(t => t.name === TaskArcSSE_1.TaskArcadeSSE.taskName);
        return (_a = (await (sseTask === null || sseTask === void 0 ? void 0 : sseTask.fetchNow()))) !== null && _a !== void 0 ? _a : 0;
    }
    /**
     * Process reorg event received from Chaintracks
     *
     * Reorgs can move recent transactions to new blocks at new index positions.
     * Affected transaction proofs become invalid and must be updated.
     *
     * It is possible for a transaction to become invalid.
     *
     * Coinbase transactions always become invalid.
     */
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    processReorg(depth, oldTip, newTip, deactivatedHeaders) {
        if (deactivatedHeaders) {
            for (const header of deactivatedHeaders) {
                this.deactivatedHeaders.push({
                    whenMsecs: Date.now(),
                    tries: 0,
                    header
                });
            }
        }
    }
    /**
     * Handler for new header events from Chaintracks.
     *
     * To minimize reorg processing, new headers are aged before processing via TaskNewHeader.
     * Therefore this handler is intentionally a no-op.
     *
     * @param header
     */
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    processHeader(header) { }
}
exports.Monitor = Monitor;
Monitor.oneSecond = 1000;
Monitor.oneMinute = 60 * Monitor.oneSecond;
Monitor.oneHour = 60 * Monitor.oneMinute;
Monitor.oneDay = 24 * Monitor.oneHour;
Monitor.oneWeek = 7 * Monitor.oneDay;
//# sourceMappingURL=Monitor.js.map