/**
 * Persistent Audit Log Service (SQLite)
 *
 * Provides durable, queryable audit logging using better-sqlite3.
 * Replaces the append-only JSONL approach with a proper relational store
 * that supports efficient filtering, pagination, and aggregation.
 *
 * Single WAL-mode database shared across all sessions. Thread-safe for
 * the single-threaded Node.js event loop (better-sqlite3 is synchronous).
 *
 * @module services/agent/crewly-agent/audit-log.service
 */
import Database from 'better-sqlite3';
import { join } from 'path';
import { mkdirSync } from 'fs';
/** Default database filename */
const DB_FILENAME = 'audit-log.sqlite';
/** Default directory under .crewly */
const AUDIT_DIR = 'audit-logs';
/** Maximum rows returned by a single query */
const MAX_QUERY_LIMIT = 10000;
/**
 * SQLite-backed persistent audit log service.
 *
 * @example
 * ```typescript
 * const log = new AuditLogService('/path/to/.crewly');
 * log.initialize();
 * log.append(auditEntry);
 * log.logApprovalEvent('approval-123', 'session-1', 'edit_file', 'destructive', 'approved', 'api');
 * const entries = log.query({ limit: 50, sessionName: 'session-1' });
 * log.close();
 * ```
 */
export class AuditLogService {
    db = null;
    dbPath;
    initialized = false;
    /** Prepared statements for performance */
    stmtInsert = null;
    stmtInsertApproval = null;
    /**
     * Create a new AuditLogService.
     *
     * @param crewlyHome - Path to the .crewly directory
     * @param dbFilename - Database filename (default: audit-log.sqlite)
     */
    constructor(crewlyHome, dbFilename = DB_FILENAME) {
        const dir = join(crewlyHome, AUDIT_DIR);
        this.dbPath = join(dir, dbFilename);
    }
    /**
     * Initialize the database, creating tables if needed.
     * Enables WAL mode for better concurrent read performance.
     */
    initialize() {
        if (this.initialized)
            return;
        // Ensure directory exists
        const dir = join(this.dbPath, '..');
        mkdirSync(dir, { recursive: true });
        this.db = new Database(this.dbPath);
        this.db.pragma('journal_mode = WAL');
        this.db.pragma('foreign_keys = ON');
        this.db.exec(`
      CREATE TABLE IF NOT EXISTS audit_log (
        id            INTEGER PRIMARY KEY AUTOINCREMENT,
        timestamp     TEXT    NOT NULL,
        session_name  TEXT,
        tool_name     TEXT    NOT NULL,
        sensitivity   TEXT    NOT NULL,
        args          TEXT    NOT NULL DEFAULT '{}',
        success       INTEGER NOT NULL DEFAULT 1,
        error         TEXT,
        duration_ms   REAL    NOT NULL DEFAULT 0,
        action        TEXT    NOT NULL DEFAULT 'tool_call',
        resolved_by   TEXT,
        approval_id   TEXT
      );

      CREATE INDEX IF NOT EXISTS idx_audit_timestamp ON audit_log(timestamp);
      CREATE INDEX IF NOT EXISTS idx_audit_session   ON audit_log(session_name);
      CREATE INDEX IF NOT EXISTS idx_audit_action    ON audit_log(action);
      CREATE INDEX IF NOT EXISTS idx_audit_tool      ON audit_log(tool_name);
    `);
        this.stmtInsert = this.db.prepare(`
      INSERT INTO audit_log (timestamp, session_name, tool_name, sensitivity, args, success, error, duration_ms, action)
      VALUES (@timestamp, @sessionName, @toolName, @sensitivity, @args, @success, @error, @durationMs, 'tool_call')
    `);
        this.stmtInsertApproval = this.db.prepare(`
      INSERT INTO audit_log (timestamp, session_name, tool_name, sensitivity, args, success, duration_ms, action, resolved_by, approval_id)
      VALUES (@timestamp, @sessionName, @toolName, @sensitivity, @args, 1, 0, @action, @resolvedBy, @approvalId)
    `);
        this.initialized = true;
    }
    /**
     * Append a tool-call audit entry.
     *
     * @param entry - The AuditEntry from the agent runtime
     */
    append(entry) {
        this.ensureInitialized();
        this.stmtInsert.run({
            timestamp: entry.timestamp,
            sessionName: entry.sessionName ?? null,
            toolName: entry.toolName,
            sensitivity: entry.sensitivity,
            args: JSON.stringify(entry.args),
            success: entry.success ? 1 : 0,
            error: entry.error ?? null,
            durationMs: entry.durationMs,
        });
    }
    /**
     * Log an approval lifecycle event (granted, rejected, expired).
     *
     * @param approvalId - The approval request ID
     * @param sessionName - Agent session name
     * @param toolName - Tool that was approved/rejected
     * @param sensitivity - Sensitivity classification
     * @param action - 'approval_granted' | 'approval_rejected' | 'approval_expired'
     * @param resolvedBy - Who resolved it
     */
    logApprovalEvent(approvalId, sessionName, toolName, sensitivity, action, resolvedBy) {
        this.ensureInitialized();
        this.stmtInsertApproval.run({
            timestamp: new Date().toISOString(),
            sessionName,
            toolName,
            sensitivity,
            args: '{}',
            action,
            resolvedBy,
            approvalId,
        });
    }
    /**
     * Query audit log entries with filters and pagination.
     *
     * @param filters - Query filters
     * @returns Array of audit log rows, most recent first
     */
    query(filters) {
        this.ensureInitialized();
        const conditions = [];
        const params = {};
        if (filters.sessionName) {
            conditions.push('session_name = @sessionName');
            params.sessionName = filters.sessionName;
        }
        if (filters.sensitivity) {
            conditions.push('sensitivity = @sensitivity');
            params.sensitivity = filters.sensitivity;
        }
        if (filters.toolName) {
            conditions.push('tool_name = @toolName');
            params.toolName = filters.toolName;
        }
        if (filters.action) {
            conditions.push('action = @action');
            params.action = filters.action;
        }
        if (filters.since) {
            conditions.push('timestamp >= @since');
            params.since = filters.since;
        }
        if (filters.until) {
            conditions.push('timestamp <= @until');
            params.until = filters.until;
        }
        const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
        const limit = Math.min(filters.limit || 100, MAX_QUERY_LIMIT);
        const offset = filters.offset || 0;
        const sql = `
      SELECT id, timestamp, session_name, tool_name, sensitivity, args,
             success, error, duration_ms, action, resolved_by, approval_id
      FROM audit_log
      ${where}
      ORDER BY id DESC
      LIMIT @limit OFFSET @offset
    `;
        params.limit = limit;
        params.offset = offset;
        const rows = this.db.prepare(sql).all(params);
        return rows.map(row => ({
            id: row.id,
            timestamp: row.timestamp,
            sessionName: row.session_name,
            toolName: row.tool_name,
            sensitivity: row.sensitivity,
            args: JSON.parse(row.args || '{}'),
            success: row.success === 1,
            error: row.error,
            durationMs: row.duration_ms,
            action: row.action,
            resolvedBy: row.resolved_by,
            approvalId: row.approval_id,
        }));
    }
    /**
     * Get total count of entries matching filters (for pagination).
     *
     * @param filters - Same filters as query (limit/offset ignored)
     * @returns Total matching row count
     */
    count(filters = {}) {
        this.ensureInitialized();
        const conditions = [];
        const params = {};
        if (filters.sessionName) {
            conditions.push('session_name = @sessionName');
            params.sessionName = filters.sessionName;
        }
        if (filters.action) {
            conditions.push('action = @action');
            params.action = filters.action;
        }
        const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
        const sql = `SELECT COUNT(*) as cnt FROM audit_log ${where}`;
        const row = this.db.prepare(sql).get(params);
        return row.cnt;
    }
    /**
     * Get the database file path.
     *
     * @returns Absolute path to the SQLite database
     */
    getDbPath() {
        return this.dbPath;
    }
    /**
     * Check if the service is initialized.
     *
     * @returns True if initialize() has been called
     */
    isInitialized() {
        return this.initialized;
    }
    /**
     * Close the database connection.
     * Safe to call multiple times.
     */
    close() {
        if (this.db) {
            this.db.close();
            this.db = null;
            this.stmtInsert = null;
            this.stmtInsertApproval = null;
            this.initialized = false;
        }
    }
    /**
     * Ensure the service is initialized before operations.
     *
     * @throws Error if not initialized
     */
    ensureInitialized() {
        if (!this.initialized || !this.db) {
            throw new Error('AuditLogService not initialized. Call initialize() first.');
        }
    }
}
//# sourceMappingURL=audit-log.service.js.map