"use strict";
/**
 * Nightgate Indexer Service Implementation
 *
 * Exposes sync state, health metrics, and reorg history.
 */
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const cds_1 = __importDefault(require("@sap/cds"));
const { SELECT, UPDATE, DELETE } = cds_1.default.ql;
const cds_model_1 = require("./utils/cds-model");
const nightgate_config_1 = require("./utils/nightgate-config");
const sync_state_1 = require("./utils/sync-state");
const crawler_1 = require("./crawler");
const midnight_1 = require("#cds-models/midnight");
const processStartTime = Date.now();
const metricPrefix = 'odatano_nightgate';
class NightgateIndexerService extends cds_1.default.ApplicationService {
    db;
    resolveCrawlerStartConfig() {
        const { crawlerConfig, crawlerNodeUrl } = (0, nightgate_config_1.resolveNightgateRuntimeConfig)((0, nightgate_config_1.getNightgatePluginConfig)());
        return {
            ...crawlerConfig,
            enabled: true,
            nodeUrl: crawlerNodeUrl,
            requestTimeout: crawlerConfig.requestTimeout || 30000
        };
    }
    async rollbackFromHeight(fromHeight) {
        const blocksToRollback = await this.db.run(SELECT.from(midnight_1.Blocks).where({ height: { '>=': fromHeight } })) || [];
        if (blocksToRollback.length === 0) {
            const forkBlock = await this.db.run(SELECT.one.from(midnight_1.Blocks)
                .where({ height: { '<': fromHeight } })
                .orderBy('height desc'));
            const effectiveStartHeight = forkBlock?.height != null ? Number(forkBlock.height) + 1 : 0;
            return {
                blocksRolledBack: 0,
                transactionsRolledBack: 0,
                effectiveStartHeight
            };
        }
        const blockIds = blocksToRollback.map(b => b.ID).filter(Boolean);
        const txsToDelete = blockIds.length > 0
            ? await this.db.run(SELECT.from(midnight_1.Transactions).columns('ID').where({ block_ID: { in: blockIds } })) || []
            : [];
        const txIds = txsToDelete.map(t => t.ID).filter(Boolean);
        if (txIds.length > 0) {
            const actionsToDelete = await this.db.run(SELECT.from(midnight_1.ContractActions).columns('ID').where({ transaction_ID: { in: txIds } })) || [];
            const actionIds = actionsToDelete.map(a => a.ID).filter(Boolean);
            if (actionIds.length > 0) {
                await this.db.run(DELETE.from(midnight_1.ContractBalances).where({ contractAction_ID: { in: actionIds } }));
            }
            await this.db.run(DELETE.from(midnight_1.ContractActions).where({ transaction_ID: { in: txIds } }));
            await this.db.run(DELETE.from(midnight_1.UnshieldedUtxos).where({ createdAtTransaction_ID: { in: txIds } }));
            await this.db.run(DELETE.from(midnight_1.ZswapLedgerEvents).where({ transaction_ID: { in: txIds } }));
            await this.db.run(DELETE.from(midnight_1.DustLedgerEvents).where({ transaction_ID: { in: txIds } }));
            await this.db.run(DELETE.from(midnight_1.TransactionFees).where({ transaction_ID: { in: txIds } }));
            const resultsToDelete = await this.db.run(SELECT.from(midnight_1.TransactionResults).columns('ID').where({ transaction_ID: { in: txIds } })) || [];
            const resultIds = resultsToDelete.map(r => r.ID).filter(Boolean);
            if (resultIds.length > 0) {
                await this.db.run(DELETE.from(midnight_1.TransactionSegments).where({ transactionResult_ID: { in: resultIds } }));
            }
            await this.db.run(DELETE.from(midnight_1.TransactionResults).where({ transaction_ID: { in: txIds } }));
            await this.db.run(UPDATE.entity(midnight_1.UnshieldedUtxos)
                .set({ spentAtTransaction_ID: null })
                .where({ spentAtTransaction_ID: { in: txIds } }));
            await this.db.run(DELETE.from(midnight_1.Transactions).where({ ID: { in: txIds } }));
        }
        await this.db.run(DELETE.from(midnight_1.Blocks).where({ ID: { in: blockIds } }));
        const forkBlock = await this.db.run(SELECT.one.from(midnight_1.Blocks)
            .where({ height: { '<': fromHeight } })
            .orderBy('height desc'));
        const effectiveStartHeight = forkBlock?.height != null ? Number(forkBlock.height) + 1 : 0;
        await this.db.run(UPDATE.entity(midnight_1.SyncState).set({
            lastIndexedHeight: forkBlock?.height ?? 0,
            lastIndexedHash: forkBlock?.hash ?? null,
            lastIndexedAt: new Date().toISOString(),
            syncStatus: 'stopped',
            syncProgress: 0
        }).where({ ID: 'SINGLETON' }));
        return {
            blocksRolledBack: blocksToRollback.length,
            transactionsRolledBack: txIds.length,
            effectiveStartHeight
        };
    }
    async init() {
        await (0, cds_model_1.ensureNightgateModelLoaded)();
        this.db = await cds_1.default.connect.to('db');
        // Ensure SyncState row exists (even before crawler starts)
        try {
            await (0, sync_state_1.ensureSyncStateSingleton)(this.db);
        }
        catch (err) {
            console.warn('[IndexerService] SyncState init skipped:', err.message);
        }
        this.on('getSyncStatus', async () => {
            const syncState = await this.db.run(SELECT.one.from(midnight_1.SyncState).where({ ID: 'SINGLETON' }));
            return syncState || {
                ID: 'SINGLETON',
                syncStatus: 'stopped',
                lastIndexedHeight: 0,
                chainHeight: 0,
                consecutiveErrors: 0
            };
        });
        this.on('getHealth', async () => {
            const syncState = await this.db.run(SELECT.one.from(midnight_1.SyncState).where({ ID: 'SINGLETON' }));
            if (!syncState) {
                return {
                    status: 'unknown',
                    chainHeight: 0,
                    indexedHeight: 0,
                    finalizedHeight: 0,
                    lag: 0,
                    finalizedLag: 0,
                    blocksPerSecond: 0,
                    syncStatus: 'stopped'
                };
            }
            const chainHeight = syncState.chainHeight || 0;
            const indexedHeight = syncState.lastIndexedHeight || 0;
            const finalizedHeight = syncState.lastFinalizedHeight || 0;
            const lag = Math.max(chainHeight - indexedHeight, 0);
            const finalizedLag = Math.max(chainHeight - finalizedHeight, 0);
            let status = 'healthy';
            if (lag > 100)
                status = 'unhealthy';
            else if (lag > 10)
                status = 'degraded';
            return {
                status,
                chainHeight,
                indexedHeight,
                finalizedHeight,
                lag,
                finalizedLag,
                blocksPerSecond: syncState.blocksPerSecond || 0,
                syncStatus: syncState.syncStatus || 'stopped'
            };
        });
        this.on('getReorgHistory', async (req) => {
            const { limit } = req.data;
            const effectiveLimit = Math.min(Math.max(limit || 10, 1), 100);
            return this.db.run(SELECT.from(midnight_1.ReorgLog)
                .orderBy('detectedAt desc')
                .limit(effectiveLimit));
        });
        this.on('getLiveness', async () => {
            return {
                status: 'alive',
                timestamp: new Date().toISOString(),
                uptime: Math.floor((Date.now() - processStartTime) / 1000)
            };
        });
        this.on('getReadiness', async () => {
            const checks = {
                database: false,
                crawler: false,
                node: false
            };
            try {
                const syncState = await this.db.run(SELECT.one.from(midnight_1.SyncState).where({ ID: 'SINGLETON' }));
                checks.database = true;
                if (syncState) {
                    checks.crawler = syncState.syncStatus === 'syncing' || syncState.syncStatus === 'synced';
                    if (syncState.lastIndexedAt) {
                        const lastActivity = new Date(syncState.lastIndexedAt).getTime();
                        checks.node = (Date.now() - lastActivity) < 5 * 60 * 1000;
                    }
                }
            }
            catch {
                // Database not available
            }
            return {
                ready: checks.database && checks.crawler && checks.node,
                checks
            };
        });
        this.on('getMetrics', async () => {
            const syncState = await this.db.run(SELECT.one.from(midnight_1.SyncState).where({ ID: 'SINGLETON' }));
            const lines = [];
            const chainHeight = syncState?.chainHeight || 0;
            const indexedHeight = syncState?.lastIndexedHeight || 0;
            const lag = chainHeight - indexedHeight;
            const bps = syncState?.blocksPerSecond || 0;
            const errors = syncState?.consecutiveErrors || 0;
            const uptimeSec = Math.floor((Date.now() - processStartTime) / 1000);
            const syncStatus = syncState?.syncStatus || 'stopped';
            lines.push(`# HELP ${metricPrefix}_chain_height Current chain height`);
            lines.push(`# TYPE ${metricPrefix}_chain_height gauge`);
            lines.push(`${metricPrefix}_chain_height ${chainHeight}`);
            lines.push(`# HELP ${metricPrefix}_indexed_height Last indexed block height`);
            lines.push(`# TYPE ${metricPrefix}_indexed_height gauge`);
            lines.push(`${metricPrefix}_indexed_height ${indexedHeight}`);
            lines.push(`# HELP ${metricPrefix}_sync_lag Blocks behind chain tip`);
            lines.push(`# TYPE ${metricPrefix}_sync_lag gauge`);
            lines.push(`${metricPrefix}_sync_lag ${lag}`);
            lines.push(`# HELP ${metricPrefix}_blocks_per_second Indexing throughput`);
            lines.push(`# TYPE ${metricPrefix}_blocks_per_second gauge`);
            lines.push(`${metricPrefix}_blocks_per_second ${bps}`);
            lines.push(`# HELP ${metricPrefix}_consecutive_errors Consecutive indexing errors`);
            lines.push(`# TYPE ${metricPrefix}_consecutive_errors gauge`);
            lines.push(`${metricPrefix}_consecutive_errors ${errors}`);
            lines.push(`# HELP ${metricPrefix}_uptime_seconds Process uptime in seconds`);
            lines.push(`# TYPE ${metricPrefix}_uptime_seconds gauge`);
            lines.push(`${metricPrefix}_uptime_seconds ${uptimeSec}`);
            lines.push(`# HELP ${metricPrefix}_sync_status Sync status (0=stopped, 1=syncing, 2=synced, 3=error)`);
            lines.push(`# TYPE ${metricPrefix}_sync_status gauge`);
            const statusMap = { stopped: 0, syncing: 1, synced: 2, error: 3 };
            lines.push(`${metricPrefix}_sync_status ${statusMap[syncStatus] ?? 0}`);
            return lines.join('\n') + '\n';
        });
        this.on('pauseCrawler', async () => {
            if (!(0, crawler_1.isCrawlerRunning)()) {
                return {
                    status: 'ok',
                    running: false,
                    message: 'Crawler is already paused'
                };
            }
            await (0, crawler_1.stopCrawler)();
            await this.db.run(UPDATE.entity(midnight_1.SyncState).set({
                syncStatus: 'stopped'
            }).where({ ID: 'SINGLETON' }));
            return {
                status: 'ok',
                running: false,
                message: 'Crawler paused'
            };
        });
        this.on('resumeCrawler', async (req) => {
            if ((0, crawler_1.isCrawlerRunning)()) {
                return {
                    status: 'ok',
                    running: true,
                    message: 'Crawler already running'
                };
            }
            try {
                await (0, crawler_1.startCrawler)(this.resolveCrawlerStartConfig());
                return {
                    status: 'ok',
                    running: true,
                    message: 'Crawler resumed'
                };
            }
            catch (err) {
                const message = err instanceof Error ? err.message : String(err);
                return req.reject(500, `Failed to resume crawler: ${message}`);
            }
        });
        this.on('reindexFromHeight', async (req) => {
            const { height } = req.data;
            const requestedHeight = Number(height);
            if (!Number.isInteger(requestedHeight) || requestedHeight < 0) {
                return req.reject(400, 'height must be a non-negative integer');
            }
            const wasRunning = (0, crawler_1.isCrawlerRunning)();
            if (wasRunning) {
                await (0, crawler_1.stopCrawler)();
            }
            const rollback = await this.rollbackFromHeight(requestedHeight);
            let crawlerResumed = false;
            let resumeError = null;
            if (wasRunning) {
                try {
                    await (0, crawler_1.startCrawler)(this.resolveCrawlerStartConfig());
                    crawlerResumed = true;
                }
                catch (err) {
                    resumeError = err instanceof Error ? err.message : String(err);
                    console.error('[IndexerService] Failed to resume crawler after reindex:', resumeError);
                }
            }
            return {
                status: resumeError ? 'partial' : 'ok',
                message: resumeError
                    ? `Reindex prepared but crawler resume failed: ${resumeError}`
                    : 'Reindex prepared',
                requestedHeight,
                effectiveStartHeight: rollback.effectiveStartHeight,
                blocksRolledBack: rollback.blocksRolledBack,
                transactionsRolledBack: rollback.transactionsRolledBack,
                crawlerResumed
            };
        });
        await super.init();
    }
}
exports.default = NightgateIndexerService;
