Good. Now let me find the `ai_chat` case in extension2v1 and read both versions to understand the differences.


No matches found


2278:        case 'ai_chat':

2278:        case 'ai_chat':


Interesting - `extension2v1` doesn't have an `ai_chat` case at all! Let me check what it has instead. Let me look for the AI-related cases.


`extension2v1` doesn't have `ai_chat` at all! Let me find what AI chat endpoints it has.


2278:        case 'ai_chat':
4278:        case 'ai_chat_sessions':
4307:        case 'ai_chat_session':
4331:        case 'ai_chat_session_delete':
4344:        case 'ai_contact_memories':
4359:        case 'ai_contact_memory_add':
4386:        case 'ai_contact_memory_delete':


So `extension2v1` has NONE of our new AI chat endpoints. I need to find where its `default` case is and add all our new endpoints before it. Let me also check what the `ask_ai` case looks like in v1 (it was the predecessor).


2164:        case 'ask_ai':

2164:        case 'ask_ai':


3202:                default:
5056:        default:


Now let me read the `ask_ai` case in v1 and our `ai_chat` case in extension2 to understand the full scope.


  2164→        case 'ask_ai':
  2165→            if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
  2166→                jsonError('POST required', 405);
  2167→            }
  2168→
  2169→            // Handle JSON or form data
  2170→            $contentType = $_SERVER['CONTENT_TYPE'] ?? '';
  2171→            if (strpos($contentType, 'application/json') !== false) {
  2172→                $jsonData = json_decode(file_get_contents('php://input'), true);
  2173→                $question = trim($jsonData['question'] ?? '');
  2174→                $threadId = $jsonData['thread_id'] ?? null;
  2175→                $contactId = (int)($jsonData['contact_id'] ?? 0);
  2176→            } else {
  2177→                $question = trim($_POST['question'] ?? '');
  2178→                $threadId = $_POST['thread_id'] ?? null;
  2179→                $contactId = (int)($_POST['contact_id'] ?? 0);
  2180→            }
  2181→
  2182→            if (!$question) jsonError('question required');
  2183→
  2184→            // Auto-resolve long URL thread ID to short hex ID from cache
  2185→            if ($threadId) {
  2186→                $threadId = resolveThreadId($threadId);
  2187→            }
  2188→
  2189→            // Build context
  2190→            $context = [];
  2191→
  2192→            if ($threadId) {
  2193→                $emails = $db->fetchAll(
  2194→                    "SELECT * FROM emails WHERE thread_id = ? ORDER BY date ASC",
  2195→                    [$threadId]
  2196→                );
  2197→                $context['thread'] = $emails;
  2198→            }
  2199→
  2200→            if ($contactId) {
  2201→                $contact = $db->fetchOne("SELECT * FROM contacts WHERE id = ?", [$contactId]);
  2202→                $profile = $db->fetchOne("SELECT * FROM contact_profiles WHERE contact_id = ?", [$contactId]);
  2203→                $context['contact'] = $contact;
  2204→                $context['profile'] = $profile;
  2205→            }
  2206→
  2207→            // Build prompt with context
  2208→            $contextText = "";
  2209→
  2210→            // Add Important Context
  2211→            $impCtx = new ImportantContext();
  2212→            $contextText .= $impCtx->getFormattedContext($contactId, $threadId);
  2213→
  2214→            if (!empty($context['thread'])) {
  2215→                $contextText .= "EMAIL THREAD:\n";
  2216→                foreach ($context['thread'] as $email) {
  2217→                    $dir = $email['direction'] === 'inbound' ? 'FROM THEM' : 'FROM ME';
  2218→                    $contextText .= "{$dir} ({$email['date']}): {$email['subject']}\n";
  2219→                    $contextText .= substr($email['body_text'], 0, 500) . "\n\n";
  2220→                }
  2221→            }
  2222→            if (!empty($context['contact'])) {
  2223→                $contextText .= "CONTACT: {$context['contact']['name']} ({$context['contact']['email']})\n";
  2224→            }
  2225→            if (!empty($context['profile'])) {
  2226→                $contextText .= "PROFILE: Trust score: " . ($context['profile']['overall_trust_score'] ?? 'N/A') . "\n";
  2227→            }
  2228→
  2229→            $prompt = <<<PROMPT
  2230→Based on this context:
  2231→
  2232→{$contextText}
  2233→
  2234→User question: {$question}
  2235→
  2236→Provide a helpful, concise answer based on the email content and any available profile information.
  2237→Focus on actionable insights and specific observations from the emails.
  2238→PROMPT;
  2239→
  2240→            // Call Claude
  2241→            $analyzer = new ThreadAnalyzer();
  2242→
  2243→            try {
  2244→                $response = $analyzer->askClaude($prompt, $threadId);
  2245→                jsonResponse([
  2246→                    'success' => true,
  2247→                    'answer' => $response
  2248→                ]);
  2249→            } catch (Exception $e) {
  2250→                jsonError('AI request failed: ' . $e->getMessage());
  2251→            }
  2252→            break;
  2253→
  2254→        // ==========================================
  2255→        // Generate Reply Suggestions (with ChromaDB context)
  2256→        // ==========================================
  2257→        case 'generate_reply':
  2258→            if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
  2259→                jsonError('POST required', 405);
  2260→            }
  2261→
  2262→            // Handle JSON or form data
  2263→            $contentType = $_SERVER['CONTENT_TYPE'] ?? '';
  2264→            if (strpos($contentType, 'application/json') !== false) {
  2265→                $jsonData = json_decode(file_get_contents('php://input'), true);
  2266→                $threadId = $jsonData['thread_id'] ?? '';
  2267→                $tone = $jsonData['tone'] ?? 'professional';
  2268→                $intent = $jsonData['intent'] ?? '';
  2269→                $customInstructions = $jsonData['custom_instructions'] ?? '';
  2270→                $aiModel = $jsonData['ai_model'] ?? 'openai';
  2271→            } else {
  2272→                $threadId = $_POST['thread_id'] ?? '';
  2273→                $tone = $_POST['tone'] ?? 'professional';
  2274→                $intent = $_POST['intent'] ?? '';
  2275→                $customInstructions = $_POST['custom_instructions'] ?? '';
  2276→                $aiModel = $_POST['ai_model'] ?? 'openai';
  2277→            }
  2278→
  2279→            if (!$threadId) jsonError('thread_id required');
  2280→
  2281→            // Auto-resolve long URL thread ID to short hex ID from cache
  2282→            $threadId = resolveThreadId($threadId);
  2283→
  2284→            // Get thread context from MongoDB
  2285→            $mongo = \EmailIntelligence\MongoDatabase::getInstance();
  2286→            $emails = $mongo->findAll('emails', ['thread_id' => $threadId], [
  2287→                'sort' => ['date' => 1]
  2288→            ]);
  2289→
  2290→            if (empty($emails)) {
  2291→                jsonError('Thread not found');
  2292→            }
  2293→
  2294→            // Get contact ID from the first email
  2295→            $contactId = $emails[0]['contact_id'] ?? null;
  2296→
  2297→            // Get contact profile if available
  2298→            $profile = null;
  2299→            $profileStr = '';
  2300→            if ($contactId) {
  2301→                $profile = $mongo->findOne('contact_profiles', ['contact_id' => (int)$contactId]);
  2302→                if ($profile) {
  2303→                    $profileStr = "\n\nCLIENT PROFILE:
  2304→- Communication Style: " . ($profile['communication_style'] ?? 'Unknown') . "
  2305→- Honesty Score: " . ($profile['honesty_score'] ?? 'N/A') . "%
  2306→- Best Approach: " . implode(', ', $profile['best_approach'] ?? []);
  2307→                }
  2308→            }
  2309→
  2310→            // Build thread summary from MongoDB emails
  2311→            $threadSummary = "";
  2312→            foreach ($emails as $email) {
  2313→                $direction = ($email['direction'] ?? '') === 'inbound' ? 'FROM THEM' : 'FROM US';

        case 'ask_ai':
            if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
                jsonError('POST required', 405);
            }

            // Handle JSON or form data
            $contentType = $_SERVER['CONTENT_TYPE'] ?? '';
            if (strpos($contentType, 'application/json') !== false) {
                $jsonData = json_decode(file_get_contents('php://input'), true);
                $question = trim($jsonData['question'] ?? '');
                $threadId = $jsonData['thread_id'] ?? null;
                $contactId = (int)($jsonData['contact_id'] ?? 0);
            } else {
                $question = trim($_POST['question'] ?? '');
                $threadId = $_POST['thread_id'] ?? null;
                $contactId = (int)($_POST['contact_id'] ?? 0);
            }

            if (!$question) jsonError('question required');

            // Auto-resolve long URL thread ID to short hex ID from cache
            if ($threadId) {
                $threadId = resolveThreadId($threadId);
            }

            // Build context
            $context = [];

            if ($threadId) {
                $emails = $db->fetchAll(
                    "SELECT * FROM emails WHERE thread_id = ? ORDER BY date ASC",
                    [$threadId]
                );
                $context['thread'] = $emails;
            }

            if ($contactId) {
                $contact = $db->fetchOne("SELECT * FROM contacts WHERE id = ?", [$contactId]);
                $profile = $db->fetchOne("SELECT * FROM contact_profiles WHERE contact_id = ?", [$contactId]);
                $context['contact'] = $contact;
                $context['profile'] = $profile;
            }

            // Build prompt with context
            $contextText = "";

            // Add Important Context
            $impCtx = new ImportantContext();
            $contextText .= $impCtx->getFormattedContext($contactId, $threadId);

            if (!empty($context['thread'])) {
                $contextText .= "EMAIL THREAD:\n";
                foreach ($context['thread'] as $email) {
                    $dir = $email['direction'] === 'inbound' ? 'FROM THEM' : 'FROM ME';
                    $contextText .= "{$dir} ({$email['date']}): {$email['subject']}\n";
                    $contextText .= substr($email['body_text'], 0, 500) . "\n\n";
                }
            }
            if (!empty($context['contact'])) {
                $contextText .= "CONTACT: {$context['contact']['name']} ({$context['contact']['email']})\n";
            }
            if (!empty($context['profile'])) {
                $contextText .= "PROFILE: Trust score: " . ($context['profile']['overall_trust_score'] ?? 'N/A') . "\n";
            }

            $prompt = <<<PROMPT
Based on this context:

{$contextText}

User question: {$question}

Provide a helpful, concise answer based on the email content and any available profile information.
Focus on actionable insights and specific observations from the emails.
PROMPT;

            // Call Claude
            $analyzer = new ThreadAnalyzer();

            try {
                $response = $analyzer->askClaude($prompt, $threadId);
                jsonResponse([
                    'success' => true,
                    'answer' => $response
                ]);
            } catch (Exception $e) {
                jsonError('AI request failed: ' . $e->getMessage());
            }
            break;

        // ==========================================
        // Generate Reply Suggestions (with ChromaDB context)
        // ==========================================
        case 'generate_reply':
            if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
                jsonError('POST required', 405);
            }

            // Handle JSON or form data
            $contentType = $_SERVER['CONTENT_TYPE'] ?? '';
            if (strpos($contentType, 'application/json') !== false) {
                $jsonData = json_decode(file_get_contents('php://input'), true);
                $threadId = $jsonData['thread_id'] ?? '';
                $tone = $jsonData['tone'] ?? 'professional';
                $intent = $jsonData['intent'] ?? '';
                $customInstructions = $jsonData['custom_instructions'] ?? '';
                $aiModel = $jsonData['ai_model'] ?? 'openai';
            } else {
                $threadId = $_POST['thread_id'] ?? '';
                $tone = $_POST['tone'] ?? 'professional';
                $intent = $_POST['intent'] ?? '';
                $customInstructions = $_POST['custom_instructions'] ?? '';
                $aiModel = $_POST['ai_model'] ?? 'openai';
            }

            if (!$threadId) jsonError('thread_id required');

            // Auto-resolve long URL thread ID to short hex ID from cache
            $threadId = resolveThreadId($threadId);

            // Get thread context from MongoDB
            $mongo = \EmailIntelligence\MongoDatabase::getInstance();
            $emails = $mongo->findAll('emails', ['thread_id' => $threadId], [
                'sort' => ['date' => 1]
            ]);

            if (empty($emails)) {
                jsonError('Thread not found');
            }

            // Get contact ID from the first email
            $contactId = $emails[0]['contact_id'] ?? null;

            // Get contact profile if available
            $profile = null;
            $profileStr = '';
            if ($contactId) {
                $profile = $mongo->findOne('contact_profiles', ['contact_id' => (int)$contactId]);
                if ($profile) {
                    $profileStr = "\n\nCLIENT PROFILE:
- Communication Style: " . ($profile['communication_style'] ?? 'Unknown') . "
- Honesty Score: " . ($profile['honesty_score'] ?? 'N/A') . "%
- Best Approach: " . implode(', ', $profile['best_approach'] ?? []);
                }
            }

            // Build thread summary from MongoDB emails
            $threadSummary = "";
            foreach ($emails as $email) {
                $direction = ($email['direction'] ?? '') === 'inbound' ? 'FROM THEM' : 'FROM US';


  2275→        // ==========================================
  2276→        // AI Chat - Multi-model chat with full context, agents & memory
  2277→        // ==========================================
  2278→        case 'ai_chat':
  2279→            if ($_SERVER['REQUEST_METHOD'] !== 'POST') jsonError('POST required', 405);
  2280→
  2281→            $jsonData = json_decode(file_get_contents('php://input'), true) ?? [];
  2282→            $chatMessage   = trim($jsonData['message'] ?? '');
  2283→            $chatModel     = $jsonData['model'] ?? 'gpt-4o';
  2284→            $chatProvider  = $jsonData['provider'] ?? null;       // explicit provider from frontend
  2285→            $chatMode      = $jsonData['mode'] ?? 'general';
  2286→            $chatAgent     = $jsonData['agent'] ?? null;          // agent preset ID
  2287→            $chatThreadId  = $jsonData['thread_id'] ?? null;      // single thread (backward compat)
  2288→            $chatThreadIds = $jsonData['thread_ids'] ?? [];       // multi-thread context
  2289→            $chatContactId = (int)($jsonData['contact_id'] ?? 0);
  2290→            $chatHistory   = $jsonData['history'] ?? [];          // [{role,content}]
  2291→            $chatEmailId   = (int)($jsonData['email_id'] ?? 0);
  2292→            $chatSessionId = $jsonData['session_id'] ?? null;     // session persistence
  2293→
  2294→            if (!$chatMessage) jsonError('message required');
  2295→
  2296→            // Resolve thread IDs
  2297→            if ($chatThreadId) $chatThreadId = resolveThreadId($chatThreadId);
  2298→            $resolvedThreadIds = [];
  2299→            foreach ($chatThreadIds as $tid) {
  2300→                $resolved = resolveThreadId($tid);
  2301→                if ($resolved) $resolvedThreadIds[] = $resolved;
  2302→            }
  2303→            // Single thread_id fallback into the array
  2304→            if ($chatThreadId && empty($resolvedThreadIds)) {
  2305→                $resolvedThreadIds = [$chatThreadId];
  2306→            }
  2307→
  2308→            // --- Agent presets ---
  2309→            $agentPresets = [
  2310→                'general' => [
  2311→                    'system' => "You are a business intelligence assistant with access to email and contact data. Answer questions, provide analysis, and help with business decisions.",
  2312→                    'autoMemory' => false,
  2313→                ],
  2314→                'quote_analyst' => [
  2315→                    'system' => "You are a pricing and quote analyst. Analyze email threads to find all pricing mentions, quotes sent, counter-offers, and final agreed amounts. Identify negotiation patterns, price sensitivity, and recommend optimal pricing strategies. Be specific with numbers and dates.",
  2316→                    'autoMemory' => true,
  2317→                ],
  2318→                'deception_detector' => [
  2319→                    'system' => "You are a deception and inconsistency analyst. Analyze email threads for contradictions between statements made at different times, tone shifts that suggest dishonesty, pressure tactics, evasive responses, broken promises, and manipulative language. Rate deception risk on a scale of 1-10 and cite specific evidence from the emails.",
  2320→                    'autoMemory' => true,
  2321→                ],
  2322→                'contact_profiler' => [
  2323→                    'system' => "You are a contact intelligence profiler. Analyze email threads to build a comprehensive profile of the contact: communication style, decision-making speed, priorities, negotiation habits, reliability, cultural cues, and relationship dynamics. Provide actionable insights for working with this person.",
  2324→                    'autoMemory' => true,
  2325→                ],
  2326→                'email_composer' => [
  2327→                    'system' => "You are a professional email composer. Based on the email thread context, compose a ready-to-send email reply. Match the appropriate tone and formality level. Consider the negotiation position and relationship dynamics. Write only the email text with greeting and sign-off.",
  2328→                    'autoMemory' => false,
  2329→                ],
  2330→            ];
  2331→
  2332→            // Mode-specific system instructions (used when no agent selected)
  2333→            $modeInstructions = [
  2334→                'thread'  => "You are a business intelligence assistant analyzing an email thread. Provide insights about the conversation, relationships, risks, and opportunities. Be direct and specific.",
  2335→                'contact' => "You are a CRM intelligence assistant analyzing a contact's profile and history. Help understand their behavior, communication patterns, and how best to work with them.",
  2336→                'quote'   => "You are a business consultant helping estimate project costs and create quotes. Based on the email context, suggest realistic pricing, scope, and approach for software/CRM projects.",
  2337→                'reply'   => "You are an email writing assistant. Compose professional, ready-to-send email replies based on the thread context. Write only the reply text with greeting and sign-off.",
  2338→                'general' => "You are a business intelligence assistant with access to email and contact data. Answer questions, provide analysis, and help with business decisions.",
  2339→            ];
  2340→
  2341→            // Select system prompt: agent takes priority over mode
  2342→            $activeAgent = ($chatAgent && isset($agentPresets[$chatAgent])) ? $agentPresets[$chatAgent] : null;
  2343→            $sysMsg = $activeAgent ? $activeAgent['system'] : ($modeInstructions[$chatMode] ?? $modeInstructions['general']);
  2344→
  2345→            // --- Build context block ---
  2346→            $ctxParts = [];
  2347→            $ctxAdapter = QueryAdapter::getInstance();
  2348→            $ctxMongo = \EmailIntelligence\MongoDatabase::getInstance();
  2349→            $useMongoDB = $ctxAdapter->getActiveDatabase() === 'mongodb';
  2350→            $contextStats = ['threads' => 0, 'emails' => 0, 'estimated_tokens' => 0, 'truncated' => false];
  2351→
  2352→            // Token budget per model (context window sizes)
  2353→            $modelTokenLimits = [
  2354→                'gpt-4o' => 128000, 'gpt-4o-mini' => 128000, 'o1' => 200000, 'o1-mini' => 128000,
  2355→                'gpt-5.2' => 128000, 'gpt-5.2-pro' => 128000, 'gpt-5-mini' => 128000, 'gpt-5-nano' => 128000, 'gpt-5' => 128000, 'gpt-4.1' => 128000,
  2356→                'claude-sonnet-4-5-20250929' => 200000, 'claude-opus-4-5-20251101' => 200000, 'claude-haiku-4-5-20251001' => 200000,
  2357→                'gemini-2.5-flash' => 1000000, 'gemini-3-pro' => 1000000, 'gemini-2.5-flash-lite' => 1000000,
  2358→                'grok-4' => 131000, 'grok-3' => 131000, 'grok-3-mini' => 131000,
  2359→                'deepseek-chat' => 64000, 'deepseek-reasoner' => 64000,
  2360→                'qwen-max-latest' => 128000, 'qwen-turbo-latest' => 128000,
  2361→                'mistral-large-latest' => 128000, 'mistral-small-latest' => 128000,
  2362→                'command-a-03-2025' => 128000, 'command-r7b-12-2024' => 128000,
  2363→            ];
  2364→            $tokenLimit = $modelTokenLimits[$chatModel] ?? 128000;
  2365→            $contextTokenBudget = (int)($tokenLimit * 0.6); // 60% for context, 40% for response + history
  2366→
  2367→            // Multi-thread context
  2368→            $totalEmailCount = 0;
  2369→            $threadCount = count($resolvedThreadIds);
  2370→            foreach ($resolvedThreadIds as $idx => $tid) {
  2371→                if ($useMongoDB) {
  2372→                    $ctxEmails = $ctxMongo->findAll('emails', ['thread_id' => $tid], ['sort' => ['date' => 1]]);
  2373→                } else {
  2374→                    $ctxEmails = $db->fetchAll(
  2375→                        "SELECT direction, date, from_email, to_email, subject, body_text FROM emails WHERE thread_id = ? ORDER BY date ASC",
  2376→                        [$tid]
  2377→                    );
  2378→                }
  2379→                if ($ctxEmails) {
  2380→                    $threadLabel = $threadCount > 1 ? "THREAD " . ($idx + 1) . " OF {$threadCount}" : "EMAIL THREAD";
  2381→                    $ctxParts[] = "=== {$threadLabel} (" . count($ctxEmails) . " emails) ===";
  2382→                    foreach ($ctxEmails as $i => $em) {
  2383→                        $dir = ($em['direction'] ?? '') === 'inbound' ? 'FROM THEM' : 'FROM US';
  2384→                        // Strip quoted/duplicate content from replies, then truncate
  2385→                        $body = ThreadAnalyzer::stripQuotedContent($em['body_text'] ?? '');
  2386→                        $maxBody = $threadCount > 3 ? 500 : 800;
  2387→                        if (strlen($body) > $maxBody) $body = substr($body, 0, $maxBody) . '...';
  2388→                        $ctxParts[] = "Email #".($i+1)." {$dir} ({$em['date']})\nSubject: {$em['subject']}\n{$body}";
  2389→                    }
  2390→                    $totalEmailCount += count($ctxEmails);
  2391→
  2392→                    // Include existing thread analysis summary if available
  2393→                    if ($useMongoDB) {
  2394→                        $ctxAnalysis = $ctxMongo->findOne('thread_analysis', ['thread_id' => $tid]);
  2395→                    } else {
  2396→                        $ctxAnalysis = $db->fetchOne("SELECT summary FROM thread_analysis WHERE thread_id = ? AND is_analyzed=1", [$tid]);
  2397→                    }
  2398→                    if ($ctxAnalysis && !empty($ctxAnalysis['summary'])) {
  2399→                        $ctxParts[] = "Analysis: " . $ctxAnalysis['summary'];
  2400→                    }
  2401→                }
  2402→            }
  2403→
  2404→            // Add multi-thread summary header
  2405→            if ($threadCount > 1) {
  2406→                array_unshift($ctxParts, "=== MULTI-THREAD CONTEXT: {$totalEmailCount} total emails across {$threadCount} threads ===");
  2407→            }
  2408→            $contextStats['threads'] = $threadCount;
  2409→            $contextStats['emails'] = $totalEmailCount;
  2410→
  2411→            // Contact context
  2412→            if ($chatContactId) {
  2413→                if ($useMongoDB) {
  2414→                    $ctxContact = $ctxMongo->findOne('contacts', ['id' => $chatContactId]);
  2415→                    $ctxProfile = $ctxMongo->findOne('contact_profiles', ['contact_id' => $chatContactId]);
  2416→                } else {
  2417→                    $ctxContact = $db->fetchOne("SELECT name, email, type, company FROM contacts WHERE id = ?", [$chatContactId]);
  2418→                    $ctxProfile = $db->fetchOne("SELECT * FROM contact_profiles WHERE contact_id = ?", [$chatContactId]);
  2419→                }
  2420→                if ($ctxContact) {
  2421→                    $ctxParts[] = "=== CONTACT: {$ctxContact['name']} ({$ctxContact['email']}) ===";
  2422→                    if ($ctxProfile) {
  2423→                        $ctxParts[] = "Trust score: " . ($ctxProfile['overall_trust_score'] ?? 'N/A') .
  2424→                            " | Communication style: " . ($ctxProfile['communication_style'] ?? 'N/A');
  2425→                    }
  2426→                }
  2427→
  2428→                // Inject contact memory (persistent facts)
  2429→                $contactMemory = $ctxMongo->findOne('ai_contact_memory', ['contact_id' => $chatContactId]);
  2430→                if ($contactMemory && !empty($contactMemory['memories'])) {
  2431→                    $memFacts = array_map(function($m) { return "- " . $m['fact']; }, $contactMemory['memories']);
  2432→                    $ctxParts[] = "=== KNOWN FACTS ABOUT THIS CONTACT (from previous analyses) ===\n" . implode("\n", $memFacts);
  2433→                }
  2434→            }
  2435→
  2436→            // Single email context (for reply mode)
  2437→            if ($chatEmailId) {
  2438→                if ($useMongoDB) {
  2439→                    $ctxEmail = $ctxMongo->findOne('emails', ['id' => $chatEmailId]);
  2440→                } else {
  2441→                    $ctxEmail = $db->fetchOne("SELECT * FROM emails WHERE id = ?", [$chatEmailId]);
  2442→                }
  2443→                if ($ctxEmail) {
  2444→                    $replyBody = ThreadAnalyzer::stripQuotedContent($ctxEmail['body_text'] ?? '');
  2445→                    $ctxParts[] = "=== SPECIFIC EMAIL TO REPLY TO ===\nFrom: {$ctxEmail['from_email']}\nSubject: {$ctxEmail['subject']}\n{$replyBody}";
  2446→                }
  2447→            }
  2448→
  2449→            // Assemble context and apply token budget
  2450→            $contextBlock = implode("\n\n", $ctxParts);
  2451→            $estimatedTokens = (int)(strlen($contextBlock) / 3.5);
  2452→            $contextStats['estimated_tokens'] = $estimatedTokens;
  2453→
  2454→            if ($estimatedTokens > $contextTokenBudget && $contextTokenBudget > 0) {
  2455→                // Truncate context to fit budget, keeping the end (most recent) content
  2456→                $maxChars = (int)($contextTokenBudget * 3.5);
  2457→                $contextBlock = substr($contextBlock, -$maxChars);
  2458→                $contextBlock = "[... Earlier context omitted to fit model token limit ({$estimatedTokens} tokens estimated, budget: {$contextTokenBudget}) ...]\n\n" . $contextBlock;
  2459→                $contextStats['truncated'] = true;
  2460→                $contextStats['estimated_tokens'] = $contextTokenBudget;
  2461→            }
  2462→
  2463→            if ($contextBlock) {
  2464→                $sysMsg .= "\n\nCONTEXT:\n" . $contextBlock;
  2465→            }
  2466→
  2467→            // Auto-memory instruction for agents that support it
  2468→            if ($activeAgent && $activeAgent['autoMemory'] && $chatContactId) {
  2469→                $sysMsg .= "\n\nIMPORTANT: If you discover important new facts about this contact (pricing patterns, contradictions, preferences, behavioral traits), include them at the end of your response in this exact format:\n[MEMORY]fact 1[/MEMORY]\n[MEMORY]fact 2[/MEMORY]\nKeep each fact concise (one sentence). Only include genuinely new, important insights.";
  2470→            }
  2471→
  2472→            // Build messages array with history
  2473→            $messages = [];
  2474→            foreach ($chatHistory as $h) {
  2475→                if (isset($h['role'], $h['content'])) {
  2476→                    $messages[] = ['role' => $h['role'], 'content' => $h['content']];
  2477→                }
  2478→            }
  2479→            $messages[] = ['role' => 'user', 'content' => $chatMessage];
  2480→
  2481→            // Determine provider (explicit field takes priority, then infer from model name)
  2482→            if (!$chatProvider) {
  2483→                $chatProvider = 'openai';
  2484→                if (strpos($chatModel, 'claude') !== false) $chatProvider = 'claude';
  2485→                elseif (strpos($chatModel, 'gemini') !== false) $chatProvider = 'gemini';
  2486→                elseif (strpos($chatModel, 'grok') !== false) $chatProvider = 'grok';
  2487→                elseif (strpos($chatModel, 'deepseek') !== false) $chatProvider = 'deepseek';
  2488→                elseif (strpos($chatModel, 'qwen') !== false) $chatProvider = 'qwen';
  2489→                elseif (strpos($chatModel, 'mistral') !== false || strpos($chatModel, 'codestral') !== false) $chatProvider = 'mistral';
  2490→                elseif (strpos($chatModel, 'command') !== false) $chatProvider = 'cohere';
  2491→            }
  2492→
  2493→            // Route through proxy for multi-model support
  2494→            $proxyUrl = 'https://mssql.danmarcrm.com/dev1/extension2/aiapi/api_codex51/api/index.php';
  2495→            $proxyPayload = json_encode([
  2496→                'action'       => 'ask',
  2497→                'user_content' => $chatMessage,
  2498→                'system'       => $sysMsg,
  2499→                'models'       => [$chatProvider => $chatModel],  // keyed by provider code
  2500→                'providers'    => [$chatProvider],
  2501→                'history'      => array_slice($chatHistory, -10),
  2502→            ]);
  2503→
  2504→            $ch = curl_init($proxyUrl);
  2505→            curl_setopt_array($ch, [
  2506→                CURLOPT_RETURNTRANSFER => true,
  2507→                CURLOPT_POST => true,
  2508→                CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
  2509→                CURLOPT_POSTFIELDS => $proxyPayload,
  2510→                CURLOPT_TIMEOUT => 120,
  2511→            ]);
  2512→            $proxyResponse = curl_exec($ch);
  2513→            $curlError = curl_error($ch);
  2514→            curl_close($ch);
  2515→
  2516→            if ($curlError) {
  2517→                // Fallback to direct ThreadAnalyzer if proxy fails
  2518→                try {
  2519→                    $analyzer = new ThreadAnalyzer($chatProvider === 'openai' ? 'openai' : 'claude', $chatModel);
  2520→                    $fullPrompt = $sysMsg . "\n\nUser: " . $chatMessage;
  2521→                    $answer = $analyzer->askClaude($fullPrompt);
  2522→                    jsonResponse(['success' => true, 'answer' => $answer, 'model' => $chatModel, 'via' => 'direct', 'context_stats' => $contextStats]);
  2523→                } catch (Exception $e) {
  2524→                    jsonError('AI request failed: ' . $e->getMessage());
  2525→                }
  2526→                break;
  2527→            }
  2528→
  2529→            $proxyResult = json_decode($proxyResponse, true);
  2530→            if (!$proxyResult || !$proxyResult['success']) {
  2531→                jsonError('AI proxy error: ' . ($proxyResult['error'] ?? 'Unknown error'));
  2532→            }
  2533→
  2534→            // Extract answer from proxy response
  2535→            $answer = '';
  2536→            $usedModel = $chatModel;
  2537→            $responses = $proxyResult['responses'] ?? [];
  2538→            if (!empty($responses)) {
  2539→                $firstResponse = reset($responses);
  2540→                $answer = $firstResponse['response'] ?? $firstResponse['content'] ?? '';
  2541→                $usedModel = $firstResponse['model'] ?? $chatModel;
  2542→            }
  2543→            if (!$answer) {
  2544→                $answer = $proxyResult['content'] ?? $proxyResult['response'] ?? '';
  2545→            }
  2546→            if (!$answer) {
  2547→                jsonError('AI proxy returned empty response. Raw: ' . substr($proxyResponse, 0, 200));
  2548→            }
  2549→
  2550→            // Auto-extract memories from AI response
  2551→            $extractedMemories = [];
  2552→            if ($activeAgent && $activeAgent['autoMemory'] && $chatContactId) {
  2553→                if (preg_match_all('/\[MEMORY\](.*?)\[\/MEMORY\]/s', $answer, $memMatches)) {
  2554→                    foreach ($memMatches[1] as $memFact) {
  2555→                        $memFact = trim($memFact);
  2556→                        if ($memFact) $extractedMemories[] = $memFact;
  2557→                    }
  2558→                    // Strip [MEMORY] tags from the displayed answer
  2559→                    $answer = preg_replace('/\s*\[MEMORY\].*?\[\/MEMORY\]\s*/s', '', $answer);
  2560→                    $answer = trim($answer);
  2561→
  2562→                    // Save extracted memories to ai_contact_memory
  2563→                    if (!empty($extractedMemories)) {
  2564→                        $existingMemory = $ctxMongo->findOne('ai_contact_memory', ['contact_id' => $chatContactId]);
  2565→                        $memories = $existingMemory ? ($existingMemory['memories'] ?? []) : [];
  2566→                        foreach ($extractedMemories as $fact) {
  2567→                            $memories[] = [
  2568→                                'id' => uniqid('mem_', true),
  2569→                                'fact' => $fact,
  2570→                                'source' => 'auto',
  2571→                                'agent' => $chatAgent,
  2572→                                'session_id' => $chatSessionId,
  2573→                                'created_at' => date('Y-m-d H:i:s'),
  2574→                            ];
  2575→                        }
  2576→                        $ctxMongo->insertOrUpdate('ai_contact_memory', [
  2577→                                'contact_id' => $chatContactId,
  2578→                                'contact_email' => $ctxContact['email'] ?? '',
  2579→                                'memories' => $memories,
  2580→                                'updated_at' => date('Y-m-d H:i:s'),
  2581→                            ], ['contact_id']);
  2582→                    }
  2583→                }
  2584→            }
  2585→
  2586→            // Auto-save chat session
  2587→            if (!$chatSessionId) {
  2588→                $chatSessionId = uniqid('sess_', true);
  2589→            }
  2590→            $sessionMessages = [];
  2591→            // Load existing session messages if continuing
  2592→            $existingSession = $ctxMongo->findOne('ai_chat_sessions', ['session_id' => $chatSessionId]);
  2593→            if ($existingSession && !empty($existingSession['messages'])) {
  2594→                $sessionMessages = $existingSession['messages'];
  2595→            }
  2596→            // Append new message pair
  2597→            $sessionMessages[] = ['role' => 'user', 'content' => $chatMessage, 'timestamp' => date('Y-m-d H:i:s')];
  2598→            $sessionMessages[] = ['role' => 'assistant', 'content' => $answer, 'timestamp' => date('Y-m-d H:i:s'), 'model' => $usedModel];
  2599→
  2600→            $sessionTitle = $existingSession['title'] ?? substr($chatMessage, 0, 60);
  2601→            $ctxMongo->insertOrUpdate('ai_chat_sessions', [
  2602→                    'session_id' => $chatSessionId,
  2603→                    'created_at' => $existingSession['created_at'] ?? date('Y-m-d H:i:s'),
  2604→                    'updated_at' => date('Y-m-d H:i:s'),
  2605→                    'title' => $sessionTitle,
  2606→                    'agent' => $chatAgent ?? $chatMode,
  2607→                    'provider' => $chatProvider,
  2608→                    'model' => $usedModel,
  2609→                    'context_thread_ids' => $resolvedThreadIds,
  2610→                    'contact_id' => $chatContactId ?: null,
  2611→                    'contact_name' => $ctxContact['name'] ?? null,
  2612→                    'messages' => $sessionMessages,
  2613→                ], ['session_id']);
  2614→
  2615→            jsonResponse([
  2616→                'success'     => true,
  2617→                'answer'      => $answer,
  2618→                'model'       => $usedModel,
  2619→                'provider'    => $chatProvider,
  2620→                'via'         => 'proxy',
  2621→                'session_id'  => $chatSessionId,
  2622→                'context_stats' => $contextStats,
  2623→                'extracted_memories' => $extractedMemories,
  2624→            ]);
  2625→            break;
  2626→
  2627→
  2628→
  2629→        // ==========================================
  2630→        case 'generate_reply':
  2631→            if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
  2632→                jsonError('POST required', 405);
  2633→            }
  2634→

        // ==========================================
        // AI Chat - Multi-model chat with full context, agents & memory
        // ==========================================
        case 'ai_chat':
            if ($_SERVER['REQUEST_METHOD'] !== 'POST') jsonError('POST required', 405);

            $jsonData = json_decode(file_get_contents('php://input'), true) ?? [];
            $chatMessage   = trim($jsonData['message'] ?? '');
            $chatModel     = $jsonData['model'] ?? 'gpt-4o';
            $chatProvider  = $jsonData['provider'] ?? null;       // explicit provider from frontend
            $chatMode      = $jsonData['mode'] ?? 'general';
            $chatAgent     = $jsonData['agent'] ?? null;          // agent preset ID
            $chatThreadId  = $jsonData['thread_id'] ?? null;      // single thread (backward compat)
            $chatThreadIds = $jsonData['thread_ids'] ?? [];       // multi-thread context
            $chatContactId = (int)($jsonData['contact_id'] ?? 0);
            $chatHistory   = $jsonData['history'] ?? [];          // [{role,content}]
            $chatEmailId   = (int)($jsonData['email_id'] ?? 0);
            $chatSessionId = $jsonData['session_id'] ?? null;     // session persistence

            if (!$chatMessage) jsonError('message required');

            // Resolve thread IDs
            if ($chatThreadId) $chatThreadId = resolveThreadId($chatThreadId);
            $resolvedThreadIds = [];
            foreach ($chatThreadIds as $tid) {
                $resolved = resolveThreadId($tid);
                if ($resolved) $resolvedThreadIds[] = $resolved;
            }
            // Single thread_id fallback into the array
            if ($chatThreadId && empty($resolvedThreadIds)) {
                $resolvedThreadIds = [$chatThreadId];
            }

            // --- Agent presets ---
            $agentPresets = [
                'general' => [
                    'system' => "You are a business intelligence assistant with access to email and contact data. Answer questions, provide analysis, and help with business decisions.",
                    'autoMemory' => false,
                ],
                'quote_analyst' => [
                    'system' => "You are a pricing and quote analyst. Analyze email threads to find all pricing mentions, quotes sent, counter-offers, and final agreed amounts. Identify negotiation patterns, price sensitivity, and recommend optimal pricing strategies. Be specific with numbers and dates.",
                    'autoMemory' => true,
                ],
                'deception_detector' => [
                    'system' => "You are a deception and inconsistency analyst. Analyze email threads for contradictions between statements made at different times, tone shifts that suggest dishonesty, pressure tactics, evasive responses, broken promises, and manipulative language. Rate deception risk on a scale of 1-10 and cite specific evidence from the emails.",
                    'autoMemory' => true,
                ],
                'contact_profiler' => [
                    'system' => "You are a contact intelligence profiler. Analyze email threads to build a comprehensive profile of the contact: communication style, decision-making speed, priorities, negotiation habits, reliability, cultural cues, and relationship dynamics. Provide actionable insights for working with this person.",
                    'autoMemory' => true,
                ],
                'email_composer' => [
                    'system' => "You are a professional email composer. Based on the email thread context, compose a ready-to-send email reply. Match the appropriate tone and formality level. Consider the negotiation position and relationship dynamics. Write only the email text with greeting and sign-off.",
                    'autoMemory' => false,
                ],
            ];

            // Mode-specific system instructions (used when no agent selected)
            $modeInstructions = [
                'thread'  => "You are a business intelligence assistant analyzing an email thread. Provide insights about the conversation, relationships, risks, and opportunities. Be direct and specific.",
                'contact' => "You are a CRM intelligence assistant analyzing a contact's profile and history. Help understand their behavior, communication patterns, and how best to work with them.",
                'quote'   => "You are a business consultant helping estimate project costs and create quotes. Based on the email context, suggest realistic pricing, scope, and approach for software/CRM projects.",
                'reply'   => "You are an email writing assistant. Compose professional, ready-to-send email replies based on the thread context. Write only the reply text with greeting and sign-off.",
                'general' => "You are a business intelligence assistant with access to email and contact data. Answer questions, provide analysis, and help with business decisions.",
            ];

            // Select system prompt: agent takes priority over mode
            $activeAgent = ($chatAgent && isset($agentPresets[$chatAgent])) ? $agentPresets[$chatAgent] : null;
            $sysMsg = $activeAgent ? $activeAgent['system'] : ($modeInstructions[$chatMode] ?? $modeInstructions['general']);

            // --- Build context block ---
            $ctxParts = [];
            $ctxAdapter = QueryAdapter::getInstance();
            $ctxMongo = \EmailIntelligence\MongoDatabase::getInstance();
            $useMongoDB = $ctxAdapter->getActiveDatabase() === 'mongodb';
            $contextStats = ['threads' => 0, 'emails' => 0, 'estimated_tokens' => 0, 'truncated' => false];

            // Token budget per model (context window sizes)
            $modelTokenLimits = [
                'gpt-4o' => 128000, 'gpt-4o-mini' => 128000, 'o1' => 200000, 'o1-mini' => 128000,
                'gpt-5.2' => 128000, 'gpt-5.2-pro' => 128000, 'gpt-5-mini' => 128000, 'gpt-5-nano' => 128000, 'gpt-5' => 128000, 'gpt-4.1' => 128000,
                'claude-sonnet-4-5-20250929' => 200000, 'claude-opus-4-5-20251101' => 200000, 'claude-haiku-4-5-20251001' => 200000,
                'gemini-2.5-flash' => 1000000, 'gemini-3-pro' => 1000000, 'gemini-2.5-flash-lite' => 1000000,
                'grok-4' => 131000, 'grok-3' => 131000, 'grok-3-mini' => 131000,
                'deepseek-chat' => 64000, 'deepseek-reasoner' => 64000,
                'qwen-max-latest' => 128000, 'qwen-turbo-latest' => 128000,
                'mistral-large-latest' => 128000, 'mistral-small-latest' => 128000,
                'command-a-03-2025' => 128000, 'command-r7b-12-2024' => 128000,
            ];
            $tokenLimit = $modelTokenLimits[$chatModel] ?? 128000;
            $contextTokenBudget = (int)($tokenLimit * 0.6); // 60% for context, 40% for response + history

            // Multi-thread context
            $totalEmailCount = 0;
            $threadCount = count($resolvedThreadIds);
            foreach ($resolvedThreadIds as $idx => $tid) {
                if ($useMongoDB) {
                    $ctxEmails = $ctxMongo->findAll('emails', ['thread_id' => $tid], ['sort' => ['date' => 1]]);
                } else {
                    $ctxEmails = $db->fetchAll(
                        "SELECT direction, date, from_email, to_email, subject, body_text FROM emails WHERE thread_id = ? ORDER BY date ASC",
                        [$tid]
                    );
                }
                if ($ctxEmails) {
                    $threadLabel = $threadCount > 1 ? "THREAD " . ($idx + 1) . " OF {$threadCount}" : "EMAIL THREAD";
                    $ctxParts[] = "=== {$threadLabel} (" . count($ctxEmails) . " emails) ===";
                    foreach ($ctxEmails as $i => $em) {
                        $dir = ($em['direction'] ?? '') === 'inbound' ? 'FROM THEM' : 'FROM US';
                        // Strip quoted/duplicate content from replies, then truncate
                        $body = ThreadAnalyzer::stripQuotedContent($em['body_text'] ?? '');
                        $maxBody = $threadCount > 3 ? 500 : 800;
                        if (strlen($body) > $maxBody) $body = substr($body, 0, $maxBody) . '...';
                        $ctxParts[] = "Email #".($i+1)." {$dir} ({$em['date']})\nSubject: {$em['subject']}\n{$body}";
                    }
                    $totalEmailCount += count($ctxEmails);

                    // Include existing thread analysis summary if available
                    if ($useMongoDB) {
                        $ctxAnalysis = $ctxMongo->findOne('thread_analysis', ['thread_id' => $tid]);
                    } else {
                        $ctxAnalysis = $db->fetchOne("SELECT summary FROM thread_analysis WHERE thread_id = ? AND is_analyzed=1", [$tid]);
                    }
                    if ($ctxAnalysis && !empty($ctxAnalysis['summary'])) {
                        $ctxParts[] = "Analysis: " . $ctxAnalysis['summary'];
                    }
                }
            }

            // Add multi-thread summary header
            if ($threadCount > 1) {
                array_unshift($ctxParts, "=== MULTI-THREAD CONTEXT: {$totalEmailCount} total emails across {$threadCount} threads ===");
            }
            $contextStats['threads'] = $threadCount;
            $contextStats['emails'] = $totalEmailCount;

            // Contact context
            if ($chatContactId) {
                if ($useMongoDB) {
                    $ctxContact = $ctxMongo->findOne('contacts', ['id' => $chatContactId]);
                    $ctxProfile = $ctxMongo->findOne('contact_profiles', ['contact_id' => $chatContactId]);
                } else {
                    $ctxContact = $db->fetchOne("SELECT name, email, type, company FROM contacts WHERE id = ?", [$chatContactId]);
                    $ctxProfile = $db->fetchOne("SELECT * FROM contact_profiles WHERE contact_id = ?", [$chatContactId]);
                }
                if ($ctxContact) {
                    $ctxParts[] = "=== CONTACT: {$ctxContact['name']} ({$ctxContact['email']}) ===";
                    if ($ctxProfile) {
                        $ctxParts[] = "Trust score: " . ($ctxProfile['overall_trust_score'] ?? 'N/A') .
                            " | Communication style: " . ($ctxProfile['communication_style'] ?? 'N/A');
                    }
                }

                // Inject contact memory (persistent facts)
                $contactMemory = $ctxMongo->findOne('ai_contact_memory', ['contact_id' => $chatContactId]);
                if ($contactMemory && !empty($contactMemory['memories'])) {
                    $memFacts = array_map(function($m) { return "- " . $m['fact']; }, $contactMemory['memories']);
                    $ctxParts[] = "=== KNOWN FACTS ABOUT THIS CONTACT (from previous analyses) ===\n" . implode("\n", $memFacts);
                }
            }

            // Single email context (for reply mode)
            if ($chatEmailId) {
                if ($useMongoDB) {
                    $ctxEmail = $ctxMongo->findOne('emails', ['id' => $chatEmailId]);
                } else {
                    $ctxEmail = $db->fetchOne("SELECT * FROM emails WHERE id = ?", [$chatEmailId]);
                }
                if ($ctxEmail) {
                    $replyBody = ThreadAnalyzer::stripQuotedContent($ctxEmail['body_text'] ?? '');
                    $ctxParts[] = "=== SPECIFIC EMAIL TO REPLY TO ===\nFrom: {$ctxEmail['from_email']}\nSubject: {$ctxEmail['subject']}\n{$replyBody}";
                }
            }

            // Assemble context and apply token budget
            $contextBlock = implode("\n\n", $ctxParts);
            $estimatedTokens = (int)(strlen($contextBlock) / 3.5);
            $contextStats['estimated_tokens'] = $estimatedTokens;

            if ($estimatedTokens > $contextTokenBudget && $contextTokenBudget > 0) {
                // Truncate context to fit budget, keeping the end (most recent) content
                $maxChars = (int)($contextTokenBudget * 3.5);
                $contextBlock = substr($contextBlock, -$maxChars);
                $contextBlock = "[... Earlier context omitted to fit model token limit ({$estimatedTokens} tokens estimated, budget: {$contextTokenBudget}) ...]\n\n" . $contextBlock;
                $contextStats['truncated'] = true;
                $contextStats['estimated_tokens'] = $contextTokenBudget;
            }

            if ($contextBlock) {
                $sysMsg .= "\n\nCONTEXT:\n" . $contextBlock;
            }

            // Auto-memory instruction for agents that support it
            if ($activeAgent && $activeAgent['autoMemory'] && $chatContactId) {
                $sysMsg .= "\n\nIMPORTANT: If you discover important new facts about this contact (pricing patterns, contradictions, preferences, behavioral traits), include them at the end of your response in this exact format:\n[MEMORY]fact 1[/MEMORY]\n[MEMORY]fact 2[/MEMORY]\nKeep each fact concise (one sentence). Only include genuinely new, important insights.";
            }

            // Build messages array with history
            $messages = [];
            foreach ($chatHistory as $h) {
                if (isset($h['role'], $h['content'])) {
                    $messages[] = ['role' => $h['role'], 'content' => $h['content']];
                }
            }
            $messages[] = ['role' => 'user', 'content' => $chatMessage];

            // Determine provider (explicit field takes priority, then infer from model name)
            if (!$chatProvider) {
                $chatProvider = 'openai';
                if (strpos($chatModel, 'claude') !== false) $chatProvider = 'claude';
                elseif (strpos($chatModel, 'gemini') !== false) $chatProvider = 'gemini';
                elseif (strpos($chatModel, 'grok') !== false) $chatProvider = 'grok';
                elseif (strpos($chatModel, 'deepseek') !== false) $chatProvider = 'deepseek';
                elseif (strpos($chatModel, 'qwen') !== false) $chatProvider = 'qwen';
                elseif (strpos($chatModel, 'mistral') !== false || strpos($chatModel, 'codestral') !== false) $chatProvider = 'mistral';
                elseif (strpos($chatModel, 'command') !== false) $chatProvider = 'cohere';
            }

            // Route through proxy for multi-model support
            $proxyUrl = 'https://mssql.danmarcrm.com/dev1/extension2/aiapi/api_codex51/api/index.php';
            $proxyPayload = json_encode([
                'action'       => 'ask',
                'user_content' => $chatMessage,
                'system'       => $sysMsg,
                'models'       => [$chatProvider => $chatModel],  // keyed by provider code
                'providers'    => [$chatProvider],
                'history'      => array_slice($chatHistory, -10),
            ]);

            $ch = curl_init($proxyUrl);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_POST => true,
                CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
                CURLOPT_POSTFIELDS => $proxyPayload,
                CURLOPT_TIMEOUT => 120,
            ]);
            $proxyResponse = curl_exec($ch);
            $curlError = curl_error($ch);
            curl_close($ch);

            if ($curlError) {
                // Fallback to direct ThreadAnalyzer if proxy fails
                try {
                    $analyzer = new ThreadAnalyzer($chatProvider === 'openai' ? 'openai' : 'claude', $chatModel);
                    $fullPrompt = $sysMsg . "\n\nUser: " . $chatMessage;
                    $answer = $analyzer->askClaude($fullPrompt);
                    jsonResponse(['success' => true, 'answer' => $answer, 'model' => $chatModel, 'via' => 'direct', 'context_stats' => $contextStats]);
                } catch (Exception $e) {
                    jsonError('AI request failed: ' . $e->getMessage());
                }
                break;
            }

            $proxyResult = json_decode($proxyResponse, true);
            if (!$proxyResult || !$proxyResult['success']) {
                jsonError('AI proxy error: ' . ($proxyResult['error'] ?? 'Unknown error'));
            }

            // Extract answer from proxy response
            $answer = '';
            $usedModel = $chatModel;
            $responses = $proxyResult['responses'] ?? [];
            if (!empty($responses)) {
                $firstResponse = reset($responses);
                $answer = $firstResponse['response'] ?? $firstResponse['content'] ?? '';
                $usedModel = $firstResponse['model'] ?? $chatModel;
            }
            if (!$answer) {
                $answer = $proxyResult['content'] ?? $proxyResult['response'] ?? '';
            }
            if (!$answer) {
                jsonError('AI proxy returned empty response. Raw: ' . substr($proxyResponse, 0, 200));
            }

            // Auto-extract memories from AI response
            $extractedMemories = [];
            if ($activeAgent && $activeAgent['autoMemory'] && $chatContactId) {
                if (preg_match_all('/\[MEMORY\](.*?)\[\/MEMORY\]/s', $answer, $memMatches)) {
                    foreach ($memMatches[1] as $memFact) {
                        $memFact = trim($memFact);
                        if ($memFact) $extractedMemories[] = $memFact;
                    }
                    // Strip [MEMORY] tags from the displayed answer
                    $answer = preg_replace('/\s*\[MEMORY\].*?\[\/MEMORY\]\s*/s', '', $answer);
                    $answer = trim($answer);

                    // Save extracted memories to ai_contact_memory
                    if (!empty($extractedMemories)) {
                        $existingMemory = $ctxMongo->findOne('ai_contact_memory', ['contact_id' => $chatContactId]);
                        $memories = $existingMemory ? ($existingMemory['memories'] ?? []) : [];
                        foreach ($extractedMemories as $fact) {
                            $memories[] = [
                                'id' => uniqid('mem_', true),
                                'fact' => $fact,
                                'source' => 'auto',
                                'agent' => $chatAgent,
                                'session_id' => $chatSessionId,
                                'created_at' => date('Y-m-d H:i:s'),
                            ];
                        }
                        $ctxMongo->insertOrUpdate('ai_contact_memory', [
                                'contact_id' => $chatContactId,
                                'contact_email' => $ctxContact['email'] ?? '',
                                'memories' => $memories,
                                'updated_at' => date('Y-m-d H:i:s'),
                            ], ['contact_id']);
                    }
                }
            }

            // Auto-save chat session
            if (!$chatSessionId) {
                $chatSessionId = uniqid('sess_', true);
            }
            $sessionMessages = [];
            // Load existing session messages if continuing
            $existingSession = $ctxMongo->findOne('ai_chat_sessions', ['session_id' => $chatSessionId]);
            if ($existingSession && !empty($existingSession['messages'])) {
                $sessionMessages = $existingSession['messages'];
            }
            // Append new message pair
            $sessionMessages[] = ['role' => 'user', 'content' => $chatMessage, 'timestamp' => date('Y-m-d H:i:s')];
            $sessionMessages[] = ['role' => 'assistant', 'content' => $answer, 'timestamp' => date('Y-m-d H:i:s'), 'model' => $usedModel];

            $sessionTitle = $existingSession['title'] ?? substr($chatMessage, 0, 60);
            $ctxMongo->insertOrUpdate('ai_chat_sessions', [
                    'session_id' => $chatSessionId,
                    'created_at' => $existingSession['created_at'] ?? date('Y-m-d H:i:s'),
                    'updated_at' => date('Y-m-d H:i:s'),
                    'title' => $sessionTitle,
                    'agent' => $chatAgent ?? $chatMode,
                    'provider' => $chatProvider,
                    'model' => $usedModel,
                    'context_thread_ids' => $resolvedThreadIds,
                    'contact_id' => $chatContactId ?: null,
                    'contact_name' => $ctxContact['name'] ?? null,
                    'messages' => $sessionMessages,
                ], ['session_id']);

            jsonResponse([
                'success'     => true,
                'answer'      => $answer,
                'model'       => $usedModel,
                'provider'    => $chatProvider,
                'via'         => 'proxy',
                'session_id'  => $chatSessionId,
                'context_stats' => $contextStats,
                'extracted_memories' => $extractedMemories,
            ]);
            break;

        // ==========================================
        case 'generate_reply':
            if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
                jsonError('POST required', 405);
            }


Now I see the full picture. In `extension2v1`, `ask_ai` (line 2164) is the old simple version. I need to add our new `ai_chat` case. Let me also find where to add the 6 new endpoints. Let me read around the `default` case in v1.


  5045→            $mongo->update('extension_scrape_queue',
  5046→                ['_id' => new \MongoDB\BSON\ObjectId($taskId)],
  5047→                ['status' => 'completed', 'completed_at' => date('Y-m-d H:i:s')]
  5048→            );
  5049→
  5050→            jsonResponse(['success' => true]);
  5051→            break;
  5052→
  5053→        // ==========================================
  5054→        // Default
  5055→        // ==========================================
  5056→        default:
  5057→            jsonError('Unknown action: ' . $action, 400);
  5058→    }
  5059→
  5060→} catch (Exception $e) {
  5061→    jsonError('Server error: ' . $e->getMessage(), 500);
  5062→}
  5063→

            $mongo->update('extension_scrape_queue',
                ['_id' => new \MongoDB\BSON\ObjectId($taskId)],
                ['status' => 'completed', 'completed_at' => date('Y-m-d H:i:s')]
            );

            jsonResponse(['success' => true]);
            break;

        // ==========================================
        // Default
        // ==========================================
        default:
            jsonError('Unknown action: ' . $action, 400);
    }

} catch (Exception $e) {
    jsonError('Server error: ' . $e->getMessage(), 500);
}


  4275→        // ==========================================
  4276→        // AI Chat Sessions - List recent sessions
  4277→        // ==========================================
  4278→        case 'ai_chat_sessions':
  4279→            $ctxMongo = \EmailIntelligence\MongoDatabase::getInstance();
  4280→            $limit = (int)($_GET['limit'] ?? 20);
  4281→            $sessions = $ctxMongo->findAll('ai_chat_sessions', [], [
  4282→                'sort' => ['updated_at' => -1],
  4283→                'limit' => $limit,
  4284→            ]);
  4285→            // Strip full messages from list view (just return metadata)
  4286→            $sessionList = array_map(function($s) {
  4287→                return [
  4288→                    'session_id' => $s['session_id'] ?? '',
  4289→                    'title' => $s['title'] ?? 'Untitled',
  4290→                    'agent' => $s['agent'] ?? 'general',
  4291→                    'provider' => $s['provider'] ?? '',
  4292→                    'model' => $s['model'] ?? '',
  4293→                    'contact_name' => $s['contact_name'] ?? null,
  4294→                    'contact_id' => $s['contact_id'] ?? null,
  4295→                    'context_thread_ids' => $s['context_thread_ids'] ?? [],
  4296→                    'message_count' => count($s['messages'] ?? []),
  4297→                    'created_at' => $s['created_at'] ?? '',
  4298→                    'updated_at' => $s['updated_at'] ?? '',
  4299→                ];
  4300→            }, $sessions);
  4301→            jsonResponse(['success' => true, 'sessions' => $sessionList]);
  4302→            break;
  4303→
  4304→        // ==========================================
  4305→        // AI Chat Session - Load a specific session
  4306→        // ==========================================
  4307→        case 'ai_chat_session':
  4308→            $sessionId = $_GET['session_id'] ?? '';
  4309→            if (!$sessionId) jsonError('session_id required');
  4310→            $ctxMongo = \EmailIntelligence\MongoDatabase::getInstance();
  4311→            $session = $ctxMongo->findOne('ai_chat_sessions', ['session_id' => $sessionId]);
  4312→            if (!$session) jsonError('Session not found', 404);
  4313→            jsonResponse(['success' => true, 'session' => [
  4314→                'session_id' => $session['session_id'],
  4315→                'title' => $session['title'] ?? 'Untitled',
  4316→                'agent' => $session['agent'] ?? 'general',
  4317→                'provider' => $session['provider'] ?? '',
  4318→                'model' => $session['model'] ?? '',
  4319→                'contact_id' => $session['contact_id'] ?? null,
  4320→                'contact_name' => $session['contact_name'] ?? null,
  4321→                'context_thread_ids' => $session['context_thread_ids'] ?? [],
  4322→                'messages' => $session['messages'] ?? [],
  4323→                'created_at' => $session['created_at'] ?? '',
  4324→                'updated_at' => $session['updated_at'] ?? '',
  4325→            ]]);
  4326→            break;
  4327→
  4328→        // ==========================================
  4329→        // AI Chat Session Delete
  4330→        // ==========================================
  4331→        case 'ai_chat_session_delete':
  4332→            if ($_SERVER['REQUEST_METHOD'] !== 'POST') jsonError('POST required', 405);
  4333→            $jsonData = json_decode(file_get_contents('php://input'), true) ?? [];
  4334→            $sessionId = $jsonData['session_id'] ?? '';
  4335→            if (!$sessionId) jsonError('session_id required');
  4336→            $ctxMongo = \EmailIntelligence\MongoDatabase::getInstance();
  4337→            $deleted = $ctxMongo->delete('ai_chat_sessions', ['session_id' => $sessionId]);
  4338→            jsonResponse(['success' => true, 'deleted' => $deleted]);
  4339→            break;
  4340→
  4341→        // ==========================================
  4342→        // AI Contact Memories - Get memories for a contact
  4343→        // ==========================================
  4344→        case 'ai_contact_memories':
  4345→            $contactId = (int)($_GET['contact_id'] ?? 0);
  4346→            if (!$contactId) jsonError('contact_id required');
  4347→            $ctxMongo = \EmailIntelligence\MongoDatabase::getInstance();
  4348→            $memDoc = $ctxMongo->findOne('ai_contact_memory', ['contact_id' => $contactId]);
  4349→            jsonResponse([
  4350→                'success' => true,
  4351→                'contact_id' => $contactId,
  4352→                'memories' => $memDoc ? ($memDoc['memories'] ?? []) : [],
  4353→            ]);
  4354→            break;
  4355→
  4356→        // ==========================================
  4357→        // AI Contact Memory Add - Manually add a memory fact
  4358→        // ==========================================
  4359→        case 'ai_contact_memory_add':
  4360→            if ($_SERVER['REQUEST_METHOD'] !== 'POST') jsonError('POST required', 405);
  4361→            $jsonData = json_decode(file_get_contents('php://input'), true) ?? [];
  4362→            $contactId = (int)($jsonData['contact_id'] ?? 0);
  4363→            $fact = trim($jsonData['fact'] ?? '');
  4364→            if (!$contactId || !$fact) jsonError('contact_id and fact required');
  4365→            $ctxMongo = \EmailIntelligence\MongoDatabase::getInstance();
  4366→            $existing = $ctxMongo->findOne('ai_contact_memory', ['contact_id' => $contactId]);
  4367→            $memories = $existing ? ($existing['memories'] ?? []) : [];
  4368→            $newMem = [
  4369→                'id' => uniqid('mem_', true),
  4370→                'fact' => $fact,
  4371→                'source' => 'manual',
  4372→                'created_at' => date('Y-m-d H:i:s'),
  4373→            ];
  4374→            $memories[] = $newMem;
  4375→            $ctxMongo->insertOrUpdate('ai_contact_memory', [
  4376→                'contact_id' => $contactId,
  4377→                'memories' => $memories,
  4378→                'updated_at' => date('Y-m-d H:i:s'),
  4379→            ], ['contact_id']);
  4380→            jsonResponse(['success' => true, 'memory' => $newMem]);
  4381→            break;
  4382→
  4383→        // ==========================================
  4384→        // AI Contact Memory Delete - Remove a memory fact
  4385→        // ==========================================
  4386→        case 'ai_contact_memory_delete':
  4387→            if ($_SERVER['REQUEST_METHOD'] !== 'POST') jsonError('POST required', 405);
  4388→            $jsonData = json_decode(file_get_contents('php://input'), true) ?? [];
  4389→            $contactId = (int)($jsonData['contact_id'] ?? 0);
  4390→            $memoryId = $jsonData['memory_id'] ?? '';
  4391→            if (!$contactId || !$memoryId) jsonError('contact_id and memory_id required');
  4392→            $ctxMongo = \EmailIntelligence\MongoDatabase::getInstance();
  4393→            $existing = $ctxMongo->findOne('ai_contact_memory', ['contact_id' => $contactId]);
  4394→            if ($existing && !empty($existing['memories'])) {
  4395→                $memories = array_values(array_filter($existing['memories'], function($m) use ($memoryId) {
  4396→                    return ($m['id'] ?? '') !== $memoryId;
  4397→                }));
  4398→                $ctxMongo->insertOrUpdate('ai_contact_memory', [
  4399→                    'contact_id' => $contactId,
  4400→                    'memories' => $memories,
  4401→                    'updated_at' => date('Y-m-d H:i:s'),
  4402→                ], ['contact_id']);
  4403→            }
  4404→            jsonResponse(['success' => true]);
  4405→            break;
  4406→
  4407→        // ==========================================
  4408→        // Default
  4409→        // ==========================================
  4410→        default:
  4411→            jsonError('Unknown action: ' . $action, 400);
  4412→    }
  4413→
  4414→} catch (Exception $e) {

        // ==========================================
        // AI Chat Sessions - List recent sessions
        // ==========================================
        case 'ai_chat_sessions':
            $ctxMongo = \EmailIntelligence\MongoDatabase::getInstance();
            $limit = (int)($_GET['limit'] ?? 20);
            $sessions = $ctxMongo->findAll('ai_chat_sessions', [], [
                'sort' => ['updated_at' => -1],
                'limit' => $limit,
            ]);
            // Strip full messages from list view (just return metadata)
            $sessionList = array_map(function($s) {
                return [
                    'session_id' => $s['session_id'] ?? '',
                    'title' => $s['title'] ?? 'Untitled',
                    'agent' => $s['agent'] ?? 'general',
                    'provider' => $s['provider'] ?? '',
                    'model' => $s['model'] ?? '',
                    'contact_name' => $s['contact_name'] ?? null,
                    'contact_id' => $s['contact_id'] ?? null,
                    'context_thread_ids' => $s['context_thread_ids'] ?? [],
                    'message_count' => count($s['messages'] ?? []),
                    'created_at' => $s['created_at'] ?? '',
                    'updated_at' => $s['updated_at'] ?? '',
                ];
            }, $sessions);
            jsonResponse(['success' => true, 'sessions' => $sessionList]);
            break;

        // ==========================================
        // AI Chat Session - Load a specific session
        // ==========================================
        case 'ai_chat_session':
            $sessionId = $_GET['session_id'] ?? '';
            if (!$sessionId) jsonError('session_id required');
            $ctxMongo = \EmailIntelligence\MongoDatabase::getInstance();
            $session = $ctxMongo->findOne('ai_chat_sessions', ['session_id' => $sessionId]);
            if (!$session) jsonError('Session not found', 404);
            jsonResponse(['success' => true, 'session' => [
                'session_id' => $session['session_id'],
                'title' => $session['title'] ?? 'Untitled',
                'agent' => $session['agent'] ?? 'general',
                'provider' => $session['provider'] ?? '',
                'model' => $session['model'] ?? '',
                'contact_id' => $session['contact_id'] ?? null,
                'contact_name' => $session['contact_name'] ?? null,
                'context_thread_ids' => $session['context_thread_ids'] ?? [],
                'messages' => $session['messages'] ?? [],
                'created_at' => $session['created_at'] ?? '',
                'updated_at' => $session['updated_at'] ?? '',
            ]]);
            break;

        // ==========================================
        // AI Chat Session Delete
        // ==========================================
        case 'ai_chat_session_delete':
            if ($_SERVER['REQUEST_METHOD'] !== 'POST') jsonError('POST required', 405);
            $jsonData = json_decode(file_get_contents('php://input'), true) ?? [];
            $sessionId = $jsonData['session_id'] ?? '';
            if (!$sessionId) jsonError('session_id required');
            $ctxMongo = \EmailIntelligence\MongoDatabase::getInstance();
            $deleted = $ctxMongo->delete('ai_chat_sessions', ['session_id' => $sessionId]);
            jsonResponse(['success' => true, 'deleted' => $deleted]);
            break;

        // ==========================================
        // AI Contact Memories - Get memories for a contact
        // ==========================================
        case 'ai_contact_memories':
            $contactId = (int)($_GET['contact_id'] ?? 0);
            if (!$contactId) jsonError('contact_id required');
            $ctxMongo = \EmailIntelligence\MongoDatabase::getInstance();
            $memDoc = $ctxMongo->findOne('ai_contact_memory', ['contact_id' => $contactId]);
            jsonResponse([
                'success' => true,
                'contact_id' => $contactId,
                'memories' => $memDoc ? ($memDoc['memories'] ?? []) : [],
            ]);
            break;

        // ==========================================
        // AI Contact Memory Add - Manually add a memory fact
        // ==========================================
        case 'ai_contact_memory_add':
            if ($_SERVER['REQUEST_METHOD'] !== 'POST') jsonError('POST required', 405);
            $jsonData = json_decode(file_get_contents('php://input'), true) ?? [];
            $contactId = (int)($jsonData['contact_id'] ?? 0);
            $fact = trim($jsonData['fact'] ?? '');
            if (!$contactId || !$fact) jsonError('contact_id and fact required');
            $ctxMongo = \EmailIntelligence\MongoDatabase::getInstance();
            $existing = $ctxMongo->findOne('ai_contact_memory', ['contact_id' => $contactId]);
            $memories = $existing ? ($existing['memories'] ?? []) : [];
            $newMem = [
                'id' => uniqid('mem_', true),
                'fact' => $fact,
                'source' => 'manual',
                'created_at' => date('Y-m-d H:i:s'),
            ];
            $memories[] = $newMem;
            $ctxMongo->insertOrUpdate('ai_contact_memory', [
                'contact_id' => $contactId,
                'memories' => $memories,
                'updated_at' => date('Y-m-d H:i:s'),
            ], ['contact_id']);
            jsonResponse(['success' => true, 'memory' => $newMem]);
            break;

        // ==========================================
        // AI Contact Memory Delete - Remove a memory fact
        // ==========================================
        case 'ai_contact_memory_delete':
            if ($_SERVER['REQUEST_METHOD'] !== 'POST') jsonError('POST required', 405);
            $jsonData = json_decode(file_get_contents('php://input'), true) ?? [];
            $contactId = (int)($jsonData['contact_id'] ?? 0);
            $memoryId = $jsonData['memory_id'] ?? '';
            if (!$contactId || !$memoryId) jsonError('contact_id and memory_id required');
            $ctxMongo = \EmailIntelligence\MongoDatabase::getInstance();
            $existing = $ctxMongo->findOne('ai_contact_memory', ['contact_id' => $contactId]);
            if ($existing && !empty($existing['memories'])) {
                $memories = array_values(array_filter($existing['memories'], function($m) use ($memoryId) {
                    return ($m['id'] ?? '') !== $memoryId;
                }));
                $ctxMongo->insertOrUpdate('ai_contact_memory', [
                    'contact_id' => $contactId,
                    'memories' => $memories,
                    'updated_at' => date('Y-m-d H:i:s'),
                ], ['contact_id']);
            }
            jsonResponse(['success' => true]);
            break;

        // ==========================================
        // Default
        // ==========================================
        default:
            jsonError('Unknown action: ' . $action, 400);
    }

} catch (Exception $e) {


Now I have a clear picture. I need to add three blocks to `extension2v1/public/api.php`:

1. The `ai_chat` case (after `ask_ai`)
2. The 6 new endpoints (before `default`)

Let me read the area right after `ask_ai` ends in v1 to know the exact insertion point.


  2248→                ]);
  2249→            } catch (Exception $e) {
  2250→                jsonError('AI request failed: ' . $e->getMessage());
  2251→            }
  2252→            break;
  2253→
  2254→        // ==========================================
  2255→        // Generate Reply Suggestions (with ChromaDB context)
  2256→        // ==========================================
  2257→        case 'generate_reply':
  2258→            if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
  2259→                jsonError('POST required', 405);
  2260→            }
  2261→
  2262→            // Handle JSON or form data

                ]);
            } catch (Exception $e) {
                jsonError('AI request failed: ' . $e->getMessage());
            }
            break;

        // ==========================================
        // Generate Reply Suggestions (with ChromaDB context)
        // ==========================================
        case 'generate_reply':
            if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
                jsonError('POST required', 405);
            }

            // Handle JSON or form data


Now I'll insert the `ai_chat` case right after `ask_ai` ends (after line 2252) and the 6 new endpoints before the `default` case (before line 5053). Let me do both insertions.


Edit

            break;

        // ==========================================
        // Generate Reply Suggestions (with ChromaDB context)
        // ==========================================
        case 'generate_reply':
            if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
                jsonError('POST required', 405);
            }

            // Handle JSON or form data

            break;

        // ==========================================
        // AI Chat - Multi-model chat with full context, agents & memory
        // ==========================================
        case 'ai_chat':
            if ($_SERVER['REQUEST_METHOD'] !== 'POST') jsonError('POST required', 405);

            $jsonData = json_decode(file_get_contents('php://input'), true) ?? [];
            $chatMessage   = trim($jsonData['message'] ?? '');
            $chatModel     = $jsonData['model'] ?? 'gpt-4o';
            $chatProvider  = $jsonData['provider'] ?? null;       // explicit provider from frontend
            $chatMode      = $jsonData['mode'] ?? 'general';
            $chatAgent     = $jsonData['agent'] ?? null;          // agent preset ID
            $chatThreadId  = $jsonData['thread_id'] ?? null;      // single thread (backward compat)
            $chatThreadIds = $jsonData['thread_ids'] ?? [];       // multi-thread context
            $chatContactId = (int)($jsonData['contact_id'] ?? 0);
            $chatHistory   = $jsonData['history'] ?? [];          // [{role,content}]
            $chatEmailId   = (int)($jsonData['email_id'] ?? 0);
            $chatSessionId = $jsonData['session_id'] ?? null;     // session persistence

            if (!$chatMessage) jsonError('message required');

            // Resolve thread IDs
            if ($chatThreadId) $chatThreadId = resolveThreadId($chatThreadId);
            $resolvedThreadIds = [];
            foreach ($chatThreadIds as $tid) {
                $resolved = resolveThreadId($tid);
                if ($resolved) $resolvedThreadIds[] = $resolved;
            }
            // Single thread_id fallback into the array
            if ($chatThreadId && empty($resolvedThreadIds)) {
                $resolvedThreadIds = [$chatThreadId];
            }

            // --- Agent presets ---
            $agentPresets = [
                'general' => [
                    'system' => "You are a business intelligence assistant with access to email and contact data. Answer questions, provide analysis, and help with business decisions.",
                    'autoMemory' => false,
                ],
                'quote_analyst' => [
                    'system' => "You are a pricing and quote analyst. Analyze email threads to find all pricing mentions, quotes sent, counter-offers, and final agreed amounts. Identify negotiation patterns, price sensitivity, and recommend optimal pricing strategies. Be specific with numbers and dates.",
                    'autoMemory' => true,
                ],
                'deception_detector' => [
                    'system' => "You are a deception and inconsistency analyst. Analyze email threads for contradictions between statements made at different times, tone shifts that suggest dishonesty, pressure tactics, evasive responses, broken promises, and manipulative language. Rate deception risk on a scale of 1-10 and cite specific evidence from the emails.",
                    'autoMemory' => true,
                ],
                'contact_profiler' => [
                    'system' => "You are a contact intelligence profiler. Analyze email threads to build a comprehensive profile of the contact: communication style, decision-making speed, priorities, negotiation habits, reliability, cultural cues, and relationship dynamics. Provide actionable insights for working with this person.",
                    'autoMemory' => true,
                ],
                'email_composer' => [
                    'system' => "You are a professional email composer. Based on the email thread context, compose a ready-to-send email reply. Match the appropriate tone and formality level. Consider the negotiation position and relationship dynamics. Write only the email text with greeting and sign-off.",
                    'autoMemory' => false,
                ],
            ];

            // Mode-specific system instructions (used when no agent selected)
            $modeInstructions = [
                'thread'  => "You are a business intelligence assistant analyzing an email thread. Provide insights about the conversation, relationships, risks, and opportunities. Be direct and specific.",
                'contact' => "You are a CRM intelligence assistant analyzing a contact's profile and history. Help understand their behavior, communication patterns, and how best to work with them.",
                'quote'   => "You are a business consultant helping estimate project costs and create quotes. Based on the email context, suggest realistic pricing, scope, and approach for software/CRM projects.",
                'reply'   => "You are an email writing assistant. Compose professional, ready-to-send email replies based on the thread context. Write only the reply text with greeting and sign-off.",
                'general' => "You are a business intelligence assistant with access to email and contact data. Answer questions, provide analysis, and help with business decisions.",
            ];

            // Select system prompt: agent takes priority over mode
            $activeAgent = ($chatAgent && isset($agentPresets[$chatAgent])) ? $agentPresets[$chatAgent] : null;
            $sysMsg = $activeAgent ? $activeAgent['system'] : ($modeInstructions[$chatMode] ?? $modeInstructions['general']);

            // --- Build context block ---
            $ctxParts = [];
            $ctxAdapter = QueryAdapter::getInstance();
            $ctxMongo = \EmailIntelligence\MongoDatabase::getInstance();
            $useMongoDB = $ctxAdapter->getActiveDatabase() === 'mongodb';
            $contextStats = ['threads' => 0, 'emails' => 0, 'estimated_tokens' => 0, 'truncated' => false];

            // Token budget per model (context window sizes)
            $modelTokenLimits = [
                'gpt-4o' => 128000, 'gpt-4o-mini' => 128000, 'o1' => 200000, 'o1-mini' => 128000,
                'gpt-5.2' => 128000, 'gpt-5.2-pro' => 128000, 'gpt-5-mini' => 128000, 'gpt-5-nano' => 128000, 'gpt-5' => 128000, 'gpt-4.1' => 128000,
                'claude-sonnet-4-5-20250929' => 200000, 'claude-opus-4-5-20251101' => 200000, 'claude-haiku-4-5-20251001' => 200000,
                'gemini-2.5-flash' => 1000000, 'gemini-3-pro' => 1000000, 'gemini-2.5-flash-lite' => 1000000,
                'grok-4' => 131000, 'grok-3' => 131000, 'grok-3-mini' => 131000,
                'deepseek-chat' => 64000, 'deepseek-reasoner' => 64000,
                'qwen-max-latest' => 128000, 'qwen-turbo-latest' => 128000,
                'mistral-large-latest' => 128000, 'mistral-small-latest' => 128000,
                'command-a-03-2025' => 128000, 'command-r7b-12-2024' => 128000,
            ];
            $tokenLimit = $modelTokenLimits[$chatModel] ?? 128000;
            $contextTokenBudget = (int)($tokenLimit * 0.6); // 60% for context, 40% for response + history

            // Multi-thread context
            $totalEmailCount = 0;
            $threadCount = count($resolvedThreadIds);
            foreach ($resolvedThreadIds as $idx => $tid) {
                if ($useMongoDB) {
                    $ctxEmails = $ctxMongo->findAll('emails', ['thread_id' => $tid], ['sort' => ['date' => 1]]);
                } else {
                    $ctxEmails = $db->fetchAll(
                        "SELECT direction, date, from_email, to_email, subject, body_text FROM emails WHERE thread_id = ? ORDER BY date ASC",
                        [$tid]
                    );
                }
                if ($ctxEmails) {
                    $threadLabel = $threadCount > 1 ? "THREAD " . ($idx + 1) . " OF {$threadCount}" : "EMAIL THREAD";
                    $ctxParts[] = "=== {$threadLabel} (" . count($ctxEmails) . " emails) ===";
                    foreach ($ctxEmails as $i => $em) {
                        $dir = ($em['direction'] ?? '') === 'inbound' ? 'FROM THEM' : 'FROM US';
                        // Strip quoted/duplicate content from replies, then truncate
                        $body = ThreadAnalyzer::stripQuotedContent($em['body_text'] ?? '');
                        $maxBody = $threadCount > 3 ? 500 : 800;
                        if (strlen($body) > $maxBody) $body = substr($body, 0, $maxBody) . '...';
                        $ctxParts[] = "Email #".($i+1)." {$dir} ({$em['date']})\nSubject: {$em['subject']}\n{$body}";
                    }
                    $totalEmailCount += count($ctxEmails);

                    // Include existing thread analysis summary if available
                    if ($useMongoDB) {
                        $ctxAnalysis = $ctxMongo->findOne('thread_analysis', ['thread_id' => $tid]);
                    } else {
                        $ctxAnalysis = $db->fetchOne("SELECT summary FROM thread_analysis WHERE thread_id = ? AND is_analyzed=1", [$tid]);
                    }
                    if ($ctxAnalysis && !empty($ctxAnalysis['summary'])) {
                        $ctxParts[] = "Analysis: " . $ctxAnalysis['summary'];
                    }
                }
            }

            // Add multi-thread summary header
            if ($threadCount > 1) {
                array_unshift($ctxParts, "=== MULTI-THREAD CONTEXT: {$totalEmailCount} total emails across {$threadCount} threads ===");
            }
            $contextStats['threads'] = $threadCount;
            $contextStats['emails'] = $totalEmailCount;

            // Contact context
            if ($chatContactId) {
                if ($useMongoDB) {
                    $ctxContact = $ctxMongo->findOne('contacts', ['id' => $chatContactId]);
                    $ctxProfile = $ctxMongo->findOne('contact_profiles', ['contact_id' => $chatContactId]);
                } else {
                    $ctxContact = $db->fetchOne("SELECT name, email, type, company FROM contacts WHERE id = ?", [$chatContactId]);
                    $ctxProfile = $db->fetchOne("SELECT * FROM contact_profiles WHERE contact_id = ?", [$chatContactId]);
                }
                if ($ctxContact) {
                    $ctxParts[] = "=== CONTACT: {$ctxContact['name']} ({$ctxContact['email']}) ===";
                    if ($ctxProfile) {
                        $ctxParts[] = "Trust score: " . ($ctxProfile['overall_trust_score'] ?? 'N/A') .
                            " | Communication style: " . ($ctxProfile['communication_style'] ?? 'N/A');
                    }
                }

                // Inject contact memory (persistent facts)
                $contactMemory = $ctxMongo->findOne('ai_contact_memory', ['contact_id' => $chatContactId]);
                if ($contactMemory && !empty($contactMemory['memories'])) {
                    $memFacts = array_map(function($m) { return "- " . $m['fact']; }, $contactMemory['memories']);
                    $ctxParts[] = "=== KNOWN FACTS ABOUT THIS CONTACT (from previous analyses) ===\n" . implode("\n", $memFacts);
                }
            }

            // Single email context (for reply mode)
            if ($chatEmailId) {
                if ($useMongoDB) {
                    $ctxEmail = $ctxMongo->findOne('emails', ['id' => $chatEmailId]);
                } else {
                    $ctxEmail = $db->fetchOne("SELECT * FROM emails WHERE id = ?", [$chatEmailId]);
                }
                if ($ctxEmail) {
                    $replyBody = ThreadAnalyzer::stripQuotedContent($ctxEmail['body_text'] ?? '');
                    $ctxParts[] = "=== SPECIFIC EMAIL TO REPLY TO ===\nFrom: {$ctxEmail['from_email']}\nSubject: {$ctxEmail['subject']}\n{$replyBody}";
                }
            }

            // Assemble context and apply token budget
            $contextBlock = implode("\n\n", $ctxParts);
            $estimatedTokens = (int)(strlen($contextBlock) / 3.5);
            $contextStats['estimated_tokens'] = $estimatedTokens;

            if ($estimatedTokens > $contextTokenBudget && $contextTokenBudget > 0) {
                // Truncate context to fit budget, keeping the end (most recent) content
                $maxChars = (int)($contextTokenBudget * 3.5);
                $contextBlock = substr($contextBlock, -$maxChars);
                $contextBlock = "[... Earlier context omitted to fit model token limit ({$estimatedTokens} tokens estimated, budget: {$contextTokenBudget}) ...]\n\n" . $contextBlock;
                $contextStats['truncated'] = true;
                $contextStats['estimated_tokens'] = $contextTokenBudget;
            }

            if ($contextBlock) {
                $sysMsg .= "\n\nCONTEXT:\n" . $contextBlock;
            }

            // Auto-memory instruction for agents that support it
            if ($activeAgent && $activeAgent['autoMemory'] && $chatContactId) {
                $sysMsg .= "\n\nIMPORTANT: If you discover important new facts about this contact (pricing patterns, contradictions, preferences, behavioral traits), include them at the end of your response in this exact format:\n[MEMORY]fact 1[/MEMORY]\n[MEMORY]fact 2[/MEMORY]\nKeep each fact concise (one sentence). Only include genuinely new, important insights.";
            }

            // Build messages array with history
            $messages = [];
            foreach ($chatHistory as $h) {
                if (isset($h['role'], $h['content'])) {
                    $messages[] = ['role' => $h['role'], 'content' => $h['content']];
                }
            }
            $messages[] = ['role' => 'user', 'content' => $chatMessage];

            // Determine provider (explicit field takes priority, then infer from model name)
            if (!$chatProvider) {
                $chatProvider = 'openai';
                if (strpos($chatModel, 'claude') !== false) $chatProvider = 'claude';
                elseif (strpos($chatModel, 'gemini') !== false) $chatProvider = 'gemini';
                elseif (strpos($chatModel, 'grok') !== false) $chatProvider = 'grok';
                elseif (strpos($chatModel, 'deepseek') !== false) $chatProvider = 'deepseek';
                elseif (strpos($chatModel, 'qwen') !== false) $chatProvider = 'qwen';
                elseif (strpos($chatModel, 'mistral') !== false || strpos($chatModel, 'codestral') !== false) $chatProvider = 'mistral';
                elseif (strpos($chatModel, 'command') !== false) $chatProvider = 'cohere';
            }

            // Route through proxy for multi-model support
            $proxyUrl = 'https://mssql.danmarcrm.com/dev1/extension2/aiapi/api_codex51/api/index.php';
            $proxyPayload = json_encode([
                'action'       => 'ask',
                'user_content' => $chatMessage,
                'system'       => $sysMsg,
                'models'       => [$chatProvider => $chatModel],  // keyed by provider code
                'providers'    => [$chatProvider],
                'history'      => array_slice($chatHistory, -10),
            ]);

            $ch = curl_init($proxyUrl);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_POST => true,
                CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
                CURLOPT_POSTFIELDS => $proxyPayload,
                CURLOPT_TIMEOUT => 120,
            ]);
            $proxyResponse = curl_exec($ch);
            $curlError = curl_error($ch);
            curl_close($ch);

            if ($curlError) {
                // Fallback to direct ThreadAnalyzer if proxy fails
                try {
                    $analyzer = new ThreadAnalyzer($chatProvider === 'openai' ? 'openai' : 'claude', $chatModel);
                    $fullPrompt = $sysMsg . "\n\nUser: " . $chatMessage;
                    $answer = $analyzer->askClaude($fullPrompt);
                    jsonResponse(['success' => true, 'answer' => $answer, 'model' => $chatModel, 'via' => 'direct', 'context_stats' => $contextStats]);
                } catch (Exception $e) {
                    jsonError('AI request failed: ' . $e->getMessage());
                }
                break;
            }

            $proxyResult = json_decode($proxyResponse, true);
            if (!$proxyResult || !$proxyResult['success']) {
                jsonError('AI proxy error: ' . ($proxyResult['error'] ?? 'Unknown error'));
            }

            // Extract answer from proxy response
            $answer = '';
            $usedModel = $chatModel;
            $responses = $proxyResult['responses'] ?? [];
            if (!empty($responses)) {
                $firstResponse = reset($responses);
                $answer = $firstResponse['response'] ?? $firstResponse['content'] ?? '';
                $usedModel = $firstResponse['model'] ?? $chatModel;
            }
            if (!$answer) {
                $answer = $proxyResult['content'] ?? $proxyResult['response'] ?? '';
            }
            if (!$answer) {
                jsonError('AI proxy returned empty response. Raw: ' . substr($proxyResponse, 0, 200));
            }

            // Auto-extract memories from AI response
            $extractedMemories = [];
            if ($activeAgent && $activeAgent['autoMemory'] && $chatContactId) {
                if (preg_match_all('/\[MEMORY\](.*?)\[\/MEMORY\]/s', $answer, $memMatches)) {
                    foreach ($memMatches[1] as $memFact) {
                        $memFact = trim($memFact);
                        if ($memFact) $extractedMemories[] = $memFact;
                    }
                    // Strip [MEMORY] tags from the displayed answer
                    $answer = preg_replace('/\s*\[MEMORY\].*?\[\/MEMORY\]\s*/s', '', $answer);
                    $answer = trim($answer);

                    // Save extracted memories to ai_contact_memory
                    if (!empty($extractedMemories)) {
                        $existingMemory = $ctxMongo->findOne('ai_contact_memory', ['contact_id' => $chatContactId]);
                        $memories = $existingMemory ? ($existingMemory['memories'] ?? []) : [];
                        foreach ($extractedMemories as $fact) {
                            $memories[] = [
                                'id' => uniqid('mem_', true),
                                'fact' => $fact,
                                'source' => 'auto',
                                'agent' => $chatAgent,
                                'session_id' => $chatSessionId,
                                'created_at' => date('Y-m-d H:i:s'),
                            ];
                        }
                        $ctxMongo->insertOrUpdate('ai_contact_memory', [
                                'contact_id' => $chatContactId,
                                'contact_email' => $ctxContact['email'] ?? '',
                                'memories' => $memories,
                                'updated_at' => date('Y-m-d H:i:s'),
                            ], ['contact_id']);
                    }
                }
            }

            // Auto-save chat session
            if (!$chatSessionId) {
                $chatSessionId = uniqid('sess_', true);
            }
            $sessionMessages = [];
            // Load existing session messages if continuing
            $existingSession = $ctxMongo->findOne('ai_chat_sessions', ['session_id' => $chatSessionId]);
            if ($existingSession && !empty($existingSession['messages'])) {
                $sessionMessages = $existingSession['messages'];
            }
            // Append new message pair
            $sessionMessages[] = ['role' => 'user', 'content' => $chatMessage, 'timestamp' => date('Y-m-d H:i:s')];
            $sessionMessages[] = ['role' => 'assistant', 'content' => $answer, 'timestamp' => date('Y-m-d H:i:s'), 'model' => $usedModel];

            $sessionTitle = $existingSession['title'] ?? substr($chatMessage, 0, 60);
            $ctxMongo->insertOrUpdate('ai_chat_sessions', [
                    'session_id' => $chatSessionId,
                    'created_at' => $existingSession['created_at'] ?? date('Y-m-d H:i:s'),
                    'updated_at' => date('Y-m-d H:i:s'),
                    'title' => $sessionTitle,
                    'agent' => $chatAgent ?? $chatMode,
                    'provider' => $chatProvider,
                    'model' => $usedModel,
                    'context_thread_ids' => $resolvedThreadIds,
                    'contact_id' => $chatContactId ?: null,
                    'contact_name' => $ctxContact['name'] ?? null,
                    'messages' => $sessionMessages,
                ], ['session_id']);

            jsonResponse([
                'success'     => true,
                'answer'      => $answer,
                'model'       => $usedModel,
                'provider'    => $chatProvider,
                'via'         => 'proxy',
                'session_id'  => $chatSessionId,
                'context_stats' => $contextStats,
                'extracted_memories' => $extractedMemories,
            ]);
            break;

        // ==========================================
        // Generate Reply Suggestions (with ChromaDB context)
        // ==========================================
        case 'generate_reply':
            if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
                jsonError('POST required', 405);
            }

            // Handle JSON or form data