/**
 * src/cron/catchup.ts
 *
 * "While you were away" digest. When the gateway boots, find all enabled
 * cron jobs that should have fired between the last successful execution
 * and now, then surface a single grouped summary so the owner isn't
 * surprised by silence.
 *
 * Why this exists: Muassis runs in-process node-cron. When the laptop
 * sleeps or the gateway crashes, every scheduled job dies with it. Without
 * catch-up, an 8am `morning-brief` that didn't fire is invisible — the
 * owner just thinks the agent is broken. With catch-up, the agent says:
 * "While you were away, 3 jobs missed: morning-brief (08:00), …".
 *
 * We do NOT re-fire missed jobs. Most of them are time-sensitive
 * (Fajr reminder fired 4h late is worse than not firing). The owner can
 * manually replay any specific job from the dashboard. Compliance and
 * follow-up jobs that involve money/customers are explicitly excluded
 * from any auto-fire path.
 */
import parser from 'cron-parser';
import { createChildLogger } from '../utils/logger.js';
import { supabase } from '../utils/supabase.js';
import { record as journalRecord } from '../autonomy/journal.js';
import { deliverMessage } from '../delivery/delivery.js';
import { getFact } from '../autonomy/identity.js';
const log = createChildLogger('cron:catchup');
const MIN_GAP_MS = 5 * 60 * 1000;
const HARD_CAP_FIRES_PER_JOB = 50;
export async function runCatchupScan(tenantId) {
    const result = { scanned: 0, missed: [], digestDelivered: false };
    const { data: lastExecRow } = await supabase
        .from('cron_executions')
        .select('executed_at')
        .eq('tenant_id', tenantId)
        .order('executed_at', { ascending: false })
        .limit(1)
        .maybeSingle();
    const lastExecAt = lastExecRow?.executed_at ? new Date(lastExecRow.executed_at) : null;
    const now = new Date();
    if (!lastExecAt) {
        log.info({ tenantId }, 'no prior cron executions on record — skipping catch-up (fresh install)');
        return result;
    }
    const offlineMs = now.getTime() - lastExecAt.getTime();
    if (offlineMs < MIN_GAP_MS) {
        log.debug({ tenantId, offlineMs }, 'gateway gap too short — no catch-up needed');
        return result;
    }
    const { data: jobs, error } = await supabase
        .from('cron_jobs')
        .select('*')
        .eq('tenant_id', tenantId)
        .eq('enabled', true);
    if (error) {
        log.warn({ tenantId, error: error.message }, 'catch-up scan: cron_jobs query failed');
        return result;
    }
    for (const raw of (jobs || [])) {
        result.scanned++;
        const tz = raw.timezone || 'Asia/Riyadh';
        try {
            const interval = parser.parseExpression(raw.schedule, {
                currentDate: lastExecAt,
                endDate: now,
                tz,
            });
            let count = 0;
            let lastExpected = null;
            while (count < HARD_CAP_FIRES_PER_JOB) {
                try {
                    const next = interval.next();
                    const fireAt = next.toDate();
                    if (fireAt > now)
                        break;
                    lastExpected = fireAt;
                    count++;
                }
                catch {
                    break;
                }
            }
            if (count > 0 && lastExpected) {
                const lastExpectedAt = lastExpected;
                result.missed.push({
                    jobId: raw.id,
                    name: raw.name,
                    missedFires: count,
                    lastExpectedAt: lastExpectedAt.toISOString(),
                });
            }
        }
        catch (err) {
            log.debug({ tenantId, jobId: raw.id, schedule: raw.schedule, err: err.message }, 'cron parse failed during catch-up');
        }
    }
    if (result.missed.length === 0)
        return result;
    const digestLines = result.missed
        .sort((a, b) => b.missedFires - a.missedFires)
        .slice(0, 12)
        .map((m) => `• ${m.name} — ${m.missedFires}× (آخرها: ${formatRelative(new Date(m.lastExpectedAt), now)})`);
    const digestText = `📭 بينما كنت بعيداً (${formatDuration(offlineMs)}) فاتت ${result.missed.length} مهمة مجدولة:\n` +
        digestLines.join('\n') +
        `\n\nلم أنفذها تلقائياً — كثير منها وقتي ولا يستحق إعادة بأثر رجعي. ` +
        `لو تبي تشغل واحدة الآن، قول لي اسمها.`;
    try {
        await journalRecord({
            tenantId,
            kind: 'catchup_digest',
            action: 'gateway_resumed',
            target: null,
            outcome: 'success',
            reason: digestText.slice(0, 500),
            metadata: { offlineMs, missedCount: result.missed.length, missed: result.missed },
        });
    }
    catch (err) {
        log.debug({ err: err.message }, 'catch-up: journal write failed');
    }
    // (Audit H8) Digest delivery errors used to be silenced. Owners need to
    // know when their "while you were away" recap actually reached them — a
    // silent failure here defeats the entire feature.
    try {
        const ownerPhoneFact = await getFact(tenantId, 'owner.whatsapp');
        if (ownerPhoneFact?.fact_value) {
            try {
                await deliverMessage({
                    tenantId,
                    to: ownerPhoneFact.fact_value,
                    message: digestText,
                    channel: 'whatsapp',
                });
                result.digestDelivered = true;
            }
            catch (deliverErr) {
                // Log loud + journal the failure so it shows up in dashboards.
                log.warn({ tenantId, err: deliverErr.message }, 'catch-up: WA delivery FAILED — owner did not receive digest');
                try {
                    await journalRecord({
                        tenantId,
                        kind: 'catchup_digest',
                        action: 'delivery_failed',
                        outcome: 'failure',
                        reason: deliverErr.message,
                        metadata: { missedCount: result.missed.length, channel: 'whatsapp' },
                    });
                }
                catch { /* journal failure is non-fatal here */ }
            }
        }
        else {
            log.info({ tenantId, missedCount: result.missed.length }, 'catch-up: digest journaled but no owner.whatsapp fact set — owner will see it on dashboard only');
        }
    }
    catch (err) {
        log.warn({ err: err.message, tenantId }, 'catch-up: WA delivery path threw');
    }
    log.info({ tenantId, ...result }, 'catch-up scan complete');
    return result;
}
function formatDuration(ms) {
    const minutes = Math.floor(ms / 60000);
    if (minutes < 60)
        return `${minutes} دقيقة`;
    const hours = Math.floor(minutes / 60);
    if (hours < 24)
        return `${hours} ساعة`;
    const days = Math.floor(hours / 24);
    return `${days} يوم`;
}
function formatRelative(when, now) {
    const ms = now.getTime() - when.getTime();
    const minutes = Math.floor(ms / 60000);
    if (minutes < 60)
        return `قبل ${minutes}د`;
    const hours = Math.floor(minutes / 60);
    if (hours < 24)
        return `قبل ${hours}س`;
    const days = Math.floor(hours / 24);
    return `قبل ${days}ي`;
}
//# sourceMappingURL=catchup.js.map