/**
 * History Store
 *
 * Append-only storage for certification and scan history.
 * Uses JSONL format for efficient streaming reads/writes.
 *
 * @module history/store
 */
import { appendFile, mkdir, access, stat, writeFile } from "fs/promises";
import { join } from "path";
import { createReadStream } from "fs";
import { createInterface } from "readline";
import { randomUUID, createHash } from "crypto";
import { sign as sigstoreSign } from "sigstore";
import { logger } from "../logger.js";
const HISTORY_DIR = ".vaspera";
const HISTORY_FILE = "history.jsonl";
/** Genesis hash for the first entry in a chain */
const GENESIS_HASH = "0000000000000000000000000000000000000000000000000000000000000000";
/**
 * Calculate SHA-256 hash of an entry (excluding the integrity field)
 */
export function calculateEntryHash(entry) {
    // Create a copy without the integrity field for hashing
    const { integrity: _integrity, ...entryWithoutIntegrity } = entry;
    const serialized = JSON.stringify(entryWithoutIntegrity, Object.keys(entryWithoutIntegrity).sort());
    return createHash("sha256").update(serialized, "utf8").digest("hex");
}
/**
 * Get the hash of the last entry in the history file
 */
export async function getLastEntryHash(projectPath) {
    const historyFile = join(projectPath, HISTORY_DIR, HISTORY_FILE);
    try {
        await access(historyFile);
    }
    catch {
        return GENESIS_HASH;
    }
    // Read the file and get the last valid entry
    const fileStream = createReadStream(historyFile);
    const rl = createInterface({
        input: fileStream,
        crlfDelay: Infinity,
    });
    let lastEntry = null;
    for await (const line of rl) {
        if (!line.trim())
            continue;
        try {
            lastEntry = JSON.parse(line);
        }
        catch {
            // Skip malformed lines
        }
    }
    if (!lastEntry) {
        return GENESIS_HASH;
    }
    // Return the stored hash if available, otherwise calculate it
    if (lastEntry.integrity?.hash) {
        return lastEntry.integrity.hash;
    }
    // For legacy entries without integrity, calculate the hash
    return calculateEntryHash(lastEntry);
}
/**
 * Ensure history file exists
 */
async function ensureHistoryFile(projectPath) {
    const historyDir = join(projectPath, HISTORY_DIR);
    const historyFile = join(historyDir, HISTORY_FILE);
    try {
        await access(historyDir);
    }
    catch {
        await mkdir(historyDir, { recursive: true });
    }
    return historyFile;
}
/**
 * Append a history entry
 */
export async function appendHistoryEntry(projectPath, entry, options = {}) {
    const historyFile = await ensureHistoryFile(projectPath);
    const { actor, enableIntegrity = true } = options;
    // Build base entry
    const baseEntry = {
        ...entry,
        id: randomUUID().slice(0, 8),
        timestamp: new Date().toISOString(),
        projectPath,
        ...(actor && { actor }),
    };
    // Add integrity proof if enabled
    if (enableIntegrity) {
        const previousHash = await getLastEntryHash(projectPath);
        const hash = calculateEntryHash(baseEntry);
        const integrity = {
            hash,
            previousHash,
        };
        // Sign with Sigstore if requested
        if (options.sign) {
            try {
                const bundle = await sigstoreSign(Buffer.from(hash, "utf8"));
                // Extract signature from bundle
                if (bundle.verificationMaterial?.tlogEntries?.[0]) {
                    const tlogEntry = bundle.verificationMaterial.tlogEntries[0];
                    integrity.rekorLogIndex = Number(tlogEntry.logIndex);
                    if (tlogEntry.logId?.keyId) {
                        integrity.rekorLogId = Buffer.from(tlogEntry.logId.keyId).toString("base64");
                    }
                }
                // Store the bundle signature
                if (bundle.messageSignature?.signature) {
                    integrity.signature = Buffer.from(bundle.messageSignature.signature).toString("base64");
                }
                logger.info("history.entry_signed", {
                    hash: hash.slice(0, 16),
                    rekorLogIndex: integrity.rekorLogIndex,
                });
            }
            catch (signError) {
                logger.warn("history.signing_failed", {
                    error: signError instanceof Error ? signError.message : String(signError),
                });
                // Continue without signature - signing is optional enhancement
            }
        }
        baseEntry.integrity = integrity;
    }
    const line = JSON.stringify(baseEntry) + "\n";
    await appendFile(historyFile, line, "utf-8");
    logger.debug("history.entry_appended", {
        type: entry.type,
        id: baseEntry.id,
        hasIntegrity: !!baseEntry.integrity,
        hasActor: !!baseEntry.actor,
    });
    return baseEntry;
}
/**
 * Query history entries with filtering and pagination
 */
export async function queryHistory(projectPath, options = {}) {
    const historyFile = join(projectPath, HISTORY_DIR, HISTORY_FILE);
    const { type, certificationId, startDate, endDate, limit = 100, offset = 0, order = "desc", } = options;
    const entries = [];
    try {
        await access(historyFile);
    }
    catch {
        // History file doesn't exist yet
        return { entries: [], total: 0, hasMore: false };
    }
    // Stream read the JSONL file
    const fileStream = createReadStream(historyFile);
    const rl = createInterface({
        input: fileStream,
        crlfDelay: Infinity,
    });
    const allMatching = [];
    for await (const line of rl) {
        if (!line.trim())
            continue;
        try {
            const entry = JSON.parse(line);
            // Apply filters
            if (type) {
                const types = Array.isArray(type) ? type : [type];
                if (!types.includes(entry.type))
                    continue;
            }
            if (certificationId && entry.certificationId !== certificationId)
                continue;
            if (startDate && entry.timestamp < startDate)
                continue;
            if (endDate && entry.timestamp > endDate)
                continue;
            allMatching.push(entry);
        }
        catch {
            // Skip malformed lines
            logger.warn("history.malformed_line", { line: line.slice(0, 100) });
        }
    }
    // Sort
    allMatching.sort((a, b) => {
        const cmp = a.timestamp.localeCompare(b.timestamp);
        return order === "desc" ? -cmp : cmp;
    });
    // Paginate
    const total = allMatching.length;
    const paginated = allMatching.slice(offset, offset + limit);
    return {
        entries: paginated,
        total,
        hasMore: offset + limit < total,
    };
}
/**
 * Get period boundaries for trend calculation
 */
function getPeriodBoundaries(startDate, endDate, period) {
    const boundaries = [];
    let current = new Date(startDate);
    while (current < endDate) {
        let periodStart = new Date(current);
        let periodEnd;
        let label;
        switch (period) {
            case "day":
                periodEnd = new Date(current);
                periodEnd.setDate(periodEnd.getDate() + 1);
                label = current.toISOString().slice(0, 10);
                break;
            case "week":
                periodEnd = new Date(current);
                periodEnd.setDate(periodEnd.getDate() + 7);
                label = `W${getWeekNumber(current)}-${current.getFullYear()}`;
                break;
            case "month":
                periodEnd = new Date(current);
                periodEnd.setMonth(periodEnd.getMonth() + 1);
                label = `${current.getFullYear()}-${String(current.getMonth() + 1).padStart(2, "0")}`;
                break;
            case "quarter":
                periodEnd = new Date(current);
                periodEnd.setMonth(periodEnd.getMonth() + 3);
                label = `Q${Math.floor(current.getMonth() / 3) + 1}-${current.getFullYear()}`;
                break;
            case "year":
                periodEnd = new Date(current);
                periodEnd.setFullYear(periodEnd.getFullYear() + 1);
                label = String(current.getFullYear());
                break;
        }
        boundaries.push({
            start: periodStart,
            end: periodEnd > endDate ? endDate : periodEnd,
            label,
        });
        current = periodEnd;
    }
    return boundaries;
}
/**
 * Get ISO week number
 */
function getWeekNumber(date) {
    const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
    const dayNum = d.getUTCDay() || 7;
    d.setUTCDate(d.getUTCDate() + 4 - dayNum);
    const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
    return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
}
/**
 * Calculate trends from history data
 */
export async function calculateTrends(projectPath, options = {}) {
    const { period = "week", startDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000).toISOString(), // 90 days ago
    endDate = new Date().toISOString(), } = options;
    // Get all relevant history entries
    const result = await queryHistory(projectPath, {
        startDate,
        endDate,
        limit: 10000,
        order: "asc",
    });
    const boundaries = getPeriodBoundaries(new Date(startDate), new Date(endDate), period);
    const dataPoints = [];
    const severities = ["critical", "high", "medium", "low", "info"];
    for (const boundary of boundaries) {
        const periodEntries = result.entries.filter((e) => {
            const entryDate = new Date(e.timestamp);
            return entryDate >= boundary.start && entryDate < boundary.end;
        });
        const certifications = periodEntries.filter((e) => e.type === "certification_completed");
        const scans = periodEntries.filter((e) => e.type === "scan_completed");
        const findings = periodEntries.filter((e) => e.type === "finding_submitted");
        const fixed = periodEntries.filter((e) => e.type === "finding_fixed");
        const bySeverity = {
            critical: 0,
            high: 0,
            medium: 0,
            low: 0,
            info: 0,
        };
        for (const finding of findings) {
            bySeverity[finding.severity] = (bySeverity[finding.severity] || 0) + 1;
        }
        const avgScore = certifications.length > 0
            ? certifications.reduce((sum, c) => sum + c.score, 0) / certifications.length
            : 0;
        dataPoints.push({
            period: boundary.label,
            startDate: boundary.start.toISOString(),
            endDate: boundary.end.toISOString(),
            certificationCount: certifications.length,
            scanCount: scans.length,
            findingsTotal: findings.length,
            findingsFixed: fixed.length,
            avgScore: Math.round(avgScore * 100) / 100,
            bySeverity,
        });
    }
    // Calculate summary
    const totalCertifications = dataPoints.reduce((sum, dp) => sum + dp.certificationCount, 0);
    const totalScans = dataPoints.reduce((sum, dp) => sum + dp.scanCount, 0);
    const totalFindings = dataPoints.reduce((sum, dp) => sum + dp.findingsTotal, 0);
    const totalFixed = dataPoints.reduce((sum, dp) => sum + dp.findingsFixed, 0);
    const scoredPoints = dataPoints.filter((dp) => dp.avgScore > 0);
    const avgScore = scoredPoints.length > 0
        ? scoredPoints.reduce((sum, dp) => sum + dp.avgScore, 0) / scoredPoints.length
        : 0;
    // Calculate change (compare first half to second half)
    const midpoint = Math.floor(dataPoints.length / 2);
    const firstHalf = dataPoints.slice(0, midpoint);
    const secondHalf = dataPoints.slice(midpoint);
    const firstHalfScore = firstHalf.filter(dp => dp.avgScore > 0).reduce((sum, dp) => sum + dp.avgScore, 0) / (firstHalf.filter(dp => dp.avgScore > 0).length || 1);
    const secondHalfScore = secondHalf.filter(dp => dp.avgScore > 0).reduce((sum, dp) => sum + dp.avgScore, 0) / (secondHalf.filter(dp => dp.avgScore > 0).length || 1);
    const firstHalfFindings = firstHalf.reduce((sum, dp) => sum + dp.findingsTotal, 0);
    const secondHalfFindings = secondHalf.reduce((sum, dp) => sum + dp.findingsTotal, 0);
    return {
        projectPath,
        period,
        dataPoints,
        summary: {
            totalCertifications,
            totalScans,
            totalFindings,
            totalFixed,
            avgScore: Math.round(avgScore * 100) / 100,
            scoreChange: Math.round((secondHalfScore - firstHalfScore) * 100) / 100,
            findingsChange: secondHalfFindings - firstHalfFindings,
        },
    };
}
/**
 * Get recent activity summary
 */
export async function getRecentActivity(projectPath, days = 7) {
    const startDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
    const result = await queryHistory(projectPath, {
        startDate,
        limit: 1000,
        order: "desc",
    });
    const certifications = result.entries.filter((e) => e.type === "certification_completed");
    const scans = result.entries.filter((e) => e.type === "scan_completed");
    const findings = result.entries.filter((e) => e.type === "finding_submitted");
    const fixed = result.entries.filter((e) => e.type === "finding_fixed");
    const latest = certifications[0];
    return {
        certifications: certifications.length,
        scans: scans.length,
        findings: findings.length,
        fixed: fixed.length,
        latestCertification: latest
            ? {
                id: latest.certificationId,
                score: latest.score,
                level: latest.level,
                timestamp: latest.timestamp,
            }
            : undefined,
    };
}
/**
 * Format trends as markdown
 */
export function formatTrendsAsMarkdown(analysis) {
    const lines = [
        `# Security Trends Report`,
        "",
        `**Project**: ${analysis.projectPath}`,
        `**Period**: ${analysis.period}`,
        `**Data Points**: ${analysis.dataPoints.length}`,
        "",
        "## Summary",
        "",
        `| Metric | Value |`,
        `|--------|-------|`,
        `| Total Certifications | ${analysis.summary.totalCertifications} |`,
        `| Total Scans | ${analysis.summary.totalScans} |`,
        `| Total Findings | ${analysis.summary.totalFindings} |`,
        `| Findings Fixed | ${analysis.summary.totalFixed} |`,
        `| Average Score | ${analysis.summary.avgScore} |`,
        `| Score Change | ${analysis.summary.scoreChange >= 0 ? "+" : ""}${analysis.summary.scoreChange} |`,
        `| Findings Change | ${analysis.summary.findingsChange >= 0 ? "+" : ""}${analysis.summary.findingsChange} |`,
        "",
        "## Trend Data",
        "",
        "| Period | Certs | Scans | Findings | Fixed | Avg Score | Critical | High |",
        "|--------|-------|-------|----------|-------|-----------|----------|------|",
    ];
    for (const dp of analysis.dataPoints) {
        lines.push(`| ${dp.period} | ${dp.certificationCount} | ${dp.scanCount} | ${dp.findingsTotal} | ${dp.findingsFixed} | ${dp.avgScore || "-"} | ${dp.bySeverity.critical} | ${dp.bySeverity.high} |`);
    }
    return lines.join("\n");
}
/**
 * Get history file stats
 */
export async function getHistoryStats(projectPath) {
    const historyFile = join(projectPath, HISTORY_DIR, HISTORY_FILE);
    try {
        await access(historyFile);
    }
    catch {
        return { exists: false, entries: 0, sizeBytes: 0 };
    }
    const stats = await stat(historyFile);
    const result = await queryHistory(projectPath, { limit: 1, order: "asc" });
    const newest = await queryHistory(projectPath, { limit: 1, order: "desc" });
    return {
        exists: true,
        entries: result.total,
        sizeBytes: stats.size,
        oldestEntry: result.entries[0]?.timestamp,
        newestEntry: newest.entries[0]?.timestamp,
    };
}
/**
 * Export audit trail for compliance purposes
 *
 * Creates a portable, verifiable export of the audit history
 * suitable for external compliance review.
 */
export async function exportAuditTrail(projectPath, options = {}) {
    const { format = "json", startDate, endDate, types, includeIntegrity = true, outputPath, } = options;
    const exportedAt = new Date().toISOString();
    // Query all matching entries
    const result = await queryHistory(projectPath, {
        type: types,
        startDate,
        endDate,
        limit: 100000,
        order: "asc",
    });
    // Calculate chain integrity info
    let genesisHash;
    let headHash;
    let entriesWithIntegrity = 0;
    for (const entry of result.entries) {
        if (entry.integrity?.hash) {
            entriesWithIntegrity++;
            if (!genesisHash) {
                genesisHash = entry.integrity.hash;
            }
            headHash = entry.integrity.hash;
        }
    }
    // Prepare entries for export
    const exportEntries = result.entries.map((entry) => {
        if (!includeIntegrity) {
            const { integrity: _, ...rest } = entry;
            return rest;
        }
        return entry;
    });
    // Generate content based on format
    let content;
    switch (format) {
        case "jsonl":
            content = exportEntries.map((e) => JSON.stringify(e)).join("\n") + "\n";
            break;
        case "csv":
            const headers = [
                "id",
                "timestamp",
                "type",
                "certificationId",
                "actor_type",
                "actor_id",
                "hash",
                "previousHash",
                "signature",
                "rekorLogIndex",
            ];
            const rows = [headers.join(",")];
            for (const entry of result.entries) {
                rows.push([
                    entry.id,
                    entry.timestamp,
                    entry.type,
                    entry.certificationId || "",
                    entry.actor?.type || "",
                    entry.actor?.id || "",
                    includeIntegrity ? (entry.integrity?.hash || "") : "",
                    includeIntegrity ? (entry.integrity?.previousHash || "") : "",
                    includeIntegrity && entry.integrity?.signature ? "present" : "",
                    includeIntegrity ? (entry.integrity?.rekorLogIndex?.toString() || "") : "",
                ]
                    .map((v) => `"${String(v).replace(/"/g, '""')}"`)
                    .join(","));
            }
            content = rows.join("\n") + "\n";
            break;
        case "json":
        default:
            const exportPackage = {
                exportedAt,
                projectPath,
                entryCount: result.entries.length,
                dateRange: {
                    start: result.entries[0]?.timestamp || exportedAt,
                    end: result.entries[result.entries.length - 1]?.timestamp || exportedAt,
                },
                chainIntegrity: {
                    genesisHash,
                    headHash,
                    entriesWithIntegrity,
                },
                entries: exportEntries,
            };
            content = JSON.stringify(exportPackage, null, 2);
            break;
    }
    // Write to file if path provided
    if (outputPath) {
        await writeFile(outputPath, content, "utf-8");
        logger.info("history.audit_exported", {
            outputPath,
            entryCount: result.entries.length,
            format,
        });
    }
    return {
        exportedAt,
        projectPath,
        entryCount: result.entries.length,
        dateRange: {
            start: result.entries[0]?.timestamp || exportedAt,
            end: result.entries[result.entries.length - 1]?.timestamp || exportedAt,
        },
        chainIntegrity: {
            genesisHash,
            headHash,
            entriesWithIntegrity,
        },
        outputPath,
        content: outputPath ? undefined : content,
    };
}
//# sourceMappingURL=store.js.map