/**
 * src/autonomy/identity.ts
 *
 * Persistent business identity Muassis carries across every conversation.
 * "What is this business?" "Who are the team?" "What products?" — facts
 * loaded into every system prompt so the agent never re-asks.
 */
import { randomUUID } from 'node:crypto';
import { supabase } from '../utils/supabase.js';
import { createChildLogger } from '../utils/logger.js';
import { encrypt as encryptWithMaster, decrypt as decryptWithMaster, isEncrypted } from '../security/cred-store.js';
import { decrypt as decryptLegacy } from '../utils/crypto.js';
const log = createChildLogger('autonomy:identity');
// ─── At-rest encryption for sensitive fact values ────────────────────
//
// Some identity facts are genuinely sensitive: API tokens stored as
// `channel.telegram_bot_token`, business credentials, customer secrets.
// These get encrypted at rest using the per-install key (TENANT_ENCRYPTION_KEY
// env if set, else derived from the random crypto.salt + machine fingerprint).
// Stolen-laptop scenario: the SQLite DB is unreadable without the salt file
// AND the machine. (Defense-in-depth alongside FileVault/BitLocker.)
//
// Pattern: prefix encrypted values with `ENC:v1:` so we know on read that
// they need decryption. Plaintext values stay plaintext (most facts are
// non-secret like `business.name`).
const SENSITIVE_KEY_PATTERNS = [
    /token$/i, /secret$/i, /password$/i, /credential$/i,
    /api[_-]?key$/i, /private[_-]?key$/i, /\.key$/i,
    /credit[_-]?card/i, /cvv/i, /pin$/i,
    /^channel\.\w+_bot_token$/i, /^integration\.\w+_secret$/i,
];
function isSensitive(factKey) {
    return SENSITIVE_KEY_PATTERNS.some(re => re.test(factKey));
}
/**
 * Encrypt sensitive fact values by default using the master key
 * (`<dataDir>/.muassis-master.key`, auto-generated on first boot).
 *
 * Wire format hierarchy:
 *   • `mc1:...`     — v2.1+ default, master-key AES-256-GCM (cred-store.ts)
 *   • `ENC:v1:...`  — legacy, env-var TENANT_ENCRYPTION_KEY required to read
 *   • everything else — plaintext (will be re-encrypted on next write)
 *
 * Migration is lazy: on read we transparently decrypt either format; on write
 * we always store the new mc1 format. Existing customers don't lose data;
 * their plaintext facts get encrypted the next time they're touched.
 */
function maybeEncrypt(factKey, factValue) {
    if (!isSensitive(factKey) || factValue === '')
        return factValue;
    try {
        return encryptWithMaster(factValue);
    }
    catch (err) {
        log.warn({ err: err.message, factKey }, 'identity fact encryption failed — storing plaintext');
        return factValue;
    }
}
function maybeDecrypt(factKey, storedValue) {
    // New master-key format
    if (isEncrypted(storedValue)) {
        try {
            return decryptWithMaster(storedValue);
        }
        catch (err) {
            log.warn({ err: err.message, factKey }, 'master-key decryption failed — value remains opaque');
            return storedValue;
        }
    }
    // Legacy env-var format
    if (storedValue.startsWith('ENC:v1:')) {
        const legacyKey = process.env.TENANT_ENCRYPTION_KEY;
        if (!legacyKey) {
            log.warn({ factKey }, 'legacy ENC:v1: fact found but TENANT_ENCRYPTION_KEY not set — cannot decrypt');
            return storedValue;
        }
        try {
            return decryptLegacy(storedValue.slice('ENC:v1:'.length), legacyKey);
        }
        catch (err) {
            log.warn({ err: err.message, factKey }, 'legacy fact decryption failed');
            return storedValue;
        }
    }
    return storedValue;
}
export async function upsertFact(tenantId, factKey, factValue, category = 'business', source = 'owner') {
    const now = new Date().toISOString();
    const stored = maybeEncrypt(factKey, factValue);
    const existing = await getFact(tenantId, factKey);
    if (existing) {
        const patch = { fact_value: stored, category, source, updated_at: now };
        const { error } = await supabase
            .from('identity_facts')
            .update(patch)
            .eq('tenant_id', tenantId)
            .eq('id', existing.id);
        if (error)
            throw new Error(`identity upsert update failed: ${error.message}`);
        // Return decrypted to caller for consistency
        return { ...existing, ...patch, fact_value: factValue };
    }
    const row = {
        id: randomUUID(),
        tenant_id: tenantId,
        fact_key: factKey,
        fact_value: stored,
        category,
        source,
        confidence: 1,
        created_at: now,
        updated_at: now,
    };
    const { error } = await supabase.from('identity_facts').insert(row);
    if (error)
        throw new Error(`identity upsert insert failed: ${error.message}`);
    log.info({ tenantId, factKey, category, encrypted: stored !== factValue }, 'identity fact stored');
    return { ...row, fact_value: factValue };
}
export async function getFact(tenantId, factKey) {
    const { data, error } = await supabase
        .from('identity_facts')
        .select('*')
        .eq('tenant_id', tenantId)
        .eq('fact_key', factKey)
        .maybeSingle();
    if (error) {
        log.warn({ error: error.message }, 'getFact failed');
        return null;
    }
    if (!data)
        return null;
    const row = data;
    return { ...row, fact_value: maybeDecrypt(row.fact_key, row.fact_value) };
}
export async function listFacts(tenantId, category) {
    let q = supabase
        .from('identity_facts')
        .select('*')
        .eq('tenant_id', tenantId)
        .order('category', { ascending: true });
    if (category)
        q = q.eq('category', category);
    const { data, error } = await q;
    if (error) {
        log.warn({ error: error.message }, 'listFacts failed');
        return [];
    }
    return (data ?? []).map(row => ({
        ...row,
        fact_value: maybeDecrypt(row.fact_key, row.fact_value),
    }));
}
export async function deleteFact(tenantId, factKey) {
    await supabase
        .from('identity_facts')
        .delete()
        .eq('tenant_id', tenantId)
        .eq('fact_key', factKey);
}
/** Renders the identity card for system-prompt injection. Compact, scannable. */
export async function renderIdentityCard(tenantId) {
    const facts = await listFacts(tenantId);
    if (facts.length === 0)
        return '';
    const byCategory = new Map();
    for (const f of facts) {
        const list = byCategory.get(f.category) ?? [];
        list.push(f);
        byCategory.set(f.category, list);
    }
    const sections = ['## What you know about this business'];
    for (const [category, items] of byCategory) {
        sections.push(`\n### ${category}`);
        for (const item of items) {
            sections.push(`- **${item.fact_key}**: ${item.fact_value}`);
        }
    }
    return sections.join('\n');
}
//# sourceMappingURL=identity.js.map