"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ModelCallAnalyticsWriter = void 0;
exports.getModelCallsTable = getModelCallsTable;
exports.getModelCallAnalyticsWriter = getModelCallAnalyticsWriter;
const databases_ts_1 = require("../databases.js");
const commonUtility_ts_1 = require("../../utility/lmdb/commonUtility.js");
const harper_logger_ts_1 = __importDefault(require("../../utility/logging/harper_logger.js"));
const log = harper_logger_ts_1.default.forComponent('models').conditional;
const DEFAULT_FLUSH_INTERVAL_MS = 10_000; // 10s
const DEFAULT_MAX_BUFFER_SIZE = 1000;
const DEFAULT_CLEANUP_INTERVAL_MS = 60 * 60 * 1000; // 1h
// 90-day default tuned for billing windows. Operator-tunable config key will land
// in Phase 2 alongside the YAML→registry bootstrapper (Harper's `getConfigValue`
// only reads keys registered in `CONFIG_PARAM_MAP`, so we defer config plumbing
// until the first real backend ships and the key has a documented owner).
const DEFAULT_RETENTION_MS = 90 * 24 * 60 * 60 * 1000;
let _table;
/**
 * Lazy-getter for `hdb_model_calls`. Matches the convention used by
 * `getRawAnalyticsTable()` / `getAnalyticsTable()` in
 * `resources/analytics/write.ts:656-700` and the system-table declarations in
 * `server/DurableSubscriptionsSession.ts:14-50`.
 */
function getModelCallsTable() {
    if (_table)
        return _table;
    _table = (0, databases_ts_1.table)({
        table: 'hdb_model_calls',
        database: 'system',
        audit: true,
        trackDeletes: false,
        attributes: [
            { name: 'id', isPrimaryKey: true },
            { name: 'tenant', type: 'string', indexed: true },
            { name: 'app', type: 'string', indexed: true },
            { name: 'model', type: 'string', indexed: true },
            { name: 'backend', type: 'string', indexed: true },
            { name: 'method', type: 'string', indexed: true },
            { name: 'adapter', type: 'string', indexed: true },
            { name: 'conversation_id', type: 'string', indexed: true },
            { name: 'prompt_tokens', type: 'number' },
            { name: 'completion_tokens', type: 'number' },
            { name: 'embedding_tokens', type: 'number' },
            { name: 'gpu_ms', type: 'number' },
            { name: 'latency_ms', type: 'number', indexed: true },
            { name: 'success', type: 'boolean', indexed: true },
            { name: 'error_code', type: 'string' },
        ],
    });
    return _table;
}
/**
 * In-memory buffered writer for per-call model analytics. Rows are batched and
 * flushed periodically (or when the buffer is full) to keep the analytics path
 * off the hot model-call path. Shape mirrors the pattern in
 * `resources/analytics/write.ts` but writes per-call rows rather than
 * aggregating counters.
 *
 * The intervals are `.unref()`ed so they never hold the process open during
 * shutdown; rows buffered at shutdown are dropped (best-effort, same posture
 * as the existing analytics writer).
 */
class ModelCallAnalyticsWriter {
    #buffer = [];
    #flushTimer;
    #cleanupTimer;
    #maxBufferSize;
    #retentionMs;
    #getTable;
    #stopped = false;
    constructor(opts = {}) {
        const flushIntervalMs = opts.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
        const cleanupIntervalMs = opts.cleanupIntervalMs ?? DEFAULT_CLEANUP_INTERVAL_MS;
        this.#maxBufferSize = opts.maxBufferSize ?? DEFAULT_MAX_BUFFER_SIZE;
        this.#retentionMs = opts.retentionMs ?? DEFAULT_RETENTION_MS;
        this.#getTable = opts.getTable ?? getModelCallsTable;
        this.#flushTimer = setInterval(() => {
            this.flush().catch((err) => log.warn?.(`Model-call analytics flush failed: ${err?.message ?? err}`));
        }, flushIntervalMs);
        this.#flushTimer.unref?.();
        this.#cleanupTimer = setInterval(() => {
            this.cleanup().catch((err) => log.warn?.(`Model-call analytics cleanup failed: ${err?.message ?? err}`));
        }, cleanupIntervalMs);
        this.#cleanupTimer.unref?.();
    }
    write(record) {
        if (this.#stopped)
            return;
        // id last so a record that accidentally carries an `id` field can't override
        // the monotonic primary key.
        this.#buffer.push({ ...record, id: (0, commonUtility_ts_1.getNextMonotonicTime)() });
        if (this.#buffer.length >= this.#maxBufferSize) {
            // Out-of-cadence flush; swallow errors so a failing flush doesn't escape into the caller.
            this.flush().catch((err) => log.warn?.(`Model-call analytics flush failed: ${err?.message ?? err}`));
        }
    }
    async flush() {
        if (this.#buffer.length === 0)
            return;
        // Read-only nodes (followers, recovery mode) shouldn't accumulate doomed writes —
        // drop the buffer and skip. Matches `resources/analytics/write.ts:643-650`.
        if ((0, databases_ts_1.isReadOnlyMode)()) {
            this.#buffer = [];
            return;
        }
        const batch = this.#buffer;
        this.#buffer = [];
        const tbl = this.#getTable();
        const puts = [];
        for (const record of batch) {
            try {
                const result = tbl.primaryStore.put(record.id, record);
                if (result && typeof result.then === 'function') {
                    puts.push({ id: record.id, promise: result });
                }
            }
            catch (err) {
                log.warn?.(`Model-call analytics put failed for id=${record.id}: ${err?.message ?? err}`);
            }
        }
        if (puts.length > 0) {
            const results = await Promise.allSettled(puts.map((p) => p.promise));
            for (let i = 0; i < results.length; i++) {
                const r = results[i];
                if (r.status === 'rejected') {
                    const reason = r.reason;
                    log.warn?.(`Model-call analytics async put failed for id=${puts[i].id}: ${reason?.message ?? r.reason}`);
                }
            }
        }
    }
    async cleanup() {
        if ((0, databases_ts_1.isReadOnlyMode)())
            return;
        const end = Date.now() - this.#retentionMs;
        const tbl = this.#getTable();
        for (const key of tbl.primaryStore.getKeys({ start: false, end })) {
            tbl.primaryStore.remove(key);
        }
    }
    /** Stop the periodic timers. After stop, `write()` is a no-op and `flush()` still works. */
    stop() {
        this.#stopped = true;
        if (this.#flushTimer)
            clearInterval(this.#flushTimer);
        if (this.#cleanupTimer)
            clearInterval(this.#cleanupTimer);
    }
    /** Test-only: inspect current buffer size without flushing. */
    get bufferSize() {
        return this.#buffer.length;
    }
}
exports.ModelCallAnalyticsWriter = ModelCallAnalyticsWriter;
let _writer;
/** Process-wide singleton writer. Constructed on first access. */
function getModelCallAnalyticsWriter() {
    if (!_writer)
        _writer = new ModelCallAnalyticsWriter();
    return _writer;
}
//# sourceMappingURL=analyticsTable.js.map