"use strict";
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 Gracker (Chris)
// This file is part of SmartPerfetto. See LICENSE for details.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.CaseLibrary = void 0;
/**
 * CaseLibrary — durable storage for `CaseNode` records (Plan 54 M0).
 *
 * The double-control publish gate from §5.2 of the unified design doc
 * lives here. A case can become `status='published'` ONLY through the
 * dedicated `publishCase()` path AND only when:
 *   1. `redactionState === 'redacted'` — the trace artifact has been
 *      anonymized and approved as such.
 *   2. A curator has signed off — `curatedBy` is set, and
 *      `publishCase()` requires the reviewer to be passed explicitly.
 *
 * `saveCase()` itself rejects records arriving with `status='published'`
 * — the only way through is the dedicated path. This makes the
 * promotion to public a deliberate API call instead of an accidental
 * field update.
 *
 * Out of scope here (M1 / M2):
 * - Case graph / edge management (`caseGraph.ts`).
 * - MCP tools (`recall_similar_case`, `cite_case_in_report`).
 * - Express CRUD route.
 *
 * @module caseLibrary
 */
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const sparkContracts_1 = require("../types/sparkContracts");
const scopedKnowledgeStore_1 = require("./scopedKnowledgeStore");
const KNOWLEDGE_KIND = 'case_node';
const CASE_ROW_SCOPE_PREFIX = 'case:';
/**
 * CaseLibrary — local file-backed case storage. Single instance per
 * storage path; cross-process writers explicitly out of scope (matches
 * ragStore / baselineStore / projectMemory).
 */
class CaseLibrary {
    constructor(storagePath) {
        this.cases = new Map();
        this.loaded = false;
        this.storagePath = storagePath;
    }
    load() {
        if (this.loaded)
            return;
        this.loaded = true;
        if (!fs.existsSync(this.storagePath))
            return;
        try {
            const raw = fs.readFileSync(this.storagePath, 'utf-8');
            const parsed = JSON.parse(raw);
            if (parsed.schemaVersion !== 1 || !Array.isArray(parsed.cases))
                return;
            for (const c of parsed.cases)
                this.cases.set(c.caseId, c);
        }
        catch {
            // Corrupted JSON: file preserved, in-memory cache stays empty.
        }
    }
    /**
     * Save (insert or replace) a case. Throws when the record arrives
     * with `status='published'` — the only legitimate path to publish
     * is the dedicated `publishCase()` call so the gate cannot be
     * bypassed by a field update.
     */
    saveCase(record, scope) {
        this.load();
        if (record.status === 'published') {
            throw new Error(`Use publishCase() to advance a case to 'published'; saveCase() rejects published records to keep the gate auditable`);
        }
        if ((0, scopedKnowledgeStore_1.legacyKnowledgeFilesystemWritesEnabled)()) {
            this.cases.set(record.caseId, record);
            this.persist();
        }
        if ((0, scopedKnowledgeStore_1.enterpriseKnowledgeDbWritesEnabled)()) {
            (0, scopedKnowledgeStore_1.upsertScopedKnowledgeRecord)(KNOWLEDGE_KIND, record.caseId, caseRowScope(record.status), record, scope, { createdAt: record.createdAt, updatedAt: Date.now() });
        }
    }
    getCase(caseId, scope) {
        if ((0, scopedKnowledgeStore_1.enterpriseKnowledgeStoreEnabled)()) {
            return (0, scopedKnowledgeStore_1.getScopedKnowledgeRecord)(KNOWLEDGE_KIND, caseId, scope)?.record;
        }
        this.load();
        return this.cases.get(caseId);
    }
    removeCase(caseId, scope) {
        let removed = false;
        if ((0, scopedKnowledgeStore_1.enterpriseKnowledgeDbWritesEnabled)()) {
            removed = (0, scopedKnowledgeStore_1.removeScopedKnowledgeRecord)(KNOWLEDGE_KIND, caseId, scope) || removed;
        }
        if ((0, scopedKnowledgeStore_1.legacyKnowledgeFilesystemWritesEnabled)()) {
            this.load();
            const had = this.cases.delete(caseId);
            if (had)
                this.persist();
            removed = had || removed;
        }
        return removed;
    }
    listCases(opts = {}, scope) {
        this.load();
        let out = (0, scopedKnowledgeStore_1.enterpriseKnowledgeStoreEnabled)()
            ? (0, scopedKnowledgeStore_1.listScopedKnowledgeRecords)(KNOWLEDGE_KIND, scope, {
                rowScope: opts.status ? caseRowScope(opts.status) : undefined,
                rowScopePrefix: opts.status ? undefined : CASE_ROW_SCOPE_PREFIX,
            }).map(row => row.record)
            : Array.from(this.cases.values());
        if (opts.status)
            out = out.filter(c => c.status === opts.status);
        if (opts.educationalLevel)
            out = out.filter(c => c.educationalLevel === opts.educationalLevel);
        if (opts.anyOfTags && opts.anyOfTags.length > 0) {
            const wanted = new Set(opts.anyOfTags);
            out = out.filter(c => c.tags.some(t => wanted.has(t)));
        }
        out.sort((a, b) => a.caseId.localeCompare(b.caseId));
        return out;
    }
    /**
     * Advance a case to `status='published'`. Enforces the double-control
     * gate:
     *   - Case must already exist (we publish a known record).
     *   - `redactionState === 'redacted'` — anonymizer must have run.
     *   - Reviewer name supplied — curator signoff is mandatory.
     *
     * Returns the published case so callers can render the new state
     * without a follow-up read. Stamps `curatedBy` / `curatedAt` from
     * the reviewer + wall clock.
     */
    publishCase(caseId, opts, scope) {
        this.load();
        const trimmedReviewer = opts.reviewer?.trim();
        if (!trimmedReviewer) {
            throw new Error(`Cannot publish case '${caseId}' without a reviewer signoff`);
        }
        const existing = (0, scopedKnowledgeStore_1.enterpriseKnowledgeStoreEnabled)()
            ? (0, scopedKnowledgeStore_1.getScopedKnowledgeRecord)(KNOWLEDGE_KIND, caseId, scope)?.record
            : this.cases.get(caseId);
        if (!existing) {
            throw new Error(`Cannot publish case '${caseId}': not found`);
        }
        if (existing.redactionState !== 'redacted') {
            throw new Error(`Cannot publish case '${caseId}': redactionState='${existing.redactionState}' (must be 'redacted')`);
        }
        const published = {
            ...existing,
            status: 'published',
            curatedBy: trimmedReviewer,
            curatedAt: Date.now(),
        };
        if ((0, scopedKnowledgeStore_1.legacyKnowledgeFilesystemWritesEnabled)()) {
            this.cases.set(caseId, published);
            this.persist();
        }
        if ((0, scopedKnowledgeStore_1.enterpriseKnowledgeDbWritesEnabled)()) {
            (0, scopedKnowledgeStore_1.upsertScopedKnowledgeRecord)(KNOWLEDGE_KIND, caseId, caseRowScope(published.status), published, scope, { createdAt: published.createdAt, updatedAt: published.curatedAt });
        }
        return published;
    }
    /**
     * Archive a case: drops the trace artifact pointer (so the artifact
     * store can evict the underlying file) while keeping the case
     * metadata in place for backward references. Records the supplied
     * reason on `traceUnavailableReason` so consumers see why the trace
     * is gone.
     */
    archiveCase(caseId, opts, scope) {
        this.load();
        const reason = opts.reason?.trim();
        if (!reason) {
            throw new Error(`archiveCase requires a non-empty reason`);
        }
        const existing = (0, scopedKnowledgeStore_1.enterpriseKnowledgeStoreEnabled)()
            ? (0, scopedKnowledgeStore_1.getScopedKnowledgeRecord)(KNOWLEDGE_KIND, caseId, scope)?.record
            : this.cases.get(caseId);
        if (!existing) {
            throw new Error(`Cannot archive case '${caseId}': not found`);
        }
        const archived = {
            ...existing,
            ...(0, sparkContracts_1.makeSparkProvenance)({
                source: existing.source,
                notes: `archived via archiveCase`,
            }),
            traceArtifactId: undefined,
            traceUnavailableReason: reason,
        };
        if ((0, scopedKnowledgeStore_1.legacyKnowledgeFilesystemWritesEnabled)()) {
            this.cases.set(caseId, archived);
            this.persist();
        }
        if ((0, scopedKnowledgeStore_1.enterpriseKnowledgeDbWritesEnabled)()) {
            (0, scopedKnowledgeStore_1.upsertScopedKnowledgeRecord)(KNOWLEDGE_KIND, caseId, caseRowScope(archived.status), archived, scope, { createdAt: archived.createdAt, updatedAt: Date.now() });
        }
        return archived;
    }
    /** Stats by status — useful for the admin dashboard. */
    getStats(scope) {
        this.load();
        const out = {
            draft: 0,
            reviewed: 0,
            published: 0,
            private: 0,
        };
        const cases = (0, scopedKnowledgeStore_1.enterpriseKnowledgeStoreEnabled)()
            ? (0, scopedKnowledgeStore_1.listScopedKnowledgeRecords)(KNOWLEDGE_KIND, scope, { rowScopePrefix: CASE_ROW_SCOPE_PREFIX }).map(row => row.record)
            : Array.from(this.cases.values());
        for (const c of cases)
            out[c.status]++;
        return out;
    }
    persist() {
        const dir = path.dirname(this.storagePath);
        if (!fs.existsSync(dir))
            fs.mkdirSync(dir, { recursive: true });
        // Per-process unique tmp suffix — Codex round E P1#5.
        const tmp = `${this.storagePath}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
        const envelope = {
            schemaVersion: 1,
            cases: Array.from(this.cases.values()),
        };
        fs.writeFileSync(tmp, JSON.stringify(envelope, null, 2), 'utf-8');
        fs.renameSync(tmp, this.storagePath);
    }
}
exports.CaseLibrary = CaseLibrary;
function caseRowScope(status) {
    return `${CASE_ROW_SCOPE_PREFIX}${status}`;
}
//# sourceMappingURL=caseLibrary.js.map