/**
 * Database-backed Audit Log Store
 *
 * Provides persistent storage for security audit events using Drizzle ORM
 * with PostgreSQL. Addresses the security audit finding that in-memory
 * audit logs in secrets-rotation.ts are not persisted.
 *
 * Features:
 * - Full CRUD operations with proper indexing
 * - Query support with flexible filtering
 * - Request ID correlation for tracing
 * - Retention management with soft delete
 * - Both database and in-memory implementations
 * - Factory function for environment-based selection
 *
 * @packageDocumentation
 * @module audit/db-store
 */
import { eq, and, desc, lt, gte, lte, sql } from 'drizzle-orm';
import { pgTable, uuid, text, timestamp, jsonb, index, pgEnum, } from 'drizzle-orm/pg-core';
import { getDatabase } from '../common/db.js';
import { createLogger } from '../common/logger.js';
import { getConfig } from '../common/config.js';
const logger = createLogger({ component: 'audit-db-store' });
// =============================================================================
// DATABASE SCHEMA
// =============================================================================
/**
 * Audit severity enum for database
 */
export const auditEntrySeverityEnum = pgEnum('audit_entry_severity', [
    'info',
    'low',
    'medium',
    'high',
    'critical',
]);
/**
 * Audit outcome enum for database
 */
export const auditEntryOutcomeEnum = pgEnum('audit_entry_outcome', [
    'success',
    'failure',
    'blocked',
    'escalated',
]);
/**
 * Audit entries table schema
 *
 * Stores security audit events with comprehensive indexing
 * for efficient querying and compliance reporting.
 */
export const auditEntries = pgTable('audit_entries', {
    id: uuid('id').primaryKey().defaultRandom(),
    // Timestamps
    timestamp: timestamp('timestamp', { withTimezone: true }).notNull().defaultNow(),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    // Event identification
    eventType: text('event_type').notNull(),
    category: text('category').notNull(),
    severity: auditEntrySeverityEnum('severity').notNull().default('info'),
    // Actor information
    actorId: text('actor_id').notNull(),
    actorType: text('actor_type').notNull(),
    actorName: text('actor_name'),
    actorIp: text('actor_ip'),
    // Action and resource
    action: text('action').notNull(),
    resourceType: text('resource_type').notNull(),
    resourceId: text('resource_id').notNull(),
    resourceName: text('resource_name'),
    // Outcome
    outcome: auditEntryOutcomeEnum('outcome').notNull().default('success'),
    reason: text('reason'),
    // Context
    tenantId: text('tenant_id').notNull(),
    requestId: text('request_id'),
    traceId: text('trace_id'),
    // Additional data
    metadata: jsonb('metadata').$type(),
}, (table) => ({
    // Index for tenant-based queries (required for multi-tenant isolation)
    tenantIdIdx: index('audit_entries_tenant_id_idx').on(table.tenantId),
    // Index for time-based queries and retention
    timestampIdx: index('audit_entries_timestamp_idx').on(table.timestamp),
    // Composite index for common query patterns
    tenantTimestampIdx: index('audit_entries_tenant_timestamp_idx').on(table.tenantId, table.timestamp),
    // Index for request ID correlation
    requestIdIdx: index('audit_entries_request_id_idx').on(table.requestId),
    // Index for event type filtering
    eventTypeIdx: index('audit_entries_event_type_idx').on(table.eventType),
    // Index for category filtering
    categoryIdx: index('audit_entries_category_idx').on(table.category),
    // Index for severity-based queries (for alerting)
    severityIdx: index('audit_entries_severity_idx').on(table.severity),
    // Index for actor-based queries
    actorIdIdx: index('audit_entries_actor_id_idx').on(table.actorId),
    // Index for resource-based queries
    resourceIdx: index('audit_entries_resource_idx').on(table.resourceType, table.resourceId),
    // Index for outcome-based queries
    outcomeIdx: index('audit_entries_outcome_idx').on(table.outcome),
}));
// =============================================================================
// DATABASE AUDIT STORE IMPLEMENTATION
// =============================================================================
/**
 * Database-backed audit log store implementation
 *
 * Uses PostgreSQL via Drizzle ORM for persistent storage of audit events.
 * Provides comprehensive querying capabilities for compliance reporting
 * and forensic analysis.
 */
export class DatabaseAuditStore {
    db;
    /**
     * Create a new DatabaseAuditStore
     *
     * @param options - Configuration options
     */
    constructor(options) {
        this.db = options?.database ?? getDatabase();
        logger.info('DatabaseAuditStore initialized');
    }
    /**
     * Log an audit entry to the database
     */
    async log(entry) {
        try {
            await this.db.insert(auditEntries).values({
                id: entry.id,
                timestamp: new Date(entry.timestamp),
                eventType: entry.eventType,
                category: entry.category,
                severity: entry.severity,
                actorId: entry.actorId,
                actorType: entry.actorType,
                actorName: entry.actorName,
                actorIp: entry.actorIp,
                action: entry.action,
                resourceType: entry.resourceType,
                resourceId: entry.resourceId,
                resourceName: entry.resourceName,
                outcome: entry.outcome,
                reason: entry.reason,
                tenantId: entry.tenantId,
                requestId: entry.requestId,
                traceId: entry.traceId,
                metadata: entry.metadata,
            });
            logger.debug({
                entryId: entry.id,
                eventType: entry.eventType,
                tenantId: entry.tenantId,
            }, 'Audit entry logged to database');
        }
        catch (error) {
            logger.error({ error, entryId: entry.id, eventType: entry.eventType }, 'Failed to log audit entry to database');
            throw error;
        }
    }
    /**
     * Query audit entries with filters
     */
    async query(filter) {
        try {
            const conditions = [];
            if (filter.tenantId) {
                conditions.push(eq(auditEntries.tenantId, filter.tenantId));
            }
            if (filter.eventType) {
                conditions.push(eq(auditEntries.eventType, filter.eventType));
            }
            if (filter.category) {
                conditions.push(eq(auditEntries.category, filter.category));
            }
            if (filter.severity) {
                conditions.push(eq(auditEntries.severity, filter.severity));
            }
            if (filter.actorId) {
                conditions.push(eq(auditEntries.actorId, filter.actorId));
            }
            if (filter.actorType) {
                conditions.push(eq(auditEntries.actorType, filter.actorType));
            }
            if (filter.resourceType) {
                conditions.push(eq(auditEntries.resourceType, filter.resourceType));
            }
            if (filter.resourceId) {
                conditions.push(eq(auditEntries.resourceId, filter.resourceId));
            }
            if (filter.outcome) {
                conditions.push(eq(auditEntries.outcome, filter.outcome));
            }
            if (filter.requestId) {
                conditions.push(eq(auditEntries.requestId, filter.requestId));
            }
            if (filter.startTime) {
                conditions.push(gte(auditEntries.timestamp, new Date(filter.startTime)));
            }
            if (filter.endTime) {
                conditions.push(lte(auditEntries.timestamp, new Date(filter.endTime)));
            }
            const limit = Math.min(filter.limit ?? 100, 1000);
            const offset = filter.offset ?? 0;
            const orderFn = filter.orderDirection === 'asc'
                ? (col) => col
                : desc;
            let query = this.db
                .select()
                .from(auditEntries)
                .orderBy(orderFn(auditEntries.timestamp))
                .limit(limit)
                .offset(offset);
            if (conditions.length > 0) {
                query = query.where(and(...conditions));
            }
            const records = await query;
            return records.map((record) => this.mapRecordToEntry(record));
        }
        catch (error) {
            logger.error({ error, filter }, 'Failed to query audit entries');
            throw error;
        }
    }
    /**
     * Get all audit entries for a specific request ID
     */
    async getByRequestId(requestId) {
        try {
            const records = await this.db
                .select()
                .from(auditEntries)
                .where(eq(auditEntries.requestId, requestId))
                .orderBy(auditEntries.timestamp);
            return records.map((record) => this.mapRecordToEntry(record));
        }
        catch (error) {
            logger.error({ error, requestId }, 'Failed to get audit entries by request ID');
            throw error;
        }
    }
    /**
     * Delete entries older than the specified date
     *
     * This is used for retention management. Entries older than the
     * specified date are permanently deleted.
     */
    async retention(olderThan) {
        try {
            const result = await this.db
                .delete(auditEntries)
                .where(lt(auditEntries.timestamp, olderThan))
                .returning({ id: auditEntries.id });
            const deletedCount = result.length;
            if (deletedCount > 0) {
                logger.info({ deletedCount, olderThan: olderThan.toISOString() }, 'Audit entries purged');
            }
            return deletedCount;
        }
        catch (error) {
            logger.error({ error, olderThan }, 'Failed to purge audit entries');
            throw error;
        }
    }
    /**
     * Get statistics about audit entries
     */
    async getStats(tenantId) {
        try {
            const conditions = tenantId ? [eq(auditEntries.tenantId, tenantId)] : [];
            // Total count
            const [totalResult] = await this.db
                .select({ count: sql `count(*)::int` })
                .from(auditEntries)
                .where(conditions.length > 0 ? and(...conditions) : undefined);
            // By category
            const categoryResults = await this.db
                .select({
                category: auditEntries.category,
                count: sql `count(*)::int`,
            })
                .from(auditEntries)
                .where(conditions.length > 0 ? and(...conditions) : undefined)
                .groupBy(auditEntries.category);
            // By severity
            const severityResults = await this.db
                .select({
                severity: auditEntries.severity,
                count: sql `count(*)::int`,
            })
                .from(auditEntries)
                .where(conditions.length > 0 ? and(...conditions) : undefined)
                .groupBy(auditEntries.severity);
            // By outcome
            const outcomeResults = await this.db
                .select({
                outcome: auditEntries.outcome,
                count: sql `count(*)::int`,
            })
                .from(auditEntries)
                .where(conditions.length > 0 ? and(...conditions) : undefined)
                .groupBy(auditEntries.outcome);
            return {
                totalEntries: totalResult?.count ?? 0,
                byCategory: Object.fromEntries(categoryResults.map((r) => [r.category, r.count])),
                bySeverity: Object.fromEntries(severityResults.map((r) => [r.severity, r.count])),
                byOutcome: Object.fromEntries(outcomeResults.map((r) => [r.outcome, r.count])),
            };
        }
        catch (error) {
            logger.error({ error, tenantId }, 'Failed to get audit stats');
            throw error;
        }
    }
    /**
     * Map a database record to an AuditEntry
     */
    mapRecordToEntry(record) {
        return {
            id: record.id,
            timestamp: record.timestamp.toISOString(),
            eventType: record.eventType,
            category: record.category,
            severity: record.severity,
            actorId: record.actorId,
            actorType: record.actorType,
            actorName: record.actorName ?? undefined,
            actorIp: record.actorIp ?? undefined,
            action: record.action,
            resourceType: record.resourceType,
            resourceId: record.resourceId,
            resourceName: record.resourceName ?? undefined,
            outcome: record.outcome,
            reason: record.reason ?? undefined,
            tenantId: record.tenantId,
            requestId: record.requestId ?? undefined,
            traceId: record.traceId ?? undefined,
            metadata: record.metadata ?? undefined,
        };
    }
}
// =============================================================================
// IN-MEMORY AUDIT STORE IMPLEMENTATION
// =============================================================================
/**
 * In-memory audit log store for testing
 *
 * Stores audit entries in memory. Useful for unit tests and development.
 * Not suitable for production use as data is lost on restart.
 */
export class InMemoryAuditStore {
    entries = [];
    maxEntries;
    /**
     * Create a new InMemoryAuditStore
     *
     * @param options - Configuration options
     */
    constructor(options) {
        this.maxEntries = options?.maxEntries ?? 10000;
        logger.info({ maxEntries: this.maxEntries }, 'InMemoryAuditStore initialized');
    }
    /**
     * Log an audit entry to memory
     */
    async log(entry) {
        this.entries.push(entry);
        // Keep bounded to prevent memory issues
        if (this.entries.length > this.maxEntries) {
            this.entries = this.entries.slice(-this.maxEntries);
        }
        logger.debug({ entryId: entry.id, eventType: entry.eventType }, 'Audit entry logged to memory');
    }
    /**
     * Query audit entries with filters
     */
    async query(filter) {
        let results = [...this.entries];
        if (filter.tenantId) {
            results = results.filter((e) => e.tenantId === filter.tenantId);
        }
        if (filter.eventType) {
            results = results.filter((e) => e.eventType === filter.eventType);
        }
        if (filter.category) {
            results = results.filter((e) => e.category === filter.category);
        }
        if (filter.severity) {
            results = results.filter((e) => e.severity === filter.severity);
        }
        if (filter.actorId) {
            results = results.filter((e) => e.actorId === filter.actorId);
        }
        if (filter.actorType) {
            results = results.filter((e) => e.actorType === filter.actorType);
        }
        if (filter.resourceType) {
            results = results.filter((e) => e.resourceType === filter.resourceType);
        }
        if (filter.resourceId) {
            results = results.filter((e) => e.resourceId === filter.resourceId);
        }
        if (filter.outcome) {
            results = results.filter((e) => e.outcome === filter.outcome);
        }
        if (filter.requestId) {
            results = results.filter((e) => e.requestId === filter.requestId);
        }
        if (filter.startTime) {
            const startDate = new Date(filter.startTime);
            results = results.filter((e) => new Date(e.timestamp) >= startDate);
        }
        if (filter.endTime) {
            const endDate = new Date(filter.endTime);
            results = results.filter((e) => new Date(e.timestamp) <= endDate);
        }
        // Sort by timestamp
        results.sort((a, b) => {
            const aTime = new Date(a.timestamp).getTime();
            const bTime = new Date(b.timestamp).getTime();
            return filter.orderDirection === 'asc' ? aTime - bTime : bTime - aTime;
        });
        // Apply pagination
        const offset = filter.offset ?? 0;
        const limit = filter.limit ?? 100;
        results = results.slice(offset, offset + limit);
        return results;
    }
    /**
     * Get all audit entries for a specific request ID
     */
    async getByRequestId(requestId) {
        return this.entries
            .filter((e) => e.requestId === requestId)
            .sort((a, b) => {
            const aTime = new Date(a.timestamp).getTime();
            const bTime = new Date(b.timestamp).getTime();
            return aTime - bTime;
        });
    }
    /**
     * Delete entries older than the specified date
     */
    async retention(olderThan) {
        const before = this.entries.length;
        this.entries = this.entries.filter((e) => new Date(e.timestamp) >= olderThan);
        const deleted = before - this.entries.length;
        if (deleted > 0) {
            logger.info({ deletedCount: deleted, olderThan: olderThan.toISOString() }, 'Audit entries purged from memory');
        }
        return deleted;
    }
    /**
     * Clear all entries (for testing)
     */
    clear() {
        this.entries = [];
        logger.debug('In-memory audit store cleared');
    }
    /**
     * Get all entries (for testing)
     */
    getAll() {
        return [...this.entries];
    }
    /**
     * Get entry count
     */
    get count() {
        return this.entries.length;
    }
}
/**
 * Create an audit log store based on environment configuration
 *
 * This factory function creates the appropriate audit store based on
 * the current environment. In production/staging, it uses the database
 * store. In development/test, it can use either based on configuration.
 *
 * @param options - Store configuration options
 * @returns An AuditLogStore implementation
 *
 * @example
 * ```typescript
 * // Auto-detect based on environment
 * const store = createAuditStore();
 *
 * // Force database store
 * const dbStore = createAuditStore({ type: 'database' });
 *
 * // Force in-memory store (for testing)
 * const memStore = createAuditStore({ type: 'memory' });
 * ```
 */
export function createAuditStore(options) {
    const config = getConfig();
    const env = config.env;
    // Determine store type
    let storeType;
    if (options?.type) {
        storeType = options.type;
    }
    else if (env === 'production' || env === 'staging') {
        // Always use database in production/staging
        storeType = 'database';
    }
    else {
        // Use environment variable or default to database
        const envStoreType = process.env['VORION_AUDIT_STORE_TYPE'];
        storeType = (envStoreType === 'memory' ? 'memory' : 'database');
    }
    if (storeType === 'database') {
        logger.info({ env }, 'Creating database audit store');
        return new DatabaseAuditStore({ database: options?.database });
    }
    else {
        logger.info({ env, maxEntries: options?.maxEntries }, 'Creating in-memory audit store');
        return new InMemoryAuditStore({ maxEntries: options?.maxEntries });
    }
}
// =============================================================================
// SINGLETON
// =============================================================================
let auditStoreInstance = null;
/**
 * Get the singleton audit store instance
 *
 * @param options - Store configuration options (only used on first call)
 * @returns The singleton AuditLogStore instance
 */
export function getAuditStore(options) {
    if (!auditStoreInstance) {
        auditStoreInstance = createAuditStore(options);
    }
    return auditStoreInstance;
}
/**
 * Reset the singleton audit store (for testing)
 */
export function resetAuditStore() {
    auditStoreInstance = null;
    logger.info('Audit store singleton reset');
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
/**
 * Create an audit entry with a new UUID
 *
 * @param data - Partial audit entry data
 * @returns Complete AuditEntry with generated ID and timestamp
 */
export function createAuditEntry(data) {
    return {
        id: crypto.randomUUID(),
        timestamp: data.timestamp ?? new Date().toISOString(),
        ...data,
    };
}
/**
 * Helper to log a security event to the audit store
 *
 * @param store - The audit store to use
 * @param event - Event details
 * @returns Promise resolving when the event is logged
 */
export async function logSecurityEvent(store, event) {
    const entry = createAuditEntry({
        eventType: event.eventType,
        category: event.category,
        severity: event.severity,
        actorId: event.actor.id,
        actorType: event.actor.type,
        actorName: event.actor.name,
        actorIp: event.actor.ip,
        action: event.action,
        resourceType: event.resource.type,
        resourceId: event.resource.id,
        resourceName: event.resource.name,
        outcome: event.outcome,
        reason: event.reason,
        tenantId: event.tenantId,
        requestId: event.requestId,
        traceId: event.traceId,
        metadata: event.metadata,
    });
    await store.log(entry);
}
//# sourceMappingURL=db-store.js.map