"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LogRepository = void 0;
const db_1 = require("../db/db");
const schema_1 = require("../db/schema");
const drizzle_orm_1 = require("drizzle-orm");
function generateUUID() {
    return globalThis.crypto?.randomUUID
        ? globalThis.crypto.randomUUID()
        : 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
            const r = (Math.random() * 16) | 0;
            const v = c === 'x' ? r : (r & 0x3) | 0x8;
            return v.toString(16);
        });
}
class LogRepository {
    /**
     * Get project configuration
     */
    async getProjectConfig(projectId) {
        const drizzle = await db_1.db.getDrizzle();
        const result = await drizzle
            .select({ config: schema_1.projects.config })
            .from(schema_1.projects)
            .where((0, drizzle_orm_1.eq)(schema_1.projects.id, projectId))
            .limit(1);
        if (result.length === 0) {
            return { trackedMetadataKeys: [] };
        }
        try {
            return JSON.parse(result[0].config);
        }
        catch {
            return { trackedMetadataKeys: [] };
        }
    }
    /**
     * Create a new log with metadata
     */
    async createLog(data, tsx = null) {
        const logId = generateUUID();
        const { metadata: logMetadataArray, ...logData } = data;
        const drizzle = tsx ? tsx : await db_1.db.getDrizzle();
        // Get project configuration to determine which metadata should be tracked
        const projectConfig = await this.getProjectConfig(logData.projectId);
        const trackedKeys = new Set(projectConfig.trackedMetadataKeys || []);
        // Separate metadata into tracked and embedded
        const trackedMetadata = [];
        const embeddedMetadata = {};
        if (logMetadataArray && logMetadataArray.length > 0) {
            logMetadataArray.forEach(meta => {
                if (trackedKeys.has(meta.key)) {
                    trackedMetadata.push(meta);
                }
                else {
                    embeddedMetadata[meta.key] = meta.value;
                }
            });
        }
        // Create log entry with embedded metadata
        const newLog = {
            id: logId,
            projectId: logData.projectId,
            level: logData.level,
            message: logData.message,
            timestamp: logData.timestamp,
            embeddedMetadata: JSON.stringify(embeddedMetadata),
        };
        // Use a transaction to insert both log and tracked metadata
        await drizzle.transaction(async (tx) => {
            // Insert log
            await tx.insert(schema_1.logs).values(newLog);
            // Insert tracked metadata if any
            if (trackedMetadata.length > 0) {
                // For each tracked metadata, find or create metadata entry, then create association
                for (const meta of trackedMetadata) {
                    // Check if metadata already exists for this project
                    let existingMetadata = await tx
                        .select({ id: schema_1.metadata.id })
                        .from(schema_1.metadata)
                        .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.metadata.projectId, logData.projectId), (0, drizzle_orm_1.eq)(schema_1.metadata.key, meta.key), (0, drizzle_orm_1.eq)(schema_1.metadata.value, meta.value)))
                        .limit(1);
                    let metadataId;
                    if (existingMetadata.length > 0) {
                        // Use existing metadata
                        metadataId = existingMetadata[0].id;
                    }
                    else {
                        // Create new metadata entry
                        metadataId = generateUUID();
                        await tx.insert(schema_1.metadata).values({
                            id: metadataId,
                            projectId: logData.projectId,
                            key: meta.key,
                            value: meta.value,
                        });
                    }
                    // Create log-metadata association
                    await tx.insert(schema_1.logMetadata).values({
                        id: generateUUID(),
                        logId,
                        metadataId,
                    });
                }
            }
        });
        // Return complete log with all metadata combined
        const allMetadata = [
            ...trackedMetadata,
            ...Object.entries(embeddedMetadata).map(([key, value]) => ({ key, value }))
        ];
        return {
            id: logId,
            projectId: logData.projectId,
            level: logData.level,
            message: logData.message,
            timestamp: logData.timestamp,
            metadata: allMetadata,
        };
    }
    /**
     * Create multiple logs in a single transaction
     */
    async createBulkLog(logsData) {
        const drizzle = await db_1.db.getDrizzle();
        const createdLogs = [];
        await drizzle.transaction(async (tx) => {
            for (const logData of logsData) {
                const createdLog = await this.createLog(logData, tx);
                createdLogs.push(createdLog);
            }
        });
        return createdLogs;
    }
    /**
     * Get a log by its ID, including metadata using a JOIN operation
     */
    async getLogById(id) {
        const drizzle = await db_1.db.getDrizzle();
        // Get the log with tracked metadata using JOINs through the bridge table
        const result = await drizzle
            .select({
            log: schema_1.logs,
            meta: schema_1.metadata,
        })
            .from(schema_1.logs)
            .leftJoin(schema_1.logMetadata, (0, drizzle_orm_1.eq)(schema_1.logs.id, schema_1.logMetadata.logId))
            .leftJoin(schema_1.metadata, (0, drizzle_orm_1.eq)(schema_1.logMetadata.metadataId, schema_1.metadata.id))
            .where((0, drizzle_orm_1.eq)(schema_1.logs.id, id));
        if (result.length === 0)
            return undefined;
        // Process results to create the Log object
        const logEntry = result[0].log;
        const trackedMetadata = result
            .filter(item => item.meta !== null)
            .map(item => ({ key: item.meta.key, value: item.meta.value }));
        // Parse embedded metadata
        let embeddedMetadata = {};
        try {
            embeddedMetadata = JSON.parse(logEntry.embeddedMetadata || '{}');
        }
        catch {
            embeddedMetadata = {};
        }
        // Combine tracked and embedded metadata
        const allMetadata = [
            ...trackedMetadata,
            ...Object.entries(embeddedMetadata).map(([key, value]) => ({ key, value }))
        ];
        return {
            id: logEntry.id,
            projectId: logEntry.projectId,
            level: logEntry.level,
            message: logEntry.message,
            timestamp: logEntry.timestamp,
            metadata: allMetadata,
        };
    }
    /**
     * Get logs for a project, using simple filtering
     */
    async getLogsByProjectId(projectId) {
        const { logs } = await this.getLogsWithFilters({ projectId });
        return logs;
    }
    /**
     * Get all unique metadata keys for a specific project
     * This is useful for building UIs that show available metadata fields
     */
    async getUniqueMetadataKeysByProjectId(projectId) {
        const drizzle = await db_1.db.getDrizzle();
        const keysSet = new Set();
        // Get tracked metadata keys
        const trackedResult = await drizzle
            .select({ key: schema_1.metadata.key })
            .from(schema_1.metadata)
            .where((0, drizzle_orm_1.eq)(schema_1.metadata.projectId, projectId))
            .groupBy(schema_1.metadata.key);
        trackedResult.forEach(row => keysSet.add(row.key));
        return Array.from(keysSet).sort();
    }
    /**
     * Get all unique metadata keys across all projects
     * This is useful for building UIs that show available metadata fields globally
     */
    async getMetadataKeys() {
        const drizzle = await db_1.db.getDrizzle();
        const keysSet = new Set();
        // Get tracked metadata keys
        const trackedResult = await drizzle
            .select({ key: schema_1.metadata.key })
            .from(schema_1.metadata)
            .groupBy(schema_1.metadata.key);
        trackedResult.forEach(row => keysSet.add(row.key));
        return Array.from(keysSet).sort();
    }
    /**
     * Get logs with advanced filtering and cursor-based pagination
     * Uses a subquery to correctly apply limits to distinct logs before fetching metadata.
     */
    async getLogsWithFilters(filters) {
        const drizzle = await db_1.db.getDrizzle();
        const sortDirection = filters.sortDirection || 'desc';
        const limit = filters.limit || 50; // The number of unique logs to return
        const fetchLimitPlusOne = limit + 1; // Fetch one extra unique log to determine hasNextPage
        // --- Conditions for the subquery selecting distinct log IDs ---
        const allSubQueryConditions = [];
        // Project ID filter
        if (filters.projectId && filters.projectId !== '*') {
            allSubQueryConditions.push((0, drizzle_orm_1.eq)(schema_1.logs.projectId, filters.projectId));
        }
        // Level filter
        if (filters.level) {
            allSubQueryConditions.push(Array.isArray(filters.level) ? (0, drizzle_orm_1.inArray)(schema_1.logs.level, filters.level) : (0, drizzle_orm_1.eq)(schema_1.logs.level, filters.level));
        }
        // Message contains filter
        if (filters.messageContains) {
            allSubQueryConditions.push((0, drizzle_orm_1.like)(schema_1.logs.message, `%${filters.messageContains}%`));
        }
        // Date range filters
        if (filters.fromDate) {
            allSubQueryConditions.push((0, drizzle_orm_1.sql) `${schema_1.logs.timestamp} >= ${new Date(filters.fromDate).toISOString()}`);
        }
        if (filters.toDate) {
            allSubQueryConditions.push((0, drizzle_orm_1.sql) `${schema_1.logs.timestamp} <= ${new Date(filters.toDate).toISOString()}`);
        }
        // Cursor conditions
        if (filters.cursor) {
            const cursorTimestamp = filters.cursor.timestamp;
            const cursorId = filters.cursor.id;
            if (sortDirection === 'desc') {
                allSubQueryConditions.push((0, drizzle_orm_1.or)((0, drizzle_orm_1.lt)(schema_1.logs.timestamp, cursorTimestamp), (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.logs.timestamp, cursorTimestamp), (0, drizzle_orm_1.lt)(schema_1.logs.id, cursorId))));
            }
            else { // asc
                allSubQueryConditions.push((0, drizzle_orm_1.or)((0, drizzle_orm_1.gt)(schema_1.logs.timestamp, cursorTimestamp), (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.logs.timestamp, cursorTimestamp), (0, drizzle_orm_1.gt)(schema_1.logs.id, cursorId))));
            }
        }
        // Metadata filters - only check tracked metadata using JOIN
        const metadataFilters = [];
        if (filters.metadata && filters.metadata.length > 0) {
            metadataFilters.push(...filters.metadata);
        }
        // Handle legacy metadata filtering
        if (filters.metaContains && Object.keys(filters.metaContains).length > 0) {
            Object.entries(filters.metaContains).forEach(([key, value]) => {
                metadataFilters.push({ key, value });
            });
        }
        // --- Subquery to get IDs and timestamps of `fetchLimitPlusOne` distinct logs ---
        let baseSubQuery = drizzle
            .selectDistinct({ id: schema_1.logs.id, timestamp: schema_1.logs.timestamp }) // timestamp for ordering
            .from(schema_1.logs);
        // Add JOINs for metadata filtering
        if (metadataFilters.length > 0) {
            metadataFilters.forEach((meta, index) => {
                const logMetadataAlias = drizzle
                    .select()
                    .from(schema_1.logMetadata)
                    .as(`lm_filter_${index}`);
                const metadataAlias = drizzle
                    .select()
                    .from(schema_1.metadata)
                    .as(`m_filter_${index}`);
                baseSubQuery = baseSubQuery
                    .innerJoin(logMetadataAlias, (0, drizzle_orm_1.eq)(logMetadataAlias.logId, schema_1.logs.id))
                    .innerJoin(metadataAlias, (0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(logMetadataAlias.metadataId, metadataAlias.id), (0, drizzle_orm_1.eq)(metadataAlias.key, meta.key), (0, drizzle_orm_1.eq)(metadataAlias.value, meta.value)));
            });
        }
        // Apply conditions if any
        const conditionalSubQuery = allSubQueryConditions.length > 0
            ? baseSubQuery.where((0, drizzle_orm_1.and)(...allSubQueryConditions))
            : baseSubQuery;
        const orderedAndLimitedLogIdsSubQuery = conditionalSubQuery
            .orderBy(sortDirection === 'desc' ? (0, drizzle_orm_1.desc)(schema_1.logs.timestamp) : (0, drizzle_orm_1.asc)(schema_1.logs.timestamp), sortDirection === 'desc' ? (0, drizzle_orm_1.desc)(schema_1.logs.id) : (0, drizzle_orm_1.asc)(schema_1.logs.id) // Tie-breaker for consistent ordering
        )
            .limit(fetchLimitPlusOne)
            .as('filtered_logs_subquery'); // Alias for the subquery
        // --- Main query to fetch full log data and their metadata for the selected IDs ---
        const fetchedLogEntries = await drizzle
            .select({
            log: schema_1.logs,
            metadata: schema_1.metadata,
        })
            .from(schema_1.logs)
            .innerJoin(orderedAndLimitedLogIdsSubQuery, (0, drizzle_orm_1.eq)(schema_1.logs.id, orderedAndLimitedLogIdsSubQuery.id))
            .leftJoin(schema_1.logMetadata, (0, drizzle_orm_1.eq)(schema_1.logs.id, schema_1.logMetadata.logId))
            .leftJoin(schema_1.metadata, (0, drizzle_orm_1.eq)(schema_1.logMetadata.metadataId, schema_1.metadata.id))
            .orderBy(// Order again based on the subquery's fields for consistency
        sortDirection === 'desc' ? (0, drizzle_orm_1.desc)(orderedAndLimitedLogIdsSubQuery.timestamp) : (0, drizzle_orm_1.asc)(orderedAndLimitedLogIdsSubQuery.timestamp), sortDirection === 'desc' ? (0, drizzle_orm_1.desc)(orderedAndLimitedLogIdsSubQuery.id) : (0, drizzle_orm_1.asc)(orderedAndLimitedLogIdsSubQuery.id));
        // --- Process results ---
        const logsMap = new Map();
        fetchedLogEntries.forEach(row => {
            const { log, metadata: metadataRow } = row; // 'log' here is the full log record from the 'logs' table
            if (!logsMap.has(log.id)) {
                // Parse embedded metadata
                let embeddedMetadata = {};
                try {
                    embeddedMetadata = JSON.parse(log.embeddedMetadata || '{}');
                }
                catch {
                    embeddedMetadata = {};
                }
                // Convert embedded metadata to LogMetadata format
                const embeddedMetadataArray = Object.entries(embeddedMetadata)
                    .map(([key, value]) => ({ key, value }));
                logsMap.set(log.id, {
                    id: log.id,
                    projectId: log.projectId,
                    level: log.level,
                    message: log.message,
                    timestamp: log.timestamp,
                    metadata: [...embeddedMetadataArray] // Start with embedded metadata
                });
            }
            if (metadataRow) {
                // Add tracked metadata (union with embedded metadata)
                const currentLog = logsMap.get(log.id);
                if (currentLog) {
                    // Check if this metadata key-value pair already exists to avoid duplicates
                    const existingMeta = currentLog.metadata.find(m => m.key === metadataRow.key && m.value === metadataRow.value);
                    if (!existingMeta) {
                        currentLog.metadata.push({ key: metadataRow.key, value: metadataRow.value });
                    }
                }
            }
        });
        const uniqueLogsRetrieved = Array.from(logsMap.values());
        // Determine if there's a next page
        const hasNextPage = uniqueLogsRetrieved.length > limit;
        // Return the first `limit` logs
        const logsToReturn = hasNextPage ? uniqueLogsRetrieved.slice(0, limit) : uniqueLogsRetrieved;
        return {
            logs: logsToReturn,
            hasNextPage,
        };
    }
    /**
     * Get metrics for a specific project
     * @param projectId The project ID to get metrics for
     */
    async getProjectMetrics(projectId) {
        const drizzle = await db_1.db.getDrizzle();
        // Get today's date as ISO string at start of day
        const todayStart = new Date();
        todayStart.setHours(0, 0, 0, 0);
        const todayStartIso = todayStart.toISOString();
        // Calculate total logs count
        const totalLogsResult = await drizzle
            .select({ count: (0, drizzle_orm_1.count)() })
            .from(schema_1.logs)
            .where((0, drizzle_orm_1.eq)(schema_1.logs.projectId, projectId));
        const totalLogs = totalLogsResult[0]?.count || 0;
        // Calculate today's logs count
        const todaysLogsResult = await drizzle
            .select({ count: (0, drizzle_orm_1.count)() })
            .from(schema_1.logs)
            .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.logs.projectId, projectId), (0, drizzle_orm_1.sql) `${schema_1.logs.timestamp} >= ${todayStartIso}`));
        const todaysLogs = todaysLogsResult[0]?.count || 0;
        // Calculate total info logs count
        const totalInfoResult = await drizzle
            .select({ count: (0, drizzle_orm_1.count)() })
            .from(schema_1.logs)
            .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.logs.projectId, projectId), (0, drizzle_orm_1.eq)(schema_1.logs.level, 'info')));
        const totalInfo = totalInfoResult[0]?.count || 0;
        // Calculate today's info logs count
        const todaysInfoResult = await drizzle
            .select({ count: (0, drizzle_orm_1.count)() })
            .from(schema_1.logs)
            .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.logs.projectId, projectId), (0, drizzle_orm_1.eq)(schema_1.logs.level, 'info'), (0, drizzle_orm_1.sql) `${schema_1.logs.timestamp} >= ${todayStartIso}`));
        const todaysInfo = todaysInfoResult[0]?.count || 0;
        // Calculate total warn logs count
        const totalWarnResult = await drizzle
            .select({ count: (0, drizzle_orm_1.count)() })
            .from(schema_1.logs)
            .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.logs.projectId, projectId), (0, drizzle_orm_1.eq)(schema_1.logs.level, 'warn')));
        const totalWarn = totalWarnResult[0]?.count || 0;
        // Calculate today's warn logs count
        const todaysWarnResult = await drizzle
            .select({ count: (0, drizzle_orm_1.count)() })
            .from(schema_1.logs)
            .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.logs.projectId, projectId), (0, drizzle_orm_1.eq)(schema_1.logs.level, 'warn'), (0, drizzle_orm_1.sql) `${schema_1.logs.timestamp} >= ${todayStartIso}`));
        const todaysWarn = todaysWarnResult[0]?.count || 0;
        // Calculate total errors count (logs with level 'error')
        const totalErrorsResult = await drizzle
            .select({ count: (0, drizzle_orm_1.count)() })
            .from(schema_1.logs)
            .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.logs.projectId, projectId), (0, drizzle_orm_1.eq)(schema_1.logs.level, 'error')));
        const totalErrors = totalErrorsResult[0]?.count || 0;
        // Calculate today's errors count
        const todaysErrorsResult = await drizzle
            .select({ count: (0, drizzle_orm_1.count)() })
            .from(schema_1.logs)
            .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.logs.projectId, projectId), (0, drizzle_orm_1.eq)(schema_1.logs.level, 'error'), (0, drizzle_orm_1.sql) `${schema_1.logs.timestamp} >= ${todayStartIso}`));
        const todaysErrors = todaysErrorsResult[0]?.count || 0;
        // Get last activity (most recent log)
        const lastActivityResult = await drizzle
            .select({
            timestamp: schema_1.logs.timestamp,
            message: schema_1.logs.message,
            level: schema_1.logs.level,
        })
            .from(schema_1.logs)
            .where((0, drizzle_orm_1.eq)(schema_1.logs.projectId, projectId))
            .orderBy((0, drizzle_orm_1.desc)(schema_1.logs.timestamp))
            .limit(1);
        const lastActivity = lastActivityResult[0] || undefined;
        return {
            totalLogs,
            todaysLogs,
            totalInfo,
            todaysInfo,
            totalWarn,
            todaysWarn,
            totalErrors,
            todaysErrors,
            lastActivity,
        };
    }
    /**
     * Get historical log counts by day for the last 7 days
     * This is used for trend charts
     * @param projectId The project ID to get metrics for
     */
    async getHistoricalLogCounts(projectId, days = 7) {
        const drizzle = await db_1.db.getDrizzle();
        // Create today's date and reset to midnight for consistent date handling
        const today = new Date();
        const todayStr = today.toISOString().split('T')[0]; // YYYY-MM-DD
        // Calculate end date - set to 23:59:59.999 of today
        const endDate = new Date(todayStr + 'T23:59:59.999Z');
        // Calculate start date - (days-1) days before today at 00:00:00
        const startDate = new Date(todayStr);
        startDate.setDate(startDate.getDate() - (days - 1));
        startDate.setHours(0, 0, 0, 0);
        // Format as ISO strings for the query
        const startISO = startDate.toISOString();
        const endISO = endDate.toISOString();
        // Get all logs in date range
        const result = await drizzle
            .select({
            timestamp: schema_1.logs.timestamp,
            level: schema_1.logs.level,
        })
            .from(schema_1.logs)
            .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_1.logs.projectId, projectId), (0, drizzle_orm_1.sql) `${schema_1.logs.timestamp} >= ${startISO}`, (0, drizzle_orm_1.sql) `${schema_1.logs.timestamp} <= ${endISO}`));
        // Create a map to store counts by day
        const dayMap = new Map();
        // Initialize all days in range (including today) with zero counts
        for (let i = 0; i < days; i++) {
            const date = new Date(startDate);
            date.setDate(date.getDate() + i);
            const dateKey = date.toISOString().split('T')[0]; // YYYY-MM-DD format
            dayMap.set(dateKey, {
                info: 0,
                warn: 0,
                error: 0,
                total: 0
            });
        }
        // Explicitly ensure today is in the map (in case of timezone issues)
        if (!dayMap.has(todayStr)) {
            dayMap.set(todayStr, {
                info: 0,
                warn: 0,
                error: 0,
                total: 0
            });
        }
        // Count logs by day and level
        result.forEach(log => {
            // Extract YYYY-MM-DD from the timestamp
            const date = new Date(log.timestamp);
            const dateKey = date.toISOString().split('T')[0];
            if (dayMap.has(dateKey)) {
                const counts = dayMap.get(dateKey);
                counts.total++;
                // Increment the appropriate level counter
                if (log.level === 'info')
                    counts.info++;
                else if (log.level === 'warn')
                    counts.warn++;
                else if (log.level === 'error')
                    counts.error++;
            }
        });
        // Convert map to array and sort by date
        return Array.from(dayMap.entries())
            .map(([date, counts]) => ({
            date,
            ...counts
        }))
            .sort((a, b) => a.date.localeCompare(b.date));
    }
    /**
     * Get all logs with their metadata using a JOIN
     * This is a convenience method that delegates to getLogsWithFilters
     */
    async getAllLogs() {
        const { logs } = await this.getLogsWithFilters({ projectId: '*' });
        return logs;
    }
    /**
     * Clear all logs for a specific project
     */
    async clearProjectLogs(projectId) {
        const drizzle = await db_1.db.getDrizzle();
        // Delete all logs for the project (metadata will be cascade deleted)
        await drizzle
            .delete(schema_1.logs)
            .where((0, drizzle_orm_1.eq)(schema_1.logs.projectId, projectId));
        // Also delete any orphaned metadata entries for this project
        await drizzle
            .delete(schema_1.metadata)
            .where((0, drizzle_orm_1.eq)(schema_1.metadata.projectId, projectId));
    }
}
exports.LogRepository = LogRepository;
//# sourceMappingURL=logRepository.js.map