/**
 * Sync Audit Logger
 *
 * Logs all sync operations for compliance and debugging.
 * Uses JSONL format for efficient appending and querying.
 *
 * Features:
 * - JSONL format (one JSON object per line)
 * - Log rotation (new file when > 100MB or new day)
 * - Configurable retention
 *
 * @module core/sync/sync-audit-logger
 */
import nodeFs from 'fs';
import { promises as fs } from 'fs';
import path from 'path';
import { consoleLogger } from '../../utils/logger.js';
/**
 * SyncAuditLogger - Comprehensive audit logging for sync operations
 *
 * All sync attempts are logged including:
 * - Successful operations
 * - Permission denials
 * - Errors during execution
 *
 * Logs are stored in JSONL format for easy parsing:
 * `.specweave/logs/sync/audit-YYYY-MM-DD.jsonl`
 *
 * Usage:
 * ```typescript
 * const logger = new SyncAuditLogger({ specweavePath });
 *
 * await logger.logSuccess('github', 'upsert-internal', 'FS-001', 150);
 * await logger.logDenied('jira', 'upsert-external', 'EXT-123', 'External updates disabled');
 * await logger.logError('ado', 'update-status', 'US-456', new Error('Network timeout'));
 * ```
 */
export class SyncAuditLogger {
    constructor(options = {}) {
        // Cache current date to detect day changes
        this.currentDate = '';
        this.currentFilePath = '';
        const specweavePath = options.specweavePath ?? path.join(process.cwd(), '.specweave');
        this.logDir = path.join(specweavePath, 'logs', 'sync');
        this.logger = options.logger ?? consoleLogger;
        this.maxFileSizeBytes = options.maxFileSizeBytes ?? 100 * 1024 * 1024; // 100MB
        this.retentionDays = options.retentionDays ?? 30;
        // Guard: disable logging if .specweave doesn't exist (prevents stale folder creation)
        this.enabled = (options.enabled ?? true) && nodeFs.existsSync(specweavePath);
    }
    /**
     * Log a successful sync operation
     *
     * @param platform - Target platform
     * @param operation - Operation type
     * @param itemId - Item ID
     * @param durationMs - Operation duration in milliseconds
     * @param context - Optional additional context
     */
    async logSuccess(platform, operation, itemId, durationMs, context) {
        await this.log({
            timestamp: new Date().toISOString(),
            platform,
            operation,
            itemId,
            result: 'success',
            durationMs,
            context,
        });
    }
    /**
     * Log a denied sync operation
     *
     * @param platform - Target platform
     * @param operation - Operation type
     * @param itemId - Item ID
     * @param reason - Denial reason
     * @param context - Optional additional context
     */
    async logDenied(platform, operation, itemId, reason, context) {
        await this.log({
            timestamp: new Date().toISOString(),
            platform,
            operation,
            itemId,
            result: 'denied',
            reason,
            context,
        });
    }
    /**
     * Log an error during sync operation
     *
     * @param platform - Target platform
     * @param operation - Operation type
     * @param itemId - Item ID
     * @param error - Error that occurred
     * @param context - Optional additional context
     */
    async logError(platform, operation, itemId, error, context) {
        await this.log({
            timestamp: new Date().toISOString(),
            platform,
            operation,
            itemId,
            result: 'error',
            error: error.message,
            context: {
                ...context,
                stack: error.stack,
            },
        });
    }
    /**
     * Log a pull sync operation (Increment 0089)
     *
     * @param platform - Source platform
     * @param operation - Operation type
     * @param itemId - Item ID
     * @param pullData - Pull-specific data
     */
    async logPull(platform, operation, itemId, pullData) {
        await this.log({
            timestamp: new Date().toISOString(),
            platform,
            operation,
            itemId,
            result: 'success',
            direction: 'pull',
            externalChangedBy: pullData.externalChangedBy,
            externalChangedAt: pullData.externalChangedAt,
            oldValue: pullData.oldValue,
            newValue: pullData.newValue,
            conflictResolution: pullData.conflictResolution,
            durationMs: pullData.durationMs,
        });
    }
    /**
     * Log a pull sync conflict resolution (Increment 0089)
     *
     * @param platform - Source platform
     * @param itemId - Item ID
     * @param resolution - How the conflict was resolved
     * @param details - Additional conflict details
     */
    async logPullConflict(platform, itemId, resolution, details) {
        await this.log({
            timestamp: new Date().toISOString(),
            platform,
            operation: 'update-status', // Conflict resolution is typically status update
            itemId,
            result: 'success',
            direction: 'pull',
            conflictResolution: resolution,
            oldValue: details.localValue,
            newValue: details.externalValue,
            context: {
                field: details.field,
                localTimestamp: details.localTimestamp,
                externalTimestamp: details.externalTimestamp,
                reason: details.reason,
            },
        });
    }
    /**
     * Get pull sync entries (Increment 0089)
     *
     * @param limit - Maximum entries
     */
    async getPullEntries(limit = 50) {
        return this.getRecentEntries(limit, (e) => e.direction === 'pull');
    }
    /**
     * Get log file path for a specific date
     *
     * @param date - Date to get log path for (defaults to today)
     */
    getLogPath(date) {
        const d = date ?? new Date();
        const dateStr = d.toISOString().split('T')[0]; // YYYY-MM-DD
        return path.join(this.logDir, `audit-${dateStr}.jsonl`);
    }
    /**
     * Get all log files in the log directory
     *
     * @returns Array of log file paths sorted by date (newest first)
     */
    async getLogFiles() {
        try {
            const files = await fs.readdir(this.logDir);
            return files
                .filter((f) => f.startsWith('audit-') && f.endsWith('.jsonl'))
                .map((f) => path.join(this.logDir, f))
                .sort()
                .reverse();
        }
        catch {
            return [];
        }
    }
    /**
     * Read log entries from a file
     *
     * @param filePath - Path to log file
     * @param limit - Maximum entries to return
     * @param filter - Optional filter function
     */
    async readEntries(filePath, limit, filter) {
        try {
            const content = await fs.readFile(filePath, 'utf-8');
            const lines = content.trim().split('\n').filter(Boolean);
            let entries = lines.map((line) => JSON.parse(line));
            if (filter) {
                entries = entries.filter(filter);
            }
            if (limit) {
                entries = entries.slice(-limit); // Last N entries
            }
            return entries;
        }
        catch {
            return [];
        }
    }
    /**
     * Get recent log entries across all log files
     *
     * @param limit - Maximum entries to return
     * @param filter - Optional filter function
     */
    async getRecentEntries(limit = 100, filter) {
        const files = await this.getLogFiles();
        const entries = [];
        for (const file of files) {
            const fileEntries = await this.readEntries(file, undefined, filter);
            entries.push(...fileEntries);
            if (entries.length >= limit) {
                break;
            }
        }
        return entries.slice(-limit);
    }
    /**
     * Get entries for a specific platform
     *
     * @param platform - Platform to filter by
     * @param limit - Maximum entries
     */
    async getEntriesForPlatform(platform, limit = 50) {
        return this.getRecentEntries(limit, (e) => e.platform === platform);
    }
    /**
     * Get all denied entries (for monitoring)
     *
     * @param limit - Maximum entries
     */
    async getDeniedEntries(limit = 50) {
        return this.getRecentEntries(limit, (e) => e.result === 'denied');
    }
    /**
     * Get all error entries (for debugging)
     *
     * @param limit - Maximum entries
     */
    async getErrorEntries(limit = 50) {
        return this.getRecentEntries(limit, (e) => e.result === 'error');
    }
    /**
     * Get statistics for a time period
     */
    async getStats(since) {
        const sinceStr = (since ?? new Date(Date.now() - 24 * 60 * 60 * 1000)).toISOString();
        const entries = await this.getRecentEntries(10000, (e) => e.timestamp >= sinceStr);
        const stats = {
            total: entries.length,
            success: entries.filter((e) => e.result === 'success').length,
            denied: entries.filter((e) => e.result === 'denied').length,
            errors: entries.filter((e) => e.result === 'error').length,
            byPlatform: { github: 0, jira: 0, ado: 0 },
            avgDurationMs: 0,
        };
        for (const entry of entries) {
            stats.byPlatform[entry.platform]++;
        }
        const durations = entries.filter((e) => e.durationMs).map((e) => e.durationMs);
        stats.avgDurationMs = durations.length > 0
            ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length)
            : 0;
        return stats;
    }
    /**
     * Clean up old log files based on retention policy
     */
    async cleanup() {
        const cutoffDate = new Date();
        cutoffDate.setDate(cutoffDate.getDate() - this.retentionDays);
        const cutoffStr = cutoffDate.toISOString().split('T')[0];
        const files = await this.getLogFiles();
        let deletedCount = 0;
        for (const file of files) {
            // Extract date from filename: audit-YYYY-MM-DD.jsonl
            const match = path.basename(file).match(/audit-(\d{4}-\d{2}-\d{2})\.jsonl/);
            if (match && match[1] < cutoffStr) {
                try {
                    await fs.unlink(file);
                    deletedCount++;
                    this.logger.debug(`Deleted old audit log: ${path.basename(file)}`);
                }
                catch (error) {
                    this.logger.debug(`Failed to delete audit log: ${file}`);
                }
            }
        }
        return deletedCount;
    }
    /**
     * Write an entry to the log file
     */
    async log(entry) {
        if (!this.enabled) {
            return;
        }
        try {
            // Ensure directory exists
            await fs.mkdir(this.logDir, { recursive: true });
            // Get current file path (may rotate on new day)
            const filePath = await this.getOrRotateFilePath();
            // Append entry as JSONL
            const line = JSON.stringify(entry) + '\n';
            await fs.appendFile(filePath, line, 'utf-8');
        }
        catch (error) {
            // Don't fail the sync operation if logging fails
            this.logger.debug(`Failed to write audit log: ${error}`);
        }
    }
    /**
     * Get current file path, rotating if needed
     */
    async getOrRotateFilePath() {
        const today = new Date().toISOString().split('T')[0];
        if (today !== this.currentDate) {
            this.currentDate = today;
            this.currentFilePath = this.getLogPath();
        }
        if (!this.currentFilePath) {
            return this.currentFilePath;
        }
        try {
            const stats = await fs.stat(this.currentFilePath);
            if (stats.size >= this.maxFileSizeBytes) {
                this.currentFilePath = await this.findNextRotatedPath();
            }
        }
        catch {
            // File doesn't exist yet
        }
        return this.currentFilePath;
    }
    /**
     * Find the next available rotated file path
     */
    async findNextRotatedPath() {
        const basePath = this.currentFilePath.replace('.jsonl', '');
        let seq = 1;
        while (true) {
            const rotatedPath = `${basePath}-${seq}.jsonl`;
            try {
                await fs.access(rotatedPath);
                seq++;
            }
            catch {
                return rotatedPath;
            }
        }
    }
}
//# sourceMappingURL=sync-audit-logger.js.map