"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SchemaNotDeployedError = exports.DEFAULT_NODE_URL = exports.DEFAULT_NETWORK = void 0;
exports.initialize = initialize;
exports.shutdown = shutdown;
exports.getStatus = getStatus;
const cds_1 = __importDefault(require("@sap/cds"));
const index_1 = require("../srv/crawler/index");
const cds_model_1 = require("../srv/utils/cds-model");
const nightgate_config_1 = require("../srv/utils/nightgate-config");
const contract_registry_1 = require("../srv/submission/contract-registry");
const sqlite_tuning_1 = require("../srv/utils/sqlite-tuning");
const wallet_worker_client_1 = require("../srv/midnight/wallet-worker-client");
const wallet_facade_builder_1 = require("../srv/submission/wallet-facade-builder");
const background_jobs_1 = require("../srv/submission/background-jobs");
var nightgate_config_2 = require("../srv/utils/nightgate-config");
Object.defineProperty(exports, "DEFAULT_NETWORK", { enumerable: true, get: function () { return nightgate_config_2.DEFAULT_NETWORK; } });
Object.defineProperty(exports, "DEFAULT_NODE_URL", { enumerable: true, get: function () { return nightgate_config_2.DEFAULT_NODE_URL; } });
let initialized = false;
let lastStatus = {
    initialized: false,
    crawlerEnabled: false,
    mode: 'idle'
};
function isLikelyNodeConnectionError(message) {
    return /ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT|socket hang up|WebSocket|Not connected to Midnight Node/i.test(message);
}
function logStartupState(state, detail) {
    const suffix = detail ? ` (${detail})` : '';
    console.log(`[odatano-nightgate] Startup state: ${state}${suffix}`);
}
/**
 * Thrown by `ensureSchemaDeployed` when a required CAP entity has no backing
 * table in the connected database. Carries the missing table name and the
 * resolved DB URL so the surfaced message is actionable without guessing.
 *
 * Production behaviour (in `src/plugin.ts::registerLifecycle`): this error is
 * caught, logged with a "RUN THIS" block, and the process exits non-zero so
 * the operator notices instead of half-booting against a stale schema.
 */
class SchemaNotDeployedError extends Error {
    missingTable;
    dbPath;
    constructor(missingTable, dbPath, cause) {
        const causeMsg = cause instanceof Error ? cause.message : String(cause);
        super(`Nightgate schema is not deployed (or out of date): ` +
            `missing table for '${missingTable}' in ${dbPath}. ` +
            `Underlying error: ${causeMsg}. ` +
            `Fix: run \`npm run deploy\` from the project root.`);
        this.missingTable = missingTable;
        this.dbPath = dbPath;
        this.name = 'SchemaNotDeployedError';
    }
}
exports.SchemaNotDeployedError = SchemaNotDeployedError;
/**
 * Resolve the database file path the runtime is connected to. Used in the
 * SchemaNotDeployedError message so users see exactly which file is missing
 * the tables bare `cds deploy` defaults to `db.sqlite` which would silently
 * deploy to the wrong file otherwise.
 */
function resolveDbPath() {
    const dbCfg = cds_1.default.env.requires?.db?.credentials || cds_1.default.env.requires?.db || {};
    return dbCfg.database || dbCfg.url || 'db.sqlite';
}
/**
 * Probe-only schema check. Returns silently when every required table exists;
 * throws `SchemaNotDeployedError` on the first miss so the operator can fix
 * it explicitly via `npm run deploy`.
 **/
async function ensureSchemaDeployed() {
    const requiredTables = [
        'midnight.Blocks',
        'midnight.SyncState',
        'midnight.PendingSubmissions',
        'midnight.PrivateStates',
        'midnight.ContractSigningKeys',
        'midnight.WalletSyncStates',
        'midnight.Attestations',
        'midnight.Documents',
        'midnight.DisclosureRoles',
        'midnight.BackgroundJobs'
    ];
    const db = cds_1.default.db || await cds_1.default.connect.to('db');
    const { SELECT } = cds_1.default.ql;
    for (const table of requiredTables) {
        try {
            await db.run(SELECT.one.from(table));
        }
        catch (probeErr) {
            throw new SchemaNotDeployedError(table, resolveDbPath(), probeErr);
        }
    }
}
async function initialize() {
    await (0, cds_model_1.ensureNightgateModelLoaded)();
    const nightgateConfig = (0, nightgate_config_1.getNightgatePluginConfig)();
    if (!(0, nightgate_config_1.isNightgatePluginConfigured)(nightgateConfig)) {
        lastStatus = {
            initialized: false,
            crawlerEnabled: false,
            mode: 'idle'
        };
        logStartupState('stopped', 'plugin not configured');
        return getStatus();
    }
    if (initialized) {
        return getStatus();
    }
    const { network, nodeUrl, crawlerConfig, crawlerNodeUrl, invalidNetwork } = (0, nightgate_config_1.resolveNightgateRuntimeConfig)(nightgateConfig);
    const crawlerEnabled = crawlerConfig.enabled !== false;
    if (invalidNetwork) {
        console.error(`[odatano-nightgate] Invalid network "${invalidNetwork}". Must be one of: ${nightgate_config_1.VALID_NIGHTGATE_NETWORKS.join(', ')}`);
        console.error(`[odatano-nightgate] Falling back to "${nightgate_config_1.DEFAULT_NETWORK}"`);
    }
    await ensureSchemaDeployed();
    // Tune SQLite for catch-up write throughput. No-op on HANA.
    try {
        const db = cds_1.default.db || await cds_1.default.connect.to('db');
        await (0, sqlite_tuning_1.applySqliteTuning)(db);
    }
    catch (err) {
        console.warn(`[odatano-nightgate] SQLite tuning skipped: ${err.message}`);
    }
    // Crash recovery: any BackgroundJobs row that was pending/running when the
    // previous process exited becomes a ghost forever otherwise. Flip them to
    // failed:PROCESS_RESTART so callers polling getJobStatus see a definitive
    // terminal state. Idempotent; safe to run on every boot.
    try {
        const recovered = await (0, background_jobs_1.recoverInterruptedJobs)();
        if (recovered > 0) {
            console.log(`[odatano-nightgate] Recovered ${recovered} interrupted background job(s) as failed:PROCESS_RESTART`);
        }
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        console.warn(`[odatano-nightgate] Background-job recovery skipped: ${msg}`);
    }
    // Load any contracts declared under cds.requires.nightgate.contracts into
    // the in-memory registry. Safe to call repeatedly, idempotent.
    try {
        (0, contract_registry_1.loadRegistryFromConfig)(nightgateConfig);
        const refs = (0, contract_registry_1.listRegisteredContracts)();
        if (refs.length) {
            console.log(`[odatano-nightgate] Registered contracts: ${refs.join(', ')}`);
        }
    }
    catch (regErr) {
        const msg = regErr instanceof Error ? regErr.message : String(regErr);
        console.warn(`[odatano-nightgate] Contract registry load warning: ${msg}`);
    }
    // Spin up the wallet worker thread now so it's ready when the first
    // connectWalletForSigning request lands. The Midnight wallet SDK
    // monopolises the microtask queue while syncing — running it in a worker
    // keeps CAP's `db.run`, OData handlers, and the crawler responsive.
    try {
        await (0, wallet_worker_client_1.startWalletWorker)();
        (0, wallet_facade_builder_1.wireWorkerStateSaveSink)();
        console.log('[odatano-nightgate] Wallet worker thread ready');
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        console.warn(`[odatano-nightgate] Wallet worker startup failed: ${msg}`);
        console.warn('[odatano-nightgate] Signing-related operations will fail until restart');
    }
    console.log(`[odatano-nightgate] Network: ${network}`);
    console.log(`[odatano-nightgate] Node: ${nodeUrl}`);
    let mode = crawlerEnabled ? 'active' : 'idle';
    let lastError;
    if (crawlerEnabled) {
        try {
            console.log('[odatano-nightgate] Initializing crawler and starting catch-up...');
            await (0, index_1.startCrawler)({
                ...crawlerConfig,
                enabled: true,
                nodeUrl: crawlerNodeUrl,
                requestTimeout: crawlerConfig.requestTimeout || 30000
            });
            logStartupState('syncing', 'crawler started');
        }
        catch (err) {
            lastError = err instanceof Error ? err.message : String(err);
            mode = 'offline';
            if (isLikelyNodeConnectionError(lastError)) {
                console.warn(`[odatano-nightgate] Node not reachable at ${crawlerNodeUrl}: ${lastError}`);
                logStartupState('offline', 'node unreachable');
                console.log('[odatano-nightgate] Running in offline mode. Start a Midnight node: docker compose -f docker/docker-compose.yml up -d');
            }
            else {
                console.warn(`[odatano-nightgate] Crawler startup failed: ${lastError}`);
                logStartupState('offline', 'startup error');
                console.log('[odatano-nightgate] Running in offline mode until the startup error is resolved');
            }
        }
    }
    else {
        logStartupState('stopped', 'crawler disabled');
    }
    initialized = true;
    lastStatus = {
        initialized,
        crawlerEnabled,
        network,
        nodeUrl,
        mode,
        lastError
    };
    return getStatus();
}
async function shutdown() {
    try {
        await (0, index_1.stopCrawler)();
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        console.warn(`[odatano-nightgate] Crawler stop error: ${message}`);
        lastStatus = {
            ...lastStatus,
            lastError: message
        };
    }
    try {
        await (0, wallet_worker_client_1.stopWalletWorker)();
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        console.warn(`[odatano-nightgate] Wallet worker stop error: ${message}`);
    }
    initialized = false;
    lastStatus = {
        ...lastStatus,
        initialized: false,
        mode: 'idle'
    };
}
function getStatus() {
    return { ...lastStatus };
}
