     1→const express = require('express');
     2→const router = express.Router();
     3→const multer = require('multer');
     4→const OpenAI = require('openai');
     5→const { supabase } = require('../config/database');
     6→const aiService = require('../services/ai');
     7→const deepgramService = require('../services/deepgram');
     8→const { recordSuggestionFeedback, getEffectiveResponses, getSimilarWins } = require('../services/learning');
     9→const perplexityService = require('../services/perplexity');
    10→const { expensiveAiLimiter, strictLimiter } = require('../middleware/rateLimiter');
    11→
    12→// Initialize OpenAI for voice parsing
    13→let openai = null;
    14→if (process.env.OPENAI_API_KEY) {
    15→  openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
    16→}
    17→
    18→// Configure multer for audio uploads
    19→const upload = multer({
    20→  storage: multer.memoryStorage(),
    21→  limits: { fileSize: 25 * 1024 * 1024 }, // 25MB max
    22→});
    23→
    24→/**
    25→ * GET /api/ai/status
    26→ * Simple status check to verify AI routes are loaded
    27→ */
    28→router.get('/status', (req, res) => {
    29→  res.json({
    30→    success: true,
    31→    message: 'AI routes loaded',
    32→    openaiConfigured: !!process.env.OPENAI_API_KEY,
    33→    deepgramConfigured: !!process.env.DEEPGRAM_API_KEY,
    34→  });
    35→});
    36→
    37→/**
    38→ * GET /api/ai/intel-brief/:leadId
    39→ * Generate pre-call intelligence brief
    40→ * Cache: 2 hours (unless ?refresh=true)
    41→ */
    42→router.get('/intel-brief/:leadId', async (req, res) => {
    43→  try {
    44→    const { leadId } = req.params;
    45→    const forceRefresh = req.query.refresh === 'true';
    46→    const CACHE_HOURS = 2;
    47→
    48→    // Get lead data
    49→    const { data: lead, error: leadError } = await supabase
    50→      .from('leads')
    51→      .select('*')
    52→      .eq('id', leadId)
    53→      .single();
    54→
    55→    if (leadError || !lead) {
    56→      return res.status(404).json({ error: 'Lead not found' });
    57→    }
    58→
    59→    // Check cache: use existing brief if recent enough
    60→    if (!forceRefresh && lead.ai_intel_brief && lead.ai_last_analyzed_at) {
    61→      const cacheAge = Date.now() - new Date(lead.ai_last_analyzed_at).getTime();
    62→      const cacheHours = cacheAge / (1000 * 60 * 60);
    63→
    64→      if (cacheHours < CACHE_HOURS) {
    65→        console.log(`[Intel Brief] Using cached brief (${cacheHours.toFixed(1)}h old) for lead ${leadId}`);
    66→        return res.json({
    67→          success: true,
    68→          brief: lead.ai_intel_brief,
    69→          cached: true,
    70→          cacheAge: Math.round(cacheHours * 60), // minutes
    71→          lead: {
    72→            id: lead.id,
    73→            name: `${lead.first_name} ${lead.last_name || ''}`.trim(),
    74→            company: lead.company,
    75→            score: lead.score,
    76→          },
    77→        });
    78→      }
    79→    }
    80→
    81→    console.log(`[Intel Brief] Generating fresh brief for lead ${leadId}`);
    82→
    83→    // Get communications history
    84→    const { data: communications } = await supabase
    85→      .from('communications')
    86→      .select('*')
    87→      .eq('lead_id', leadId)
    88→      .order('created_at', { ascending: false })
    89→      .limit(20);
    90→
    91→    // Find similar wins
    92→    const { similarWins } = await aiService.findSimilarWins(lead);
    93→
    94→    // Generate brief
    95→    const result = await aiService.generateIntelBrief(
    96→      lead,
    97→      communications || [],
    98→      similarWins || []
    99→    );
   100→
   101→    if (!result.success) {
   102→      return res.status(500).json({ error: result.error });
   103→    }
   104→
   105→    // Store brief on lead record
   106→    const { error: updateError } = await supabase
   107→      .from('leads')
   108→      .update({
   109→        ai_intel_brief: result.brief,
   110→        ai_last_analyzed_at: new Date().toISOString(),
   111→      })
   112→      .eq('id', leadId);
   113→
   114→    if (updateError) {
   115→      console.warn('Failed to store AI brief on lead:', updateError.message);
   116→    }
   117→
   118→    res.json({
   119→      success: true,
   120→      brief: result.brief,
   121→      cached: false,
   122→      lead: {
   123→        id: lead.id,
   124→        name: `${lead.first_name} ${lead.last_name || ''}`.trim(),
   125→        company: lead.company,
   126→        score: lead.score,
   127→      },
   128→    });
   129→  } catch (error) {
   130→    console.error('Intel brief error:', error);
   131→    res.status(500).json({ error: 'Failed to generate intel brief' });
   132→  }
   133→});
   134→
   135→/**
   136→ * POST /api/ai/post-call-summary
   137→ * Process call transcript/notes into structured summary
   138→ */
   139→router.post('/post-call-summary', async (req, res) => {
   140→  try {
   141→    const { leadId, transcript, notes, callDuration } = req.body;
   142→
   143→    if (!leadId || (!transcript && !notes)) {
   144→      return res.status(400).json({ error: 'leadId and transcript or notes required' });
   145→    }
   146→
   147→    // Get lead data
   148→    const { data: lead, error: leadError } = await supabase
   149→      .from('leads')
   150→      .select('*')
   151→      .eq('id', leadId)
   152→      .single();
   153→
   154→    if (leadError || !lead) {
   155→      return res.status(404).json({ error: 'Lead not found' });
   156→    }
   157→
   158→    // Generate summary
   159→    const result = await aiService.generatePostCallSummary(
   160→      lead,
   161→      transcript || notes,
   162→      callDuration || 0
   163→    );
   164→
   165→    if (!result.success) {
   166→      return res.status(500).json({ error: result.error });
   167→    }
   168→
   169→    const summary = result.summary;
   170→
   171→    // Update lead with AI recommendations
   172→    const updates = {
   173→      updated_at: new Date().toISOString(),
   174→      last_contact_at: new Date().toISOString(),
   175→      last_contact_type: 'call',
   176→    };
   177→
   178→    if (summary.score_adjustment) {
   179→      updates.score = Math.max(0, Math.min(100, (lead.score || 0) + summary.score_adjustment));
   180→    }
   181→
   182→    if (summary.status_recommendation) {
   183→      updates.status = summary.status_recommendation;
   184→    }
   185→
   186→    const { error: leadUpdateError } = await supabase.from('leads').update(updates).eq('id', leadId);
   187→    if (leadUpdateError) {
   188→      console.error('Failed to update lead:', leadUpdateError);
   189→      // Continue - don't fail the whole request for a lead update failure
   190→    }
   191→
   192→    // Log the call
   193→    const { error: commError } = await supabase.from('communications').insert({
   194→      lead_id: leadId,
   195→      type: 'call',
   196→      direction: 'outbound',
   197→      phone: lead.phone,
   198→      content: summary.summary,
   199→      metadata: {
   200→        ai_summary: summary,
   201→        duration: callDuration,
   202→        transcript: transcript?.substring(0, 5000),
   203→      },
   204→    });
   205→    if (commError) {
   206→      console.error('Failed to log call to communications:', commError);
   207→      // This is critical - return error to user so they know call wasn't logged
   208→      return res.status(500).json({ error: 'Failed to log call - please try again' });
   209→    }
   210→
   211→    // Create callback if needed
   212→    if (summary.follow_up_date) {
   213→      const { error: callbackError } = await supabase.from('callbacks').insert({
   214→        lead_id: leadId,
   215→        phone: lead.phone,
   216→        reason: summary.next_step,
   217→        scheduled_at: summary.follow_up_date,
   218→        notes: summary.coaching_note,
   219→        source_type: 'ai_recommendation',
   220→      });
   221→      if (callbackError) {
   222→        console.error('Failed to create callback:', callbackError);
   223→        // Non-critical - log but don't fail
   224→      }
   225→    }
   226→
   227→    res.json({
   228→      success: true,
   229→      summary,
   230→      updates: {
   231→        newScore: updates.score,
   232→        newStatus: updates.status,
   233→        followUpCreated: !!summary.follow_up_date,
   234→      },
   235→    });
   236→  } catch (error) {
   237→    console.error('Post-call summary error:', error);
   238→    res.status(500).json({ error: 'Failed to process call summary' });
   239→  }
   240→});
   241→
   242→/**
   243→ * POST /api/ai/draft-message
   244→ * Generate contextual SMS draft(s)
   245→ * Rate limited: 5 per minute (expensive GPT-4o operation)
   246→ * Body: { leadId, prompt, currentMessage?, tone?, count?, includeContext? }
   247→ */
   248→router.post('/draft-message', expensiveAiLimiter, async (req, res) => {
   249→  try {
   250→    const { leadId, prompt, currentMessage, tone, count = 1, includeContext = true } = req.body;
   251→
   252→    if (!leadId || !prompt) {
   253→      return res.status(400).json({ error: 'leadId and prompt required' });
   254→    }
   255→
   256→    // Get lead data
   257→    const { data: lead, error: leadError } = await supabase
   258→      .from('leads')
   259→      .select('*')
   260→      .eq('id', leadId)
   261→      .single();
   262→
   263→    if (leadError || !lead) {
   264→      return res.status(404).json({ error: 'Lead not found' });
   265→    }
   266→
   267→    // Get recent communications
   268→    const { data: communications } = await supabase
   269→      .from('communications')
   270→      .select('*')
   271→      .eq('lead_id', leadId)
   272→      .order('created_at', { ascending: false })
   273→      .limit(10);
   274→
   275→    // Generate draft(s) with options
   276→    const result = await aiService.draftMessage(
   277→      lead,
   278→      prompt,
   279→      communications || [],
   280→      currentMessage,
   281→      { tone, count: Math.min(count, 5), includeContext }
   282→    );
   283→
   284→    if (!result.success) {
   285→      return res.status(500).json({ error: result.error });
   286→    }
   287→
   288→    // Store primary draft
   289→    const { error: draftError } = await supabase.from('ai_message_drafts').insert({
   290→      lead_id: leadId,
   291→      user_prompt: prompt,
   292→      draft_message: result.draft.message,
   293→      message_type: 'sms',
   294→      tone: result.draft.tone,
   295→      context_used: {
   296→        lead_name: lead.first_name,
   297→        company: lead.company,
   298→        recent_messages: communications?.length || 0,
   299→        tone_requested: tone,
   300→        count_requested: count,
   301→      },
   302→    });
   303→
   304→    if (draftError) {
   305→      console.warn('Failed to store AI draft:', draftError.message);
   306→    }
   307→
   308→    // Return backwards-compatible + new format
   309→    res.json({
   310→      success: true,
   311→      draft: result.draft,
   312→      suggestions: result.suggestions,
   313→      context: result.context,
   314→    });
   315→  } catch (error) {
   316→    console.error('Draft message error:', error);
   317→    res.status(500).json({ error: 'Failed to draft message' });
   318→  }
   319→});
   320→
   321→/**
   322→ * POST /api/ai/draft-similar
   323→ * Find similar leads and generate personalized messages for batch sending
   324→ * Body: { leadId, originalMessage, prompt?, tone? }
   325→ */
   326→router.post('/draft-similar', async (req, res) => {
   327→  try {
   328→    const { leadId, originalMessage, prompt = 'follow up', tone = 'friendly' } = req.body;
   329→
   330→    if (!leadId || !originalMessage) {
   331→      return res.status(400).json({ error: 'leadId and originalMessage required' });
   332→    }
   333→
   334→    // Get original lead
   335→    const { data: lead, error: leadError } = await supabase
   336→      .from('leads')
   337→      .select('*')
   338→      .eq('id', leadId)
   339→      .single();
   340→
   341→    if (leadError || !lead) {
   342→      return res.status(404).json({ error: 'Lead not found' });
   343→    }
   344→
   345→    // Find similar leads
   346→    const similarLeads = await aiService.findSimilarLeads(lead, 5);
   347→
   348→    if (!similarLeads || similarLeads.length === 0) {
   349→      return res.json({
   350→        success: true,
   351→        similarLeads: [],
   352→        message: 'No similar leads found waiting on reply'
   353→      });
   354→    }
   355→
   356→    // Generate personalized messages for each
   357→    const leadsWithMessages = await aiService.generatePersonalizedBatch(
   358→      similarLeads,
   359→      prompt,
   360→      originalMessage,
   361→      tone
   362→    );
   363→
   364→    res.json({
   365→      success: true,
   366→      similarLeads: leadsWithMessages,
   367→      originalLead: {
   368→        id: lead.id,
   369→        name: `${lead.first_name || ''} ${lead.last_name || ''}`.trim()
   370→      }
   371→    });
   372→
   373→  } catch (error) {
   374→    console.error('Draft similar error:', error);
   375→    res.status(500).json({ error: 'Failed to find similar leads' });
   376→  }
   377→});
   378→
   379→/**
   380→ * POST /api/ai/transcribe
   381→ * Transcribe audio using Whisper
   382→ * Rate limited: 5 per minute (expensive operation)
   383→ */
   384→router.post('/transcribe', expensiveAiLimiter, upload.single('audio'), async (req, res) => {
   385→  try {
   386→    if (!req.file) {
   387→      return res.status(400).json({ error: 'Audio file required' });
   388→    }
   389→
   390→    const result = await aiService.transcribeAudio(
   391→      req.file.buffer,
   392→      req.file.originalname || 'audio.webm'
   393→    );
   394→
   395→    if (!result.success) {
   396→      return res.status(500).json({ error: result.error });
   397→    }
   398→
   399→    res.json({
   400→      success: true,
   401→      transcript: result.transcript,
   402→      duration: result.duration,
   403→      segments: result.segments,
   404→    });
   405→  } catch (error) {
   406→    console.error('Transcription error:', error);
   407→    res.status(500).json({ error: 'Failed to transcribe audio' });
   408→  }
   409→});
   410→
   411→/**
   412→ * POST /api/ai/extract-lead-corrections
   413→ * Extract lead corrections from call transcript
   414→ */
   415→router.post('/extract-lead-corrections', async (req, res) => {
   416→  try {
   417→    const { leadId, transcript } = req.body;
   418→
   419→    if (!leadId || !transcript) {
   420→      return res.status(400).json({ error: 'leadId and transcript required' });
   421→    }
   422→
   423→    // Get current lead data
   424→    const { data: lead, error: leadError } = await supabase
   425→      .from('leads')
   426→      .select('*')
   427→      .eq('id', leadId)
   428→      .single();
   429→
   430→    if (leadError || !lead) {
   431→      return res.status(404).json({ error: 'Lead not found' });
   432→    }
   433→
   434→    const result = await aiService.extractLeadCorrections(transcript, lead);
   435→
   436→    res.json({
   437→      success: true,
   438→      corrections: result.corrections || [],
   439→    });
   440→  } catch (error) {
   441→    console.error('Lead correction extraction error:', error);
   442→    res.status(500).json({ error: 'Failed to extract corrections' });
   443→  }
   444→});
   445→
   446→/**
   447→ * POST /api/ai/voice-command
   448→ * Parse and execute voice command
   449→ *
   450→ * Accepts optional leadId for context-aware commands (no need to say lead name)
   451→ */
   452→router.post('/voice-command', async (req, res) => {
   453→  try {
   454→    const { transcript, userEmail, leadId } = req.body;
   455→
   456→    if (!transcript) {
   457→      return res.status(400).json({ error: 'Transcript required' });
   458→    }
   459→
   460→    // Parse the command
   461→    const result = await aiService.parseVoiceCommand(transcript);
   462→
   463→    if (!result.success) {
   464→      return res.status(500).json({ error: result.error });
   465→    }
   466→
   467→    const { parsed } = result;
   468→    let actionResult = null;
   469→    let frontendAction = 'none'; // For UI callbacks: skip, call, open_sms, refresh
   470→
   471→    // Helper to get target lead (from leadId param or by name search)
   472→    const getTargetLead = async (selectFields = 'id, first_name, last_name, phone, score') => {
   473→      // If leadId is provided, use it directly (context-aware)
   474→      if (leadId) {
   475→        const { data } = await supabase
   476→          .from('leads')
   477→          .select(selectFields)
   478→          .eq('id', leadId)
   479→          .single();
   480→        return data;
   481→      }
   482→      // Otherwise search by name if provided
   483→      if (parsed.data.lead_name) {
   484→        const { data: leads } = await supabase
   485→          .from('leads')
   486→          .select(selectFields)
   487→          .or(`first_name.ilike.%${parsed.data.lead_name}%,last_name.ilike.%${parsed.data.lead_name}%`)
   488→          .limit(1);
   489→        return leads?.[0] || null;
   490→      }
   491→      return null;
   492→    };
   493→
   494→    // Execute based on intent
   495→    switch (parsed.intent) {
   496→      // ═══════════════════════════════════════════════════════════════════════
   497→      // NEW: Mark tier (hot/warm/cold) - sets score to threshold
   498→      // ═══════════════════════════════════════════════════════════════════════
   499→      case 'mark_tier': {
   500→        const lead = await getTargetLead();
   501→        if (lead && parsed.data.score !== undefined) {
   502→          await supabase
   503→            .from('leads')
   504→            .update({ score: parsed.data.score })
   505→            .eq('id', lead.id);
   506→          actionResult = {
   507→            updated: true,
   508→            leadId: lead.id,
   509→            newScore: parsed.data.score,
   510→            tier: parsed.data.tier,
   511→          };
   512→          frontendAction = 'refresh';
   513→        }
   514→        break;
   515→      }
   516→
   517→      // ═══════════════════════════════════════════════════════════════════════
   518→      // NEW: Skip lead - triggers UI navigation
   519→      // ═══════════════════════════════════════════════════════════════════════
   520→      case 'skip_lead': {
   521→        actionResult = { skipped: true };
   522→        frontendAction = 'skip';
   523→        break;
   524→      }
   525→
   526→      // ═══════════════════════════════════════════════════════════════════════
   527→      // NEW: Call lead - triggers UI call initiation
   528→      // ═══════════════════════════════════════════════════════════════════════
   529→      case 'call_lead': {
   530→        actionResult = { callInitiated: true };
   531→        frontendAction = 'call';
   532→        break;
   533→      }
   534→
   535→      // ═══════════════════════════════════════════════════════════════════════
   536→      // NEW: Send follow-up - opens SMS composer
   537→      // ═══════════════════════════════════════════════════════════════════════
   538→      case 'send_followup': {
   539→        actionResult = { openingSms: true };
   540→        frontendAction = 'open_sms';
   541→        break;
   542→      }
   543→
   544→      // ═══════════════════════════════════════════════════════════════════════
   545→      // UPDATED: Mark status - now context-aware
   546→      // ═══════════════════════════════════════════════════════════════════════
   547→      case 'update_lead':
   548→      case 'mark_status': {
   549→        const lead = await getTargetLead();
   550→        if (lead && parsed.data.status) {
   551→          await supabase
   552→            .from('leads')
   553→            .update({ status: parsed.data.status })
   554→            .eq('id', lead.id);
   555→          actionResult = { updated: true, leadId: lead.id, newStatus: parsed.data.status };
   556→          frontendAction = 'refresh';
   557→        }
   558→        break;
   559→      }
   560→
   561→      // ═══════════════════════════════════════════════════════════════════════
   562→      // UPDATED: Schedule callback - now context-aware
   563→      // ═══════════════════════════════════════════════════════════════════════
   564→      case 'schedule_callback': {
   565→        const lead = await getTargetLead();
   566→        if (lead && (parsed.data.callback_date || parsed.data.time_description)) {
   567→          // Build scheduled_at datetime
   568→          let scheduledAt = parsed.data.callback_date || new Date().toISOString().split('T')[0];
   569→          if (parsed.data.callback_time) {
   570→            scheduledAt = `${scheduledAt}T${parsed.data.callback_time}:00`;
   571→          } else {
   572→            scheduledAt = `${scheduledAt}T09:00:00`;
   573→          }
   574→
   575→          await supabase.from('callbacks').insert({
   576→            lead_id: lead.id,
   577→            phone: lead.phone,
   578→            reason: `Voice: ${parsed.data.time_description || 'Callback'}`,
   579→            scheduled_at: scheduledAt,
   580→            status: 'pending',
   581→          });
   582→          actionResult = {
   583→            scheduled: true,
   584→            leadId: lead.id,
   585→            scheduledFor: parsed.data.time_description,
   586→          };
   587→        }
   588→        break;
   589→      }
   590→
   591→      // ═══════════════════════════════════════════════════════════════════════
   592→      // UPDATED: Add note - now context-aware
   593→      // ═══════════════════════════════════════════════════════════════════════
   594→      case 'add_note': {
   595→        const lead = await getTargetLead('id');
   596→        if (lead && parsed.data.note_content) {
   597→          await supabase.from('lead_notes').insert({
   598→            lead_id: lead.id,
   599→            content: parsed.data.note_content,
   600→            created_by: userEmail || 'voice_command',
   601→          });
   602→          actionResult = { noted: true, leadId: lead.id };
   603→        }
   604→        break;
   605→      }
   606→
   607→      case 'get_recommendation':
   608→        // Return next lead to call
   609→        const { data: nextLead } = await supabase
   610→          .from('leads')
   611→          .select('id, first_name, company, score')
   612→          .is('deleted_at', null)
   613→          .in('status', ['new', 'engaged', 'callback'])
   614→          .order('score', { ascending: false })
   615→          .limit(1);
   616→
   617→        if (nextLead?.length > 0) {
   618→          actionResult = {
   619→            recommendation: `Call ${nextLead[0].first_name} from ${nextLead[0].company || 'unknown company'}. Score: ${nextLead[0].score}`,
   620→            leadId: nextLead[0].id,
   621→          };
   622→        }
   623→        break;
   624→
   625→      case 'capture_idea':
   626→        // Save idea/strategy/experiment/reminder to ideas table
   627→        if (parsed.data.content) {
   628→          const { data: idea, error: ideaError } = await supabase
   629→            .from('ideas')
   630→            .insert({
   631→              content: parsed.data.content,
   632→              category: parsed.data.category || 'idea',
   633→              user_email: userEmail,
   634→              ai_detected_category: parsed.data.ai_detected || false,
   635→            })
   636→            .select()
   637→            .single();
   638→
   639→          if (!ideaError && idea) {
   640→            actionResult = {
   641→              saved: true,
   642→              ideaId: idea.id,
   643→              category: idea.category,
   644→              content: idea.content,
   645→            };
   646→          }
   647→        }
   648→        break;
   649→
   650→      case 'set_company':
   651→        // Update lead's company and trigger background research
   652→        if (parsed.data.company_name && parsed.data.lead_id) {
   653→          // Update lead's company
   654→          await supabase
   655→            .from('leads')
   656→            .update({ company: parsed.data.company_name })
   657→            .eq('id', parsed.data.lead_id);
   658→
   659→          // Trigger background company research
   660→          if (parsed.data.trigger_research) {
   661→            perplexityService.searchCompany(parsed.data.company_name).catch(err => {
   662→              console.error('Background company research error:', err);
   663→            });
   664→          }
   665→
   666→          actionResult = {
   667→            updated: true,
   668→            leadId: parsed.data.lead_id,
   669→            company: parsed.data.company_name,
   670→            researching: !!parsed.data.trigger_research,
   671→          };
   672→        }
   673→        break;
   674→
   675→      case 'extract_contractor':
   676→        // Search for decision maker at company (contractor extraction from worker calls)
   677→        if (parsed.data.company_name) {
   678→          const searchResult = await perplexityService.searchDecisionMaker(
   679→            parsed.data.company_name,
   680→            parsed.data.project_name
   681→          );
   682→
   683→          if (searchResult.success && searchResult.found) {
   684→            actionResult = {
   685→              contractorFound: true,
   686→              candidate: {
   687→                first_name: searchResult.person.first_name,
   688→                last_name: searchResult.person.last_name,
   689→                job_title: searchResult.person.job_title,
   690→                company: parsed.data.company_name,
   691→                project: parsed.data.project_name,
   692→                linkedin_url: searchResult.person.linkedin_url,
   693→                email: searchResult.person.email,
   694→                phone: searchResult.person.phone,
   695→                confidence: searchResult.confidence,
   696→              },
   697→              company_info: searchResult.company,
   698→              notes: searchResult.notes,
   699→            };
   700→          } else {
   701→            actionResult = {
   702→              contractorFound: false,
   703→              company: parsed.data.company_name,
   704→              notes: searchResult.notes || 'No decision maker found',
   705→            };
   706→          }
   707→        }
   708→        break;
   709→    }
   710→
   711→    // Log the voice command
   712→    await supabase.from('voice_commands').insert({
   713→      transcript,
   714→      intent: parsed.intent,
   715→      parsed_data: parsed.data,
   716→      action_taken: parsed.confirmation,
   717→      affected_lead_id: actionResult?.leadId || leadId,
   718→      success: !!actionResult || frontendAction !== 'none',
   719→      user_email: userEmail,
   720→    });
   721→
   722→    res.json({
   723→      success: true,
   724→      parsed,
   725→      actionResult,
   726→      frontendAction, // For UI callbacks: skip, call, open_sms, refresh, none
   727→    });
   728→  } catch (error) {
   729→    console.error('Voice command error:', error);
   730→    res.status(500).json({ error: 'Failed to process voice command' });
   731→  }
   732→});
   733→
   734→/**
   735→ * POST /api/ai/explain-message
   736→ * AI explains what a message means and provides context
   737→ */
   738→router.post('/explain-message', async (req, res) => {
   739→  try {
   740→    const { messageContent, messageDirection, leadId, leadName, leadType, context } = req.body;
   741→
   742→    if (!messageContent) {
   743→      return res.status(400).json({ error: 'Message content required' });
   744→    }
   745→
   746→    // Check for OpenAI
   747→    if (!openai) {
   748→      return res.status(503).json({ error: 'AI service not configured' });
   749→    }
   750→
   751→    // Build context for AI
   752→    const contextStr = context ? `
   753→Lead context:
   754→- Name: ${leadName || 'Unknown'}
   755→- Type: ${leadType || 'Unknown'} (${leadType === 'worker' ? 'looking for work' : 'hiring workers'})
   756→- Company: ${context.company || 'Unknown'}
   757→- Lead Score: ${context.score || 0}/100
   758→- Stage: ${context.stage || 'Unknown'}
   759→` : '';
   760→
   761→    const prompt = `You are a sales assistant helping a salesperson understand a message from a ${messageDirection === 'inbound' ? 'lead' : 'previous outbound'}.
   762→
   763→Message: "${messageContent}"
   764→${contextStr}
   765→
   766→Analyze this message and provide:
   767→1. A clear, concise explanation of what this message means (2-3 sentences)
   768→2. The likely intent: positive, negative, question, buying_signal, callback_request, or neutral
   769→3. A suggested action for the salesperson (1 sentence)
   770→4. 2-3 key points to note
   771→
   772→Respond in JSON format:
   773→{
   774→  "summary": "What this message means in plain terms",
   775→  "intent": "positive|negative|question|buying_signal|callback_request|neutral",
   776→  "suggestedAction": "What the salesperson should do next",
   777→  "keyPoints": ["point 1", "point 2", "point 3"]
   778→}`;
   779→
   780→    const completion = await openai.chat.completions.create({
   781→      model: 'gpt-4o-mini',
   782→      messages: [{ role: 'user', content: prompt }],
   783→      max_tokens: 500,
   784→      response_format: { type: 'json_object' }
   785→    });
   786→
   787→    let result;
   788→    try {
   789→      result = JSON.parse(completion.choices[0].message.content);
   790→    } catch (parseError) {
   791→      // Fallback if JSON parsing fails
   792→      result = {
   793→        summary: completion.choices[0].message.content,
   794→        intent: 'neutral',
   795→        suggestedAction: 'Review the message and respond appropriately',
   796→        keyPoints: []
   797→      };
   798→    }
   799→
   800→    // Generate unique ID for feedback tracking
   801→    result.id = `explain_${Date.now()}`;
   802→
   803→    res.json(result);
   804→  } catch (error) {
   805→    console.error('Error explaining message:', error);
   806→    res.status(500).json({ error: 'Failed to explain message' });
   807→  }
   808→});
   809→
   810→/**
   811→ * POST /api/ai/objection-response
   812→ * Get real-time objection handling suggestion with full lead context
   813→ */
   814→router.post('/objection-response', async (req, res) => {
   815→  try {
   816→    const { objection, leadId } = req.body;
   817→
   818→    if (!objection) {
   819→      return res.status(400).json({ error: 'Objection text required' });
   820→    }
   821→
   822→    // Build rich lead context
   823→    let leadContext = {
   824→      name: 'the prospect',
   825→      company: 'their company',
   826→      trade: null,
   827→      score: 0,
   828→      status: 'unknown',
   829→      recentMessages: [],
   830→      previousObjections: [],
   831→    };
   832→
   833→    if (leadId) {
   834→      // Get full lead data
   835→      const { data: lead } = await supabase
   836→        .from('leads')
   837→        .select('first_name, last_name, company, trade, score, status, metadata')
   838→        .eq('id', leadId)
   839→        .single();
   840→
   841→      if (lead) {
   842→        leadContext.name = lead.first_name || 'the prospect';
   843→        leadContext.company = lead.company || 'their company';
   844→        leadContext.trade = lead.trade || lead.metadata?.trade;
   845→        leadContext.score = lead.score || 0;
   846→        leadContext.status = lead.status;
   847→      }
   848→
   849→      // Get recent communications for context
   850→      const { data: comms } = await supabase
   851→        .from('communications')
   852→        .select('content, direction, metadata, created_at')
   853→        .eq('lead_id', leadId)
   854→        .order('created_at', { ascending: false })
   855→        .limit(5);
   856→
   857→      if (comms) {
   858→        leadContext.recentMessages = comms.map(c => ({
   859→          direction: c.direction,
   860→          content: c.content?.substring(0, 100),
   861→          objections: c.metadata?.objections || [],
   862→        }));
   863→
   864→        // Extract any previous objections
   865→        comms.forEach(c => {
   866→          if (c.metadata?.objections) {
   867→            leadContext.previousObjections.push(...c.metadata.objections);
   868→          }
   869→        });
   870→      }
   871→    }
   872→
   873→    const result = await aiService.getObjectionResponse(objection, leadContext);
   874→
   875→    if (!result.success) {
   876→      return res.status(500).json({ error: result.error });
   877→    }
   878→
   879→    res.json({
   880→      success: true,
   881→      suggestion: result.suggestion,
   882→      objectionDetected: objection,
   883→    });
   884→  } catch (error) {
   885→    console.error('Objection response error:', error);
   886→    res.status(500).json({ error: 'Failed to get objection response' });
   887→  }
   888→});
   889→
   890→/**
   891→ * GET /api/ai/conversion-probability/:leadId
   892→ * Calculate AI conversion probability
   893→ */
   894→router.get('/conversion-probability/:leadId', async (req, res) => {
   895→  try {
   896→    const { leadId } = req.params;
   897→
   898→    // Get lead data
   899→    const { data: lead, error: leadError } = await supabase
   900→      .from('leads')
   901→      .select('*')
   902→      .eq('id', leadId)
   903→      .single();
   904→
   905→    if (leadError || !lead) {
   906→      return res.status(404).json({ error: 'Lead not found' });
   907→    }
   908→
   909→    // Get communications
   910→    const { data: communications } = await supabase
   911→      .from('communications')
   912→      .select('*')
   913→      .eq('lead_id', leadId);
   914→
   915→    // Get conversion factors
   916→    const { data: factors } = await supabase
   917→      .from('conversion_factors')
   918→      .select('*')
   919→      .eq('is_active', true);
   920→
   921→    const result = await aiService.calculateConversionProbability(
   922→      lead,
   923→      communications || [],
   924→      factors || []
   925→    );
   926→
   927→    // Update lead with new probability
   928→    if (result.success) {
   929→      await supabase
   930→        .from('leads')
   931→        .update({
   932→          ai_conversion_probability: result.probability,
   933→          ai_recommended_action: result.recommended_action,
   934→          ai_recommended_timing: result.recommended_timing,
   935→          ai_last_analyzed_at: new Date().toISOString(),
   936→        })
   937→        .eq('id', leadId);
   938→    }
   939→
   940→    res.json({
   941→      success: true,
   942→      probability: result.probability,
   943→      factors: result.factors,
   944→      reasoning: result.reasoning,
   945→      recommended_action: result.recommended_action,
   946→      recommended_timing: result.recommended_timing,
   947→    });
   948→  } catch (error) {
   949→    console.error('Conversion probability error:', error);
   950→    res.status(500).json({ error: 'Failed to calculate conversion probability' });
   951→  }
   952→});
   953→
   954→/**
   955→ * POST /api/ai/weekly-insights
   956→ * Generate weekly pattern insights
   957→ * Rate limited: 5 per minute (expensive batch operation)
   958→ */
   959→router.post('/weekly-insights', strictLimiter, async (req, res) => {
   960→  try {
   961→    const now = new Date();
   962→    const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
   963→
   964→    // Get metrics
   965→    const { data: metrics } = await supabase
   966→      .from('daily_metrics')
   967→      .select('*')
   968→      .gte('date', weekAgo.toISOString().split('T')[0])
   969→      .order('date', { ascending: false });
   970→
   971→    // Aggregate metrics
   972→    const aggregated = (metrics || []).reduce((acc, m) => ({
   973→      calls_made: (acc.calls_made || 0) + (m.calls_made || 0),
   974→      sms_sent: (acc.sms_sent || 0) + (m.sms_sent || 0),
   975→      sms_received: (acc.sms_received || 0) + (m.sms_received || 0),
   976→      new_leads: (acc.new_leads || 0) + (m.new_leads || 0),
   977→      conversions: (acc.conversions || 0) + (m.conversions || 0),
   978→    }), {});
   979→
   980→    // Get conversions
   981→    const { data: conversions } = await supabase
   982→      .from('conversions')
   983→      .select('*, leads(company, source)')
   984→      .gte('created_at', weekAgo.toISOString())
   985→      .limit(20);
   986→
   987→    // Get communications
   988→    const { data: communications } = await supabase
   989→      .from('communications')
   990→      .select('*')
   991→      .gte('created_at', weekAgo.toISOString())
   992→      .order('created_at', { ascending: false })
   993→      .limit(100);
   994→
   995→    const result = await aiService.generateWeeklyInsights(
   996→      aggregated,
   997→      conversions || [],
   998→      communications || []
   999→    );
  1000→
  1001→    if (!result.success) {
  1002→      return res.status(500).json({ error: result.error });
  1003→    }
  1004→
  1005→    // Store insights
  1006→    await supabase.from('ai_insights').insert({
  1007→      period_start: weekAgo.toISOString().split('T')[0],
  1008→      period_end: now.toISOString().split('T')[0],
  1009→      insight_type: 'weekly_summary',
  1010→      title: result.insights.top_insight,
  1011→      description: result.insights.summary,
  1012→      data: result.insights,
  1013→      recommendations: result.insights.recommendations,
  1014→      sample_size: (communications?.length || 0) + (conversions?.length || 0),
  1015→    });
  1016→
  1017→    res.json({
  1018→      success: true,
  1019→      insights: result.insights,
  1020→      period: {
  1021→        start: weekAgo.toISOString().split('T')[0],
  1022→        end: now.toISOString().split('T')[0],
  1023→      },
  1024→    });
  1025→  } catch (error) {
  1026→    console.error('Weekly insights error:', error);
  1027→    res.status(500).json({ error: 'Failed to generate weekly insights' });
  1028→  }
  1029→});
  1030→
  1031→/**
  1032→ * POST /api/ai/cleanup-notes
  1033→ * Clean up rough notes into structured format
  1034→ */
  1035→router.post('/cleanup-notes', async (req, res) => {
  1036→  try {
  1037→    const { leadId, notes } = req.body;
  1038→
  1039→    if (!notes) {
  1040→      return res.status(400).json({ error: 'Notes required' });
  1041→    }
  1042→
  1043→    let lead = { first_name: 'Unknown', company: 'Unknown' };
  1044→    if (leadId) {
  1045→      const { data } = await supabase
  1046→        .from('leads')
  1047→        .select('first_name, last_name, company')
  1048→        .eq('id', leadId)
  1049→        .single();
  1050→      if (data) lead = data;
  1051→    }
  1052→
  1053→    const result = await aiService.cleanupNotes(notes, lead);
  1054→
  1055→    if (!result.success) {
  1056→      return res.status(500).json({ error: result.error });
  1057→    }
  1058→
  1059→    res.json({
  1060→      success: true,
  1061→      cleaned: result.cleaned,
  1062→    });
  1063→  } catch (error) {
  1064→    console.error('Cleanup notes error:', error);
  1065→    res.status(500).json({ error: 'Failed to cleanup notes' });
  1066→  }
  1067→});
  1068→
  1069→/**
  1070→ * GET /api/ai/insights
  1071→ * Get recent AI insights
  1072→ */
  1073→router.get('/insights', async (req, res) => {
  1074→  try {
  1075→    const { data: insights, error } = await supabase
  1076→      .from('ai_insights')
  1077→      .select('*')
  1078→      .eq('is_active', true)
  1079→      .order('created_at', { ascending: false })
  1080→      .limit(10);
  1081→
  1082→    if (error) throw error;
  1083→
  1084→    res.json({
  1085→      success: true,
  1086→      insights: insights || [],
  1087→    });
  1088→  } catch (error) {
  1089→    console.error('Get insights error:', error);
  1090→    res.status(500).json({ error: 'Failed to get insights' });
  1091→  }
  1092→});
  1093→
  1094→/**
  1095→ * GET /api/ai/deepgram-credentials
  1096→ * Check if live transcription is configured
  1097→ * Frontend connects to /api/transcribe/ws WebSocket proxy
  1098→ */
  1099→router.get('/deepgram-credentials', (req, res) => {
  1100→  const config = deepgramService.getClientConfig();
  1101→
  1102→  if (!config.configured) {
  1103→    return res.status(503).json({
  1104→      success: false,
  1105→      configured: false,
  1106→      error: config.error || 'Live transcription not configured',
  1107→    });
  1108→  }
  1109→
  1110→  // Just return configured status - API key stays server-side
  1111→  // Frontend connects to /api/transcribe/ws WebSocket proxy
  1112→  res.json({
  1113→    success: true,
  1114→    configured: true,
  1115→  });
  1116→});
  1117→
  1118→/**
  1119→ * POST /api/ai/copilot-session
  1120→ * Save copilot session data with AI analysis
  1121→ */
  1122→router.post('/copilot-session', async (req, res) => {
  1123→  try {
  1124→    const { leadId, transcript, suggestions, duration, outcome } = req.body;
  1125→
  1126→    let aiSummary = null;
  1127→    let lead = null;
  1128→
  1129→    // Get lead data for AI processing
  1130→    if (leadId) {
  1131→      const { data: leadData } = await supabase
  1132→        .from('leads')
  1133→        .select('*')
  1134→        .eq('id', leadId)
  1135→        .single();
  1136→      lead = leadData;
  1137→    }
  1138→
  1139→    // Generate AI summary if we have transcript
  1140→    if (transcript && transcript.trim().length > 20 && lead) {
  1141→      console.log(`[CopilotSession] Generating AI summary for ${transcript.length} chars`);
  1142→
  1143→      const summaryResult = await aiService.generatePostCallSummary(
  1144→        lead,
  1145→        transcript,
  1146→        duration || 0
  1147→      );
  1148→
  1149→      if (summaryResult.success) {
  1150→        aiSummary = summaryResult.summary;
  1151→        console.log('[CopilotSession] AI summary generated:', aiSummary.summary?.substring(0, 50));
  1152→
  1153→        // Update lead with AI recommendations
  1154→        const updates = {
  1155→          updated_at: new Date().toISOString(),
  1156→          last_contact_at: new Date().toISOString(),
  1157→          last_contact_type: 'call_outbound',
  1158→        };
  1159→
  1160→        if (aiSummary.score_adjustment) {
  1161→          updates.score = Math.max(0, Math.min(100, (lead.score || 0) + aiSummary.score_adjustment));
  1162→        }
  1163→
  1164→        if (aiSummary.status_recommendation && aiSummary.status_recommendation !== lead.status) {
  1165→          updates.status = aiSummary.status_recommendation;
  1166→        }
  1167→
  1168→        // Store buying signals if detected
  1169→        if (aiSummary.buying_signals && aiSummary.buying_signals.length > 0) {
  1170→          const existingSignals = lead.metadata?.buying_signals || [];
  1171→          const newSignals = aiSummary.buying_signals.map(s => ({
  1172→            type: s.signal_type,
  1173→            score: s.signal_type === 'high' ? 30 : s.signal_type === 'medium' ? 15 : 5,
  1174→            source: 'call',
  1175→            text: s.phrase,
  1176→            timestamp_approx: s.timestamp_approx,
  1177→            detected_at: new Date().toISOString(),
  1178→          }));
  1179→          const allSignals = [...existingSignals, ...newSignals].slice(-10);
  1180→          updates.metadata = { ...lead.metadata, buying_signals: allSignals };
  1181→        }
  1182→
  1183→        await supabase.from('leads').update(updates).eq('id', leadId);
  1184→
  1185→        // Extract transcript intel (companies, people, projects mentioned)
  1186→        const intelResult = await aiService.extractTranscriptIntel(transcript);
  1187→        if (intelResult.success && intelResult.intel) {
  1188→          console.log('[CopilotSession] Intel extracted:', Object.keys(intelResult.intel));
  1189→        }
  1190→
  1191→        // NEW: Extract and SAVE personal + business intel to lead_intel
  1192→        // This captures: who they work for, location, company size, pain points, hobbies, family, etc.
  1193→        try {
  1194→          const savedIntel = await aiService.extractAndSaveCallIntel(leadId);
  1195→          if (savedIntel.success) {
  1196→            console.log('[CopilotSession] Personal + business intel saved to lead_intel');
  1197→          }
  1198→        } catch (intelErr) {
  1199→          console.warn('[CopilotSession] Intel save failed (non-blocking):', intelErr.message);
  1200→        }
  1201→      }
  1202→    }
  1203→
  1204→    // Save to copilot_sessions
  1205→    const { data, error } = await supabase
  1206→      .from('copilot_sessions')
  1207→      .insert({
  1208→        lead_id: leadId,
  1209→        full_transcript: transcript,
  1210→        suggestions_shown: suggestions || [],
  1211→        duration_seconds: duration,
  1212→        call_outcome: outcome || aiSummary?.outcome,
  1213→        ai_summary: aiSummary,
  1214→        ended_at: new Date().toISOString(),
  1215→      })
  1216→      .select()
  1217→      .single();
  1218→
  1219→    if (error) throw error;
  1220→
  1221→    // Also save to communications table for unified history
  1222→    if (transcript && transcript.trim() && leadId) {
  1223→      const durationMin = Math.floor((duration || 0) / 60);
  1224→      const durationSec = (duration || 0) % 60;
  1225→      const durationStr = `${durationMin}:${String(durationSec).padStart(2, '0')}`;
  1226→
  1227→      await supabase
  1228→        .from('communications')
  1229→        .insert({
  1230→          lead_id: leadId,
  1231→          type: 'call_outbound',
  1232→          direction: 'outbound',
  1233→          phone: lead?.phone,
  1234→          content: aiSummary?.summary || `Call with transcript (${durationStr})`,
  1235→          metadata: {
  1236→            copilot_session_id: data.id,
  1237→            transcript: transcript.substring(0, 5000),
  1238→            ai_summary: aiSummary,
  1239→            duration: duration,
  1240→            suggestions_count: suggestions?.length || 0,
  1241→            buying_signals: aiSummary?.buying_signals || [],
  1242→          },
  1243→        });
  1244→
  1245→      // Also save raw transcript to lead_notes for easy access
  1246→      await supabase
  1247→        .from('lead_notes')
  1248→        .insert({
  1249→          lead_id: leadId,
  1250→          content: `[AI Copilot Call - ${durationStr}]\n\n${transcript.trim()}${aiSummary ? `\n\n---\nAI Summary: ${aiSummary.summary}` : ''}`,
  1251→          created_by: 'copilot'
  1252→        });
  1253→    }
  1254→
  1255→    res.json({ success: true, session: data, summary: aiSummary });
  1256→  } catch (error) {
  1257→    console.error('Save copilot session error:', error);
  1258→    res.status(500).json({ error: 'Failed to save session' });
  1259→  }
  1260→});
  1261→
  1262→/**
  1263→ * POST /api/ai/suggestion-feedback
  1264→ * Record thumbs up/down feedback on copilot suggestions
  1265→ */
  1266→router.post('/suggestion-feedback', async (req, res) => {
  1267→  try {
  1268→    const {
  1269→      copilotSessionId,
  1270→      leadId,
  1271→      objectionDetected,
  1272→      objectionType,
  1273→      suggestionResponse,
  1274→      suggestionKeyPoint,
  1275→      wasHelpful,
  1276→      feedbackNote,
  1277→      userEmail,
  1278→    } = req.body;
  1279→
  1280→    if (wasHelpful === undefined) {
  1281→      return res.status(400).json({ error: 'wasHelpful is required' });
  1282→    }
  1283→
  1284→    const result = await recordSuggestionFeedback({
  1285→      copilotSessionId,
  1286→      leadId,
  1287→      objectionDetected,
  1288→      objectionType,
  1289→      suggestionResponse,
  1290→      suggestionKeyPoint,
  1291→      wasHelpful,
  1292→      feedbackNote,
  1293→      userEmail,
  1294→    });
  1295→
  1296→    if (!result.success) {
  1297→      return res.status(500).json({ error: result.error });
  1298→    }
  1299→
  1300→    res.json({ success: true });
  1301→  } catch (error) {
  1302→    console.error('Suggestion feedback error:', error);
  1303→    res.status(500).json({ error: 'Failed to record feedback' });
  1304→  }
  1305→});
  1306→
  1307→/**
  1308→ * GET /api/ai/effective-responses/:objectionType
  1309→ * Get top performing responses for an objection type
  1310→ */
  1311→router.get('/effective-responses/:objectionType', async (req, res) => {
  1312→  try {
  1313→    const { objectionType } = req.params;
  1314→    const { trade } = req.query;
  1315→
  1316→    const responses = await getEffectiveResponses(objectionType, trade);
  1317→
  1318→    res.json({
  1319→      success: true,
  1320→      responses,
  1321→      objectionType,
  1322→    });
  1323→  } catch (error) {
  1324→    console.error('Get effective responses error:', error);
  1325→    res.status(500).json({ error: 'Failed to get effective responses' });
  1326→  }
  1327→});
  1328→
  1329→/**
  1330→ * GET /api/ai/similar-wins/:leadId
  1331→ * Get similar conversion patterns for a lead
  1332→ */
  1333→router.get('/similar-wins/:leadId', async (req, res) => {
  1334→  try {
  1335→    const { leadId } = req.params;
  1336→
  1337→    // Get lead data
  1338→    const { data: lead, error: leadError } = await supabase
  1339→      .from('leads')
  1340→      .select('*')
  1341→      .eq('id', leadId)
  1342→      .single();
  1343→
  1344→    if (leadError || !lead) {
  1345→      return res.status(404).json({ error: 'Lead not found' });
  1346→    }
  1347→
  1348→    const similarWins = await getSimilarWins(lead);
  1349→
  1350→    res.json({
  1351→      success: true,
  1352→      similarWins,
  1353→      leadId,
  1354→    });
  1355→  } catch (error) {
  1356→    console.error('Get similar wins error:', error);
  1357→    res.status(500).json({ error: 'Failed to get similar wins' });
  1358→  }
  1359→});
  1360→
  1361→/**
  1362→ * GET /api/ai/live-intel/:leadId
  1363→ * Get consolidated intel for live call display
  1364→ * Returns company, person, similar wins, and previous objections in one call
  1365→ */
  1366→router.get('/live-intel/:leadId', async (req, res) => {
  1367→  try {
  1368→    const { leadId } = req.params;
  1369→
  1370→    // Get lead data with company
  1371→    const { data: lead, error: leadError } = await supabase
  1372→      .from('leads')
  1373→      .select('id, first_name, last_name, company, score, status, metadata')
  1374→      .eq('id', leadId)
  1375→      .single();
  1376→
  1377→    if (leadError || !lead) {
  1378→      return res.status(404).json({ error: 'Lead not found' });
  1379→    }
  1380→
  1381→    // Parallel fetch all intel - using allSettled for graceful partial failure handling
  1382→    const results = await Promise.allSettled([
  1383→      // Company intel (cached)
  1384→      lead.company
  1385→        ? perplexityService.getCachedCompanyIntel(lead.company)
  1386→        : Promise.resolve(null),
  1387→
  1388→      // Person intel (cached) - now includes personal profile from Perplexity
  1389→      perplexityService.getCachedLeadIntel(leadId),
  1390→
  1391→      // NEW: Extract personal intel from SMS/call conversations
  1392→      aiService.extractPersonalIntelFromConversations(leadId),
  1393→
  1394→      // Similar wins
  1395→      getSimilarWins(lead),
  1396→
  1397→      // Previous objections from copilot sessions
  1398→      supabase
  1399→        .from('copilot_sessions')
  1400→        .select('suggestions_shown, created_at')
  1401→        .eq('lead_id', leadId)
  1402→        .not('suggestions_shown', 'is', null)
  1403→        .order('created_at', { ascending: false })
  1404→        .limit(10),
  1405→
  1406→      // Business intel from dossier (pain points, hiring needs, etc.)
  1407→      supabase
  1408→        .from('lead_intel')
  1409→        .select('business_intel')
  1410→        .eq('lead_id', leadId)
  1411→        .single(),
  1412→    ]);
  1413→
  1414→    // Extract values with fallbacks for rejected promises
  1415→    const companyResult = results[0].status === 'fulfilled' ? results[0].value : null;
  1416→    const personResult = results[1].status === 'fulfilled' ? results[1].value : null;
  1417→    const conversationIntelResult = results[2].status === 'fulfilled' ? results[2].value : null;
  1418→    const similarWinsResult = results[3].status === 'fulfilled' ? results[3].value : [];
  1419→    const previousObjectionsResult = results[4].status === 'fulfilled' ? results[4].value : { data: [] };
  1420→    const dossierResult = results[5].status === 'fulfilled' ? results[5].value : { data: null };
  1421→
  1422→    // After getting personResult, fetch current news on their interests
  1423→    let interestNews = null;
  1424→    const allInterestsForNews = [
  1425→      ...(personResult?.interests || []),
  1426→      ...(personResult?.hobbies || []),
  1427→    ].filter(Boolean);
  1428→
  1429→    if (allInterestsForNews.length > 0) {
  1430→      // Fetch news in background - don't block if it fails
  1431→      try {
  1432→        const newsResult = await perplexityService.searchInterestNews(allInterestsForNews);
  1433→        if (newsResult.success) {
  1434→          interestNews = newsResult;
  1435→        }
  1436→      } catch (err) {
  1437→        console.warn('[LiveIntel] Interest news fetch failed:', err.message);
  1438→      }
  1439→    }
  1440→
  1441→    // Extract unique objection types from previous sessions
  1442→    const previousObjections = [];
  1443→    const seenObjections = new Set();
  1444→
  1445→    if (previousObjectionsResult.data) {
  1446→      for (const session of previousObjectionsResult.data) {
  1447→        const suggestions = session.suggestions_shown || [];
  1448→        for (const s of suggestions) {
  1449→          if (s.objectionType && !seenObjections.has(s.objectionType)) {
  1450→            seenObjections.add(s.objectionType);
  1451→            previousObjections.push({
  1452→              type: s.objectionType,
  1453→              date: session.created_at,
  1454→            });
  1455→          }
  1456→        }
  1457→      }
  1458→    }
  1459→
  1460→    // Combine Perplexity research + conversation intel for personal profile
  1461→    const conversationIntel = conversationIntelResult?.intel || {};
  1462→
  1463→    // Merge hobbies from both sources (Perplexity + conversations)
  1464→    const allHobbies = [
  1465→      ...(personResult?.hobbies || []),
  1466→      ...(conversationIntel.hobbies_mentioned || []).map(h => h.hobby),
  1467→    ].filter((v, i, a) => a.indexOf(v) === i); // unique
  1468→
  1469→    const allInterests = [
  1470→      ...(personResult?.interests || []),
  1471→      ...(conversationIntel.interests_mentioned || []).map(i => i.interest),
  1472→    ].filter((v, i, a) => a.indexOf(v) === i);
  1473→
  1474→    // Merge conversation starters
  1475→    const allConversationStarters = [
  1476→      ...(conversationIntel.conversation_starters || []), // Prioritize what they've told us
  1477→      ...(personResult?.conversation_starters || []),
  1478→    ].slice(0, 5);
  1479→
  1480→    // Build response
  1481→    const response = {
  1482→      success: true,
  1483→      lead: {
  1484→        id: lead.id,
  1485→        name: `${lead.first_name} ${lead.last_name || ''}`.trim(),
  1486→        company: lead.company,
  1487→        trade: lead.metadata?.trade,
  1488→      },
  1489→      company: companyResult
  1490→        ? {
  1491→            name: lead.company,
  1492→            industry: companyResult.industry,
  1493→            size: companyResult.size,
  1494→            location: companyResult.location,
  1495→            hiring_signals: companyResult.hiring_signals,
  1496→            sales_angle: companyResult.sales_angle,
  1497→            summary: companyResult.summary,
  1498→          }
  1499→        : null,
  1500→      person: personResult
  1501→        ? {
  1502→            job_title: personResult.job_title,
  1503→            years_at_company: personResult.years_at_company,
  1504→            previous_companies: personResult.previous_companies,
  1505→            summary: personResult.summary,
  1506→            linkedin_url: personResult.linkedin_url,
  1507→            // NEW: Personal profile fields
  1508→            family_status: conversationIntel.family?.status || personResult.family_status,
  1509→            kids: conversationIntel.family?.kids,
  1510→            hobbies: allHobbies,
  1511→            interests: allInterests,
  1512→            location_suburb: personResult.location_suburb,
  1513→            personality_notes: personResult.personality_notes,
  1514→            sports_teams: conversationIntel.sports_teams,
  1515→            social_profiles: personResult.social_profiles,
  1516→          }
  1517→        : null,
  1518→      // NEW: Personal intel from conversations
  1519→      personal_intel: {
  1520→        from_conversations: conversationIntelResult?.success && conversationIntel ? {
  1521→          family: conversationIntel.family,
  1522→          hobbies_mentioned: conversationIntel.hobbies_mentioned,
  1523→          interests_mentioned: conversationIntel.interests_mentioned,
  1524→          weekend_plans: conversationIntel.weekend_plans,
  1525→          personality_traits: conversationIntel.personality_traits,
  1526→          pet_peeves: conversationIntel.pet_peeves,
  1527→          important_dates: conversationIntel.important_dates,
  1528→          location_details: conversationIntel.location_details,
  1529→        } : null,
  1530→        conversation_starters: allConversationStarters,
  1531→        // NEW: Current news on their interests (Liverpool, Mayo GAA, F1, etc.)
  1532→        interest_news: interestNews ? {
  1533→          items: interestNews.news_items || [],
  1534→          talking_points: interestNews.talking_points || [],
  1535→        } : null,
  1536→      },
  1537→      similar_wins: (similarWinsResult || []).slice(0, 3).map(w => ({
  1538→        name: w.lead_name || w.company,
  1539→        outcome: 'converted',
  1540→        key_factor: w.key_factor || w.winning_approach,
  1541→        trade: w.trade,
  1542→      })),
  1543→      previous_objections: previousObjections.slice(0, 3),
  1544→      // Business intel: pain points, hiring needs, competitors, decision timeline
  1545→      business_intel: dossierResult?.data?.business_intel || null,
  1546→    };
  1547→
  1548→    res.json(response);
  1549→  } catch (error) {
  1550→    console.error('Live intel error:', error);
  1551→    res.status(500).json({ error: 'Failed to get live intel' });
  1552→  }
  1553→});
  1554→
  1555→/**
  1556→ * GET /api/ai/company-research/:companyName
  1557→ * Research a company using Perplexity
  1558→ * Rate limited: 5 per minute (external API call)
  1559→ */
  1560→router.get('/company-research/:companyName', expensiveAiLimiter, async (req, res) => {
  1561→  try {
  1562→    const { companyName } = req.params;
  1563→    const { refresh } = req.query;
  1564→
  1565→    if (!companyName || companyName.length < 2) {
  1566→      return res.status(400).json({ error: 'Company name required' });
  1567→    }
  1568→
  1569→    // If not forcing refresh, try cache first
  1570→    if (!refresh) {
  1571→      const cached = await perplexityService.getCachedCompanyIntel(companyName);
  1572→      if (cached) {
  1573→        return res.json({ success: true, data: cached, cached: true });
  1574→      }
  1575→    }
  1576→
  1577→    // Search with Perplexity
  1578→    const result = await perplexityService.searchCompany(companyName);
  1579→
  1580→    if (!result.success) {
  1581→      return res.status(500).json({ error: result.error });
  1582→    }
  1583→
  1584→    res.json(result);
  1585→  } catch (error) {
  1586→    console.error('Company research error:', error);
  1587→    res.status(500).json({ error: 'Failed to research company' });
  1588→  }
  1589→});
  1590→
  1591→/**
  1592→ * GET /api/ai/person-research/:leadId
  1593→ * Research a person/lead using Perplexity
  1594→ * Rate limited: 5 per minute (external API call)
  1595→ */
  1596→router.get('/person-research/:leadId', expensiveAiLimiter, async (req, res) => {
  1597→  try {
  1598→    const { leadId } = req.params;
  1599→    const { refresh } = req.query;
  1600→
  1601→    if (!leadId) {
  1602→      return res.status(400).json({ error: 'Lead ID required' });
  1603→    }
  1604→
  1605→    // If not forcing refresh, try cache first
  1606→    if (!refresh) {
  1607→      const cached = await perplexityService.getCachedLeadIntel(leadId);
  1608→      if (cached) {
  1609→        return res.json({ success: true, data: cached, cached: true });
  1610→      }
  1611→    }
  1612→
  1613→    // Search with Perplexity
  1614→    const result = await perplexityService.searchPerson(leadId);
  1615→
  1616→    if (!result.success) {
  1617→      return res.status(500).json({ error: result.error });
  1618→    }
  1619→
  1620→    res.json(result);
  1621→  } catch (error) {
  1622→    console.error('Person research error:', error);
  1623→    res.status(500).json({ error: 'Failed to research person' });
  1624→  }
  1625→});
  1626→
  1627→/**
  1628→ * POST /api/ai/update-intel
  1629→ * Update company or person intel with user edits
  1630→ */
  1631→router.post('/update-intel', async (req, res) => {
  1632→  try {
  1633→    const { type, companyName, leadId, updates } = req.body;
  1634→
  1635→    if (!type || !updates) {
  1636→      return res.status(400).json({ error: 'Type and updates required' });
  1637→    }
  1638→
  1639→    if (type === 'company') {
  1640→      if (!companyName) {
  1641→        return res.status(400).json({ error: 'Company name required' });
  1642→      }
  1643→
  1644→      const normalized = perplexityService.normalizeCompanyName(companyName);
  1645→
  1646→      // Find existing record
  1647→      const { data: existing } = await supabase
  1648→        .from('company_intel')
  1649→        .select('id')
  1650→        .eq('company_name_normalized', normalized)
  1651→        .single();
  1652→
  1653→      if (existing) {
  1654→        // Update existing
  1655→        const { error } = await supabase
  1656→          .from('company_intel')
  1657→          .update({
  1658→            ...updates,
  1659→            notes_updated_at: new Date().toISOString(),
  1660→          })
  1661→          .eq('id', existing.id);
  1662→
  1663→        if (error) throw error;
  1664→      } else {
  1665→        // Create new record
  1666→        const { error } = await supabase
  1667→          .from('company_intel')
  1668→          .insert({
  1669→            company_name: companyName,
  1670→            company_name_normalized: normalized,
  1671→            ...updates,
  1672→            notes_updated_at: new Date().toISOString(),
  1673→          });
  1674→
  1675→        if (error) throw error;
  1676→      }
  1677→
  1678→      res.json({ success: true, type: 'company', companyName });
  1679→    } else if (type === 'person') {
  1680→      if (!leadId) {
  1681→        return res.status(400).json({ error: 'Lead ID required' });
  1682→      }
  1683→
  1684→      // Find existing record
  1685→      const { data: existing } = await supabase
  1686→        .from('lead_intel')
  1687→        .select('id')
  1688→        .eq('lead_id', leadId)
  1689→        .single();
  1690→
  1691→      if (existing) {
  1692→        // Update existing
  1693→        const { error } = await supabase
  1694→          .from('lead_intel')
  1695→          .update({
  1696→            ...updates,
  1697→            notes_updated_at: new Date().toISOString(),
  1698→          })
  1699→          .eq('id', existing.id);
  1700→
  1701→        if (error) throw error;
  1702→      } else {
  1703→        // Create new record
  1704→        const { error } = await supabase
  1705→          .from('lead_intel')
  1706→          .insert({
  1707→            lead_id: leadId,
  1708→            ...updates,
  1709→            notes_updated_at: new Date().toISOString(),
  1710→          });
  1711→
  1712→        if (error) throw error;
  1713→      }
  1714→
  1715→      res.json({ success: true, type: 'person', leadId });
  1716→    } else {
  1717→      return res.status(400).json({ error: 'Invalid type' });
  1718→    }
  1719→  } catch (error) {
  1720→    console.error('Update intel error:', error);
  1721→    res.status(500).json({ error: 'Failed to update intel' });
  1722→  }
  1723→});
  1724→
  1725→/**
  1726→ * POST /api/ai/extract-contractor
  1727→ * Extract contractor lead from worker conversation
  1728→ * Searches for decision maker at company and optionally creates lead
  1729→ */
  1730→router.post('/extract-contractor', async (req, res) => {
  1731→  try {
  1732→    const { sourceLeadId, companyName, projectName, createLead, userEmail } = req.body;
  1733→
  1734→    if (!companyName) {
  1735→      return res.status(400).json({ error: 'Company name required' });
  1736→    }
  1737→
  1738→    // Get source worker info if provided
  1739→    let sourceWorker = null;
  1740→    if (sourceLeadId) {
  1741→      const { data: worker } = await supabase
  1742→        .from('leads')
  1743→        .select('id, first_name, last_name, company')
  1744→        .eq('id', sourceLeadId)
  1745→        .single();
  1746→      sourceWorker = worker;
  1747→    }
  1748→
  1749→    // Search for decision maker using Perplexity
  1750→    const searchResult = await perplexityService.searchDecisionMaker(companyName, projectName);
  1751→
  1752→    if (!searchResult.success) {
  1753→      return res.status(500).json({ error: searchResult.error });
  1754→    }
  1755→
  1756→    if (!searchResult.found) {
  1757→      return res.json({
  1758→        success: true,
  1759→        found: false,
  1760→        notes: searchResult.notes || 'No decision maker found',
  1761→      });
  1762→    }
  1763→
  1764→    // Build candidate object
  1765→    const candidate = {
  1766→      first_name: searchResult.person.first_name,
  1767→      last_name: searchResult.person.last_name,
  1768→      job_title: searchResult.person.job_title,
  1769→      company: searchResult.company?.full_name || companyName,
  1770→      project: projectName,
  1771→      linkedin_url: searchResult.person.linkedin_url,
  1772→      email: searchResult.person.email,
  1773→      phone: searchResult.person.phone,
  1774→      location: searchResult.company?.location,
  1775→      confidence: searchResult.confidence,
  1776→      source_worker: sourceWorker
  1777→        ? `${sourceWorker.first_name} ${sourceWorker.last_name || ''}`.trim()
  1778→        : null,
  1779→    };
  1780→
  1781→    // If createLead is true, create the lead in the database
  1782→    let createdLeadId = null;
  1783→    if (createLead) {
  1784→      const workerName = sourceWorker
  1785→        ? `${sourceWorker.first_name} ${sourceWorker.last_name || ''}`.trim()
  1786→        : 'a worker';
  1787→
  1788→      const referralContext = projectName
  1789→        ? `${workerName} works for them on ${projectName} - use as intro`
  1790→        : `${workerName} works for them - use as intro`;
  1791→
  1792→      const { data: newLead, error: createError } = await supabase
  1793→        .from('leads')
  1794→        .insert({
  1795→          first_name: searchResult.person.first_name,
  1796→          last_name: searchResult.person.last_name,
  1797→          email: searchResult.person.email,
  1798→          phone: searchResult.person.phone,
  1799→          company: searchResult.company?.full_name || companyName,
  1800→          source: 'worker_referral',
  1801→          status: 'new',
  1802→          score: 70, // Hot lead score
  1803→          referred_by_lead_id: sourceLeadId || null,
  1804→          referral_context: referralContext,
  1805→          is_hot_referral: true,
  1806→          metadata: {
  1807→            lead_type: 'contractor',
  1808→            job_title: searchResult.person.job_title,
  1809→            linkedin_url: searchResult.person.linkedin_url,
  1810→            project: projectName,
  1811→            location: searchResult.company?.location,
  1812→            size: searchResult.company?.size,
  1813→            extraction_confidence: searchResult.confidence,
  1814→            extracted_at: new Date().toISOString(),
  1815→            extracted_by: userEmail,
  1816→          },
  1817→        })
  1818→        .select()
  1819→        .single();
  1820→
  1821→      if (createError) {
  1822→        console.error('Failed to create contractor lead:', createError);
  1823→      } else {
  1824→        createdLeadId = newLead.id;
  1825→
  1826→        // Also research and cache company intel
  1827→        perplexityService.searchCompany(companyName).catch(err => {
  1828→          console.error('Background company research error:', err);
  1829→        });
  1830→
  1831→        // Add note about the referral
  1832→        await supabase.from('lead_notes').insert({
  1833→          lead_id: newLead.id,
  1834→          content: `[Auto-Extracted Contractor Lead]\n\nFound via worker intel from ${workerName}.${projectName ? `\nProject: ${projectName}` : ''}\n\nJob Title: ${searchResult.person.job_title || 'Unknown'}\nConfidence: ${Math.round((searchResult.confidence || 0.5) * 100)}%`,
  1835→          created_by: userEmail || 'contractor-extraction',
  1836→        });
  1837→      }
  1838→    }
  1839→
  1840→    res.json({
  1841→      success: true,
  1842→      found: true,
  1843→      candidate,
  1844→      lead_id: createdLeadId,
  1845→      notes: searchResult.notes,
  1846→    });
  1847→  } catch (error) {
  1848→    console.error('Extract contractor error:', error);
  1849→    res.status(500).json({ error: 'Failed to extract contractor' });
  1850→  }
  1851→});
  1852→
  1853→/**
  1854→ * POST /api/ai/parse-intel-voice
  1855→ * Parse voice note and extract intel updates
  1856→ */
  1857→router.post('/parse-intel-voice', async (req, res) => {
  1858→  try {
  1859→    const { transcript, type, currentData } = req.body;
  1860→
  1861→    if (!transcript) {
  1862→      return res.status(400).json({ error: 'Transcript required' });
  1863→    }
  1864→
  1865→    const prompt = `Parse this voice note about a ${type === 'company' ? 'company' : 'person'} and extract any updates.
  1866→
  1867→VOICE NOTE: "${transcript}"
  1868→
  1869→CURRENT DATA:
  1870→${JSON.stringify(currentData, null, 2)}
  1871→
  1872→Extract any updates mentioned. Common patterns:
  1873→- "he's the owner" → job_title: "Owner"
  1874→- "12 guys on the books" → size: "small" (or note about team size)
  1875→- "they use WorkPac" → competitor info (add to notes)
  1876→- "big job starting March" → project info (add to notes)
  1877→- "it's a formwork company" → industry: "Formwork"
  1878→
  1879→Return JSON:
  1880→{
  1881→  "field_updates": {
  1882→    "job_title": "new value if mentioned",
  1883→    "size": "new value if mentioned",
  1884→    "industry": "new value if mentioned",
  1885→    "years_at_company": number if mentioned
  1886→  },
  1887→  "note_to_add": "Any info that should be saved as a note (competitor info, project details, etc.)"
  1888→}
  1889→
  1890→Only include fields that were actually mentioned. If nothing specific was mentioned, return empty objects.`;
  1891→
  1892→    if (!openai) {
  1893→      // If AI not available, just return the transcript as a note
  1894→      return res.json({
  1895→        success: true,
  1896→        parsed: {
  1897→          field_updates: {},
  1898→          note_to_add: transcript,
  1899→        },
  1900→      });
  1901→    }
  1902→
  1903→    const response = await openai.chat.completions.create({
  1904→      model: 'gpt-4o-mini',
  1905→      messages: [{ role: 'user', content: prompt }],
  1906→      response_format: { type: 'json_object' },
  1907→      temperature: 0.3,
  1908→    });
  1909→
  1910→    if (!response?.choices?.[0]?.message?.content) {
  1911→      // If AI not available, just return the transcript as a note
  1912→      return res.json({
  1913→        success: true,
  1914→        parsed: {
  1915→          field_updates: {},
  1916→          note_to_add: transcript,
  1917→        },
  1918→      });
  1919→    }
  1920→
  1921→    const parsed = JSON.parse(response.choices[0].message.content);
  1922→
  1923→    res.json({
  1924→      success: true,
  1925→      parsed,
  1926→    });
  1927→  } catch (error) {
  1928→    console.error('Parse intel voice error:', error);
  1929→    // Fallback: just return transcript as note
  1930→    res.json({
  1931→      success: true,
  1932→      parsed: {
  1933→        field_updates: {},
  1934→        note_to_add: req.body.transcript,
  1935→      },
  1936→    });
  1937→  }
  1938→});
  1939→
  1940→module.exports = router;
  1941→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>