"use strict";
/**
 * Nightgate Service Implementation, OData V4 API
 *
 * Thin service layer: all data comes from local SQLite (populated by Crawler).
 * OData queries run against the local DB. Wallet sessions are handled separately.
 *
 * Data flow: Midnight Node -> Crawler -> SQLite -> Nightgate OData V4
 */
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 wallet_sessions_1 = require("./sessions/wallet-sessions");
const cds_model_1 = require("./utils/cds-model");
const handlers_1 = require("./submission/handlers");
const background_jobs_1 = require("./submission/background-jobs");
const midnight_1 = require("#cds-models/midnight");
class NightgateService extends cds_1.default.ApplicationService {
    db;
    _cleanupTimer;
    async init() {
        await (0, cds_model_1.ensureNightgateModelLoaded)();
        this.db = await cds_1.default.connect.to('db');
        // ====================================================================
        // Block Handlers
        // ====================================================================
        this.on('READ', 'Blocks', async (req) => {
            return await this.db.run(req.query) || [];
        });
        this.on('latest', 'Blocks', async () => {
            return this.db.run(cds_1.default.ql.SELECT.one.from(midnight_1.Blocks).orderBy('height desc'));
        });
        this.on('byHeight', 'Blocks', async (req) => {
            const { height } = req.data;
            if (height == null)
                return req.reject(400, 'height is required');
            return this.db.run(cds_1.default.ql.SELECT.one.from(midnight_1.Blocks).where({ height }));
        });
        this.on('range', 'Blocks', async (req) => {
            const { startHeight, endHeight, limit } = req.data;
            if (startHeight == null || endHeight == null) {
                return req.reject(400, 'startHeight and endHeight are required');
            }
            if (!Number.isInteger(startHeight) || !Number.isInteger(endHeight) || startHeight < 0 || endHeight < 0) {
                return req.reject(400, 'startHeight and endHeight must be non-negative integers');
            }
            if (endHeight < startHeight) {
                return req.reject(400, 'endHeight must be greater than or equal to startHeight');
            }
            const effectiveLimit = Math.min(Math.max(limit || 100, 1), 5000);
            // Use a tagged-template predicate for the range window. The object form
            // `{ height: { '>=': s, '<=': e } }` (two operators on one field) produces
            // CQN without the connective and is silently dropped by @cap-js/sqlite,
            // so the window would not filter at all.
            return this.db.run(cds_1.default.ql.SELECT.from(midnight_1.Blocks)
                .where `height >= ${startHeight} and height <= ${endHeight}`
                .orderBy('height asc')
                .limit(effectiveLimit));
        });
        // ====================================================================
        // Transaction Handlers
        // ====================================================================
        this.on('READ', 'Transactions', async (req) => {
            return await this.db.run(req.query) || [];
        });
        this.on('byHash', 'Transactions', async (req) => {
            const { hash } = req.data;
            if (!hash)
                return req.reject(400, 'hash is required');
            return this.db.run(cds_1.default.ql.SELECT.from(midnight_1.Transactions).where({ hash }));
        });
        this.on('byType', 'Transactions', async (req) => {
            const { txType, limit } = req.data;
            if (!txType)
                return req.reject(400, 'txType is required');
            const effectiveLimit = Math.min(Math.max(limit || 100, 1), 2000);
            return this.db.run(cds_1.default.ql.SELECT.from(midnight_1.Transactions)
                .where({ txType })
                .orderBy('createdAt desc')
                .limit(effectiveLimit));
        });
        // ====================================================================
        // Contract Handlers
        // ====================================================================
        this.on('READ', 'ContractActions', async (req) => {
            return await this.db.run(req.query) || [];
        });
        this.on('byAddress', 'ContractActions', async (req) => {
            const { address } = req.data;
            if (!address)
                return req.reject(400, 'address is required');
            return this.db.run(cds_1.default.ql.SELECT.from(midnight_1.ContractActions).where({ address }));
        });
        this.on('history', 'ContractActions', async (req) => {
            const { address } = req.data;
            if (!address)
                return req.reject(400, 'address is required');
            return this.db.run(cds_1.default.ql.SELECT.from(midnight_1.ContractActions)
                .where({ address })
                .orderBy('createdAt desc')
                .limit(100));
        });
        // ====================================================================
        // UTXO Handlers
        // ====================================================================
        this.on('READ', 'UnshieldedUtxos', async (req) => {
            return await this.db.run(req.query) || [];
        });
        this.on('byOwner', 'UnshieldedUtxos', async (req) => {
            const { owner } = req.data;
            if (!owner)
                return req.reject(400, 'owner is required');
            return this.db.run(cds_1.default.ql.SELECT.from(midnight_1.UnshieldedUtxos).where({ owner }));
        });
        this.on('unspent', 'UnshieldedUtxos', async () => {
            return this.db.run(cds_1.default.ql.SELECT.from(midnight_1.UnshieldedUtxos).where({ spentAtTransaction_ID: null }));
        });
        // ====================================================================
        // Balance & Token Tracking Handlers
        // ====================================================================
        this.on('getBalance', 'NightBalances', async (req) => {
            const { address } = req.data;
            if (!address)
                return req.reject(400, 'address is required');
            return this.db.run(cds_1.default.ql.SELECT.one.from(midnight_1.NightBalances).where({ address }));
        });
        this.on('getTopHolders', 'NightBalances', async (req) => {
            const { limit } = req.data;
            const effectiveLimit = Math.min(Math.max(limit || 10, 1), 1000);
            return this.db.run(cds_1.default.ql.SELECT.from(midnight_1.NightBalances)
                .orderBy('balance desc')
                .limit(effectiveLimit));
        });
        // ====================================================================
        // Wallet Sessions (delegated)
        // ====================================================================
        (0, wallet_sessions_1.registerWalletSessionHandlers)(this, this.db);
        // ====================================================================
        // Submission actions: deployContract, submitContractCall
        // ====================================================================
        this.on('READ', 'PendingSubmissions', async (req) => {
            return await this.db.run(req.query) || [];
        });
        (0, handlers_1.registerSubmissionHandlers)(this, this.db);
        // ====================================================================
        // Background Jobs (0.2.0 async submission lifecycle)
        // ====================================================================
        this.on('getJobStatus', async (req) => {
            const { jobId, sessionId } = req.data;
            if (!jobId)
                return req.reject(400, 'jobId is required');
            if (!sessionId)
                return req.reject(400, 'sessionId is required');
            const job = await (0, background_jobs_1.getJobById)(jobId);
            // 404 on foreign sessionId — same shape as not-found so a probe
            // for someone else's jobId can't distinguish "unknown" from
            // "exists but not yours".
            if (!job || job.sessionId !== sessionId) {
                return req.reject(404, 'Job not found');
            }
            return {
                jobId: job.ID,
                kind: job.kind,
                status: job.status,
                result: job.result,
                errorCode: job.errorCode,
                errorMessage: job.errorMessage,
                submittedAt: job.createdAt,
                startedAt: job.startedAt,
                finishedAt: job.finishedAt
            };
        });
        // ====================================================================
        // Read-only Enforcement
        // ====================================================================
        this.before(['CREATE', 'UPDATE', 'DELETE'], [
            'Blocks', 'Transactions', 'ContractActions', 'UnshieldedUtxos',
            'ZswapLedgerEvents', 'DustLedgerEvents',
            'NightBalances', 'WalletSessions', 'PendingSubmissions'
        ], (req) => {
            req.reject?.(405, 'Blockchain data is read-only');
        });
        // Session cleanup timer
        this._cleanupTimer = (0, wallet_sessions_1.startSessionCleanup)(this.db);
        cds_1.default.on('shutdown', () => {
            if (this._cleanupTimer) {
                clearInterval(this._cleanupTimer);
                this._cleanupTimer = undefined;
            }
        });
        await super.init();
    }
}
exports.default = NightgateService;
