/**
 * src/autonomy/goal-pursuit.ts
 *
 * "Aggressive goal pursuit" — the autonomy loop that takes active goals
 * and actually moves them forward without waiting for chat kickoff.
 *
 * The agenda module stores goals. The Operations tab lets the owner add
 * them. But until this module, a saved goal just sat there: the only
 * thing pursuing it was a manual `/goal` chat prompt.
 *
 * Now: a periodic cron tick (`goal-pursuit`) calls into `pursueNextGoal`,
 * which picks the most-stale active goal, composes a context-rich kickoff
 * (description + related KG entities + recent journal events for that
 * goal), and posts it as a system-originated agent turn. The agent then
 * does real work (tools, subagents, escalations) — and every step it
 * records lands back in the journal/KG, so the next tick has fresher
 * context.
 *
 * Cooldown:
 *   - 30 minutes between attempts on the same goal (avoids spam).
 *   - Skips goals where the previous tick failed 3 times in a row
 *     (raises a single escalation instead and stops auto-pursuing).
 *
 * Honored signals:
 *   - goal.metadata.auto_execute === false   → never auto-pursued
 *   - goal.metadata.paused === true          → skipped this tick
 *   - goal.status !== 'active'               → skipped
 */
import { supabase } from '../utils/supabase.js';
import { createChildLogger } from '../utils/logger.js';
import { listActiveGoals, getGoal } from './agenda.js';
import { record as recordJournal, list as listJournal } from './journal.js';
const log = createChildLogger('autonomy:goal-pursuit');
const COOLDOWN_MS = 30 * 60 * 1000; // 30 minutes
const MAX_CONSECUTIVE_FAILS = 3;
/**
 * Pick the most-stale active goal and emit a kickoff message that the
 * brain (running as the system-cron actor) will pick up.
 *
 * Returns metadata about what was picked, OR null if nothing was ready.
 * The caller (cron job handler) is responsible for actually feeding the
 * kickoff message back into the agent turn pipeline.
 */
export async function pursueNextGoal(tenantId) {
    const goals = await listActiveGoals(tenantId);
    if (goals.length === 0) {
        return { picked: null, reason: 'no active goals' };
    }
    const now = Date.now();
    const eligible = [];
    for (const goal of goals) {
        const metadata = (goal.metadata ?? {});
        if (metadata.auto_execute === false)
            continue;
        if (metadata.paused === true)
            continue;
        const fails = Number(metadata.consecutive_pursuit_fails ?? 0);
        if (fails >= MAX_CONSECUTIVE_FAILS)
            continue;
        const lastPursued = metadata.last_pursued_at
            ? Date.parse(String(metadata.last_pursued_at))
            : 0;
        const sinceMs = now - lastPursued;
        if (sinceMs < COOLDOWN_MS)
            continue;
        eligible.push({ goal, staleness: sinceMs });
    }
    if (eligible.length === 0) {
        return { picked: null, reason: 'all active goals are within cooldown or paused' };
    }
    // Most-stale first, with priority as tiebreaker (lower priority number = higher importance)
    eligible.sort((a, b) => {
        if (a.staleness !== b.staleness)
            return b.staleness - a.staleness;
        return (a.goal.priority ?? 3) - (b.goal.priority ?? 3);
    });
    const winner = eligible[0].goal;
    const context = await buildGoalContext(tenantId, winner);
    const kickoff = composeKickoffMessage(winner, context);
    // Mark the attempt — even if the brain turn fails downstream, we don't
    // want to re-pick this goal again in the next 30 minutes.
    await markPursuitAttempt(tenantId, winner.id, kickoff);
    log.info({ tenantId, goalId: winner.id, title: winner.title, staleness_min: Math.round(eligible[0].staleness / 60000) }, 'goal pursuit picked');
    return {
        picked: { goalId: winner.id, title: winner.title },
        reason: 'pursued',
        kickoffMessage: kickoff,
    };
}
/**
 * Build the kickoff prompt for a SPECIFIC goal (used by on-demand "pursue
 * now" from the dashboard). Mirrors what pursueNextGoal does for the stalest
 * goal, but targets the goal the owner clicked.
 */
export async function buildGoalKickoff(tenantId, goalId) {
    const goal = await getGoal(tenantId, goalId);
    if (!goal)
        return null;
    const context = await buildGoalContext(tenantId, goal);
    const kickoff = composeKickoffMessage(goal, context);
    await markPursuitAttempt(tenantId, goalId, kickoff);
    return { goal, kickoff };
}
/**
 * Record each tool the agent ran during a pursuit as a journal entry LINKED
 * to the goal (related_goal_id). Without this, only a single "success" stamp
 * was linked per goal — the owner couldn't see the actual steps. Now the
 * goal's activity timeline shows the real process: which tools ran and
 * whether each succeeded.
 */
export async function recordPursuitSteps(tenantId, goalId, toolsExecuted) {
    for (const t of toolsExecuted) {
        try {
            await recordJournal({
                tenantId,
                kind: 'tool',
                actor: 'goal-pursuit',
                action: t.name,
                target: goalId,
                outcome: t.success ? 'success' : 'failure',
                reason: t.output ? String(t.output).slice(0, 160) : undefined,
                relatedGoalId: goalId,
            });
        }
        catch { /* best-effort — a logging failure must not break pursuit */ }
    }
}
/**
 * Pull the last ~10 journal entries linked to this goal so the kickoff
 * message can reference real prior work instead of restarting from
 * scratch every tick.
 */
async function buildGoalContext(tenantId, goal) {
    // Pull journal entries via related_goal_id link
    const entries = await listJournal({
        tenantId,
        limit: 10,
        // (Note) listJournal doesn't currently filter by related_goal_id —
        // we use the supabase client directly for that until we extend the
        // helper. Fall back to recent-overall if the direct query fails.
    });
    let recent = [];
    try {
        const { data } = await supabase
            .from('journal_entries')
            .select('kind, action, outcome, created_at')
            .eq('tenant_id', tenantId)
            .eq('related_goal_id', goal.id)
            .order('created_at', { ascending: false })
            .limit(10);
        recent = (data ?? []).map((r) => ({
            kind: r.kind,
            action: r.action,
            outcome: r.outcome,
            at: r.created_at,
        }));
    }
    catch (err) {
        // Best-effort — empty context is fine
        log.debug({ err: err.message }, 'goal-linked journal query failed');
        recent = entries.slice(0, 5).map((e) => ({
            kind: e.kind,
            action: e.action,
            outcome: String(e.outcome ?? ''),
            at: e.created_at,
        }));
    }
    const progressPct = goal.target_value && goal.target_value > 0
        ? Math.min(100, Math.round((goal.current_value / goal.target_value) * 100))
        : null;
    return { recentEntries: recent, progressPct };
}
/**
 * Build the actual prompt that gets fed to the agent. Arabic by default
 * since the user-facing app is Arabic-first; the agent itself is
 * bilingual and will follow the goal's language if it's English.
 */
function composeKickoffMessage(goal, ctx) {
    const lines = [];
    lines.push(`[نظام · سعي تلقائي للأهداف]`);
    lines.push(`الهدف: "${goal.title}"`);
    if (goal.description)
        lines.push(`الوصف: ${goal.description}`);
    if (goal.deadline)
        lines.push(`الموعد النهائي: ${goal.deadline}`);
    if (ctx.progressPct !== null)
        lines.push(`التقدم الحالي: ${ctx.progressPct}%`);
    if (ctx.recentEntries.length > 0) {
        lines.push('');
        lines.push('آخر الخطوات المُنجزة على هذا الهدف:');
        for (const e of ctx.recentEntries.slice(0, 5)) {
            const when = new Date(e.at).toISOString().slice(0, 16).replace('T', ' ');
            lines.push(`  - [${when}] ${e.kind}: ${e.action} → ${e.outcome}`);
        }
    }
    else {
        lines.push('');
        lines.push('(لم يُسجّل أي تقدّم بعد — هذه أول دفعة عمل على هذا الهدف.)');
    }
    lines.push('');
    lines.push('المطلوب الآن:');
    lines.push('  1. حدّد أصغر خطوة ملموسة يمكن إنجازها الآن تقرّبنا للهدف.');
    lines.push('  2. نفّذها بأدواتك (web_search, create_excel, send_whatsapp, إلخ).');
    lines.push('  3. إن احتجت قرار من المالك، ارفع escalation وتوقف.');
    lines.push('  4. سجّل ما أنجزته في journal بحيث أراه في المرة القادمة.');
    lines.push('  5. لا تنتظر إذني — اعمل واحكِ لي ما فعلت.');
    return lines.join('\n');
}
/**
 * Update goal metadata with last_pursued_at + a brief snapshot so we can
 * detect repeated failures. Fail-open: a metadata write failure must
 * not block the actual pursuit attempt.
 */
async function markPursuitAttempt(tenantId, goalId, kickoff) {
    try {
        const current = await getGoal(tenantId, goalId);
        if (!current)
            return;
        const metadata = { ...(current.metadata ?? {}) };
        metadata.last_pursued_at = new Date().toISOString();
        metadata.last_pursuit_kickoff_preview = kickoff.slice(0, 200);
        await supabase
            .from('agenda_goals')
            .update({ metadata, updated_at: new Date().toISOString() })
            .eq('tenant_id', tenantId)
            .eq('id', goalId);
    }
    catch (err) {
        log.warn({ tenantId, goalId, err: err.message }, 'markPursuitAttempt failed (non-fatal)');
    }
}
/**
 * Called by the cron handler when the brain turn finished. Resets the
 * failure counter on success, increments it on failure. After 3 fails
 * in a row, the goal stops being auto-picked (until the owner manually
 * unblocks it via the dashboard).
 */
export async function recordPursuitOutcome(tenantId, goalId, outcome, detail) {
    try {
        const current = await getGoal(tenantId, goalId);
        if (!current)
            return;
        const metadata = { ...(current.metadata ?? {}) };
        if (outcome === 'success') {
            metadata.consecutive_pursuit_fails = 0;
            metadata.last_pursuit_outcome = 'success';
        }
        else {
            const prev = Number(metadata.consecutive_pursuit_fails ?? 0);
            metadata.consecutive_pursuit_fails = prev + 1;
            metadata.last_pursuit_outcome = 'failure';
            metadata.last_pursuit_failure_detail = detail?.slice(0, 200);
        }
        await supabase
            .from('agenda_goals')
            .update({ metadata, updated_at: new Date().toISOString() })
            .eq('tenant_id', tenantId)
            .eq('id', goalId);
        // Mirror the outcome into the journal so the KG sees it.
        await recordJournal({
            tenantId,
            kind: outcome === 'success' ? 'goal_progress' : 'escalation',
            actor: 'system-cron',
            action: `goal_pursuit_${outcome}`,
            target: goalId,
            outcome,
            reason: detail,
            relatedGoalId: goalId,
        });
    }
    catch (err) {
        log.warn({ tenantId, goalId, err: err.message }, 'recordPursuitOutcome failed (non-fatal)');
    }
}
//# sourceMappingURL=goal-pursuit.js.map