/**
 * src/autonomy/seeder.ts
 *
 * Idempotent first-run seeder. The reason Muassis felt like a chatbot was the
 * cron table being empty by default. This seeds:
 *   • 11 default cron jobs covering daily ops (briefs, prayers, market, follow-ups)
 *   • 16 default autonomy policies covering common SMB actions
 *   • A starter "stay-on-top-of-business" goal so the agenda card isn't empty
 *
 * Runs once per tenant. Adds a flag to identity_facts so re-running is a no-op.
 */
import { randomUUID } from 'node:crypto';
import { supabase } from '../utils/supabase.js';
import { createChildLogger } from '../utils/logger.js';
import { upsertPolicy } from './policies.js';
import { upsertFact, getFact } from './identity.js';
import { createGoal, listActiveGoals } from './agenda.js';
const log = createChildLogger('autonomy:seeder');
const SEED_FLAG_KEY = 'system.autonomy_seeded_at';
const DEFAULT_CRON_JOBS = [
    {
        name: 'morning-brief',
        schedule: '0 8 * * *',
        message: 'Compose the morning brief: weather + prayer times + Hijri date + Tadawul status + agenda highlights + escalation queue + overnight inbox. Keep it under 200 words. Arabic if owner language is ar.',
    },
    {
        name: 'evening-recap',
        schedule: '0 18 * * *',
        message: 'Compose the evening recap: what got done today (from journal), open follow-ups, tomorrow\'s top 3, anything I should sleep on. Arabic if owner language is ar.',
    },
    {
        name: 'weekly-review',
        schedule: '0 19 * * 0',
        message: 'Compose this week\'s review: KPI movement against agenda goals, biggest wins, biggest stalls, recommended focus for next week. Cite numbers from the journal.',
    },
    {
        name: 'fajr-reminder',
        schedule: '30 4 * * *',
        message: 'Send a soft Fajr reminder with today\'s prayer schedule. One line. Arabic.',
    },
    {
        name: 'asr-reminder',
        schedule: '0 15 * * *',
        message: 'Send an Asr prayer reminder with the exact time today.',
    },
    {
        name: 'tadawul-open',
        schedule: '0 10 * * 0-4',
        message: 'Tadawul market is opening now. If the owner has any tracked tickers in identity_facts (key prefix portfolio.), summarize today\'s pre-open price moves. Otherwise just announce the open.',
    },
    {
        name: 'tadawul-close',
        schedule: '0 15 * * 0-4',
        message: 'Tadawul market closed for the day. Brief recap of TASI movement. Skip if no portfolio facts.',
    },
    {
        name: 'overdue-invoice-scan',
        schedule: '0 9 * * 1-5',
        message: 'Scan zatca_invoices and customer accounts for invoices >30 days overdue. For each one: queue a polite Arabic dunning message draft as an escalation (NOT auto-sent). Stop after 5.',
    },
    {
        name: 'hot-lead-followup',
        schedule: '0 11 * * 1-5',
        message: 'From customer_memories and recent messages, identify leads that went quiet 4-7 days ago. Draft a short re-engagement message for each (max 3) as escalations.',
    },
    {
        name: 'agenda-checkin',
        schedule: '0 12 * * 1-6',
        message: 'Check agenda goals. For any goal with deadline <72h and progress <80%, raise it. For any goal achieved overnight, celebrate it. Keep it tight.',
    },
    {
        name: 'compliance-radar',
        schedule: '0 9 * * 1',
        message: 'Saudi compliance radar: check ZATCA filing windows, GOSI cycles, and Nitaqat color status. If anything is due in <14 days, surface as a nudge. Quiet otherwise.',
    },
    // ─── v1.1 daily-task expansion: deeper proactive coverage ─────────
    // These are conversational cron jobs (the agent runs the actual work
    // through brain prompts + tools), not system events. Each one cites
    // which skills it's drawing on so the owner sees the agent's thinking.
    {
        name: 'customer-followup-suggestions',
        schedule: '0 12 * * 1-5',
        message: 'Suggest 3 customer follow-ups for today. For each: who, why now (silent for N days, last touchpoint context), recommended message tone, channel. Cite which skills you used (follow-up, qualify-lead, customer-segmentation). Auto-allow recipients via add_allowed_contact only after I approve. Limit: 3.',
    },
    {
        name: 'competitor-news-scan',
        schedule: '0 19 * * 0-4',
        message: 'Pull today\'s news/blog posts from competitors stored in identity_facts (key prefix competitor.*). For each, summarize what changed, threat level, and one strategic counter-move. Cite the competitive-intel skill. Skip if no competitors registered.',
    },
    {
        name: 'industry-trend-scan',
        schedule: '0 10 * * 6',
        message: 'Saturday weekly industry scan: search for top 3 trends affecting our business this week. Pull from Saudi business news + Vision 2030 announcements + sector reports. Cite the seo-content / financial-modeling / saudi-government-services skills as relevant. Output 1-page summary with action items.',
    },
    {
        name: 'seo-position-check',
        schedule: '0 11 * * 1',
        message: 'Monday SEO check: query Google for our top 5 target keywords (from identity_facts.seo.keywords or business.story). Note rank changes vs last week if we have data. Suggest one quick-win content gap to fill this week. Cite seo-content skill.',
    },
    {
        name: 'vendor-pricing-changes',
        schedule: '0 9 1 * *',
        message: 'Monthly vendor scan: check identity_facts vendor.* entries for the suppliers I have on file. Visit their pricing pages, flag any changes vs the stored values, recommend renegotiation if increase >10%. Cite the procurement-rfq skill.',
    },
    {
        name: 'business-health-check',
        schedule: '0 20 * * 0',
        message: 'Sunday evening business health check: pull last 7 days of journal entries, agenda progress, escalation backlog, AR aging if known. Score: green/yellow/red on each of (sales velocity, response SLA, agenda burn-down, compliance, cash). One sentence per dimension + one focused recommendation for the upcoming week. Cite financial-modeling + autonomy-self skills.',
    },
    {
        name: 'customer-segment-analysis',
        schedule: '0 10 1 * *',
        message: 'Monthly customer segmentation: pull customers from DB, segment by recency × frequency × monetary (RFM) if signals exist. Identify the 5 customers needing attention this month + 5 we should thank publicly. Cite the customer-segmentation skill (in social-media-content).',
    },
    {
        name: 'skill-library-review',
        schedule: '0 8 * * 6',
        message: 'Saturday morning: review the skills discovered overnight (kg_entities where kind=skill, install_status=discovered). Pick top 1-2 worth installing based on this business\'s context. Surface them via escalate_decision (recommendation: install, counterargument: maintenance overhead, alternatives: bookmark for later). Skip if no new candidates.',
    },
];
const INTERNAL_SYSTEM_JOBS = [
    {
        name: 'research-cycle',
        schedule: '30 */3 * * *', // every 3h (offset 30min from skill-discovery)
        sentinel: '__autonomy:research-cycle__',
    },
    {
        // Daily 4am KSA — searches GitHub for new skills/improvements relevant to
        // this business and stores top candidates as kg_entities (kind=skill).
        // The Saturday 'skill-library-review' agent job then triages the queue.
        name: 'skill-discovery',
        schedule: '0 */3 * * *', // every 3h — continuous self-improvement, low cost
        sentinel: '__autonomy:skill-discovery__',
    },
    {
        // Autonomous council meetings — every 3h (offset). Cheaply decides whether
        // any subject warrants a meeting; only convenes when it genuinely does, so
        // it's near-zero cost most cycles. Convened meetings show LIVE in the Office
        // meeting room and the outcome is reported to the owner's channel.
        name: 'autonomous-meeting',
        schedule: '45 */3 * * *',
        sentinel: '__autonomy:meeting__',
    },
    {
        // Hourly heartbeat. Self-throttled (max 1/h, max 5/day, owner can disable).
        // Detects: stale critical escalations, goal deadlines <24h, completed
        // subagents while owner was away, >8h silence. Delivers via WhatsApp.
        name: 'heartbeat',
        schedule: '0 * * * *',
        sentinel: '__autonomy:heartbeat__',
    },
    {
        // Daily reflection at 23:00 KSA. Reads last 24h failures/escalations, asks
        // the LLM to extract concrete lessons, attaches them to the relevant
        // skill rows in the KG. Tomorrow's prompts auto-include yesterday's
        // lessons. This is what makes Muassis a learning employee, not a static
        // chatbot. Costs ~1 reasoning-tier LLM call per day.
        name: 'reflection',
        schedule: '0 */3 * * *', // every 3h — learn lessons + patterns continuously
        sentinel: '__autonomy:reflection__',
    },
    {
        // (Round-3) Daily housekeeping at 02:30 — purges stale rows from
        // approval_requests, escalation_queue, delivery_queue, cron_executions,
        // subagent_runs, kg_episodes (>90 days). Without this, tables grow
        // unbounded over months/years. Cheap operation; runs in <1s typically.
        name: 'housekeeping',
        schedule: '30 2 * * *',
        sentinel: '__autonomy:housekeeping__',
    },
    {
        // (Round-3) Hash-chain integrity check at 04:00 weekly Saturday. Runs
        // verifyChain() on the journal and alerts the owner if any tampering is
        // detected. Cheap on bounded recent history; expensive at full table
        // scan, so we cap to last 30 days.
        name: 'audit-verify',
        schedule: '0 4 * * 6',
        sentinel: '__autonomy:audit-verify__',
    },
    {
        // (Round-4) Proactive actions during business hours. Every 90 min Sun-Thu
        // 9am-5pm KSA: scans for silent hot leads, near-deadline goals, made
        // promises. For each, drafts an action and routes through policy:
        // 'auto' → executes silently (only for owner-facing types like
        // deadline_check). 'ask' → escalates with draft attached for approval.
        // 'never' → skips. This is what makes Muassis ACT, not just announce.
        name: 'proactive-actions',
        schedule: '0,30 9-17 * * 0-4',
        sentinel: '__autonomy:proactive-actions__',
    },
    {
        // (Round-4) Daily scorecard at 18:00 KSA. Quantifies value delivered
        // ("Today I did X actions, saved you ~Y hours"). The piece that makes
        // the agent feel indispensable instead of invisible.
        name: 'daily-scorecard',
        schedule: '0 18 * * *',
        sentinel: '__autonomy:scorecard__',
    },
    {
        // (Round-6) Daily delivery-health check at 09:00 KSA. Detects when
        // WhatsApp has silently disconnected (Baileys session expired, device
        // offline) — alerts the owner via fallback email so daily scorecard +
        // proactive notifications don't disappear without explanation.
        name: 'delivery-health',
        schedule: '0 9 * * *',
        sentinel: '__autonomy:delivery-health__',
    },
    {
        // (v3.0.5) Aggressive goal pursuit — every 30 minutes during waking
        // hours, pick the most-stale active goal with auto_execute=true and
        // make the agent take the next concrete step. Goals that languish in
        // agenda_goals get actually MOVED FORWARD instead of just being
        // listed. Honors per-goal cooldown (30 min) and consecutive-failure
        // escalation (after 3 fails in a row, stops auto-pursuing until the
        // owner unblocks). The brain handles the actual work via tools +
        // subagents; this cron just keeps the agent honest about its
        // backlog.
        name: 'goal-pursuit',
        schedule: '*/30 7-23 * * *',
        sentinel: '__autonomy:goal-pursuit__',
    },
    {
        // (v3.1) Generative initiative cycle — 3x on working days (Sun-Thu KSA).
        // Snapshots real business state, asks the brain what beneficial actions to
        // take now, auto-executes reversible ones (act-then-tell) and escalates the
        // rest. Offset from proactive/goal-pursuit cadences to avoid LLM contention.
        // NOTE: like all clock-time crons this only fires while the app is awake —
        // run-on-launch coverage for a sleeping desktop is the next build.
        name: 'initiative',
        schedule: '0 */2 * * *', // every 2h — departments think + act for the business
        sentinel: '__autonomy:initiative__',
    },
    {
        // (v3.1) Lead discovery — twice weekly (Sun + Wed 11:00 KSA). Derives the
        // ICP from the business profile, searches the public web for matching
        // businesses, classifies lawful basis, dedupes, stores as leads. Never
        // contacts anyone — the initiative engine surfaces each as a cold-outreach
        // approval. Run-on-launch does NOT catch this up (it's a hunt, not a daily
        // must-do; firing it on every cold boot would over-search).
        name: 'lead-discovery',
        schedule: '0 11 * * 0,3',
        sentinel: '__autonomy:lead-discovery__',
    },
    {
        // (v3.1) Daily 03:20 — derive KG relationships from real tool usage in the
        // journal (goal→tool, tool↔tool co-occurrence) so the knowledge graph
        // reflects how entities actually connect, not just that they exist.
        name: 'kg-relink',
        schedule: '20 3 * * *',
        sentinel: '__autonomy:kg-relink__',
    },
];
const DEFAULT_POLICIES = [
    // Auto — clearly safe day-to-day actions
    { policy_key: 'send_email', description: 'Send an email to a customer or supplier', mode: 'auto', threshold_amount: undefined },
    { policy_key: 'send_whatsapp', description: 'Send a WhatsApp reply', mode: 'auto', threshold_amount: undefined },
    { policy_key: 'auto_reply_faq', description: 'Auto-reply to FAQ-class customer questions', mode: 'auto', threshold_amount: undefined },
    { policy_key: 'schedule_meeting', description: 'Book a meeting on the calendar', mode: 'auto', threshold_amount: undefined },
    { policy_key: 'create_note', description: 'Create a note', mode: 'auto', threshold_amount: undefined },
    { policy_key: 'add_reminder', description: 'Add a reminder', mode: 'auto', threshold_amount: undefined },
    { policy_key: 'web_search', description: 'Search the web for information', mode: 'auto', threshold_amount: undefined },
    { policy_key: 'log_lead', description: 'Log a new lead in the CRM', mode: 'auto', threshold_amount: undefined },
    { policy_key: 'qualify_lead', description: 'Score and qualify an inbound lead', mode: 'auto', threshold_amount: undefined },
    // Auto with a money cap — quotes/refunds need an approval threshold
    { policy_key: 'send_quote', description: 'Send a price quote to a prospect', mode: 'auto', threshold_amount: 5000 },
    { policy_key: 'issue_invoice', description: 'Issue a ZATCA-compliant invoice', mode: 'auto', threshold_amount: 5000 },
    // Ask — strategy / customer-facing decisions
    { policy_key: 'post_social', description: 'Post on owner-controlled social accounts', mode: 'ask' },
    { policy_key: 'cold_outreach', description: 'Run a cold outbound sequence to new leads', mode: 'ask' },
    // Hard gates — money, contracts, people decisions
    { policy_key: 'pay_supplier', description: 'Pay a supplier or invoice', mode: 'ask' },
    { policy_key: 'refund_customer', description: 'Refund a customer', mode: 'ask' },
    { policy_key: 'sign_contract', description: 'Sign a contract or NDA on owner\'s behalf', mode: 'ask' },
    { policy_key: 'commit_purchase', description: 'Commit a purchase order', mode: 'ask' },
    { policy_key: 'hire_or_fire', description: 'Hiring or termination decisions', mode: 'ask' },
    { policy_key: 'change_pricing', description: 'Change product pricing publicly', mode: 'ask' },
    { policy_key: 'public_statement', description: 'Make a public statement on behalf of the company', mode: 'ask' },
    { policy_key: 'transfer_funds', description: 'Move money between accounts', mode: 'ask' },
    { policy_key: 'delete_data', description: 'Delete customer or business data', mode: 'ask' },
    // Inbound access — what to do when an unknown number/handle DMs the agent.
    // Default 'auto': reply on first contact + auto-add the sender so the
    // conversation flows. Without this, every new customer would silently
    // queue an "approve this stranger" escalation and the agent never replies
    // — which is exactly what was happening when the owner tested their own
    // number. Customer-service shops are the target persona; "0-friction
    // engagement" is the value prop. Owners who want manual approval can
    // flip to 'ask' from Settings → Policies.
    { policy_key: 'inbound_access', description: 'Behavior for unknown inbound DMs (auto / ask / never)', mode: 'auto' },
];
export async function seedAutonomyDefaults(opts) {
    const flag = await getFact(opts.tenantId, SEED_FLAG_KEY);
    const isFreshSeed = !flag || opts.force;
    // Cron jobs are idempotent by name, so always run the cron seeder. New
    // versions add new default jobs (like research-cycle) that existing
    // installs need without re-running the full wizard.
    await seedCronJobs(opts.tenantId, opts.ownerPhone ?? null);
    if (!isFreshSeed) {
        return { seeded: false, reason: `already seeded at ${flag.fact_value} (cron jobs reconciled)` };
    }
    log.info({ tenantId: opts.tenantId, force: !!opts.force }, 'seeding autonomy defaults (full)');
    // 2. Policies
    for (const p of DEFAULT_POLICIES) {
        try {
            await upsertPolicy(opts.tenantId, {
                policy_key: p.policy_key,
                description: p.description,
                mode: p.mode,
                threshold_amount: p.threshold_amount,
            });
        }
        catch (err) {
            log.warn({ key: p.policy_key, error: err.message }, 'policy seed failed');
        }
    }
    // 3. Starter goal — gives the agenda card content on day one
    const existingGoals = await listActiveGoals(opts.tenantId);
    if (existingGoals.length === 0) {
        try {
            await createGoal(opts.tenantId, {
                title: 'Stay on top of business',
                description: 'Daily morning brief read, escalations resolved within 24h, no overdue invoices >7 days.',
                category: 'general',
                priority: 1,
            });
        }
        catch (err) {
            log.warn({ error: err.message }, 'starter goal seed failed');
        }
    }
    // 4. Identity flag — so we don't re-seed
    await upsertFact(opts.tenantId, SEED_FLAG_KEY, new Date().toISOString(), 'business', 'inferred');
    return { seeded: true, reason: 'fresh seed complete' };
}
async function seedCronJobs(tenantId, ownerPhone) {
    const { data: existingJobs } = await supabase
        .from('cron_jobs')
        .select('name')
        .eq('tenant_id', tenantId);
    const existingNames = new Set((existingJobs ?? []).map(j => j.name));
    // (H8) Resolve tenant timezone from identity_facts before seeding crons.
    // Fallback to tenant.timezone, then to Asia/Riyadh. Owner can override
    // by setting owner.timezone fact.
    let tz = 'Asia/Riyadh';
    try {
        const ownerTz = await getFact(tenantId, 'owner.timezone');
        if (ownerTz?.fact_value) {
            tz = ownerTz.fact_value;
        }
        else {
            const { data: tenant } = await supabase
                .from('tenants')
                .select('timezone')
                .eq('id', tenantId)
                .maybeSingle();
            if (tenant?.timezone) {
                tz = tenant.timezone;
            }
        }
    }
    catch { /* keep default */ }
    const now = new Date();
    // (Round-3 audit) Default-disable jobs that only make sense once the tenant
    // configures the relevant context. Without this, a bakery owner gets daily
    // Tadawul stock alerts they never asked for.
    const CONTEXT_DEPENDENT = {
        'tadawul-open': async () => {
            const r = await supabase.from('identity_facts').select('fact_key').eq('tenant_id', tenantId).like('fact_key', 'portfolio.%').limit(1);
            return (r.data ?? []).length > 0;
        },
        'tadawul-close': async () => {
            const r = await supabase.from('identity_facts').select('fact_key').eq('tenant_id', tenantId).like('fact_key', 'portfolio.%').limit(1);
            return (r.data ?? []).length > 0;
        },
        'competitor-news-scan': async () => {
            const r = await supabase.from('identity_facts').select('fact_key').eq('tenant_id', tenantId).like('fact_key', 'competitor.%').limit(1);
            return (r.data ?? []).length > 0;
        },
        'vendor-pricing-changes': async () => {
            const r = await supabase.from('identity_facts').select('fact_key').eq('tenant_id', tenantId).like('fact_key', 'vendor.%').limit(1);
            return (r.data ?? []).length > 0;
        },
        'seo-position-check': async () => {
            const r = await supabase.from('identity_facts').select('fact_key').eq('tenant_id', tenantId).like('fact_key', 'seo.%').limit(1);
            return (r.data ?? []).length > 0;
        },
    };
    for (const job of DEFAULT_CRON_JOBS) {
        if (existingNames.has(job.name))
            continue;
        let enabled = true;
        if (CONTEXT_DEPENDENT[job.name]) {
            try {
                enabled = await CONTEXT_DEPENDENT[job.name]();
                if (!enabled) {
                    log.info({ job: job.name }, 'context-dependent cron seeded as disabled (will auto-enable when relevant facts appear)');
                }
            }
            catch {
                enabled = false;
            }
        }
        const row = {
            id: randomUUID(),
            tenant_id: tenantId,
            name: job.name,
            schedule: job.schedule,
            timezone: tz,
            payload: {
                kind: 'agentTurn',
                message: job.message,
            },
            delivery: ownerPhone
                ? { mode: 'announce', channel: 'whatsapp', to: ownerPhone, bestEffort: true }
                : { mode: 'announce', channel: 'webchat', bestEffort: true },
            enabled,
            last_run_at: null,
            next_run_at: now.toISOString(),
            run_count: 0,
            created_at: now.toISOString(),
            updated_at: now.toISOString(),
        };
        const { error } = await supabase.from('cron_jobs').insert(row);
        if (error) {
            log.warn({ name: job.name, error: error.message }, 'cron seed failed');
        }
        else {
            log.info({ name: job.name, schedule: job.schedule, enabled }, 'cron seeded');
        }
    }
    for (const job of INTERNAL_SYSTEM_JOBS) {
        if (existingNames.has(job.name))
            continue;
        const row = {
            id: randomUUID(),
            tenant_id: tenantId,
            name: job.name,
            schedule: job.schedule,
            timezone: tz,
            payload: { kind: 'systemEvent', text: job.sentinel },
            delivery: { mode: 'none', bestEffort: true },
            enabled: true,
            last_run_at: null,
            next_run_at: now.toISOString(),
            run_count: 0,
            created_at: now.toISOString(),
            updated_at: now.toISOString(),
        };
        const { error } = await supabase.from('cron_jobs').insert(row);
        if (error) {
            log.warn({ name: job.name, error: error.message }, 'autonomy cron seed failed');
        }
        else {
            log.info({ name: job.name, schedule: job.schedule }, 'autonomy cron seeded');
        }
    }
}
//# sourceMappingURL=seeder.js.map