"use strict";
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 Gracker (Chris)
// This file is part of SmartPerfetto. See LICENSE for details.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CiGateRunStore = void 0;
/**
 * CiGateRunStore — durable, SQLite-backed log of every CI gate
 * evaluation (Plan 51 first slice).
 *
 * SQLite (rather than JSON) because CI runs are append-heavy, queried
 * by gate/status/source/createdAt, and referenced by IM/Bug/PR
 * adapters that need a durable index. The schema is intentionally
 * narrow — full record JSON in a `payload` column, indexed columns
 * carry only what the list/filter API needs. That keeps migrations
 * cheap and lets new fields land without breaking older readers.
 *
 * Retention: a 90-day window enforced inside the same transaction as
 * each insert, so a long-lived process never lets old rows grow
 * unbounded.
 */
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
const runtimePaths_1 = require("../runtimePaths");
const RETENTION_MS = 90 * 24 * 60 * 60 * 1000;
const MIGRATIONS = [
    {
        version: 1,
        up: db => {
            db.exec(`
        CREATE TABLE ci_gate_runs (
          run_id TEXT PRIMARY KEY,
          gate_id TEXT NOT NULL,
          baseline_id TEXT NOT NULL,
          ci_source TEXT NOT NULL,
          status TEXT NOT NULL,
          created_at INTEGER NOT NULL,
          payload TEXT NOT NULL
        );
        CREATE INDEX idx_ci_gate_runs_created_at ON ci_gate_runs(created_at);
        CREATE INDEX idx_ci_gate_runs_gate_created ON ci_gate_runs(gate_id, created_at);
        CREATE INDEX idx_ci_gate_runs_status_created ON ci_gate_runs(status, created_at);
        CREATE INDEX idx_ci_gate_runs_source ON ci_gate_runs(ci_source);
      `);
        },
    },
];
function defaultDbPath() {
    return (0, runtimePaths_1.backendDataPath)('ci_gate', 'ci_gate_runs.db');
}
const DEFAULT_LIST_LIMIT = 50;
const MAX_LIST_LIMIT = 200;
/**
 * SQLite-backed store. Wraps a single connection — single-process
 * writers only, mirroring `BaselineStore`. Tests should construct
 * with `dbPath: ':memory:'` so each suite gets an isolated store.
 */
class CiGateRunStore {
    constructor(opts = {}) {
        const dbPath = opts.dbPath ?? defaultDbPath();
        if (dbPath !== ':memory:') {
            const dir = path.dirname(dbPath);
            if (!fs.existsSync(dir))
                fs.mkdirSync(dir, { recursive: true });
        }
        this.db = new better_sqlite3_1.default(dbPath);
        this.db.pragma('journal_mode = WAL');
        this.db.pragma('busy_timeout = 5000');
        this.db.pragma('foreign_keys = ON');
        this.applyMigrations();
        this.retentionMs = opts.retentionMs ?? RETENTION_MS;
        this.insertStmt = this.db.prepare(`
      INSERT INTO ci_gate_runs
        (run_id, gate_id, baseline_id, ci_source, status, created_at, payload)
      VALUES (?, ?, ?, ?, ?, ?, ?)
    `);
        this.evictStmt = this.db.prepare('DELETE FROM ci_gate_runs WHERE created_at < ?');
        this.getStmt = this.db.prepare('SELECT payload FROM ci_gate_runs WHERE run_id = ?');
    }
    close() {
        this.db.close();
    }
    /**
     * Append a run. Eviction of expired rows runs inside the same
     * transaction so a long-lived process never accumulates beyond the
     * retention window — an important property for the tests that
     * exercise this through the route layer.
     */
    recordRun(record) {
        const cutoff = Date.now() - this.retentionMs;
        const tx = this.db.transaction(() => {
            this.insertStmt.run(record.runId, record.gateId, record.baselineId, record.ciSource, record.result.status, record.createdAt, JSON.stringify(record));
            this.evictStmt.run(cutoff);
        });
        tx();
    }
    getRun(runId) {
        const row = this.getStmt.get(runId);
        if (!row)
            return undefined;
        return safeParse(row.payload);
    }
    listRuns(opts = {}) {
        const limit = clampLimit(opts.limit);
        const where = [];
        const params = [];
        if (opts.gateId) {
            where.push('gate_id = ?');
            params.push(opts.gateId);
        }
        if (opts.status) {
            where.push('status = ?');
            params.push(opts.status);
        }
        if (opts.ciSource) {
            where.push('ci_source = ?');
            params.push(opts.ciSource);
        }
        const whereSql = where.length ? `WHERE ${where.join(' AND ')}` : '';
        const sql = `SELECT payload FROM ci_gate_runs ${whereSql} ` +
            'ORDER BY created_at DESC, run_id DESC LIMIT ?';
        params.push(limit);
        const rows = this.db.prepare(sql).all(...params);
        return rows
            .map(r => safeParse(r.payload))
            .filter((r) => Boolean(r));
    }
    applyMigrations() {
        this.db.exec(`
      CREATE TABLE IF NOT EXISTS schema_migrations (
        version INTEGER PRIMARY KEY,
        applied_at INTEGER NOT NULL
      );
    `);
        const applied = new Set(this.db
            .prepare('SELECT version FROM schema_migrations')
            .all()
            .map(r => r.version));
        for (const step of MIGRATIONS) {
            if (applied.has(step.version))
                continue;
            const tx = this.db.transaction(() => {
                step.up(this.db);
                this.db
                    .prepare('INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)')
                    .run(step.version, Date.now());
            });
            tx();
        }
    }
}
exports.CiGateRunStore = CiGateRunStore;
function clampLimit(requested) {
    if (typeof requested !== 'number' || !Number.isFinite(requested)) {
        return DEFAULT_LIST_LIMIT;
    }
    if (requested < 1)
        return 1;
    if (requested > MAX_LIST_LIMIT)
        return MAX_LIST_LIMIT;
    return Math.floor(requested);
}
function safeParse(payload) {
    try {
        return JSON.parse(payload);
    }
    catch {
        return undefined;
    }
}
//# sourceMappingURL=ciGateRunStore.js.map