/**
 * src/autonomy/proactive-actions.ts
 *
 * What turns Muassis from a polite chatbot into an indispensable employee:
 * **the agent ACTS on opportunities instead of announcing them.** This module
 * runs every hour during business hours, scans for situations where a real
 * employee would draft + send something, and either:
 *   - executes silently (policy mode='auto')
 *   - escalates with a pre-written draft (policy mode='ask')
 *   - skips (policy mode='never')
 *
 * Opportunity scanners:
 *   1. Silent hot lead — customer marked "interested" went 5+ days without
 *      contact. Action: draft a re-engagement message.
 *   2. Overdue invoice (>15 days) — action: draft a polite dunning message.
 *   3. Upcoming deadline (<24h) on an agenda goal — action: surface progress
 *      check + suggest next step.
 *   4. Customer follow-up promised in conversation — action: surface to owner.
 *   5. Birthday / contract anniversary — action: draft greeting.
 *
 * Every action is journaled. Daily scorecard (run by a separate cron) rolls
 * these up so the owner sees: "Today I did 14 actions, drafted 6 messages,
 * sent 3 reminders — saved ~2.5 hours."
 *
 * Policy keys (created by seedProactivePolicies; default to 'ask' for safety):
 *   - proactive_followup_silent       (default: ask)
 *   - proactive_invoice_dunning       (default: ask)
 *   - proactive_deadline_check        (default: auto — internal nudge, no $$ risk)
 *   - proactive_promise_reminder      (default: auto — owner-facing)
 *   - proactive_birthday_greeting     (default: ask)
 */
import { supabase } from '../utils/supabase.js';
import { createChildLogger } from '../utils/logger.js';
import { record as journalRecord, list as listJournal } from './journal.js';
import { upsertPolicy, evaluate as evaluatePolicy } from './policies.js';
import { listActiveGoals } from './agenda.js';
import { deliverMessage } from '../delivery/delivery.js';
import { getFact } from './identity.js';
const log = createChildLogger('autonomy:proactive');
const days = (n) => new Date(Date.now() - n * 24 * 60 * 60 * 1000).toISOString();
/**
 * Idempotently seed the proactive policies. Defaults are conservative — most
 * are 'ask' so the owner sees the draft + approves. The owner can flip them
 * to 'auto' from the dashboard once they trust the agent.
 */
export async function seedProactivePolicies(tenantId) {
    const policies = [
        { key: 'proactive_followup_silent', mode: 'ask', description: 'Re-engage a silent hot lead with a drafted message' },
        { key: 'proactive_invoice_dunning', mode: 'ask', description: 'Send a polite reminder for an overdue invoice' },
        { key: 'proactive_deadline_check', mode: 'auto', description: 'Internal progress nudge on a near-deadline goal' },
        { key: 'proactive_promise_reminder', mode: 'auto', description: 'Surface a promise we made to a customer (no external send)' },
        { key: 'proactive_birthday_greeting', mode: 'ask', description: 'Send a short greeting on customer birthday/anniversary' },
    ];
    for (const p of policies) {
        try {
            await upsertPolicy(tenantId, {
                policy_key: p.key,
                description: p.description,
                mode: p.mode,
                threshold_amount: p.threshold,
            });
        }
        catch (err) {
            log.debug({ key: p.key, err: err.message }, 'policy seed skipped');
        }
    }
}
// ─── Scanners ───
async function scanSilentHotLeads(tenantId) {
    // Customers tagged 'hot' or 'interested' with no inbound message in 5-14 days
    const { data } = await supabase
        .from('customers')
        .select('id, name, phone, last_contact, status, metadata')
        .eq('tenant_id', tenantId)
        .gte('last_contact', days(14))
        .lt('last_contact', days(5))
        .limit(20);
    const candidates = (data ?? []);
    // Avoid re-firing within 7 days for the same customer
    const recent = await listJournal({
        tenantId,
        kind: 'proactive_followup_silent',
        since: days(7),
        limit: 100,
    });
    const recentTargets = new Set(recent.map((e) => e.target).filter(Boolean));
    return candidates
        .filter((c) => !recentTargets.has(c.id))
        .filter((c) => {
        const tags = c.metadata?.tags ?? [];
        return tags.includes('hot') || tags.includes('interested') || c.status === 'lead';
    })
        .slice(0, 5)
        .map((c) => {
        const daysSilent = Math.floor((Date.now() - new Date(c.last_contact).getTime()) / (24 * 60 * 60 * 1000));
        return {
            kind: 'silent_hot_lead',
            policy_key: 'proactive_followup_silent',
            target: c.id,
            draft: `هلا ${c.name}، أمل تكون بخير 🌟\n\n` +
                `كنا تكلمنا قبل ${daysSilent} يوم وما رجعنا لبعض. ودي أتأكد إذا الموضوع لازال مهم لك أو في شي ` +
                `أقدر أساعدك فيه؟ سواء جاوبت بـ"نعم" أو "لا" — يفيدني أعرف.`,
            rationale: `Customer last contacted ${daysSilent} days ago, marked as ${c.status} / hot lead`,
            metadata: { customer_id: c.id, days_silent: daysSilent, phone: c.phone },
        };
    });
}
async function scanNearDeadlineGoals(tenantId) {
    const goals = await listActiveGoals(tenantId).catch(() => []);
    const now = Date.now();
    return goals
        .filter((g) => g.deadline)
        .map((g) => {
        const hoursLeft = (new Date(g.deadline).getTime() - now) / (1000 * 60 * 60);
        const progress = g.target_value && g.current_value !== undefined
            ? (g.current_value / g.target_value) * 100
            : 0;
        return { goal: g, hoursLeft, progress };
    })
        .filter((x) => x.hoursLeft > 0 && x.hoursLeft <= 36 && x.progress < 75)
        .slice(0, 3)
        .map((x) => ({
        kind: 'deadline_approaching',
        policy_key: 'proactive_deadline_check',
        target: x.goal.id,
        draft: `⏰ هدف "${x.goal.title}" ينتهي خلال ${Math.round(x.hoursLeft)} ساعة.\n` +
            `التقدم الحالي: ${Math.round(x.progress)}% من المستهدف.\n\n` +
            `اقتراح: راجع الحالة وحدد الخطوة التالية. لو احتجت تأجيل الموعد قول.`,
        rationale: `Goal "${x.goal.title}" — ${x.hoursLeft.toFixed(1)}h left, ${x.progress.toFixed(0)}% done`,
        metadata: { goal_id: x.goal.id, hours_left: x.hoursLeft, progress: x.progress },
    }));
}
async function scanPromisesMade(tenantId) {
    // Look for journal entries where the agent said "سأرد بكره" / "I'll get back" /
    // a date hint, but no follow-up cron exists for that customer in the next 24h.
    // Conservative scanner — only fires for the strongest patterns.
    const recent = await listJournal({ tenantId, since: days(2), limit: 200 });
    const promiseRegex = /(?:بكره|بكرة|غدا|tomorrow|first thing|اول شي|أول شي|will follow up|سأتابع|سأرد|سأرسل)/i;
    const candidates = recent.filter((e) => e.kind === 'message' || e.kind === 'tool')
        .filter((e) => promiseRegex.test(e.reason ?? ''))
        .slice(0, 5);
    // Dedupe via prior journal entries for this kind
    const recentRems = await listJournal({
        tenantId,
        kind: 'proactive_promise_reminder',
        since: days(1),
        limit: 50,
    });
    const recentTargets = new Set(recentRems.map((e) => e.target).filter(Boolean));
    return candidates
        .filter((e) => e.target && !recentTargets.has(e.target))
        .map((e) => ({
        kind: 'promise_reminder',
        policy_key: 'proactive_promise_reminder',
        target: e.target,
        draft: `📌 وعد سابق محتاج متابعة:\n` +
            `لـ: ${e.target}\n` +
            `السياق: ${(e.reason ?? '').slice(0, 200)}\n\n` +
            `اقتراح: ${e.action} للتنفيذ الآن.`,
        rationale: `Earlier message contained a promise pattern matched in journal entry ${e.id}`,
        metadata: { source_journal_id: e.id, customer_id: e.target },
    }));
}
// ─── Main runner ───
export async function runProactiveActions(tenantId) {
    const result = {
        scanned: 0, drafted: 0, autoExecuted: 0, escalated: 0, skipped: 0,
        byKind: {},
    };
    // Make sure policies exist
    await seedProactivePolicies(tenantId).catch(() => undefined);
    // Run scanners in parallel
    const [silent, deadlines, promises] = await Promise.all([
        scanSilentHotLeads(tenantId).catch(() => []),
        scanNearDeadlineGoals(tenantId).catch(() => []),
        scanPromisesMade(tenantId).catch(() => []),
    ]);
    const allActions = [...silent, ...deadlines, ...promises];
    result.scanned = allActions.length;
    // Per-run cap to avoid bombardment
    const PER_RUN_CAP = 8;
    const actions = allActions.slice(0, PER_RUN_CAP);
    for (const action of actions) {
        result.drafted++;
        result.byKind[action.kind] = (result.byKind[action.kind] ?? 0) + 1;
        let policyMode = 'ask';
        try {
            const decision = await evaluatePolicy({ tenantId, policyKey: action.policy_key });
            const decisionMode = decision.mode;
            if (decisionMode === 'auto' || decisionMode === 'ask' || decisionMode === 'never') {
                policyMode = decisionMode;
            }
        }
        catch { /* default 'ask' */ }
        if (policyMode === 'never') {
            result.skipped++;
            continue;
        }
        if (policyMode === 'auto') {
            // Execute the draft. For owner-facing actions (deadline_check,
            // promise_reminder), deliver to owner via WhatsApp. For customer-
            // facing actions (silent_hot_lead, etc.), gate behind a stricter
            // check — never auto-send to a customer without owner-level opt-in.
            if (action.policy_key === 'proactive_deadline_check' || action.policy_key === 'proactive_promise_reminder') {
                try {
                    const ownerPhone = (await getFact(tenantId, 'owner.whatsapp'))?.fact_value;
                    if (ownerPhone) {
                        await deliverMessage({
                            tenantId,
                            to: ownerPhone,
                            channel: 'whatsapp',
                            message: action.draft,
                        });
                        await journalRecord({
                            tenantId,
                            kind: action.policy_key,
                            action: action.kind,
                            target: action.target,
                            outcome: 'success',
                            reason: action.rationale,
                            metadata: { ...action.metadata, draft: action.draft.slice(0, 500), policy_mode: 'auto' },
                        });
                        result.autoExecuted++;
                        continue;
                    }
                }
                catch (err) {
                    log.debug({ err: err.message, kind: action.kind }, 'auto-deliver failed; falling back to escalation');
                }
            }
            // For customer-facing auto actions, treat as escalation by default
            // (defense in depth). Owner can explicitly enable customer-facing
            // auto-send by toggling the relevant policy mode in the dashboard.
        }
        // Default: create an escalation row + journal it. The escalation surfaces
        // in the dashboard with the draft attached; owner approves/rejects.
        try {
            const { default: escalationModule } = await import('./escalation.js').then((m) => ({ default: m }));
            const create = escalationModule.create;
            if (typeof create === 'function') {
                await create({
                    tenantId,
                    title: `${action.kind}: ${action.target}`,
                    context: action.rationale,
                    recommendation: 'Send the drafted message',
                    counterargument: 'Skip — customer may interpret as nagging',
                    alternatives: ['Send draft as-is', 'Edit draft first', 'Skip this customer'],
                    riskLevel: action.kind === 'silent_hot_lead' ? 'medium' : 'low',
                    customer_id: action.metadata?.customer_id,
                    metadata: { ...action.metadata, draft: action.draft, source: 'proactive' },
                });
            }
            await journalRecord({
                tenantId,
                kind: action.policy_key,
                action: action.kind,
                target: action.target,
                outcome: 'escalated',
                reason: action.rationale,
                metadata: { ...action.metadata, draft: action.draft.slice(0, 500), policy_mode: 'ask' },
            });
            result.escalated++;
        }
        catch (err) {
            log.warn({ kind: action.kind, err: err.message }, 'escalation create failed — falling back to journal-only');
            await journalRecord({
                tenantId,
                kind: action.policy_key,
                action: action.kind,
                target: action.target,
                outcome: 'escalated',
                reason: action.rationale,
                metadata: { ...action.metadata, draft: action.draft.slice(0, 500) },
            }).catch(() => undefined);
            result.escalated++;
        }
    }
    log.info({ tenantId, ...result }, 'proactive actions cycle complete');
    return result;
}
//# sourceMappingURL=proactive-actions.js.map