/**
 * CrossSessionMemoryStore -- Persistent memory that survives session
 * boundaries, with mandatory pseudonymisation of protected identifiers.
 *
 * On every write the payload is scanned for PII, PHI, and HMDA demographic
 * fields. Detected identifiers are replaced with deterministic SHA-256
 * pseudonyms (SHA-256(subjectId + pepper)). The reverse-lookup map
 * (`subjectPseudonymMap`) is protected by the governance key-manager and
 * is only accessible to callers that present a `reidentificationToken`
 * earned through an approval-queue route with the required ReviewerTier.
 *
 * Pack-binding determines which identifier classes are protected:
 *   - GDPR: subject names + contact info
 *   - HIPAA: PHI fields (18 safe harbour identifiers)
 *   - HMDA: demographic fields (race, ethnicity, sex, age, national origin)
 *
 * P3-T1 coverage gap: Cross-session memory with pseudonymisation support.
 *
 * Compliance bindings:
 *   - GDPR Art. 4(5) + Recital 26: pseudonymisation as a data protection measure
 *   - HIPAA 164.514(b): safe harbour de-identification
 *   - HMDA Reg C 1003.6: data privacy in HMDA reporting
 *   - CCPA 1798.100: right to know / access
 *
 * @connexum/ai-governance
 */
import * as crypto from 'crypto';
// ---------------------------------------------------------------------------
// Errors
// ---------------------------------------------------------------------------
export class ReidentificationTokenRequiredError extends Error {
    memoryKey;
    constructor(memoryKey) {
        super(`Memory key '${memoryKey}' contains pseudonymised identifiers. ` +
            `A reidentificationToken from the approval queue (legal-reviewer or privacy-officer tier) is required.`);
        this.name = 'ReidentificationTokenRequiredError';
        this.memoryKey = memoryKey;
    }
}
export class InvalidReidentificationTokenError extends Error {
    constructor() {
        super('The supplied reidentificationToken is invalid or has expired.');
        this.name = 'InvalidReidentificationTokenError';
    }
}
const PACK_POLICIES = [
    {
        packId: 'gdpr',
        protectedClasses: ['gdpr-contact'],
        reviewerTierRequired: 'privacy-officer',
    },
    {
        packId: 'hipaa',
        protectedClasses: ['phi'],
        reviewerTierRequired: 'privacy-officer',
    },
    {
        packId: 'hmda',
        protectedClasses: ['hmda-demographic'],
        reviewerTierRequired: 'legal-reviewer',
    },
];
// ---------------------------------------------------------------------------
// PII / PHI / HMDA pattern detection
// ---------------------------------------------------------------------------
/** Patterns for each identifier class. Match against stringified value. */
const IDENTIFIER_PATTERNS = {
    'gdpr-contact': [
        // email address
        /\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b/,
        // phone (loose)
        /\b(\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/,
        // full name pattern (two capitalised words)
        /\b[A-Z][a-z]+\s[A-Z][a-z]+\b/,
        // postal / zip code
        /\b\d{5}(?:-\d{4})?\b/,
    ],
    phi: [
        // SSN
        /\b\d{3}-\d{2}-\d{4}\b/,
        // MRN (common prefix pattern)
        /\b(?:MRN|mrn|PatientId)[:\s#]*\d{5,12}\b/i,
        // DOB pattern
        /\b(?:dob|date[-\s]?of[-\s]?birth)[:\s]*\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}\b/i,
        // ICD / diagnosis codes
        /\b[A-Z]\d{2}\.?\d{0,4}\b/,
    ],
    'hmda-demographic': [
        // Explicit demographic field labels (matches in JSON key form e.g. "race":"White")
        /(?:race|ethnicity|sex|gender|national[-\s]?origin)["\s]*[:=]["\s]*\S+/i,
        // Age
        /(?:^|\W)age["\s]*[:=]["\s]*\d+/i,
    ],
};
function detectIdentifierClasses(payload) {
    const text = JSON.stringify(payload);
    const found = new Set();
    for (const [cls, patterns] of Object.entries(IDENTIFIER_PATTERNS)) {
        for (const pattern of patterns) {
            if (pattern.test(text)) {
                found.add(cls);
                break;
            }
        }
    }
    return found;
}
// ---------------------------------------------------------------------------
// Pseudonymisation helpers
// ---------------------------------------------------------------------------
function pseudonymise(value, pepper) {
    return crypto.createHash('sha256').update(value + pepper).digest('hex').slice(0, 32);
}
// ---------------------------------------------------------------------------
// Reidentification token store (in-memory; production would use key-manager)
// ---------------------------------------------------------------------------
const REIDENTIFICATION_TOKEN_TTL_MS = 15 * 60 * 1000; // 15 min
// ---------------------------------------------------------------------------
// CrossSessionMemoryStore
// ---------------------------------------------------------------------------
export class CrossSessionMemoryStore {
    store = new Map();
    /**
     * Reverse lookup: pseudonym -> original value. Protected; only accessible
     * with a valid reidentificationToken.
     */
    subjectPseudonymMap = new Map();
    tokenStore = new Map();
    pepper;
    activePackIds;
    constructor(config = {}) {
        this.pepper =
            config.pepper && config.pepper.length >= 32
                ? config.pepper
                : crypto.randomBytes(32).toString('hex');
        this.activePackIds = config.activePackIds ?? [];
    }
    // -------------------------------------------------------------------------
    // Write
    // -------------------------------------------------------------------------
    /**
     * Persist a value under `key`. Scans for protected identifiers and
     * pseudonymises them before storing. Emits a write receipt.
     */
    write(agentId, key, payload) {
        const protectedClasses = this.resolveProtectedClasses();
        const detectedClasses = detectIdentifierClasses(payload);
        const toProtect = [...detectedClasses].filter((c) => protectedClasses.has(c));
        let finalPayload = payload;
        let pseudonymised = false;
        if (toProtect.length > 0) {
            finalPayload = this.pseudonymisePayload(payload, toProtect);
            pseudonymised = true;
        }
        const entry = {
            payload: finalPayload,
            pseudonymised,
            pseudonymisedClasses: toProtect,
            writtenAt: new Date().toISOString(),
            writtenBy: agentId,
        };
        this.store.set(key, entry);
        return entry;
    }
    // -------------------------------------------------------------------------
    // Read
    // -------------------------------------------------------------------------
    /**
     * Read a value. Returns pseudonymised payload unless a valid
     * `reidentificationToken` is supplied.
     */
    read(agentId, key, reidentificationToken) {
        const entry = this.store.get(key);
        if (!entry)
            return { entry: undefined, reidentified: false };
        if (!entry.pseudonymised) {
            return { entry, reidentified: false };
        }
        // Pseudonymised entry: require a valid token to get original values.
        if (reidentificationToken) {
            if (!this.validateToken(agentId, reidentificationToken)) {
                throw new InvalidReidentificationTokenError();
            }
            // Return payload with pseudonyms resolved.
            const reidentifiedPayload = this.resolvePayload(entry.payload);
            return {
                entry: { ...entry, payload: reidentifiedPayload },
                reidentified: true,
            };
        }
        // No token: caller gets pseudonymised values only.
        return { entry, reidentified: false };
    }
    /** Return a raw entry without attempting reidentification. */
    peek(key) {
        return this.store.get(key);
    }
    delete(key) {
        return this.store.delete(key);
    }
    keys() {
        return [...this.store.keys()];
    }
    // -------------------------------------------------------------------------
    // Reidentification token management
    // -------------------------------------------------------------------------
    /**
     * Issue a reidentification token for an agent. In production this is
     * called by the approval-queue route after the required ReviewerTier
     * decision is recorded. Token is valid for 15 minutes.
     */
    issueReidentificationToken(agentId, reviewerTier) {
        const token = crypto.randomBytes(32).toString('hex');
        this.tokenStore.set(token, {
            agentId,
            issuedAt: Date.now(),
            reviewerTier,
        });
        return token;
    }
    // -------------------------------------------------------------------------
    // Internal helpers
    // -------------------------------------------------------------------------
    resolveProtectedClasses() {
        const classes = new Set();
        for (const policy of PACK_POLICIES) {
            if (this.activePackIds.includes(policy.packId)) {
                for (const cls of policy.protectedClasses)
                    classes.add(cls);
            }
        }
        return classes;
    }
    pseudonymisePayload(payload, classes) {
        const text = JSON.stringify(payload);
        let result = text;
        for (const cls of classes) {
            for (const pattern of IDENTIFIER_PATTERNS[cls]) {
                result = result.replace(new RegExp(pattern.source, pattern.flags + 'g'), (match) => {
                    const pseudo = pseudonymise(match, this.pepper);
                    // Store reverse lookup.
                    this.subjectPseudonymMap.set(pseudo, match);
                    return pseudo;
                });
            }
        }
        try {
            return JSON.parse(result);
        }
        catch {
            return result;
        }
    }
    resolvePayload(payload) {
        let text = JSON.stringify(payload);
        for (const [pseudo, original] of this.subjectPseudonymMap) {
            text = text.replaceAll(pseudo, original);
        }
        try {
            return JSON.parse(text);
        }
        catch {
            return text;
        }
    }
    validateToken(agentId, token) {
        const record = this.tokenStore.get(token);
        if (!record)
            return false;
        if (record.agentId !== agentId)
            return false;
        if (Date.now() - record.issuedAt > REIDENTIFICATION_TOKEN_TTL_MS) {
            this.tokenStore.delete(token);
            return false;
        }
        return true;
    }
}
// ---------------------------------------------------------------------------
// Pack identifier policy accessors (for test introspection)
// ---------------------------------------------------------------------------
export { PACK_POLICIES };
//# sourceMappingURL=cross-session-memory.js.map