/**
 * src/autonomy/reflection.ts
 *
 * Daily reflection cycle — Muassis grades its own work and learns from it.
 *
 * Runs on a schedule (default 23:00 Riyadh) via the `__autonomy:reflection__`
 * sentinel. Pulls the last 24h of journal entries, identifies failures /
 * escalations / customer-friction events, asks the LLM to extract concrete
 * lessons, and writes those lessons back into the knowledge graph as
 * `properties.lessons_learned` on the relevant skill rows.
 *
 * The point: today's mistakes become tomorrow's prompt context. If the agent
 * over-promised on a delivery date today, tomorrow when a similar customer
 * asks the same question, the relevant skill body has a "Last time you over-
 * promised — give a window not a date" addendum that loads automatically.
 *
 * This is what turns Muassis from a "smart chatbot" into "an employee who's
 * been here longer than you have".
 *
 * Costs ~1 LLM call per day per tenant (~2k tokens in / ~500 out). Negligible.
 */
import { randomUUID } from 'node:crypto';
import { supabase } from '../utils/supabase.js';
import { createChildLogger } from '../utils/logger.js';
import { list as listJournal, record as journalRecord } from './journal.js';
import { deliverMessage } from '../delivery/delivery.js';
import { getFact } from './identity.js';
import { callWithFailover } from '../models/failover.js';
const log = createChildLogger('autonomy:reflection');
const FAILURE_OUTCOMES = ['failure', 'escalated', 'declined'];
const HUMAN_OVERRIDE_KINDS = ['escalation', 'human_handoff', 'team_handoff'];
/**
 * Non-behavioural SYSTEM noise — failures that say nothing about how the agent
 * is doing its JOB (they're infra/extraction internals). Feeding these to the
 * lesson extractor is exactly why it returned "57 failures, 0 lessons": the
 * real signal drowned in 44 kg_extraction_failed rows. Filtered out at source.
 */
const SYSTEM_NOISE_KINDS = new Set([
    'kg_extraction_failed',
    'reflection', // reflection's own failures aren't a business lesson
    'audit-verify',
    'telemetry',
    'housekeeping',
    'delivery-health',
]);
/**
 * Pull all signals from the last `hoursBack` window that suggest the agent
 * could have done better — failures, escalations, declines, plus any
 * conversation marked as customer-dissatisfied.
 */
async function gatherFailureSignals(tenantId, hoursBack) {
    const since = new Date(Date.now() - hoursBack * 60 * 60 * 1000).toISOString();
    const all = await listJournal({ tenantId, since, limit: 500 });
    return all
        .filter((e) => {
        // Drop non-behavioural system noise BEFORE any other test.
        if (SYSTEM_NOISE_KINDS.has(e.kind))
            return false;
        if (FAILURE_OUTCOMES.includes(e.outcome))
            return true;
        if (HUMAN_OVERRIDE_KINDS.includes(e.kind))
            return true;
        const md = e.metadata || {};
        if (md.customer_dissatisfied === true)
            return true;
        if (typeof md.sentiment === 'string' && md.sentiment === 'negative')
            return true;
        return false;
    })
        .map((e) => ({
        id: e.id,
        kind: e.kind,
        action: e.action,
        outcome: e.outcome,
        reason: e.reason ?? null,
        metadata: e.metadata || {},
        created_at: e.created_at,
    }));
}
/**
 * (User-asked: success learning) Pull notable SUCCESS signals — turns where
 * the agent did well: closed deals, accepted proposals, resolved escalations
 * with positive customer sentiment, multi-tool workflows that completed, etc.
 * These feed the same lesson-extraction pass so the agent learns what to
 * REPEAT, not just what to avoid.
 */
async function gatherSuccessSignals(tenantId, hoursBack) {
    const since = new Date(Date.now() - hoursBack * 60 * 60 * 1000).toISOString();
    const all = await listJournal({ tenantId, since, limit: 500 });
    return all
        .filter((e) => {
        if (e.outcome !== 'success')
            return false;
        // High-signal success kinds:
        // - completed agenda goals
        // - workflow recipes (multi-tool chains that worked)
        // - tool calls flagged customer_satisfied=true or sentiment=positive
        // - sales closed / quotes accepted (action contains keywords)
        const md = e.metadata || {};
        if (md.customer_satisfied === true)
            return true;
        if (typeof md.sentiment === 'string' && md.sentiment === 'positive')
            return true;
        if (e.kind === 'workflow_recorded' || e.kind === 'goal_completed')
            return true;
        if (e.kind === 'cron_job_updated' && md.lessonsLearned > 0)
            return true;
        const action = (e.action || '').toLowerCase();
        if (/(?:closed|accepted|signed|booked|paid|delivered|resolved)/i.test(action))
            return true;
        if (/(?:تمت|أنجز|أُنجز|نجح|أُكمل|قُبل|تم.*دفع|تم.*استلام)/u.test(action))
            return true;
        return false;
    })
        .map((e) => ({
        id: e.id,
        kind: e.kind,
        action: e.action,
        outcome: e.outcome,
        reason: e.reason ?? null,
        metadata: e.metadata || {},
        created_at: e.created_at,
    }));
}
/**
 * OWNER CORRECTIONS — the single highest-value learning signal. When the owner
 * DENIES or MODIFIES an escalation, that's a direct "you got this wrong, here's
 * the right call" — far more valuable than any inferred failure. We pull these
 * from `escalation_resolved` journal entries (resolution starts with denied/
 * modified) and from explicit `owner_correction` events. Sparse but gold, so we
 * scan a wider window than the 24h behavioural window.
 */
async function gatherOwnerCorrections(tenantId, hoursBack) {
    const since = new Date(Date.now() - hoursBack * 60 * 60 * 1000).toISOString();
    const all = await listJournal({ tenantId, since, limit: 500 });
    return all
        .filter((e) => {
        if (e.kind === 'owner_correction')
            return true;
        if (e.kind === 'escalation_resolved') {
            const res = String(e.metadata?.resolution ?? e.reason ?? '').toLowerCase();
            const outcome = e.outcome;
            return outcome === 'declined' || res.startsWith('denied') || res.startsWith('modified');
        }
        const md = e.metadata || {};
        return md.owner_overrode === true;
    })
        .map((e) => ({
        id: e.id,
        kind: e.kind,
        action: e.action,
        outcome: e.outcome,
        reason: e.reason ?? null,
        metadata: e.metadata || {},
        created_at: e.created_at,
    }));
}
/**
 * Collapse repeated identical failures (e.g. browser_open ×25) into ONE line
 * with a count + representative reason. A pattern of 25 identical failures is
 * a single strong lesson ("browser_open keeps failing — stop relying on it or
 * fix the env"), not 25 separate ones. This is the other half of why the
 * extractor saw noise: un-aggregated repetition looked like 100 unrelated rows.
 */
function aggregateFailures(failures) {
    const groups = new Map();
    for (const f of failures) {
        const key = `${f.kind}/${f.action}/${f.outcome}`;
        const g = groups.get(key);
        if (g)
            g.count++;
        else
            groups.set(key, { count: 1, sample: f });
    }
    const lines = [...groups.values()]
        .sort((a, b) => b.count - a.count)
        .slice(0, 30)
        .map((g, i) => {
        const f = g.sample;
        const times = g.count > 1 ? ` ×${g.count}` : '';
        return `${i + 1}. [${f.kind}/${f.outcome}]${times} action=${f.action}` +
            (f.reason ? ` — reason: ${f.reason.slice(0, 200)}` : '');
    });
    return lines.join('\n');
}
/**
 * Ask the LLM to extract structured lessons from the failure signals,
 * mapping each to an existing skill name. The LLM gets the list of skills
 * by name + whenToUse so it can pick the relevant target.
 */
async function extractLessons(tenantId, failures, successes, corrections, skillCatalog) {
    if (failures.length === 0 && successes.length === 0 && corrections.length === 0)
        return [];
    // Aggregated so repeated identical failures read as one pattern, not noise.
    const failuresBlock = aggregateFailures(failures);
    const correctionsBlock = corrections
        .slice(0, 20)
        .map((c, i) => `${i + 1}. action=${c.action}` +
        (c.reason ? ` — صاحب العمل قال: ${c.reason.slice(0, 220)}` : '') +
        (c.metadata.recommendation ? `\n   (توصيتي كانت: ${String(c.metadata.recommendation).slice(0, 150)})` : ''))
        .join('\n');
    const successesBlock = successes
        .slice(0, 20)
        .map((s, i) => `${i + 1}. [${s.kind}] action=${s.action}` +
        (s.reason ? ` — note: ${s.reason.slice(0, 200)}` : '') +
        (Object.keys(s.metadata).length > 0
            ? `\n   metadata: ${JSON.stringify(s.metadata).slice(0, 200)}`
            : ''))
        .join('\n');
    const skillsBlock = skillCatalog
        .slice(0, 80)
        .map((s) => `- ${s.name}: ${s.whenToUse.slice(0, 120)}`)
        .join('\n');
    const systemPrompt = `أنت ناقد ذاتي للمؤسس (وكيل ذكاء اصطناعي سعودي). مهمتك: قراءة سجل اليوم ` +
        `(تصحيحات المالك + الإخفاقات + النجاحات) واستخلاص دروس عملية موجَّهة لمهارات محددة.\n\n` +
        `**ترتيب الأهمية (الأهم أولاً):**\n` +
        `١. **تصحيحات صاحب العمل** — حين يرفض المالك أو يعدّل اقتراحاً، هذا أقوى درس على الإطلاق: ` +
        `حكمُه يعلوّ حكمَك. استخرج منه درساً دائماً.\n` +
        `٢. **أنماط الإخفاق المتكررة** — فشل تكرر ×N مرة = درس واحد قوي (مثلاً "أداة X تفشل دائماً — لا تعتمد عليها").\n` +
        `٣. **النجاحات** — ما الذي نجح ويستحق التكرار.\n\n` +
        `**نوعا الدروس:**\n` +
        `1. **avoid** — درس مستخلَص من إخفاق أو تصحيح ("لا تفعل X" أو "بدلاً من X افعل Y")\n` +
        `2. **repeat** — درس مستخلَص من نجاح ("استمر في فعل X لأنه نجح")\n\n` +
        `الدروس يجب أن تكون:\n` +
        `- محددة (ليست "كن أفضل") — مثل "عند سؤال العميل عن وقت التسليم، أعطِ نافذة 2-4 أيام لا تاريخاً مفرداً"\n` +
        `- مرتبطة بمهارة واحدة من القائمة فقط\n` +
        `- قصيرة (سطر-سطرين)\n\n` +
        `أرجع JSON صرف:\n` +
        `{"lessons":[{"skill_name":"...", "lesson":"...", "evidence_journal_id":"...", "severity":"low|medium|high", "kind":"avoid|repeat"}]}`;
    const userPrompt = `## المهارات المتاحة (اختر واحدة فقط لكل درس)\n${skillsBlock}\n\n` +
        `## ⭐ تصحيحات صاحب العمل (${corrections.length} — الأعلى أولوية)\n${correctionsBlock || '(لا شيء اليوم)'}\n\n` +
        `## إخفاقات اليوم — مجمّعة بالتكرار (${failures.length} حدث)\n${failuresBlock || '(لا شيء)'}\n\n` +
        `## نجاحات اليوم (${successes.length} حدث)\n${successesBlock || '(لا شيء)'}\n\n` +
        `استخرج الدروس الآن (ابدأ بتصحيحات المالك). JSON فقط.`;
    try {
        const result = await callWithFailover('reasoning', [
            { role: 'system', content: systemPrompt },
            { role: 'user', content: userPrompt },
        ], 
        // json_object forces valid JSON; 3000 tokens stops verbose multi-lesson
        // Arabic output from truncating mid-object (the old 1200 cap silently
        // produced unparseable JSON → "0 lessons" even when lessons were good).
        { maxTokens: 3000, responseFormat: { type: 'json_object' } }, tenantId);
        const raw = result.content || '';
        // (Audit M5) Multi-strategy parse:
        //   1. raw — most providers respect "JSON only" instruction
        //   2. strip ```json fences — common when model wraps output
        //   3. last-resort regex — handles prose-then-JSON
        const tryParse = (s) => {
            try {
                return JSON.parse(s);
            }
            catch {
                return null;
            }
        };
        let parsed = tryParse(raw.trim());
        if (!parsed) {
            const fence = raw.match(/```(?:json)?\s*([\s\S]+?)\s*```/i);
            if (fence)
                parsed = tryParse(fence[1].trim());
        }
        if (!parsed) {
            // Non-greedy match: stop at first balanced } at depth 0. Imperfect for
            // nested JSON but the LLM contract is one flat object.
            const m = raw.match(/\{[\s\S]*?"lessons"\s*:\s*\[[\s\S]*?\]\s*\}/);
            if (m)
                parsed = tryParse(m[0]);
        }
        if (!parsed) {
            log.warn({ tenantId, raw: raw.slice(0, 300) }, 'reflection LLM returned unparseable JSON — no lessons extracted');
            return [];
        }
        return Array.isArray(parsed.lessons) ? parsed.lessons : [];
    }
    catch (err) {
        // (H3) Don't silently return empty. Journal the failure so the owner
        // sees a pattern in the dashboard, and surface to WhatsApp if the
        // failure persists 3+ days in a row.
        const errMsg = err.message;
        log.warn({ tenantId, err: errMsg }, 'reflection LLM call failed');
        try {
            await journalRecord({
                tenantId,
                kind: 'reflection',
                action: 'extract_lessons',
                outcome: 'failure',
                reason: `LLM call failed: ${errMsg}`,
                metadata: { failureCount: failures.length },
            });
            // Check for a 3-day streak of failures and alert the owner
            const last3 = await listJournal({
                tenantId,
                kind: 'reflection',
                since: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString(),
                limit: 10,
            });
            const failures3 = last3.filter((e) => e.outcome === 'failure').length;
            if (failures3 >= 3) {
                const ownerPhoneFact = await getFact(tenantId, 'owner.whatsapp');
                if (ownerPhoneFact?.fact_value) {
                    await deliverMessage({
                        tenantId,
                        to: ownerPhoneFact.fact_value,
                        channel: 'whatsapp',
                        message: `⚠️ آلية التعلم اليومي ما اشتغلت ${failures3} أيام متتالية. ` +
                            `الخدمة تستمر طبيعي بس ما راح يتعلم من الأخطاء حتى يصلح الموضوع. ` +
                            `آخر خطأ: ${errMsg.slice(0, 100)}`,
                    }).catch(() => undefined);
                }
            }
        }
        catch { /* journal/alert failures are non-fatal */ }
        return [];
    }
}
/**
 * Persist a lesson onto a skill's KG row. Uses the existing
 * `kg_entities` row for kind=skill matching by name (case-insensitive).
 * Appends to `properties.lessons_learned` array — never overwrites old
 * lessons.
 */
/**
 * Levenshtein distance for fuzzy skill-name matching. (C7 fix.)
 * Used to catch the LLM hallucinating slight name variants
 * ("follow-up" vs "Follow-Up Strategy" vs "متابعة العملاء").
 */
function levenshtein(a, b) {
    if (a === b)
        return 0;
    if (!a.length)
        return b.length;
    if (!b.length)
        return a.length;
    const dp = Array.from({ length: b.length + 1 }, (_, i) => i);
    for (let i = 1; i <= a.length; i++) {
        let prev = dp[0];
        dp[0] = i;
        for (let j = 1; j <= b.length; j++) {
            const cost = a[i - 1] === b[j - 1] ? 0 : 1;
            const tmp = dp[j];
            dp[j] = Math.min(dp[j] + 1, dp[j - 1] + 1, prev + cost);
            prev = tmp;
        }
    }
    return dp[b.length];
}
async function attachLessonToSkill(tenantId, lesson) {
    let nameNorm = lesson.skill_name.toLowerCase().trim();
    // (C7) Resolve hallucinated skill names to real ones via Levenshtein.
    // Without this, the LLM's "Follow-Up Strategy" creates a phantom KG row
    // that loadSkills() never matches, so lessons get attached to a ghost.
    try {
        const { loadSkills } = await import('../agent/skills.js');
        const realSkills = await loadSkills(tenantId);
        const exactMatch = realSkills.find((s) => s.name.toLowerCase() === nameNorm);
        if (!exactMatch) {
            // Compute Levenshtein distance against every real skill name; pick
            // the closest if within a tolerance proportional to name length.
            //
            // (Round-7 fix) For short Arabic names (≤8 chars), the previous
            // tolerance of `Math.max(2, ...)` was too permissive — distance=2
            // matches "تقرير" → "ترقية" (one letter delta in 5 chars) which is
            // a different word entirely. Now require longer names to have ≤30%
            // edit distance AND short names (≤8) to be near-exact (distance ≤1).
            let best = null;
            for (const s of realSkills) {
                const candidate = s.name.toLowerCase();
                const d = levenshtein(nameNorm, candidate);
                const tolerance = candidate.length <= 8
                    ? 1
                    : Math.max(2, Math.floor(candidate.length * 0.3));
                if (d <= tolerance && (!best || d < best.distance)) {
                    best = { name: candidate, distance: d };
                }
            }
            if (best) {
                log.info({ tenantId, hallucinated: lesson.skill_name, resolved: best.name, distance: best.distance }, '(C7) resolved hallucinated skill name to closest real skill');
                nameNorm = best.name;
                lesson.skill_name = realSkills.find((s) => s.name.toLowerCase() === best.name).name;
            }
            else {
                // No reasonable match. Reject — don't pollute KG with phantoms.
                log.warn({ tenantId, hallucinated: lesson.skill_name, candidateCount: realSkills.length }, '(C7) lesson references non-existent skill — rejected');
                return false;
            }
        }
    }
    catch (err) {
        log.debug({ err: err.message }, 'C7 skill-name resolution failed; proceeding with raw name');
    }
    const { data } = await supabase
        .from('kg_entities')
        .select('id, properties')
        .eq('tenant_id', tenantId)
        .eq('kind', 'skill')
        .eq('name_normalized', nameNorm)
        .maybeSingle();
    let row = data;
    if (!row) {
        // Skill row doesn't exist in KG yet — create one. The skill body lives
        // on disk; the KG row carries the evolved metadata layer.
        const id = randomUUID();
        const { error: insErr } = await supabase.from('kg_entities').insert({
            id,
            tenant_id: tenantId,
            kind: 'skill',
            type: 'Skill',
            name: lesson.skill_name,
            name_normalized: nameNorm,
            properties: { lessons_learned: [] },
            source_files: [],
            episode_count: 0,
        });
        if (insErr) {
            log.warn({ tenantId, skill: lesson.skill_name, err: insErr.message }, 'kg skill create failed');
            return false;
        }
        row = { id, properties: { lessons_learned: [] } };
    }
    const props = (row.properties || {});
    const lessons = Array.isArray(props.lessons_learned) ? props.lessons_learned : [];
    // (H2) Conflict detection: if the new lesson is very similar to an existing
    // lesson but differs significantly (likely a contradiction), supersede the
    // old one rather than letting both pile up.
    const newText = lesson.lesson.toLowerCase();
    const newTokens = new Set(newText.split(/\s+/).filter((t) => t.length > 3));
    let supersededIndex = -1;
    for (let i = lessons.length - 1; i >= 0; i--) {
        const oldText = String(lessons[i].text || '').toLowerCase();
        const oldTokens = new Set(oldText.split(/\s+/).filter((t) => t.length > 3));
        let overlap = 0;
        for (const t of newTokens)
            if (oldTokens.has(t))
                overlap++;
        const minSize = Math.min(newTokens.size, oldTokens.size);
        if (minSize > 0 && overlap / minSize > 0.6) {
            supersededIndex = i;
            log.info({ tenantId, oldLesson: oldText.slice(0, 80), newLesson: newText.slice(0, 80) }, '(H2) new lesson supersedes a similar earlier lesson');
            break;
        }
    }
    if (supersededIndex >= 0) {
        lessons[supersededIndex] = {
            ...lessons[supersededIndex],
            superseded_at: new Date().toISOString(),
            superseded_by: lesson.lesson,
        };
    }
    lessons.push({
        text: lesson.lesson,
        severity: lesson.severity,
        evidence_journal_id: lesson.evidence_journal_id,
        learned_at: new Date().toISOString(),
    });
    // Cap at 25 lessons per skill — older ones drop off. Stops the prompt
    // from ballooning over months of evolution.
    const MAX_LESSONS = 25;
    const capped = lessons.slice(-MAX_LESSONS);
    props.lessons_learned = capped;
    const { error: upErr } = await supabase
        .from('kg_entities')
        .update({ properties: props, updated_at: new Date().toISOString() })
        .eq('id', row.id);
    if (upErr) {
        log.warn({ tenantId, skill: lesson.skill_name, err: upErr.message }, 'kg skill lesson update failed');
        return false;
    }
    return true;
}
/**
 * The proof-of-learning metric: what fraction of the agent's decisions did the
 * owner have to correct? A smartest-employee that's actually learning shows
 * this DROPPING over time. We snapshot it each reflection so the trend is real,
 * not claimed.
 */
export async function computeCorrectionRate(tenantId, windowDays = 7) {
    const since = new Date(Date.now() - windowDays * 24 * 60 * 60 * 1000).toISOString();
    const entries = await listJournal({ tenantId, since, limit: 1000 }).catch(() => []);
    // A "decision" = the agent made a call the owner could react to: an escalation
    // it raised, or an auto action it executed. We exclude pure system ticks.
    const decisions = entries.filter((e) => e.kind === 'escalation' ||
        e.kind === 'escalation_resolved' ||
        (e.actor === 'initiative') ||
        (e.kind === 'goal_progress')).length;
    const corrections = entries.filter((e) => {
        if (e.kind === 'owner_correction')
            return true;
        if (e.kind === 'escalation_resolved') {
            const res = String(e.metadata?.resolution ?? e.reason ?? '').toLowerCase();
            return e.outcome === 'declined' || res.startsWith('denied') || res.startsWith('modified');
        }
        return e.metadata?.owner_overrode === true;
    }).length;
    return { windowDays, decisions, corrections, rate: decisions > 0 ? corrections / decisions : 0 };
}
/**
 * Run one full reflection cycle. Called by the cron scheduler via the
 * `__autonomy:reflection__` sentinel.
 */
export async function runReflectionCycle(tenantId) {
    const result = {
        scanned: 0,
        failuresFound: 0,
        successesFound: 0,
        lessonsLearned: 0,
        briefDelivered: false,
        skillsTouched: [],
    };
    // Pull corrections (wide window — sparse + gold), failures + successes (24h).
    const [failures, successes, corrections] = await Promise.all([
        gatherFailureSignals(tenantId, 24),
        gatherSuccessSignals(tenantId, 24),
        gatherOwnerCorrections(tenantId, 72),
    ]);
    result.scanned = failures.length + successes.length + corrections.length;
    result.failuresFound = failures.length;
    result.successesFound = successes.length;
    result.correctionsFound = corrections.length;
    if (failures.length === 0 && successes.length === 0 && corrections.length === 0) {
        log.info({ tenantId }, 'reflection: no signals at all — quiet day, no LLM call');
        return result;
    }
    // Skip only if there's truly too little to learn from. An owner correction
    // ALWAYS warrants a lesson, so never skip when corrections exist.
    if (corrections.length === 0 && failures.length < 1 && successes.length < 2) {
        log.info({ tenantId, failures: failures.length, successes: successes.length }, 'reflection: too little signal — skipping LLM call');
        return result;
    }
    // Build a skill catalog so the LLM can target lessons at real skill names.
    // Use the disk-loaded skills as the canonical name list.
    const { loadSkills } = await import('../agent/skills.js');
    const skills = await loadSkills(tenantId);
    const catalog = skills.map((s) => ({ name: s.name, whenToUse: s.whenToUse || '' }));
    const lessons = await extractLessons(tenantId, failures, successes, corrections, catalog);
    if (lessons.length === 0) {
        log.info({ tenantId, failures: failures.length }, 'reflection: no actionable lessons extracted');
        return result;
    }
    const touched = new Set();
    for (const lesson of lessons) {
        const ok = await attachLessonToSkill(tenantId, lesson);
        if (ok) {
            result.lessonsLearned++;
            touched.add(lesson.skill_name);
        }
    }
    result.skillsTouched = [...touched];
    // Proof-of-learning metric — snapshot the correction rate so its trend is real.
    const cr = await computeCorrectionRate(tenantId, 7).catch(() => null);
    if (cr)
        result.correctionRate = cr.rate;
    // Graduated trust — having just measured how often the owner corrects us,
    // widen or pull back our auto-scope accordingly. Best-effort; never blocks
    // reflection. Owner is notified of any change inside reviewTrust.
    try {
        const { reviewTrust } = await import('./trust.js');
        await reviewTrust(tenantId);
    }
    catch (err) {
        log.debug({ tenantId, err: err.message }, 'trust review failed (non-fatal)');
    }
    // Journal the reflection itself so the chain stays auditable
    try {
        await journalRecord({
            tenantId,
            kind: 'reflection',
            action: 'daily_reflection',
            outcome: 'success',
            reason: `Reflected on ${result.failuresFound} failures + ${result.correctionsFound ?? 0} owner corrections; learned ${result.lessonsLearned} lessons across ${result.skillsTouched.length} skills.`,
            metadata: {
                skillsTouched: result.skillsTouched,
                lessonCount: result.lessonsLearned,
                failureCount: result.failuresFound,
                correctionsFound: result.correctionsFound ?? 0,
                correctionRate: cr ? Math.round(cr.rate * 1000) / 1000 : null,
                correctionRateDecisions: cr?.decisions ?? 0,
            },
        });
    }
    catch { /* non-fatal */ }
    // Brief the owner: short, action-oriented, human voice
    if (result.lessonsLearned > 0) {
        const ownerPhoneFact = await getFact(tenantId, 'owner.whatsapp');
        if (ownerPhoneFact?.fact_value) {
            const avoidLessons = lessons.filter((l) => l.kind !== 'repeat');
            const repeatLessons = lessons.filter((l) => l.kind === 'repeat');
            const briefLines = [
                `🎓 ملخص اليوم: راجعت ${result.correctionsFound ?? 0} تصحيح منك و ${result.failuresFound} إخفاق و ${result.successesFound ?? 0} نجاح. تعلمت ${result.lessonsLearned} درس.`,
            ];
            if (result.correctionRate !== undefined) {
                briefLines.push(`📉 معدّل تصحيحك لقراراتي (آخر ٧ أيام): ${Math.round(result.correctionRate * 100)}% — كل ما قلّ، صار أذكى.`);
            }
            if (avoidLessons.length > 0) {
                briefLines.push('', '**أتجنب من اليوم:**');
                for (const l of avoidLessons.slice(0, 4))
                    briefLines.push(`• "${l.skill_name}": ${l.lesson}`);
            }
            if (repeatLessons.length > 0) {
                briefLines.push('', '**أكرّر من اليوم (نجح):**');
                for (const l of repeatLessons.slice(0, 4))
                    briefLines.push(`• "${l.skill_name}": ${l.lesson}`);
            }
            const totalShown = Math.min(4, avoidLessons.length) + Math.min(4, repeatLessons.length);
            if (lessons.length > totalShown) {
                briefLines.push('', `+ ${lessons.length - totalShown} درس آخر — راجع لوحة العمليات للتفاصيل.`);
            }
            briefLines.push('', `كل درس مخزَّن في الذاكرة وسأطبّقه تلقائياً غداً.`);
            try {
                await deliverMessage({
                    tenantId,
                    to: ownerPhoneFact.fact_value,
                    message: briefLines.join('\n'),
                    channel: 'whatsapp',
                }).catch(() => undefined);
                result.briefDelivered = true;
            }
            catch { /* non-fatal */ }
        }
    }
    log.info({ tenantId, ...result }, 'reflection cycle complete');
    return result;
}
//# sourceMappingURL=reflection.js.map