/**
 * src/autonomy/run-on-launch.ts
 *
 * THE reliability fix for a desktop app: clock-time crons never fire while the
 * Mac is asleep, and `catchup.ts` deliberately does NOT re-fire missed jobs (it
 * only issues a "while you were away" digest). So on a machine that's only open
 * mornings, the daily thinking jobs (reflection, initiative, research, …) would
 * NEVER run. This module fixes that: when the app OPENS, it runs each
 * catch-up-eligible autonomy cycle that has gone stale — "do today's work the
 * moment you sit down," like a real employee.
 *
 * Mechanism (intentionally simple + reuses the proven path):
 *   • Each eligible job is already a seeded cron row with a `__autonomy:*`
 *     sentinel payload. We just call the existing `runJobNow(jobId)` — same
 *     execute path the scheduler uses (handler + last_run_at update + chat
 *     surfacing + the <60s duplicate-fire guard), so no new execution code.
 *   • A job fires if `now - last_run_at > maxStalenessHours` (or never run).
 *     After it runs, `last_run_at` is fresh, so it won't re-fire on the next
 *     launch the same day.
 *   • WAKING-WINDOW guard: nothing fires outside 08:00–23:00 KSA, so opening
 *     the laptop at 3am never pings customers or burns tokens.
 *   • Sequential + best-effort: a slow/failed cycle never blocks boot (the
 *     caller invokes this fire-and-forget) and never blocks the next cycle.
 */
import { createChildLogger } from '../utils/logger.js';
import { supabase } from '../utils/supabase.js';
const log = createChildLogger('autonomy:run-on-launch');
const WAKING_START_KSA = 8;
const WAKING_END_KSA = 23;
/**
 * Catch-up-eligible jobs + how stale they may get before a launch re-fires.
 * Only the once/few-times-daily cycles are here — the hourly/30-min ticks
 * (heartbeat, proactive-actions, goal-pursuit) fire often enough on their own
 * and don't need launch catch-up.
 */
// Staleness lowered (2026-05-31) so autonomy feels CONTINUOUS: with the 24/7
// daemon + app-open catch-up, the "get smarter" cycles re-fire every few hours
// instead of once a day. Owner complaint was "it doesn't improve itself / office
// agents just chill" — sparse once-daily cadence was the cause. daily-scorecard
// stays daily (owner-facing summary; firing it hourly would spam).
const CATCHUP_JOBS = [
    { name: 'reflection', maxStalenessHours: 6 }, // the "get smarter" cycle — now ~4×/day
    { name: 'research-cycle', maxStalenessHours: 4 },
    { name: 'skill-discovery', maxStalenessHours: 6 },
    { name: 'initiative', maxStalenessHours: 4 }, // "think + act for the business"
    { name: 'daily-scorecard', maxStalenessHours: 22 }, // daily owner-facing summary
];
/** Current hour in Asia/Riyadh, 0–23. Uses h23 to dodge the Node-20 "24" bug. */
function currentKsaHour() {
    const s = new Intl.DateTimeFormat('en-US', {
        timeZone: 'Asia/Riyadh',
        hour: '2-digit',
        hourCycle: 'h23',
    }).format(new Date());
    return parseInt(s, 10) % 24;
}
export async function runOnLaunchCatchup(tenantId, opts = {}) {
    const ksaHour = currentKsaHour();
    const result = { ksaHour, fired: [], skipped: [] };
    if (!opts.ignoreWakingWindow && (ksaHour < WAKING_START_KSA || ksaHour > WAKING_END_KSA)) {
        log.info({ tenantId, ksaHour }, 'launch catch-up skipped — outside waking window');
        result.skipped = CATCHUP_JOBS.map((j) => ({ name: j.name, reason: `outside waking window (${ksaHour}:00 KSA)` }));
        return result;
    }
    let rows = [];
    try {
        const { data } = await supabase
            .from('cron_jobs')
            .select('id, name, last_run_at, enabled')
            .eq('tenant_id', tenantId)
            .in('name', CATCHUP_JOBS.map((j) => j.name));
        rows = (data ?? []);
    }
    catch (err) {
        log.warn({ tenantId, err: err.message }, 'launch catch-up job lookup failed');
        return result;
    }
    for (const cfg of CATCHUP_JOBS) {
        const row = rows.find((r) => r.name === cfg.name);
        if (!row) {
            result.skipped.push({ name: cfg.name, reason: 'not seeded' });
            continue;
        }
        if (!row.enabled) {
            result.skipped.push({ name: cfg.name, reason: 'disabled by owner' });
            continue;
        }
        const lastMs = row.last_run_at ? Date.parse(row.last_run_at) : 0;
        const staleHours = lastMs ? (Date.now() - lastMs) / 3_600_000 : null;
        if (staleHours !== null && staleHours < cfg.maxStalenessHours) {
            result.skipped.push({ name: cfg.name, reason: 'ran recently', staleHours: round1(staleHours) });
            continue;
        }
        if (opts.dryRun) {
            result.fired.push({ name: cfg.name, ok: true, staleHours: staleHours === null ? null : round1(staleHours), note: 'dry-run (would fire)' });
            continue;
        }
        // Fire via the proven execute path — sequential, fully guarded.
        try {
            const { runJobNow } = await import('../cron/scheduler.js');
            const exec = await runJobNow(row.id);
            result.fired.push({
                name: cfg.name,
                ok: exec.success,
                staleHours: staleHours === null ? null : round1(staleHours),
                note: exec.error ?? undefined,
            });
            log.info({ tenantId, name: cfg.name, ok: exec.success, staleHours }, 'launch catch-up fired job');
        }
        catch (err) {
            result.fired.push({ name: cfg.name, ok: false, staleHours: staleHours === null ? null : round1(staleHours), note: err.message });
            log.warn({ tenantId, name: cfg.name, err: err.message }, 'launch catch-up job failed (non-fatal)');
        }
    }
    // Visible "here's how I got smarter while you were away" digest. Makes
    // self-improvement NOTICEABLE on every open — fire-and-forget so it never
    // blocks boot.
    void postSelfImprovementDigest(tenantId).catch((err) => log.warn({ tenantId, err: err.message }, 'self-improvement digest failed'));
    log.info({ tenantId, ksaHour, fired: result.fired.length, skipped: result.skipped.length }, 'launch catch-up complete');
    return result;
}
/**
 * Count knowledge/skill growth since the last open and post a single concise
 * headline to the owner's chat (which now also pushes to WhatsApp/Telegram).
 * The whole point: the owner SEES the agent getting measurably smarter each
 * time they open the app, instead of guessing whether anything happened.
 */
async function postSelfImprovementDigest(tenantId) {
    const { getFact, upsertFact } = await import('./identity.js');
    const sinceFact = await getFact(tenantId, 'system.last_improvement_digest_at').catch(() => null);
    const sinceIso = sinceFact?.fact_value || new Date(Date.now() - 7 * 86400_000).toISOString();
    const nowIso = new Date().toISOString();
    const countSince = async (table) => {
        try {
            const { data } = await supabase.from(table).select('id').eq('tenant_id', tenantId).gte('created_at', sinceIso);
            return (data ?? []).length;
        }
        catch {
            return 0;
        }
    };
    const newEntities = await countSince('kg_entities');
    const newRels = await countSince('kg_relationships');
    let newSkills = 0, newLessons = 0;
    try {
        const { data } = await supabase.from('kg_entities').select('type').eq('tenant_id', tenantId).gte('created_at', sinceIso);
        for (const r of (data ?? [])) {
            if (r.type === 'Skill')
                newSkills++;
            if (String(r.type).toLowerCase() === 'lesson')
                newLessons++;
        }
    }
    catch { /* best-effort */ }
    // Stamp the timestamp regardless so we never double-count.
    await upsertFact(tenantId, 'system.last_improvement_digest_at', nowIso, 'system', 'system').catch(() => { });
    // Only bother the owner when something genuinely improved.
    if (newEntities + newRels + newSkills + newLessons <= 0)
        return;
    const parts = [];
    if (newRels > 0)
        parts.push(`+${newRels} رابط معرفي جديد بين المحادثات`);
    if (newEntities > 0)
        parts.push(`+${newEntities} مفهوم/كيان تعلّمته`);
    if (newSkills > 0)
        parts.push(`+${newSkills} مهارة جديدة`);
    if (newLessons > 0)
        parts.push(`+${newLessons} درس من تجاربي`);
    // PROOF — owner asked to always see WHAT was learned, with sources, not just
    // counts. Pull the actual skill names + their GitHub source, lesson titles,
    // and the freshest concepts added to the knowledge graph since last digest.
    const proof = [];
    try {
        const { data: skillRows } = await supabase
            .from('kg_entities').select('name, properties, type')
            .eq('tenant_id', tenantId).eq('type', 'Skill').gte('created_at', sinceIso).limit(6);
        const installedSkills = (skillRows ?? []).filter(r => r.properties?.install_status === 'installed');
        if (installedSkills.length) {
            proof.push('📚 *مهارات ركّبتها فعلاً:*');
            for (const s of installedSkills.slice(0, 5)) {
                const src = s.properties?.url || s.properties?.installed_path || '';
                proof.push(`  • ${s.name}${src ? ` — ${src}` : ''}`);
            }
        }
    }
    catch { /* best-effort */ }
    try {
        const { data: lessonRows } = await supabase
            .from('kg_entities').select('name, type')
            .eq('tenant_id', tenantId).gte('created_at', sinceIso).limit(40);
        const lessons = (lessonRows ?? []).filter(r => String(r.type).toLowerCase() === 'lesson').slice(0, 4);
        if (lessons.length) {
            proof.push('🎓 *دروس + أنماط استخلصتها:*');
            for (const l of lessons)
                proof.push(`  • ${l.name}`);
        }
        const concepts = (lessonRows ?? []).filter(r => ['Topic', 'Need', 'Service', 'Company', 'Industry'].includes(r.type)).slice(0, 5);
        if (concepts.length) {
            proof.push('🔗 *ربطته بالذاكرة (KG):* ' + concepts.map(c => c.name).join('، '));
        }
    }
    catch { /* best-effort */ }
    const proofBlock = proof.length ? `\n\n${proof.join('\n')}` : '';
    const body = `🧠 **صرت أذكى وأنت بعيد**\n\n${parts.join('\n')}${proofBlock}\n\nكل هذا أضفته وربطته بذاكرتي (Knowledge Graph) عشان أفهم شغلك أعمق وأخدمك أدق — وأقدر أوريك أي عنصر منها.`;
    try {
        const { resolveOwnerPrimaryCustomerId } = await import('../web-chat/owner-thread.js');
        const customerId = await resolveOwnerPrimaryCustomerId(tenantId);
        if (!customerId)
            return;
        const { randomUUID } = await import('node:crypto');
        await supabase.from('messages').insert({
            id: randomUUID(), tenant_id: tenantId, customer_id: customerId,
            session_id: `improvement:${Date.now()}`, direction: 'outbound', type: 'text',
            content: body, delivery_status: 'sent', created_at: nowIso,
        });
        const { deliverToCustomerChannel } = await import('../delivery/delivery.js');
        await deliverToCustomerChannel(tenantId, customerId, body).catch(() => { });
    }
    catch (err) {
        log.warn({ tenantId, err: err.message }, 'self-improvement digest post failed');
    }
}
function round1(n) {
    return Math.round(n * 10) / 10;
}
//# sourceMappingURL=run-on-launch.js.map