/**
 * src/autonomy/lead-discovery.ts
 *
 * The "hunt" capability — autonomous lead discovery. This module POPULATES
 * leads; it never contacts anyone. Contact is 100% the initiative engine's job,
 * routed through the policy plane. That separation is what keeps it PDPL-safe:
 *
 *   • Discovery = reading public web + storing structured candidates. Reversible,
 *     internal → runs autonomously.
 *   • Every freshly-discovered lead is FIRST-TOUCH, so the initiative engine
 *     routes it to `initiative_cold_outreach` = ASK. "Warm=auto" applies ONLY to
 *     contacts with a prior relationship — NEVER to a scraped/discovered number.
 *
 * Flow: derive the ICP from business identity → generate targeted search queries
 * (LLM) → web_search → extract + lawful-basis-classify structured leads (LLM,
 * json_object) → dedupe against existing customers → insert as status='lead'.
 * The owner then sees them surfaced as cold-outreach approvals when the
 * initiative engine next runs.
 */
import { randomUUID } from 'node:crypto';
import { z } from 'zod';
import { createChildLogger } from '../utils/logger.js';
import { supabase } from '../utils/supabase.js';
import { callWithFailover } from '../models/failover.js';
import { pickModelOverride } from '../models/router.js';
import { listFacts } from './identity.js';
import { record as journalRecord } from './journal.js';
const log = createChildLogger('autonomy:lead-discovery');
const leadSchema = z.object({
    business_name: z.string().min(1).max(160),
    contact_name: z.string().max(120).optional().default(''),
    phone: z.string().max(40).optional().default(''),
    source_url: z.string().max(400).optional().default(''),
    context: z.string().max(400).optional().default(''),
    // Lawful-basis SIGNAL only — informs the owner; it never auto-enables sending.
    lawful_basis: z.enum(['legitimate_business_interest', 'unknown', 'personal_no_basis']).optional().default('unknown'),
});
const queriesSchema = z.object({ queries: z.array(z.string().min(3).max(160)).max(5).default([]) });
const leadsSchema = z.object({ leads: z.array(leadSchema).max(25).default([]) });
function extractJson(raw) {
    const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
    if (fenced?.[1])
        return fenced[1].trim();
    const start = raw.indexOf('{');
    const end = raw.lastIndexOf('}');
    if (start >= 0 && end > start)
        return raw.slice(start, end + 1);
    return raw.trim();
}
/**
 * Build the ideal-customer-profile string. The business profile lives in the
 * `tenants` row (business_name + settings JSON {industry, city}) from the
 * onboarding wizard — NOT in identity_facts — so we read there first, then
 * layer any richer facts (product/target/description) on top.
 */
async function deriveICP(tenantId) {
    let name = '';
    let nameAr = '';
    let industry = '';
    let city = '';
    let persona = '';
    try {
        const { data } = await supabase
            .from('tenants')
            .select('business_name, business_name_ar, agent_persona, settings, metadata')
            .eq('id', tenantId)
            .maybeSingle();
        if (data) {
            const t = data;
            name = t.business_name ?? '';
            nameAr = t.business_name_ar ?? '';
            persona = t.agent_persona ?? '';
            const parseJson = (v) => {
                if (!v)
                    return {};
                if (typeof v === 'object')
                    return v;
                try {
                    return JSON.parse(String(v));
                }
                catch {
                    return {};
                }
            };
            const settings = parseJson(t.settings);
            const metadata = parseJson(t.metadata);
            industry = String(settings.industry ?? '');
            city = String(settings.city ?? '');
            // "other" industry stores a free-form description during onboarding.
            if (industry === 'other')
                industry = String(metadata.custom_description ?? settings.custom_description ?? 'other') || 'other';
        }
    }
    catch (err) {
        log.debug({ tenantId, err: err.message }, 'tenant profile read failed for ICP');
    }
    // Layer richer detail from identity facts if the owner recorded any.
    const facts = await listFacts(tenantId).catch(() => []);
    const pick = (k) => facts.find((f) => f.fact_key === k)?.fact_value;
    const product = pick('business.product') ?? pick('business.description') ?? '';
    const target = pick('business.target_customer') ?? pick('customer.profile') ?? '';
    const parts = [
        (name || nameAr) && `العمل: ${[name, nameAr].filter(Boolean).join(' / ')}`,
        industry && industry !== 'other' && `المجال: ${industry}`,
        industry === 'other' && persona && `الوصف: ${persona.slice(0, 200)}`,
        city && `المدينة: ${city}`,
        product && `المنتج/الخدمة: ${product}`,
        target && `العميل المستهدف: ${target}`,
    ].filter(Boolean);
    return parts.join(' · ');
}
async function generateQueries(tenantId, icp) {
    const messages = [
        {
            role: 'system',
            content: 'أنت خبير تطوير أعمال سعودي. مهمتك توليد استعلامات بحث ويب دقيقة للعثور على عملاء محتملين (شركات/جهات) تناسب ملف العمل. ' +
                'ركّز على جهات أعمال عامة لها حضور إلكتروني (دلائل أعمال، مواقع، صفحات شركات) — لا أفراد. ' +
                'أعد JSON صرف: {"queries":["...","...","..."]} (٣ استعلامات كحد أقصى).',
        },
        { role: 'user', content: `ملف العمل:\n${icp || '(غير محدد — استنتج من السياق العام لعمل سعودي صغير)'}\n\nولّد استعلامات البحث الآن.` },
    ];
    try {
        const res = await callWithFailover('reasoning', messages, { responseFormat: { type: 'json_object' }, maxTokens: 400, timeoutMs: 40_000 }, tenantId, pickModelOverride('planning'));
        const parsed = queriesSchema.parse(JSON.parse(extractJson(res.content ?? '')));
        return parsed.queries.slice(0, 3);
    }
    catch (err) {
        log.warn({ tenantId, err: err.message }, 'query generation failed');
        return [];
    }
}
async function searchLeadSources(tenantId, queries) {
    const { executeTool } = await import('../tools/executor.js');
    const blocks = [];
    for (const q of queries) {
        try {
            const sr = await executeTool('web_search', { query: q, count: 6 }, tenantId);
            if (sr.success && sr.output)
                blocks.push(`### نتائج: ${q}\n${String(sr.output).slice(0, 4000)}`);
        }
        catch (err) {
            log.debug({ q, err: err.message }, 'lead search query failed');
        }
    }
    return blocks.join('\n\n');
}
async function extractLeads(tenantId, icp, searchBlob) {
    if (!searchBlob.trim())
        return [];
    const messages = [
        {
            role: 'system',
            content: 'أنت محلل تطوير أعمال. من نتائج البحث، استخرج عملاء محتملين (جهات أعمال) تناسب ملف العمل. ' +
                'لكل جهة: الاسم، جهة الاتصال إن وُجدت، الهاتف إن ظهر علناً، رابط المصدر، سياق قصير لماذا تناسب. ' +
                'صنّف الأساس النظامي (lawful_basis): "legitimate_business_interest" لجهة أعمال عامة ببيانات تواصل تجارية منشورة؛ ' +
                '"personal_no_basis" لبيانات شخص طبيعي بلا سياق تجاري؛ "unknown" عند الشك. ' +
                'لا تختلق بيانات — اترك الحقل فارغاً إن لم يظهر. أعد JSON صرف: ' +
                '{"leads":[{"business_name":"","contact_name":"","phone":"","source_url":"","context":"","lawful_basis":"legitimate_business_interest|unknown|personal_no_basis"}]}',
        },
        { role: 'user', content: `ملف العمل:\n${icp}\n\n## نتائج البحث\n${searchBlob.slice(0, 12000)}\n\nاستخرج العملاء المحتملين الآن. JSON فقط.` },
    ];
    try {
        const res = await callWithFailover('reasoning', messages, { responseFormat: { type: 'json_object' }, maxTokens: 2500, timeoutMs: 60_000 }, tenantId, pickModelOverride('planning'));
        return leadsSchema.parse(JSON.parse(extractJson(res.content ?? ''))).leads;
    }
    catch (err) {
        log.warn({ tenantId, err: err.message }, 'lead extraction failed');
        return [];
    }
}
const normalize = (s) => s.toLowerCase().replace(/\s+/g, ' ').trim();
export async function runLeadDiscovery(tenantId) {
    const result = { icp: '', queriesRun: 0, candidates: 0, inserted: 0, duplicates: 0, byBasis: {} };
    const icp = await deriveICP(tenantId);
    result.icp = icp;
    const queries = await generateQueries(tenantId, icp);
    if (queries.length === 0) {
        result.reason = 'could not derive search queries (thin business profile)';
        return result;
    }
    result.queriesRun = queries.length;
    const searchBlob = await searchLeadSources(tenantId, queries);
    const leads = await extractLeads(tenantId, icp, searchBlob);
    result.candidates = leads.length;
    if (leads.length === 0) {
        result.reason = 'no candidate leads extracted from search results';
        return result;
    }
    // Dedupe against existing customers (by phone or normalized business name).
    const { data: existing } = await supabase
        .from('customers')
        .select('phone, name, metadata')
        .eq('tenant_id', tenantId)
        .limit(2000);
    const existingPhones = new Set((existing ?? []).map((c) => (c.phone || '').replace(/\D/g, '')).filter(Boolean));
    const existingNames = new Set((existing ?? []).map((c) => normalize(c.name || '')).filter(Boolean));
    const nowIso = new Date().toISOString();
    for (const lead of leads) {
        const phoneDigits = (lead.phone || '').replace(/\D/g, '');
        const nameNorm = normalize(lead.business_name);
        if ((phoneDigits && existingPhones.has(phoneDigits)) || (nameNorm && existingNames.has(nameNorm))) {
            result.duplicates++;
            continue;
        }
        // Every discovered lead is first-touch → tagged so the initiative engine
        // routes it to cold_outreach=ASK. We never tag it 'warm'.
        const tags = ['discovered', `basis:${lead.lawful_basis}`];
        // The customers table has UNIQUE(tenant_id, phone); a discovered lead often
        // has no phone yet. Using '' would collide across every phoneless lead (so
        // only the first would persist). Use a unique sentinel instead — outreach
        // is gated regardless and the real contact lives in metadata.
        const phoneValue = lead.phone || `pending:${randomUUID().slice(0, 12)}`;
        try {
            const { error } = await supabase.from('customers').insert({
                id: randomUUID(),
                tenant_id: tenantId,
                phone: phoneValue,
                name: lead.business_name,
                language: 'ar',
                tags: JSON.stringify(tags),
                first_contact: nowIso,
                last_contact: nowIso,
                total_messages: 0,
                status: 'lead',
                metadata: JSON.stringify({
                    source: 'lead-discovery',
                    source_url: lead.source_url,
                    lawful_basis: lead.lawful_basis,
                    context: lead.context,
                    contact_name: lead.contact_name,
                    phone_known: Boolean(lead.phone),
                    discovered_at: nowIso,
                    tags, // mirror for scanners that read metadata.tags
                }),
            });
            // supabase-style clients return { error } instead of throwing — the
            // previous code ignored it and counted failed inserts as successes.
            if (error) {
                log.warn({ tenantId, business: lead.business_name, err: error.message }, 'lead insert rejected');
                continue;
            }
            result.inserted++;
            result.byBasis[lead.lawful_basis] = (result.byBasis[lead.lawful_basis] ?? 0) + 1;
            existingNames.add(nameNorm);
            if (phoneDigits)
                existingPhones.add(phoneDigits);
        }
        catch (err) {
            log.warn({ tenantId, business: lead.business_name, err: err.message }, 'lead insert failed');
        }
    }
    await journalRecord({
        tenantId,
        kind: 'lead_discovery',
        actor: 'lead-discovery',
        action: 'discover_leads',
        outcome: 'success',
        reason: `Discovered ${result.inserted} new leads (${result.duplicates} dupes) across ${result.queriesRun} queries`,
        metadata: { byBasis: result.byBasis, candidates: result.candidates },
    }).catch(() => undefined);
    log.info({ tenantId, ...result, icp: undefined }, 'lead discovery complete');
    return result;
}
//# sourceMappingURL=lead-discovery.js.map