/**
 * JSON File Fallback Storage
 *
 * Provides fallback storage when SQLite is unavailable.
 * Uses atomic writes (temp file → rename) for data safety.
 *
 * @module persistence/json-fallback
 */
import { readFile, writeFile, rename, mkdir } from "fs/promises";
import { existsSync } from "fs";
import { join } from "path";
import { randomUUID } from "crypto";
import { tmpdir } from "os";
import { logger } from "../logger.js";
const FALLBACK_VERSION = 1;
export class JsonFallbackStorage {
    data;
    filePath;
    isDirty = false;
    constructor(filePath, data) {
        this.filePath = filePath;
        this.data = data;
    }
    static async create(projectPath) {
        const fallbackDir = join(projectPath, ".vaspera");
        const filePath = join(fallbackDir, "findings-fallback.json");
        await mkdir(fallbackDir, { recursive: true });
        let data;
        if (existsSync(filePath)) {
            try {
                const content = await readFile(filePath, "utf-8");
                data = JSON.parse(content);
                if (data.version !== FALLBACK_VERSION) {
                    data = createEmptyData();
                }
            }
            catch {
                data = createEmptyData();
            }
        }
        else {
            data = createEmptyData();
        }
        logger.info("persistence.fallback.initialized", { path: filePath });
        return new JsonFallbackStorage(filePath, data);
    }
    async save() {
        if (!this.isDirty)
            return;
        this.data.lastUpdated = new Date().toISOString();
        const tempPath = join(tmpdir(), `vaspera-fallback-${randomUUID()}.json`);
        await writeFile(tempPath, JSON.stringify(this.data, null, 2), "utf-8");
        await rename(tempPath, this.filePath);
        this.isDirty = false;
    }
    findOrCreateProject(path) {
        const existing = Object.values(this.data.projects).find((p) => p.path === path);
        if (existing)
            return existing;
        const project = {
            id: randomUUID(),
            path,
            name: path.split("/").pop() || path,
            createdAt: new Date().toISOString(),
        };
        this.data.projects[project.id] = project;
        this.isDirty = true;
        return project;
    }
    findProjectByPath(path) {
        return Object.values(this.data.projects).find((p) => p.path === path);
    }
    updateProjectLastScan(projectId) {
        const project = this.data.projects[projectId];
        if (project) {
            project.lastScanAt = new Date().toISOString();
            this.isDirty = true;
        }
    }
    createFinding(finding) {
        const now = new Date().toISOString();
        const persisted = {
            ...finding,
            id: randomUUID(),
            firstSeenAt: now,
            lastSeenAt: now,
            status: finding.status || "open",
        };
        this.data.findings[persisted.id] = persisted;
        this.isDirty = true;
        return persisted;
    }
    upsertFinding(finding) {
        const existing = this.findBySignature(finding.projectId, finding.file, finding.line, finding.category, finding.scannerSource);
        if (existing) {
            existing.lastSeenAt = new Date().toISOString();
            this.isDirty = true;
            return { finding: existing, isNew: false };
        }
        const created = this.createFinding({ ...finding, status: "open" });
        return { finding: created, isNew: true };
    }
    findBySignature(projectId, file, line, category, scannerSource) {
        return Object.values(this.data.findings).find((f) => f.projectId === projectId &&
            f.file === file &&
            f.line === line &&
            f.category === category &&
            f.scannerSource === scannerSource &&
            f.status !== "fixed");
    }
    findById(id) {
        return this.data.findings[id];
    }
    findFindings(filter) {
        let results = Object.values(this.data.findings);
        if (filter.projectId) {
            results = results.filter((f) => f.projectId === filter.projectId);
        }
        if (filter.certificationId) {
            results = results.filter((f) => f.certificationId === filter.certificationId);
        }
        if (filter.severity) {
            const severities = Array.isArray(filter.severity) ? filter.severity : [filter.severity];
            results = results.filter((f) => severities.includes(f.severity));
        }
        if (filter.status) {
            const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
            results = results.filter((f) => statuses.includes(f.status));
        }
        if (filter.category) {
            const categories = Array.isArray(filter.category) ? filter.category : [filter.category];
            results = results.filter((f) => categories.includes(f.category));
        }
        if (filter.file) {
            results = results.filter((f) => f.file.includes(filter.file));
        }
        if (filter.scannerSource) {
            results = results.filter((f) => f.scannerSource === filter.scannerSource);
        }
        if (filter.sinceDate) {
            results = results.filter((f) => f.firstSeenAt >= filter.sinceDate);
        }
        if (filter.untilDate) {
            results = results.filter((f) => f.firstSeenAt <= filter.untilDate);
        }
        results.sort((a, b) => b.firstSeenAt.localeCompare(a.firstSeenAt));
        if (filter.offset) {
            results = results.slice(filter.offset);
        }
        if (filter.limit) {
            results = results.slice(0, filter.limit);
        }
        return results;
    }
    updateFindingStatus(id, status, fixedBy) {
        const finding = this.data.findings[id];
        if (!finding)
            return false;
        finding.status = status;
        if (status === "fixed") {
            finding.fixedAt = new Date().toISOString();
            if (fixedBy) {
                finding.fixedBy = fixedBy;
            }
        }
        this.isDirty = true;
        return true;
    }
    markFixed(ids, fixedBy) {
        let count = 0;
        for (const id of ids) {
            if (this.updateFindingStatus(id, "fixed", fixedBy)) {
                count++;
            }
        }
        return count;
    }
    markFalsePositive(id, reason) {
        const finding = this.data.findings[id];
        if (!finding)
            return false;
        finding.status = "false_positive";
        finding.falsePositive = true;
        finding.falsePositiveReason = reason;
        this.isDirty = true;
        return true;
    }
    getStats(projectId) {
        const findings = Object.values(this.data.findings).filter((f) => f.projectId === projectId);
        const openFindings = findings.filter((f) => f.status === "open");
        const fixedFindings = findings.filter((f) => f.status === "fixed");
        const falsePositives = findings.filter((f) => f.status === "false_positive");
        const bySeverity = {
            critical: 0,
            high: 0,
            medium: 0,
            low: 0,
            info: 0,
        };
        const byCategory = {};
        const byScanner = {};
        for (const f of openFindings) {
            bySeverity[f.severity] = (bySeverity[f.severity] || 0) + 1;
            byCategory[f.category] = (byCategory[f.category] || 0) + 1;
        }
        for (const f of findings) {
            byScanner[f.scannerSource] = (byScanner[f.scannerSource] || 0) + 1;
        }
        let avgMttrHours;
        const fixedWithTime = fixedFindings.filter((f) => f.fixedAt);
        if (fixedWithTime.length > 0) {
            const totalHours = fixedWithTime.reduce((sum, f) => {
                const fixed = new Date(f.fixedAt).getTime();
                const seen = new Date(f.firstSeenAt).getTime();
                return sum + (fixed - seen) / (1000 * 60 * 60);
            }, 0);
            avgMttrHours = totalHours / fixedWithTime.length;
        }
        const scans = Object.values(this.data.scans).filter((s) => s.projectId === projectId);
        const scanDates = scans.map((s) => s.startedAt).sort();
        return {
            projectId,
            totalFindings: findings.length,
            openFindings: openFindings.length,
            fixedFindings: fixedFindings.length,
            falsePositives: falsePositives.length,
            bySeverity,
            byCategory,
            byScanner,
            avgMttrHours,
            firstScanAt: scanDates[0],
            lastScanAt: scanDates[scanDates.length - 1],
        };
    }
    createScan(projectId, certificationId) {
        const scan = {
            id: randomUUID(),
            projectId,
            certificationId,
            startedAt: new Date().toISOString(),
            status: "running",
            totalFindings: 0,
            newFindings: 0,
            fixedFindings: 0,
            bySeverity: { critical: 0, high: 0, medium: 0, low: 0, info: 0 },
            byScanner: {},
        };
        this.data.scans[scan.id] = scan;
        this.isDirty = true;
        return scan;
    }
    completeScan(scanId, results) {
        const scan = this.data.scans[scanId];
        if (!scan)
            return;
        scan.completedAt = new Date().toISOString();
        scan.status = "completed";
        scan.totalFindings = results.totalFindings;
        scan.newFindings = results.newFindings;
        scan.fixedFindings = results.fixedFindings;
        scan.bySeverity = results.bySeverity;
        scan.byScanner = results.byScanner;
        scan.duration = results.duration;
        this.isDirty = true;
    }
    close() {
        // Sync save on close
        if (this.isDirty) {
            const content = JSON.stringify(this.data, null, 2);
            require("fs").writeFileSync(this.filePath, content, "utf-8");
        }
    }
}
function createEmptyData() {
    return {
        version: FALLBACK_VERSION,
        projects: {},
        findings: {},
        scans: {},
        lastUpdated: new Date().toISOString(),
    };
}
//# sourceMappingURL=json-fallback.js.map