/**
 * src/telemetry/uploader.ts
 *
 * Consent-based telemetry uploader. Runs daily (cron job) IF the owner has
 * explicitly opted in via identity_fact `preference.telemetry_consent` = 'on'.
 *
 * What ships: aggregate fingerprints (no message content, no PII).
 * Where: https://learn.taseesai.com/v1/ingest (TaseesAI's own endpoint).
 * Audit: every upload writes a journal entry the owner can inspect.
 * Purge: `muassis privacy purge` deletes the owner's training contributions.
 *
 * THE CONTRACT WITH THE CUSTOMER (PDPL Article 11):
 *   1. Default = OFF. Owner must explicitly opt in.
 *   2. Only structural fingerprints leave the device — never message content,
 *      never customer names/phones/financials.
 *   3. Owner can audit, pause, and purge at any time.
 *   4. Anonymized aggregates only — no individual attribution.
 */
import { createChildLogger } from '../utils/logger.js';
import { supabase } from '../utils/supabase.js';
import { record as journalRecord, list as listJournal } from '../autonomy/journal.js';
import { getFact } from '../autonomy/identity.js';
import { fingerprintMessage } from './scrubber.js';
const log = createChildLogger('telemetry:uploader');
const INGEST_ENDPOINT = process.env.MUASSIS_TELEMETRY_ENDPOINT
    ?? 'https://learn.taseesai.com/v1/ingest';
const CONSENT_KEY = 'preference.telemetry_consent';
export async function isTelemetryEnabled(tenantId) {
    try {
        const fact = await getFact(tenantId, CONSENT_KEY);
        return fact?.fact_value === 'on';
    }
    catch {
        return false;
    }
}
async function buildAggregateFromJournal(tenantId, sinceIso) {
    const entries = await listJournal({ tenantId, since: sinceIso, limit: 5000 });
    // Anonymize the tenant id — SHA-256 hash. Not reversible.
    const { createHash } = await import('node:crypto');
    const tenantAnonId = createHash('sha256').update(tenantId + ':muassis-telemetry').digest('hex').slice(0, 16);
    const langs = {};
    const intents = {};
    const tools = {};
    const lengths = {};
    const saudi = { zatca: 0, gosi: 0, nitaqat: 0, salla: 0, zid: 0, mada: 0, prayer: 0, tadawul: 0, hijri: 0 };
    let totalLatency = 0;
    let latencyCount = 0;
    for (const e of entries) {
        const md = e.metadata;
        if (e.kind === 'tool') {
            tools[e.action] = (tools[e.action] ?? 0) + 1;
        }
        if (md?.intent && typeof md.intent === 'string') {
            intents[md.intent] = (intents[md.intent] ?? 0) + 1;
        }
        if (md?.message && typeof md.message === 'string') {
            const fp = fingerprintMessage(md.message);
            langs[fp.language] = (langs[fp.language] ?? 0) + 1;
            lengths[fp.length_bucket] = (lengths[fp.length_bucket] ?? 0) + 1;
            const lc = md.message.toLowerCase();
            if (/zatca|زاتكا/.test(lc))
                saudi.zatca++;
            if (/gosi|جوسي/.test(lc))
                saudi.gosi++;
            if (/nitaqat|نطاقات/.test(lc))
                saudi.nitaqat++;
            if (/salla|سلة/.test(lc))
                saudi.salla++;
            if (/\bzid\b|زد/.test(lc))
                saudi.zid++;
            if (/mada|مدى/.test(lc))
                saudi.mada++;
            if (/prayer|صلاة/.test(lc))
                saudi.prayer++;
            if (/tadawul|تداول/.test(lc))
                saudi.tadawul++;
            if (/hijri|هجري/.test(lc))
                saudi.hijri++;
        }
        if (md?.latencyMs && typeof md.latencyMs === 'number') {
            totalLatency += md.latencyMs;
            latencyCount++;
        }
    }
    const dateOnly = new Date().toISOString().slice(0, 10);
    return {
        tenant_anon_id: tenantAnonId,
        date: dateOnly,
        message_count: entries.length,
        language_distribution: langs,
        intent_distribution: intents,
        tool_usage: tools,
        length_distribution: lengths,
        avg_response_latency_ms: latencyCount > 0 ? Math.round(totalLatency / latencyCount) : 0,
        saudi_context_signal_count: saudi,
        client_version: '1.1.0',
    };
}
export async function runTelemetryCycle(tenantId) {
    if (!(await isTelemetryEnabled(tenantId))) {
        return { uploaded: false, reason: 'telemetry not enabled (opt-in required)' };
    }
    const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
    const aggregate = await buildAggregateFromJournal(tenantId, since);
    if (aggregate.message_count === 0) {
        return { uploaded: false, reason: 'no activity in last 24h' };
    }
    // Upload (best-effort; failures journal but don't escalate)
    try {
        const r = await fetch(INGEST_ENDPOINT, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-Muassis-Version': '1.1.0',
            },
            body: JSON.stringify(aggregate),
            signal: AbortSignal.timeout(20_000),
        });
        if (!r.ok) {
            throw new Error(`HTTP ${r.status}`);
        }
    }
    catch (err) {
        log.warn({ err: err.message }, 'telemetry upload failed (non-fatal)');
        await journalRecord({
            tenantId,
            kind: 'telemetry_upload_failed',
            action: 'daily_aggregate',
            outcome: 'failure',
            reason: err.message.slice(0, 200),
        });
        return { uploaded: false, reason: 'upload failed (will retry tomorrow)', aggregate };
    }
    await journalRecord({
        tenantId,
        kind: 'telemetry_upload',
        action: 'daily_aggregate',
        outcome: 'success',
        reason: `Uploaded ${aggregate.message_count} messages → ${aggregate.tenant_anon_id.slice(0, 8)}…`,
        metadata: aggregate,
    });
    log.info({ tenantId, message_count: aggregate.message_count, anon: aggregate.tenant_anon_id }, 'telemetry uploaded');
    return { uploaded: true, aggregate };
}
/**
 * Show what was sent (audit). Returns the last 30 telemetry uploads.
 */
export async function auditTelemetry(tenantId) {
    const entries = await listJournal({
        tenantId,
        kind: 'telemetry_upload',
        limit: 30,
    });
    return entries.map(e => ({
        timestamp: e.created_at,
        summary: e.reason,
        aggregate: e.metadata,
    }));
}
/**
 * Issue a purge request to TaseesAI's training pipeline. Removes the
 * tenant's contributions within 30 days (PDPL Article 18).
 */
export async function purgeTelemetry(tenantId) {
    try {
        const { createHash } = await import('node:crypto');
        const tenantAnonId = createHash('sha256').update(tenantId + ':muassis-telemetry').digest('hex').slice(0, 16);
        const r = await fetch(`${INGEST_ENDPOINT}/purge`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ tenant_anon_id: tenantAnonId }),
            signal: AbortSignal.timeout(20_000),
        });
        if (!r.ok)
            return { ok: false, reason: `HTTP ${r.status}` };
        await journalRecord({
            tenantId,
            kind: 'telemetry_purge',
            action: 'training_data_erasure',
            outcome: 'success',
            reason: 'Owner-initiated purge under PDPL Article 18',
        });
        return { ok: true };
    }
    catch (err) {
        return { ok: false, reason: err.message };
    }
}
/**
 * Idempotent registration of the daily telemetry cron job. Only fires when
 * consent is 'on' — runTelemetryCycle gates on that.
 */
export async function ensureTelemetryCronJob(tenantId) {
    const { randomUUID } = await import('node:crypto');
    const { data: existing } = await supabase
        .from('cron_jobs')
        .select('id')
        .eq('tenant_id', tenantId)
        .eq('name', 'telemetry-daily')
        .maybeSingle();
    if (existing)
        return;
    const now = new Date().toISOString();
    await supabase.from('cron_jobs').insert({
        id: randomUUID(),
        tenant_id: tenantId,
        name: 'telemetry-daily',
        schedule: '30 3 * * *', // 3:30am KSA daily
        timezone: 'Asia/Riyadh',
        payload: { kind: 'systemEvent', text: '__autonomy:telemetry__' },
        delivery: { mode: 'none' },
        enabled: true,
        last_run_at: null,
        next_run_at: now,
        run_count: 0,
        created_at: now,
        updated_at: now,
    });
}
//# sourceMappingURL=uploader.js.map