     1→// background.js - Email Intelligence Extension
     2→"use strict";
     3→
     4→// Import product search controller for extension-based scraping
     5→importScripts('productSearchController.js');
     6→
     7→const API_BASE = 'https://mssql.danmarcrm.com/dev1/extension2v1/public/api.php';
     8→
     9→console.log("[Email Intelligence] Background service worker initialized");
    10→
    11→// API action handlers
    12→const API_ACTIONS = {
    13→  // Resolve message ID to real Gmail thread ID (also caches URL thread ID mapping)
    14→  resolveMessageThread: (params) => {
    15→    let url = `${API_BASE}?action=resolve_message_thread&message_id=${encodeURIComponent(params.message_id)}`;
    16→    // Pass URL thread ID for server-side caching
    17→    if (params.url_thread_id) {
    18→      url += `&url_thread_id=${encodeURIComponent(params.url_thread_id)}`;
    19→    }
    20→    return fetch(url);
    21→  },
    22→
    23→  // Thread operations - try search if direct lookup fails
    24→  getThreadAnalysis: async (params) => {
    25→    console.log("[Background] getThreadAnalysis params:", params);
    26→
    27→    // First try direct lookup
    28→    let response = await fetch(`${API_BASE}?action=thread&thread_id=${params.thread_id}`);
    29→    let data = await response.json();
    30→    console.log("[Background] Direct lookup result:", data);
    31→
    32→    // If not found, try searching by subject
    33→    if (data.error && params.subject) {
    34→      console.log("[Background] Trying search by subject:", params.subject);
    35→      // Clean up subject - remove "Re:", "Fwd:", etc.
    36→      const cleanSubject = params.subject.replace(/^(Re:|Fwd:|RE:|FWD:)\s*/gi, '').trim();
    37→      const searchResponse = await fetch(`${API_BASE}?action=threads&search=${encodeURIComponent(cleanSubject)}&limit=1`);
    38→      const searchData = await searchResponse.json();
    39→      console.log("[Background] Search result:", searchData);
    40→
    41→      if (searchData.threads && searchData.threads[0]) {
    42→        // Found by subject, get full thread
    43→        const foundThreadId = searchData.threads[0].thread_id;
    44→        console.log("[Background] Found thread by subject, ID:", foundThreadId);
    45→        response = await fetch(`${API_BASE}?action=thread&thread_id=${foundThreadId}`);
    46→        data = await response.json();
    47→      }
    48→    }
    49→
    50→    // Return a mock Response object - always return ok:true so we can handle in content script
    51→    return {
    52→      ok: true,
    53→      json: async () => data
    54→    };
    55→  },
    56→  analyzeThread: (params) => fetch(`${API_BASE}?action=analyze_thread`, {
    57→    method: 'POST',
    58→    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    59→    body: new URLSearchParams({ thread_id: params.thread_id })
    60→  }),
    61→  // Sync thread via Gmail API (server-side - recommended)
    62→  // Now accepts message_id for reliable thread resolution
    63→  syncThread: (params) => fetch(`${API_BASE}?action=sync_thread`, {
    64→    method: 'POST',
    65→    headers: { 'Content-Type': 'application/json' },
    66→    body: JSON.stringify({
    67→      thread_id: params.thread_id,
    68→      subject: params.subject || '',
    69→      message_id: params.message_id || ''  // NEW: For reliable thread ID resolution
    70→    })
    71→  }),
    72→
    73→  // Legacy: Save thread from DOM scraping
    74→  saveThread: (params) => fetch(`${API_BASE}?action=save_gmail_thread`, {
    75→    method: 'POST',
    76→    headers: { 'Content-Type': 'application/json' },
    77→    body: JSON.stringify({
    78→      thread_id: params.thread_id,
    79→      emails: params.emails
    80→    })
    81→  }),
    82→
    83→  // Contact operations
    84→  getContactByEmail: (params) => fetch(`${API_BASE}?action=contacts&email=${encodeURIComponent(params.email)}`),
    85→  getContactProfile: (params) => fetch(`${API_BASE}?action=contact_profile&contact_id=${params.contact_id}`),
    86→  getThreadParticipants: (params) => fetch(`${API_BASE}?action=thread_participants&thread_id=${encodeURIComponent(params.thread_id)}`),
    87→  getLogs: (params) => fetch(`${API_BASE}?action=logs&thread_id=${encodeURIComponent(params.thread_id || '')}&limit=${params.limit || 50}`),
    88→  getLearnedBehaviors: (params) => fetch(`${API_BASE}?action=learned_behaviors&contact_id=${params.contact_id || ''}&type=${params.type || ''}`),
    89→  analyzeContact: (params) => fetch(`${API_BASE}?action=analyze_contact`, {
    90→    method: 'POST',
    91→    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    92→    body: new URLSearchParams({ contact_id: params.contact_id })
    93→  }),
    94→
    95→  // Pattern operations
    96→  scanPatterns: (params) => fetch(`${API_BASE}?action=scan_patterns&contact_id=${params.contact_id}&threshold=${params.threshold || 25}`),
    97→  detectPattern: (params) => fetch(`${API_BASE}?action=detect_pattern&pattern=${params.pattern}&contact_id=${params.contact_id || ''}`),
    98→  patternStats: () => fetch(`${API_BASE}?action=pattern_stats`),
    99→
   100→  // Alert operations
   101→  getAlerts: (params) => fetch(`${API_BASE}?action=alerts&contact_id=${params.contact_id || ''}&severity=${params.severity || ''}`),
   102→  alertStats: () => fetch(`${API_BASE}?action=alert_stats`),
   103→
   104→  // AI operations
   105→  askAI: (params) => fetch(`${API_BASE}?action=ask_ai`, {
   106→    method: 'POST',
   107→    headers: { 'Content-Type': 'application/json' },
   108→    body: JSON.stringify({
   109→      question: params.question,
   110→      thread_id: params.thread_id,
   111→      contact_id: params.contact_id
   112→    })
   113→  }),
   114→  askContactAI: (params) => fetch(`${API_BASE}?action=ask_contact_ai`, {
   115→    method: 'POST',
   116→    headers: { 'Content-Type': 'application/json' },
   117→    body: JSON.stringify({
   118→      contact_id: params.contact_id,
   119→      thread_id: params.thread_id,
   120→      question: params.question,
   121→      provider: params.provider || 'claude',
   122→      model: params.model || 'claude-sonnet-4-5-20250929',
   123→      context_strategy: params.context_strategy || 'smart',
   124→      max_recent_emails: params.max_recent_emails || 20,
   125→      max_historical_threads: params.max_historical_threads || 30,
   126→      vector_search_limit: params.vector_search_limit || 10,
   127→      conversation_history: params.conversation_history || '',
   128→      saved_context: params.saved_context || ''
   129→    })
   130→  }),
   131→  generateReply: (params) => fetch(`${API_BASE}?action=generate_reply`, {
   132→    method: 'POST',
   133→    headers: { 'Content-Type': 'application/json' },
   134→    body: JSON.stringify({
   135→      thread_id: params.thread_id,
   136→      tone: params.tone || 'professional',
   137→      intent: params.intent || '',
   138→      custom_instructions: params.custom_instructions || '',
   139→      ai_model: params.ai_model || 'gpt-4o',
   140→      ai_provider: params.ai_provider || 'openai'
   141→    })
   142→  }),
   143→
   144→  // Generate new email (compose) - uses ChromaDB for context
   145→  generateEmail: (params) => fetch(`${API_BASE}?action=generate_email`, {
   146→    method: 'POST',
   147→    headers: { 'Content-Type': 'application/json' },
   148→    body: JSON.stringify({
   149→      contact_id: params.contact_id,
   150→      email_type: params.email_type || 'custom',
   151→      additional_context: params.additional_context || '',
   152→      ai_model: params.ai_model || 'gpt-4o',
   153→      ai_provider: params.ai_provider || 'openai'
   154→    })
   155→  }),
   156→
   157→  // Search
   158→  semanticSearch: (params) => fetch(`${API_BASE}?action=semantic_search&q=${encodeURIComponent(params.query)}&limit=${params.limit || 10}`),
   159→
   160→  // Reply suggestions (5 AI-generated options)
   161→  suggestReplies: (params) => fetch(`${API_BASE}?action=suggest_replies&thread_id=${encodeURIComponent(params.thread_id)}&ai_model=${params.ai_model || 'gpt-4o'}&ai_provider=${params.ai_provider || 'openai'}`),
   162→
   163→  // Data Enrichment operations
   164→  enrichContact: (params) => fetch(`${API_BASE}?action=enrich_contact`, {
   165→    method: 'POST',
   166→    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
   167→    body: new URLSearchParams({
   168→      contact_id: params.contact_id || '',
   169→      email: params.email || '',
   170→      sources: params.sources ? JSON.stringify(params.sources) : ''
   171→    })
   172→  }),
   173→  enrichCompany: (params) => fetch(`${API_BASE}?action=enrich_company`, {
   174→    method: 'POST',
   175→    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
   176→    body: new URLSearchParams({
   177→      domain: params.domain,
   178→      sources: params.sources ? JSON.stringify(params.sources) : ''
   179→    })
   180→  }),
   181→  enrichAllContacts: (params) => fetch(`${API_BASE}?action=enrich_all_contacts`, {
   182→    method: 'POST',
   183→    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
   184→    body: new URLSearchParams({ priority: params.priority || 3 })
   185→  }),
   186→  enrichAllCompanies: (params) => fetch(`${API_BASE}?action=enrich_all_companies`, {
   187→    method: 'POST',
   188→    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
   189→    body: new URLSearchParams({ priority: params.priority || 3 })
   190→  }),
   191→  suggestEnrichmentSources: (params) => fetch(`${API_BASE}?action=suggest_enrichment_sources&entity_type=${params.entity_type}&entity_id=${encodeURIComponent(params.entity_id)}`),
   192→  approveEnrichment: (params) => fetch(`${API_BASE}?action=approve_enrichment`, {
   193→    method: 'POST',
   194→    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
   195→    body: new URLSearchParams({
   196→      entity_type: params.entity_type,
   197→      entity_id: params.entity_id,
   198→      sources: params.sources ? JSON.stringify(params.sources) : '',
   199→      priority: params.priority || 5
   200→    })
   201→  }),
   202→  getEnrichmentStatus: (params) => fetch(`${API_BASE}?action=enrichment_status&entity_type=${params.entity_type}&entity_id=${encodeURIComponent(params.entity_id)}`),
   203→  getEnrichmentQueue: () => fetch(`${API_BASE}?action=enrichment_queue`),
   204→  getContactEnrichment: (params) => fetch(`${API_BASE}?action=contact_enrichment&contact_id=${encodeURIComponent(params.contact_id)}`),
   205→  getCompanyEnrichment: (params) => fetch(`${API_BASE}?action=company_enrichment&domain=${encodeURIComponent(params.domain)}`),
   206→  getApolloStatus: () => fetch(`${API_BASE}?action=apollo_status`),
   207→  getPuppeteerStatus: () => fetch(`${API_BASE}?action=puppeteer_status`),
   208→  getEnrichmentSources: () => fetch(`${API_BASE}?action=enrichment_sources`),
   209→
   210→  // Important Context
   211→  saveImportantContext: (params) => fetch(`${API_BASE}?action=save_important_context`, {
   212→    method: 'POST',
   213→    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
   214→    body: new URLSearchParams(params)
   215→  }),
   216→  getImportantContext: (params) => fetch(`${API_BASE}?action=get_important_context&contact_id=${params.contact_id || ''}&thread_id=${params.thread_id || ''}`),
   217→  deleteImportantContext: (params) => fetch(`${API_BASE}?action=delete_important_context`, {
   218→    method: 'POST',
   219→    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
   220→    body: new URLSearchParams({ id: params.id })
   221→  }),
   222→
   223→  // ==========================================
   224→  // Product Search operations
   225→  // ==========================================
   226→
   227→  // Start a new product search (uses extension-based scraping)
   228→  startProductSearch: async (params) => {
   229→    // Use extension-based scraping via ProductSearchController
   230→    if (typeof ProductSearchController !== 'undefined') {
   231→      try {
   232→        const result = await ProductSearchController.startSearch(
   233→          params.query,
   234→          params.sites || [],
   235→          { num_results: params.num_results || 5 }
   236→        );
   237→        // Return a mock Response object for consistency
   238→        return {
   239→          ok: true,
   240→          json: async () => result
   241→        };
   242→      } catch (e) {
   243→        return {
   244→          ok: false,
   245→          json: async () => ({ success: false, error: e.message })
   246→        };
   247→      }
   248→    }
   249→    // Fallback to API if controller not available
   250→    return fetch(`${API_BASE}?action=startProductSearch`, {
   251→      method: 'POST',
   252→      headers: { 'Content-Type': 'application/json' },
   253→      body: JSON.stringify({
   254→        query: params.query,
   255→        sites: params.sites || [],
   256→        num_results: params.num_results || 5,
   257→        async: params.async || false
   258→      })
   259→    });
   260→  },
   261→
   262→  // Get product search status
   263→  getProductSearchStatus: (params) => fetch(`${API_BASE}?action=getProductSearchStatus&search_id=${encodeURIComponent(params.search_id)}`),
   264→
   265→  // Get product search results
   266→  getProductSearchResults: (params) => fetch(`${API_BASE}?action=getProductSearchResults&search_id=${encodeURIComponent(params.search_id)}`),
   267→
   268→  // Execute pending search
   269→  executeProductSearch: (params) => fetch(`${API_BASE}?action=executeProductSearch`, {
   270→    method: 'POST',
   271→    headers: { 'Content-Type': 'application/json' },
   272→    body: JSON.stringify({
   273→      search_id: params.search_id,
   274→      analyze: params.analyze !== false
   275→    })
   276→  }),
   277→
   278→  // Get search history
   279→  getProductSearchHistory: (params) => fetch(`${API_BASE}?action=getProductSearchHistory&limit=${params.limit || 10}`),
   280→
   281→  // Get available sites
   282→  getProductSearchSites: () => fetch(`${API_BASE}?action=getProductSearchSites`)
   283→};
   284→
   285→// Message listener from content script
   286→chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
   287→  console.log("[Background] Message received:", request.action);
   288→
   289→  const handler = API_ACTIONS[request.action];
   290→
   291→  if (!handler) {
   292→    sendResponse({ success: false, error: `Unknown action: ${request.action}` });
   293→    return false;
   294→  }
   295→
   296→  // Execute API call
   297→  (async () => {
   298→    try {
   299→      const response = await handler(request.params || {});
   300→      const data = await response.json();
   301→
   302→      // Check for API-level errors (even if HTTP 200)
   303→      if (data.error) {
   304→        console.error(`[Background] API error for ${request.action}:`, data.message, data);
   305→        // Pass through all error data (e.g., is_draft, hint) for client-side handling
   306→        sendResponse({ success: false, error: data.message || 'API error', data });
   307→        return;
   308→      }
   309→
   310→      // Check for HTTP-level errors
   311→      if (!response.ok) {
   312→        console.error(`[Background] HTTP error for ${request.action}:`, response.status, data);
   313→        // Pass through all error data for client-side handling
   314→        sendResponse({ success: false, error: data.message || `HTTP ${response.status}`, data });
   315→        return;
   316→      }
   317→
   318→      sendResponse({ success: true, data });
   319→
   320→    } catch (error) {
   321→      console.error(`[Background] Error for ${request.action}:`, error);
   322→      sendResponse({ success: false, error: error.message });
   323→    }
   324→  })();
   325→
   326→  return true; // Keep channel open for async response
   327→});
   328→
   329→// Install/update handler
   330→chrome.runtime.onInstalled.addListener((details) => {
   331→  if (details.reason === 'install') {
   332→    console.log('[Email Intelligence] Extension installed');
   333→
   334→    // Set default settings
   335→    chrome.storage.local.set({
   336→      apiBase: API_BASE,
   337→      autoAnalyze: true,
   338→      showAlerts: true,
   339→      theme: 'light'
   340→    });
   341→  } else if (details.reason === 'update') {
   342→    console.log('[Email Intelligence] Extension updated to', chrome.runtime.getManifest().version);
   343→  }
   344→});
   345→

Prompt:
Explore the Chrome extension's "Ask AI" tab functionality thoroughly. Look at:

1. `/var/www/mssql.danmarcrm.com/dev1/extension2/chrome-extensionCopyMac20260220/chrome-extension/` - explore the full directory structure
2. Look at popup.html and popup.js for the "Ask AI" tab UI
3. Look at any JS files that handle AI chat, model selection dropdown, thread context sending
4. Look at background.js for API calls to the server
5. Look at contentScript.js for how it grabs email thread data from Gmail

I need to understand: how does the chrome extension's "Ask AI" tab work? What models are available in the dropdown? How does it send thread context to the AI? What's the UI layout?


Prompt:
Explore the Chrome extension's "Ask AI" tab functionality thoroughly. Look at:

1. `/var/www/mssql.danmarcrm.com/dev1/extension2/chrome-extensionCopyMac20260220/chrome-extension/` - explore the full directory structure
2. Look at popup.html and popup.js for the "Ask AI" tab UI
3. Look at any JS files that handle AI chat, model selection dropdown, thread context sending
4. Look at background.js for API calls to the server
5. Look at contentScript.js for how it grabs email thread data from Gmail

I need to understand: how does the chrome extension's "Ask AI" tab work? What models are available in the dropdown? How does it send thread context to the AI? What's the UI layout?


     1→{
     2→  "name": "Email Intelligence - AI Email Analysis",
     3→  "description": "AI-powered email analysis with deception detection, contact profiling, and behavioral pattern recognition",
     4→  "version": "2.2.0",
     5→  "manifest_version": 3,
     6→  "permissions": [
     7→    "activeTab",
     8→    "scripting",
     9→    "storage",
    10→    "tabs"
    11→  ],
    12→  "host_permissions": [
    13→    "https://mssql.danmarcrm.com/*",
    14→    "https://mail.google.com/*",
    15→    "https://*.amazon.com/*",
    16→    "https://*.amazon.co.uk/*",
    17→    "https://*.amazon.de/*",
    18→    "https://*.aliexpress.com/*"
    19→  ],
    20→  "background": {
    21→    "service_worker": "background.js"
    22→  },
    23→  "content_scripts": [
    24→    {
    25→      "matches": ["https://mail.google.com/*"],
    26→      "js": ["contentScript.js"],
    27→      "css": ["css/panel.css"],
    28→      "run_at": "document_end"
    29→    }
    30→  ],
    31→  "action": {
    32→    "default_popup": "popup.html",
    33→    "default_icon": {
    34→      "16": "icons/icon16.png",
    35→      "48": "icons/icon48.png",
    36→      "128": "icons/icon128.png"
    37→    },
    38→    "default_title": "Email Intelligence"
    39→  },
    40→  "web_accessible_resources": [{
    41→    "resources": ["css/*", "lib/*", "productScrapers.js"],
    42→    "matches": ["<all_urls>"]
    43→  }],
    44→  "icons": {
    45→    "16": "icons/icon16.png",
    46→    "48": "icons/icon48.png",
    47→    "128": "icons/icon128.png"
    48→  }
    49→}
    50→
    51→

Prompt:
Explore the Chrome extension's "Ask AI" tab functionality thoroughly. Look at:

1. `/var/www/mssql.danmarcrm.com/dev1/extension2/chrome-extensionCopyMac20260220/chrome-extension/` - explore the full directory structure
2. Look at popup.html and popup.js for the "Ask AI" tab UI
3. Look at any JS files that handle AI chat, model selection dropdown, thread context sending
4. Look at background.js for API calls to the server
5. Look at contentScript.js for how it grabs email thread data from Gmail

I need to understand: how does the chrome extension's "Ask AI" tab work? What models are available in the dropdown? How does it send thread context to the AI? What's the UI layout?


Explore how contacts and threads are searched/queried in the backend. Look at:

1. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/QueryAdapter.php` - how threads are queried, filtered by contact
2. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/ThreadAnalyzer.php` - how thread analysis works
3. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/ContactProfiler.php` - how contact profiles are built
4. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/MongoDatabase.php` and `MongoRepository.php` - how email threads are stored/queried in MongoDB
5. `/var/www/mssql.danmarcrm.com/dev1/extension2/public/api.php` - search for endpoints related to search, contacts, threads, filtering

I need to understand: how can we query all threads for a specific contact email? How are threads stored? What data structure do they have? How does the existing search work?

Prompt:
Explore how contacts and threads are searched/queried in the backend. Look at:

1. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/QueryAdapter.php` - how threads are queried, filtered by contact
2. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/ThreadAnalyzer.php` - how thread analysis works
3. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/ContactProfiler.php` - how contact profiles are built
4. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/MongoDatabase.php` and `MongoRepository.php` - how email threads are stored/queried in MongoDB
5. `/var/www/mssql.danmarcrm.com/dev1/extension2/public/api.php` - search for endpoints related to search, contacts, threads, filtering

I need to understand: how can we query all threads for a specific contact email? How are threads stored? What data structure do they have? How does the existing search work?


Error: File content (70356 tokens) exceeds maximum allowed tokens (25000). Please use offset and limit parameters to read specific portions of the file, or use the GrepTool to search for specific content.

Prompt:
Explore the Chrome extension's "Ask AI" tab functionality thoroughly. Look at:

1. `/var/www/mssql.danmarcrm.com/dev1/extension2/chrome-extensionCopyMac20260220/chrome-extension/` - explore the full directory structure
2. Look at popup.html and popup.js for the "Ask AI" tab UI
3. Look at any JS files that handle AI chat, model selection dropdown, thread context sending
4. Look at background.js for API calls to the server
5. Look at contentScript.js for how it grabs email thread data from Gmail

I need to understand: how does the chrome extension's "Ask AI" tab work? What models are available in the dropdown? How does it send thread context to the AI? What's the UI layout?


Prompt:
Explore how contacts and threads are searched/queried in the backend. Look at:

1. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/QueryAdapter.php` - how threads are queried, filtered by contact
2. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/ThreadAnalyzer.php` - how thread analysis works
3. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/ContactProfiler.php` - how contact profiles are built
4. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/MongoDatabase.php` and `MongoRepository.php` - how email threads are stored/queried in MongoDB
5. `/var/www/mssql.danmarcrm.com/dev1/extension2/public/api.php` - search for endpoints related to search, contacts, threads, filtering

I need to understand: how can we query all threads for a specific contact email? How are threads stored? What data structure do they have? How does the existing search work?


Prompt:
Explore how contacts and threads are searched/queried in the backend. Look at:

1. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/QueryAdapter.php` - how threads are queried, filtered by contact
2. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/ThreadAnalyzer.php` - how thread analysis works
3. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/ContactProfiler.php` - how contact profiles are built
4. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/MongoDatabase.php` and `MongoRepository.php` - how email threads are stored/queried in MongoDB
5. `/var/www/mssql.danmarcrm.com/dev1/extension2/public/api.php` - search for endpoints related to search, contacts, threads, filtering

I need to understand: how can we query all threads for a specific contact email? How are threads stored? What data structure do they have? How does the existing search work?


Bash

grep -n "aiChat\|ai_chat\|askAI\|sendQuestion" /var/www/mssql.danmarcrm.com/dev1/extension2/public/assets/app.js

Search for AI chat functions in JavaScript

Prompt:
Explore the web app's AI-related features thoroughly. I need to understand:

1. How the current AI chat works in `/var/www/mssql.danmarcrm.com/dev1/extension2/public/ai_chat.php`
2. How AI analysis works in `/var/www/mssql.danmarcrm.com/dev1/extension2/public/ai_analysis.php`
3. The AI-related API endpoints in `/var/www/mssql.danmarcrm.com/dev1/extension2/public/api.php` - search for functions like "analyze", "chat", "ai", "ask"
4. How threads are fetched and displayed in `/var/www/mssql.danmarcrm.com/dev1/extension2/public/index.php`
5. The frontend JS in `/var/www/mssql.danmarcrm.com/dev1/extension2/public/assets/app.js` - search for AI chat, analysis, thread selection features

Focus on understanding: what AI chat UI exists, how threads are selected, how context is sent to AI, and what API endpoints handle AI requests.


Prompt:
Explore how contacts and threads are searched/queried in the backend. Look at:

1. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/QueryAdapter.php` - how threads are queried, filtered by contact
2. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/ThreadAnalyzer.php` - how thread analysis works
3. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/ContactProfiler.php` - how contact profiles are built
4. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/MongoDatabase.php` and `MongoRepository.php` - how email threads are stored/queried in MongoDB
5. `/var/www/mssql.danmarcrm.com/dev1/extension2/public/api.php` - search for endpoints related to search, contacts, threads, filtering

I need to understand: how can we query all threads for a specific contact email? How are threads stored? What data structure do they have? How does the existing search work?


Prompt:
Explore the Chrome extension's "Ask AI" tab functionality thoroughly. Look at:

1. `/var/www/mssql.danmarcrm.com/dev1/extension2/chrome-extensionCopyMac20260220/chrome-extension/` - explore the full directory structure
2. Look at popup.html and popup.js for the "Ask AI" tab UI
3. Look at any JS files that handle AI chat, model selection dropdown, thread context sending
4. Look at background.js for API calls to the server
5. Look at contentScript.js for how it grabs email thread data from Gmail

I need to understand: how does the chrome extension's "Ask AI" tab work? What models are available in the dropdown? How does it send thread context to the AI? What's the UI layout?


Prompt:
Explore how contacts and threads are searched/queried in the backend. Look at:

1. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/QueryAdapter.php` - how threads are queried, filtered by contact
2. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/ThreadAnalyzer.php` - how thread analysis works
3. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/ContactProfiler.php` - how contact profiles are built
4. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/MongoDatabase.php` and `MongoRepository.php` - how email threads are stored/queried in MongoDB
5. `/var/www/mssql.danmarcrm.com/dev1/extension2/public/api.php` - search for endpoints related to search, contacts, threads, filtering

I need to understand: how can we query all threads for a specific contact email? How are threads stored? What data structure do they have? How does the existing search work?


Prompt:
Explore how contacts and threads are searched/queried in the backend. Look at:

1. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/QueryAdapter.php` - how threads are queried, filtered by contact
2. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/ThreadAnalyzer.php` - how thread analysis works
3. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/ContactProfiler.php` - how contact profiles are built
4. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/MongoDatabase.php` and `MongoRepository.php` - how email threads are stored/queried in MongoDB
5. `/var/www/mssql.danmarcrm.com/dev1/extension2/public/api.php` - search for endpoints related to search, contacts, threads, filtering

I need to understand: how can we query all threads for a specific contact email? How are threads stored? What data structure do they have? How does the existing search work?


2672:const aiChat = {
2696:    aiChat.isOpen = !aiChat.isOpen;
2697:    panel.style.display = aiChat.isOpen ? 'flex' : 'none';
2698:    if (aiChat.isOpen) { document.getElementById('ai-chat-input').focus(); renderQuickPrompts(); }
2702:    aiChat.threadId = threadId; aiChat.contactId = contactId || null;
2703:    aiChat.threadSubject = subject || threadId; aiChat.emailId = null;
2705:    if (!aiChat.isOpen) toggleAiChat(); else document.getElementById('ai-chat-input').focus();
2709:    aiChat.contactId = contactId; aiChat.contactName = contactName || 'Contact';
2710:    aiChat.threadId = null; aiChat.emailId = null;
2712:    if (!aiChat.isOpen) toggleAiChat(); else document.getElementById('ai-chat-input').focus();
2716:    aiChat.threadId = threadId; aiChat.emailId = emailId; aiChat.threadSubject = subject || threadId;
2718:    if (!aiChat.isOpen) toggleAiChat(); else document.getElementById('ai-chat-input').focus();
2722:    aiChat.mode = mode;
2732:    let ctx = modeLabels[aiChat.mode] || 'General Chat';
2733:    if (aiChat.mode === 'thread' && aiChat.threadSubject) ctx += ' · ' + aiChat.threadSubject.substring(0, 30);
2734:    if (aiChat.mode === 'contact' && aiChat.contactName) ctx += ' · ' + aiChat.contactName;
2741:    const prompts = AI_QUICK_PROMPTS[aiChat.mode] || AI_QUICK_PROMPTS.general;
2766:    aiChat.history.push({ role: 'user', content: message });
2770:        const payload = { message, model, mode: aiChat.mode, history: aiChat.history.slice(-10) };
2771:        if (aiChat.threadId) payload.thread_id = aiChat.threadId;
2772:        if (aiChat.contactId) payload.contact_id = aiChat.contactId;
2773:        if (aiChat.emailId) payload.email_id = aiChat.emailId;
2775:        const resp = await fetch('api.php?action=ai_chat', {
2782:            aiChat.history.push({ role: 'assistant', content: data.answer });
2857:    aiChat.history = [];
2868:        aiChat.isDragging = true;
2870:        aiChat.dragOffsetX = e.clientX - rect.left;
2871:        aiChat.dragOffsetY = e.clientY - rect.top;
2875:        if (!aiChat.isDragging) return;
2877:        panel.style.left = Math.max(0, Math.min(e.clientX - aiChat.dragOffsetX, window.innerWidth - panel.offsetWidth)) + 'px';
2878:        panel.style.top = Math.max(0, Math.min(e.clientY - aiChat.dragOffsetY, window.innerHeight - panel.offsetHeight)) + 'px';
2880:    document.addEventListener('mouseup', () => { aiChat.isDragging = false; });

Prompt:
Explore the web app's AI-related features thoroughly. I need to understand:

1. How the current AI chat works in `/var/www/mssql.danmarcrm.com/dev1/extension2/public/ai_chat.php`
2. How AI analysis works in `/var/www/mssql.danmarcrm.com/dev1/extension2/public/ai_analysis.php`
3. The AI-related API endpoints in `/var/www/mssql.danmarcrm.com/dev1/extension2/public/api.php` - search for functions like "analyze", "chat", "ai", "ask"
4. How threads are fetched and displayed in `/var/www/mssql.danmarcrm.com/dev1/extension2/public/index.php`
5. The frontend JS in `/var/www/mssql.danmarcrm.com/dev1/extension2/public/assets/app.js` - search for AI chat, analysis, thread selection features

Focus on understanding: what AI chat UI exists, how threads are selected, how context is sent to AI, and what API endpoints handle AI requests.


Grep

case 'analyze':|case 'chat':|case 'ai_':|function.*analyze|function.*chat

Prompt:
Explore the web app's AI-related features thoroughly. I need to understand:

1. How the current AI chat works in `/var/www/mssql.danmarcrm.com/dev1/extension2/public/ai_chat.php`
2. How AI analysis works in `/var/www/mssql.danmarcrm.com/dev1/extension2/public/ai_analysis.php`
3. The AI-related API endpoints in `/var/www/mssql.danmarcrm.com/dev1/extension2/public/api.php` - search for functions like "analyze", "chat", "ai", "ask"
4. How threads are fetched and displayed in `/var/www/mssql.danmarcrm.com/dev1/extension2/public/index.php`
5. The frontend JS in `/var/www/mssql.danmarcrm.com/dev1/extension2/public/assets/app.js` - search for AI chat, analysis, thread selection features

Focus on understanding: what AI chat UI exists, how threads are selected, how context is sent to AI, and what API endpoints handle AI requests.


Prompt:
Explore the Chrome extension's "Ask AI" tab functionality thoroughly. Look at:

1. `/var/www/mssql.danmarcrm.com/dev1/extension2/chrome-extensionCopyMac20260220/chrome-extension/` - explore the full directory structure
2. Look at popup.html and popup.js for the "Ask AI" tab UI
3. Look at any JS files that handle AI chat, model selection dropdown, thread context sending
4. Look at background.js for API calls to the server
5. Look at contentScript.js for how it grabs email thread data from Gmail

I need to understand: how does the chrome extension's "Ask AI" tab work? What models are available in the dropdown? How does it send thread context to the AI? What's the UI layout?


     1→<?php
     2→
     3→namespace EmailIntelligence;
     4→
     5→/**
     6→ * QueryAdapter - Automatically routes queries to MySQL or MongoDB
     7→ *
     8→ * This class provides a unified interface that works with both databases.
     9→ * When MySQL is enabled, it uses SQL queries.
    10→ * When MySQL is disabled, it routes to MongoRepository equivalents.
    11→ *
    12→ * Usage:
    13→ *   $adapter = QueryAdapter::getInstance();
    14→ *   $contacts = $adapter->getContactsWithProfiles($search, $limit, $offset, $sort);
    15→ */
    16→class QueryAdapter
    17→{
    18→    private static ?QueryAdapter $instance = null;
    19→    private Database $db;
    20→    private ?MongoRepository $mongoRepo = null;
    21→    private bool $useMongo = true;
    22→
    23→    private function __construct()
    24→    {
    25→        $this->db = Database::getInstance();
    26→
    27→        // Use MongoDB if MySQL is disabled
    28→        $this->useMongo = !defined('MYSQL_ENABLED') || !MYSQL_ENABLED;
    29→
    30→        if ($this->useMongo) {
    31→            $this->mongoRepo = new MongoRepository();
    32→        }
    33→    }
    34→
    35→    public static function getInstance(): QueryAdapter
    36→    {
    37→        if (self::$instance === null) {
    38→            self::$instance = new self();
    39→        }
    40→        return self::$instance;
    41→    }
    42→
    43→    /**
    44→     * Check which database is being used
    45→     */
    46→    public function getActiveDatabase(): string
    47→    {
    48→        return $this->useMongo ? 'mongodb' : 'mysql';
    49→    }
    50→
    51→    // =========================================================================
    52→    // CONTACTS
    53→    // =========================================================================
    54→
    55→    /**
    56→     * Get contacts with profiles (paginated)
    57→     *
    58→     * @param string $search Search term (name, email, company)
    59→     * @param int $limit Results per page
    60→     * @param int $offset Pagination offset
    61→     * @param string $sortColumn Column to sort by
    62→     * @param string $sortOrder ASC or DESC
    63→     * @return array ['contacts' => [...], 'total' => int]
    64→     */
    65→    public function getContactsWithProfiles(
    66→        string $search = '',
    67→        int $limit = 50,
    68→        int $offset = 0,
    69→        string $sortColumn = 'name',
    70→        string $sortOrder = 'ASC'
    71→    ): array {
    72→        if ($this->useMongo) {
    73→            return $this->getContactsWithProfilesMongo($search, $limit, $offset, $sortColumn, $sortOrder);
    74→        }
    75→
    76→        return $this->getContactsWithProfilesSQL($search, $limit, $offset, $sortColumn, $sortOrder);
    77→    }
    78→
    79→    private function getContactsWithProfilesSQL(
    80→        string $search,
    81→        int $limit,
    82→        int $offset,
    83→        string $sortColumn,
    84→        string $sortOrder
    85→    ): array {
    86→        $whereClause = '1=1';
    87→        $params = [];
    88→
    89→        if ($search) {
    90→            $whereClause = "(c.name LIKE ? OR c.email LIKE ? OR c.company LIKE ?)";
    91→            $searchParam = "%{$search}%";
    92→            $params = [$searchParam, $searchParam, $searchParam];
    93→        }
    94→
    95→        // Validate sort column
    96→        $allowedColumns = ['name', 'email', 'company', 'created_at', 'reliability_score', 'risk_level'];
    97→        if (!in_array($sortColumn, $allowedColumns)) {
    98→            $sortColumn = 'name';
    99→        }
   100→        $sortOrder = strtoupper($sortOrder) === 'DESC' ? 'DESC' : 'ASC';
   101→
   102→        $total = $this->db->fetchColumn(
   103→            "SELECT COUNT(*) FROM contacts c WHERE {$whereClause}",
   104→            $params
   105→        );
   106→
   107→        $contacts = $this->db->fetchAll(
   108→            "SELECT c.*,
   109→                cp.relationship_health, cp.reliability_score, cp.risk_level,
   110→                cp.pricing_sensitivity, cp.ai_recommendations
   111→             FROM contacts c
   112→             LEFT JOIN contact_profiles cp ON c.id = cp.contact_id
   113→             WHERE {$whereClause}
   114→             ORDER BY {$sortColumn} {$sortOrder}
   115→             LIMIT ? OFFSET ?",
   116→            array_merge($params, [$limit, $offset])
   117→        );
   118→
   119→        return ['contacts' => $contacts, 'total' => (int)$total];
   120→    }
   121→
   122→    private function getContactsWithProfilesMongo(
   123→        string $search,
   124→        int $limit,
   125→        int $offset,
   126→        string $sortColumn,
   127→        string $sortOrder
   128→    ): array {
   129→        $filter = [];
   130→        if ($search) {
   131→            $filter = [
   132→                '$or' => [
   133→                    ['name' => ['$regex' => $search, '$options' => 'i']],
   134→                    ['email' => ['$regex' => $search, '$options' => 'i']],
   135→                    ['company' => ['$regex' => $search, '$options' => 'i']]
   136→                ]
   137→            ];
   138→        }
   139→
   140→        // Get total count
   141→        $mongo = MongoDatabase::getInstance();
   142→        $total = $mongo->count('contacts', $filter);
   143→
   144→        // Map sort column to MongoDB field
   145→        $sortDir = strtoupper($sortOrder) === 'DESC' ? -1 : 1;
   146→
   147→        // Get contacts with profiles via aggregation
   148→        $pipeline = [];
   149→
   150→        if (!empty($filter)) {
   151→            $pipeline[] = ['$match' => $filter];
   152→        }
   153→
   154→        $pipeline[] = ['$lookup' => [
   155→            'from' => 'contact_profiles',
   156→            'localField' => 'id',
   157→            'foreignField' => 'contact_id',
   158→            'as' => 'profile'
   159→        ]];
   160→
   161→        $pipeline[] = ['$unwind' => [
   162→            'path' => '$profile',
   163→            'preserveNullAndEmptyArrays' => true
   164→        ]];
   165→
   166→        $pipeline[] = ['$sort' => [$sortColumn => $sortDir]];
   167→        $pipeline[] = ['$skip' => $offset];
   168→        $pipeline[] = ['$limit' => $limit];
   169→
   170→        $results = $mongo->aggregate('contacts', $pipeline);
   171→
   172→        // Flatten profile fields
   173→        $contacts = array_map(function($doc) {
   174→            if (isset($doc['profile'])) {
   175→                $doc['relationship_health'] = $doc['profile']['relationship_health'] ?? null;
   176→                $doc['reliability_score'] = $doc['profile']['reliability_score'] ?? null;
   177→                $doc['risk_level'] = $doc['profile']['risk_level'] ?? null;
   178→                $doc['pricing_sensitivity'] = $doc['profile']['pricing_sensitivity'] ?? null;
   179→                $doc['ai_recommendations'] = $doc['profile']['ai_recommendations'] ?? null;
   180→                unset($doc['profile']);
   181→            }
   182→            return $doc;
   183→        }, $results);
   184→
   185→        return ['contacts' => $contacts, 'total' => $total];
   186→    }
   187→
   188→    /**
   189→     * Get single contact with full details
   190→     */
   191→    public function getContactWithDetails(int $contactId): ?array
   192→    {
   193→        if ($this->useMongo) {
   194→            return $this->getContactWithDetailsMongo($contactId);
   195→        }
   196→
   197→        return $this->getContactWithDetailsSQL($contactId);
   198→    }
   199→
   200→    private function getContactWithDetailsSQL(int $contactId): ?array
   201→    {
   202→        $contact = $this->db->fetchOne(
   203→            "SELECT c.*, cp.*
   204→             FROM contacts c
   205→             LEFT JOIN contact_profiles cp ON c.id = cp.contact_id
   206→             WHERE c.id = ?",
   207→            [$contactId]
   208→        );
   209→
   210→        if (!$contact) return null;
   211→
   212→        // Get recent emails
   213→        $recentEmails = $this->db->fetchAll(
   214→            "SELECT e.id, e.subject, e.date, e.direction, e.snippet,
   215→                    ea.sentiment_label, ea.urgency_level, ea.summary
   216→             FROM emails e
   217→             LEFT JOIN email_analysis ea ON e.id = ea.email_id
   218→             WHERE e.contact_id = ?
   219→             ORDER BY e.date DESC
   220→             LIMIT 20",
   221→            [$contactId]
   222→        );
   223→
   224→        // Get quotes
   225→        $quotes = $this->db->fetchAll(
   226→            "SELECT * FROM quotes WHERE contact_id = ? ORDER BY quote_date DESC",
   227→            [$contactId]
   228→        );
   229→
   230→        // Parse JSON fields
   231→        $jsonFields = ['personality_traits', 'detected_inconsistencies', 'risk_factors',
   232→                      'best_approach', 'topics_to_avoid', 'quote_history'];
   233→        foreach ($jsonFields as $field) {
   234→            if (isset($contact[$field]) && is_string($contact[$field])) {
   235→                $contact[$field] = json_decode($contact[$field], true);
   236→            }
   237→        }
   238→
   239→        return [
   240→            'contact' => $contact,
   241→            'recent_emails' => $recentEmails,
   242→            'quotes' => $quotes
   243→        ];
   244→    }
   245→
   246→    private function getContactWithDetailsMongo(int $contactId): ?array
   247→    {
   248→        $mongo = MongoDatabase::getInstance();
   249→
   250→        // Get contact with profile
   251→        $pipeline = [
   252→            ['$match' => ['id' => $contactId]],
   253→            ['$lookup' => [
   254→                'from' => 'contact_profiles',
   255→                'localField' => 'id',
   256→                'foreignField' => 'contact_id',
   257→                'as' => 'profile'
   258→            ]],
   259→            ['$unwind' => ['path' => '$profile', 'preserveNullAndEmptyArrays' => true]]
   260→        ];
   261→
   262→        $results = $mongo->aggregate('contacts', $pipeline);
   263→        if (empty($results)) return null;
   264→
   265→        $contact = $results[0];
   266→
   267→        // Flatten profile
   268→        if (isset($contact['profile'])) {
   269→            foreach ($contact['profile'] as $key => $value) {
   270→                if ($key !== '_id' && $key !== 'contact_id') {
   271→                    $contact[$key] = $value;
   272→                }
   273→            }
   274→            unset($contact['profile']);
   275→        }
   276→
   277→        // Get recent emails with analysis
   278→        $emailPipeline = [
   279→            ['$match' => ['contact_id' => $contactId]],
   280→            ['$lookup' => [
   281→                'from' => 'email_analysis',
   282→                'localField' => 'id',
   283→                'foreignField' => 'email_id',
   284→                'as' => 'analysis'
   285→            ]],
   286→            ['$unwind' => ['path' => '$analysis', 'preserveNullAndEmptyArrays' => true]],
   287→            ['$sort' => ['date' => -1]],
   288→            ['$limit' => 20],
   289→            ['$project' => [
   290→                'id' => 1,
   291→                'subject' => 1,
   292→                'date' => 1,
   293→                'direction' => 1,
   294→                'snippet' => 1,
   295→                'sentiment_label' => '$analysis.sentiment_label',
   296→                'urgency_level' => '$analysis.urgency_level',
   297→                'summary' => '$analysis.summary'
   298→            ]]
   299→        ];
   300→
   301→        $recentEmails = $mongo->aggregate('emails', $emailPipeline);
   302→
   303→        // Get quotes
   304→        $quotes = $mongo->findAll('quotes',
   305→            ['contact_id' => $contactId],
   306→            ['sort' => ['quote_date' => -1]]
   307→        );
   308→
   309→        return [
   310→            'contact' => $contact,
   311→            'recent_emails' => $recentEmails,
   312→            'quotes' => $quotes
   313→        ];
   314→    }
   315→
   316→    // =========================================================================
   317→    // EMAILS
   318→    // =========================================================================
   319→
   320→    /**
   321→     * Get emails with analysis for a contact
   322→     */
   323→    public function getEmailsWithAnalysis(
   324→        int $contactId,
   325→        int $limit = 50,
   326→        int $offset = 0,
   327→        string $sortColumn = 'date',
   328→        string $sortOrder = 'DESC'
   329→    ): array {
   330→        if ($this->useMongo) {
   331→            return $this->getEmailsWithAnalysisMongo($contactId, $limit, $offset, $sortColumn, $sortOrder);
   332→        }
   333→
   334→        return $this->getEmailsWithAnalysisSQL($contactId, $limit, $offset, $sortColumn, $sortOrder);
   335→    }
   336→
   337→    private function getEmailsWithAnalysisSQL(
   338→        int $contactId,
   339→        int $limit,
   340→        int $offset,
   341→        string $sortColumn,
   342→        string $sortOrder
   343→    ): array {
   344→        $total = $this->db->fetchColumn(
   345→            "SELECT COUNT(*) FROM emails WHERE contact_id = ?",
   346→            [$contactId]
   347→        );
   348→
   349→        $sortOrder = strtoupper($sortOrder) === 'DESC' ? 'DESC' : 'ASC';
   350→
   351→        $emails = $this->db->fetchAll(
   352→            "SELECT e.*, ea.sentiment_label, ea.urgency_level, ea.summary, ea.key_points
   353→             FROM emails e
   354→             LEFT JOIN email_analysis ea ON e.id = ea.email_id
   355→             WHERE e.contact_id = ?
   356→             ORDER BY e.{$sortColumn} {$sortOrder}
   357→             LIMIT ? OFFSET ?",
   358→            [$contactId, $limit, $offset]
   359→        );
   360→
   361→        return ['emails' => $emails, 'total' => (int)$total];
   362→    }
   363→
   364→    private function getEmailsWithAnalysisMongo(
   365→        int $contactId,
   366→        int $limit,
   367→        int $offset,
   368→        string $sortColumn,
   369→        string $sortOrder
   370→    ): array {
   371→        $mongo = MongoDatabase::getInstance();
   372→
   373→        $total = $mongo->count('emails', ['contact_id' => $contactId]);
   374→
   375→        $sortDir = strtoupper($sortOrder) === 'DESC' ? -1 : 1;
   376→
   377→        $pipeline = [
   378→            ['$match' => ['contact_id' => $contactId]],
   379→            ['$lookup' => [
   380→                'from' => 'email_analysis',
   381→                'localField' => 'id',
   382→                'foreignField' => 'email_id',
   383→                'as' => 'analysis'
   384→            ]],
   385→            ['$unwind' => ['path' => '$analysis', 'preserveNullAndEmptyArrays' => true]],
   386→            ['$sort' => [$sortColumn => $sortDir]],
   387→            ['$skip' => $offset],
   388→            ['$limit' => $limit]
   389→        ];
   390→
   391→        $results = $mongo->aggregate('emails', $pipeline);
   392→
   393→        // Flatten analysis
   394→        $emails = array_map(function($doc) {
   395→            if (isset($doc['analysis'])) {
   396→                $doc['sentiment_label'] = $doc['analysis']['sentiment_label'] ?? null;
   397→                $doc['urgency_level'] = $doc['analysis']['urgency_level'] ?? null;
   398→                $doc['summary'] = $doc['analysis']['summary'] ?? null;
   399→                $doc['key_points'] = $doc['analysis']['key_points'] ?? null;
   400→                unset($doc['analysis']);
   401→            }
   402→            return $doc;
   403→        }, $results);
   404→
   405→        return ['emails' => $emails, 'total' => $total];
   406→    }
   407→
   408→    /**
   409→     * Get single email with full details
   410→     */
   411→    public function getEmailWithDetails(int $emailId): ?array
   412→    {
   413→        if ($this->useMongo) {
   414→            $repo = new MongoRepository();
   415→            return $repo->getEmailWithDetails($emailId);
   416→        }
   417→
   418→        return $this->db->fetchOne(
   419→            "SELECT e.*, c.name as contact_name, c.email as contact_email,
   420→                    ea.*, ta.summary as thread_summary, ta.key_insights
   421→             FROM emails e
   422→             LEFT JOIN contacts c ON e.contact_id = c.id
   423→             LEFT JOIN email_analysis ea ON e.id = ea.email_id
   424→             LEFT JOIN thread_analysis ta ON e.thread_id = ta.thread_id
   425→             WHERE e.id = ?",
   426→            [$emailId]
   427→        );
   428→    }
   429→
   430→    /**
   431→     * Search emails
   432→     */
   433→    public function searchEmails(
   434→        string $query,
   435→        ?int $contactId = null,
   436→        int $limit = 50
   437→    ): array {
   438→        if ($this->useMongo) {
   439→            $repo = new MongoRepository();
   440→            return $repo->searchEmails($query, [
   441→                'contact_id' => $contactId,
   442→                'limit' => $limit
   443→            ]);
   444→        }
   445→
   446→        $whereClause = "(e.subject LIKE ? OR e.body_text LIKE ?)";
   447→        $params = ["%{$query}%", "%{$query}%"];
   448→
   449→        if ($contactId) {
   450→            $whereClause .= " AND e.contact_id = ?";
   451→            $params[] = $contactId;
   452→        }
   453→
   454→        return $this->db->fetchAll(
   455→            "SELECT e.*, c.name as contact_name, c.email as contact_email
   456→             FROM emails e
   457→             LEFT JOIN contacts c ON e.contact_id = c.id
   458→             WHERE {$whereClause}
   459→             ORDER BY e.date DESC
   460→             LIMIT ?",
   461→            array_merge($params, [$limit])
   462→        );
   463→    }
   464→
   465→    // =========================================================================
   466→    // THREADS
   467→    // =========================================================================
   468→
   469→    /**
   470→     * Get thread analysis with contact info
   471→     */
   472→    public function getThreadsWithContact(
   473→        ?int $contactId = null,
   474→        int $limit = 50
   475→    ): array {
   476→        if ($this->useMongo) {
   477→            $filter = $contactId ? ['contact_id' => $contactId] : [];
   478→            $repo = new MongoRepository();
   479→            return $repo->getThreadsWithContact($filter, ['limit' => $limit]);
   480→        }
   481→
   482→        $whereClause = $contactId ? "ta.contact_id = ?" : "1=1";
   483→        $params = $contactId ? [$contactId, $limit] : [$limit];
   484→
   485→        return $this->db->fetchAll(
   486→            "SELECT ta.*, c.name as contact_name, c.email as contact_email
   487→             FROM thread_analysis ta
   488→             LEFT JOIN contacts c ON ta.contact_id = c.id
   489→             WHERE {$whereClause}
   490→             ORDER BY ta.analyzed_at DESC
   491→             LIMIT ?",
   492→            $params
   493→        );
   494→    }
   495→
   496→    /**
   497→     * Get list of threads with summary information (paginated)
   498→     *
   499→     * @param int|null $contactId Filter by contact ID
   500→     * @param string $search Search term for subject
   501→     * @param int $limit Results per page
   502→     * @param int $offset Pagination offset
   503→     * @return array ['threads' => [...], 'total' => int]
   504→     */
   505→    public function getThreads(?int $contactId = null, string $search = '', int $limit = 20, int $offset = 0): array
   506→    {
   507→        if ($this->useMongo) {
   508→            return $this->getThreadsMongo($contactId, $search, $limit, $offset);
   509→        }
   510→
   511→        return $this->getThreadsSQL($contactId, $search, $limit, $offset);
   512→    }
   513→
   514→    private function getThreadsSQL(?int $contactId, string $search, int $limit, int $offset): array
   515→    {
   516→        $where = 'e.thread_id IS NOT NULL AND e.thread_id != ""';
   517→        $params = [];
   518→
   519→        if ($contactId) {
   520→            $where .= ' AND e.contact_id = ?';
   521→            $params[] = $contactId;
   522→        }
   523→
   524→        if ($search) {
   525→            $where .= ' AND e.subject LIKE ?';
   526→            $params[] = '%' . $search . '%';
   527→        }
   528→
   529→        // Get total thread count
   530→        $total = $this->db->fetchColumn(
   531→            "SELECT COUNT(DISTINCT e.thread_id)
   532→             FROM emails e
   533→             WHERE {$where}",
   534→            $params
   535→        );
   536→
   537→        // Get threads with summary info
   538→        $threads = $this->db->fetchAll(
   539→            "SELECT
   540→                e.thread_id,
   541→                MIN(e.id) as first_email_id,
   542→                MAX(e.id) as last_email_id,
   543→                MIN(e.subject) as subject,
   544→                MIN(e.date) as first_date,
   545→                MAX(e.date) as last_date,
   546→                COUNT(*) as email_count,
   547→                SUM(CASE WHEN e.direction = 'inbound' THEN 1 ELSE 0 END) as inbound_count,
   548→                SUM(CASE WHEN e.direction = 'outbound' THEN 1 ELSE 0 END) as outbound_count,
   549→                MAX(e.contact_id) as contact_id,
   550→                MAX(c.name) as contact_name,
   551→                ta.is_analyzed,
   552→                ta.needs_reanalysis
   553→             FROM emails e
   554→             LEFT JOIN contacts c ON e.contact_id = c.id
   555→             LEFT JOIN thread_analysis ta ON e.thread_id = ta.thread_id
   556→             WHERE {$where}
   557→             GROUP BY e.thread_id
   558→             ORDER BY MAX(e.date) DESC
   559→             LIMIT ? OFFSET ?",
   560→            array_merge($params, [$limit, $offset])
   561→        );
   562→
   563→        return [
   564→            'threads' => $threads,
   565→            'total' => (int)$total
   566→        ];
   567→    }
   568→
   569→    private function getThreadsMongo(?int $contactId, string $search, int $limit, int $offset): array
   570→    {
   571→        $mongo = MongoDatabase::getInstance();
   572→
   573→        // Build match filter
   574→        $matchFilter = [
   575→            'thread_id' => ['$ne' => null, '$ne' => '']
   576→        ];
   577→
   578→        if ($contactId) {
   579→            $matchFilter['contact_id'] = $contactId;
   580→        }
   581→
   582→        if ($search) {
   583→            $matchFilter['subject'] = ['$regex' => $search, '$options' => 'i'];
   584→        }
   585→
   586→        // Count distinct threads for total
   587→        $countPipeline = [
   588→            ['$match' => $matchFilter],
   589→            ['$group' => ['_id' => '$thread_id']],
   590→            ['$count' => 'total']
   591→        ];
   592→
   593→        $countResult = $mongo->aggregate('emails', $countPipeline);
   594→        $total = !empty($countResult) ? $countResult[0]['total'] : 0;
   595→
   596→        // Get threads with aggregated data
   597→        $pipeline = [
   598→            ['$match' => $matchFilter],
   599→            ['$group' => [
   600→                '_id' => '$thread_id',
   601→                'first_email_id' => ['$min' => '$id'],
   602→                'last_email_id' => ['$max' => '$id'],
   603→                'subject' => ['$first' => '$subject'],
   604→                'first_date' => ['$min' => '$date'],
   605→                'last_date' => ['$max' => '$date'],
   606→                'email_count' => ['$sum' => 1],
   607→                'inbound_count' => ['$sum' => ['$cond' => [['$eq' => ['$direction', 'inbound']], 1, 0]]],
   608→                'outbound_count' => ['$sum' => ['$cond' => [['$eq' => ['$direction', 'outbound']], 1, 0]]],
   609→                'contact_id' => ['$max' => '$contact_id']
   610→            ]],
   611→            ['$lookup' => [
   612→                'from' => 'contacts',
   613→                'localField' => 'contact_id',
   614→                'foreignField' => 'id',
   615→                'as' => 'contact'
   616→            ]],
   617→            ['$lookup' => [
   618→                'from' => 'thread_analysis',
   619→                'localField' => '_id',
   620→                'foreignField' => 'thread_id',
   621→                'as' => 'analysis'
   622→            ]],
   623→            ['$project' => [
   624→                '_id' => 0,
   625→                'thread_id' => '$_id',
   626→                'first_email_id' => 1,
   627→                'last_email_id' => 1,
   628→                'subject' => 1,
   629→                'first_date' => 1,
   630→                'last_date' => 1,
   631→                'email_count' => 1,
   632→                'inbound_count' => 1,
   633→                'outbound_count' => 1,
   634→                'contact_id' => 1,
   635→                'contact_name' => ['$arrayElemAt' => ['$contact.name', 0]],
   636→                'is_analyzed' => ['$arrayElemAt' => ['$analysis.is_analyzed', 0]],
   637→                'needs_reanalysis' => ['$arrayElemAt' => ['$analysis.needs_reanalysis', 0]]
   638→            ]],
   639→            ['$sort' => ['last_date' => -1]],
   640→            ['$skip' => $offset],
   641→            ['$limit' => $limit]
   642→        ];
   643→
   644→        $threads = $mongo->aggregate('emails', $pipeline);
   645→
   646→        return [
   647→            'threads' => $threads,
   648→            'total' => $total
   649→        ];
   650→    }
   651→
   652→    /**
   653→     * Get full thread details with all emails and analysis
   654→     *
   655→     * @param string $threadId Gmail thread ID
   656→     * @return array ['thread_id', 'emails', 'analysis', 'email_count']
   657→     */
   658→    public function getThreadDetails(string $threadId): array
   659→    {
   660→        if ($this->useMongo) {
   661→            return $this->getThreadDetailsMongo($threadId);
   662→        }
   663→
   664→        return $this->getThreadDetailsSQL($threadId);
   665→    }
   666→
   667→    private function getThreadDetailsSQL(string $threadId): array
   668→    {
   669→        // Get all emails in thread with contact info
   670→        $emails = $this->db->fetchAll(
   671→            "SELECT e.*, c.name as contact_name, c.email as contact_email
   672→             FROM emails e
   673→             LEFT JOIN contacts c ON e.contact_id = c.id
   674→             WHERE e.thread_id = ?
   675→             ORDER BY e.date ASC",
   676→            [$threadId]
   677→        );
   678→
   679→        // Get thread analysis with email analysis
   680→        $analysis = $this->db->fetchOne(
   681→            "SELECT ta.*, COALESCE(ea.summary, ta.summary) as summary, ea.sentiment_label,
   682→                    COALESCE(ea.deception_score, ta.deception_score) as deception_score,
   683→                    COALESCE(ea.raw_analysis, ta.raw_analysis) as raw_analysis,
   684→                    COALESCE(ea.key_points, ta.key_points) as key_points
   685→             FROM thread_analysis ta
   686→             LEFT JOIN email_analysis ea ON ta.last_email_id = ea.email_id
   687→             WHERE ta.thread_id = ?",
   688→            [$threadId]
   689→        );
   690→
   691→        return [
   692→            'thread_id' => $threadId,
   693→            'emails' => $emails,
   694→            'email_count' => count($emails),
   695→            'analysis' => $analysis
   696→        ];
   697→    }
   698→
   699→    private function getThreadDetailsMongo(string $threadId): array
   700→    {
   701→        $mongo = MongoDatabase::getInstance();
   702→
   703→        // Get all emails in thread with contact info using aggregation
   704→        $emailPipeline = [
   705→            ['$match' => ['thread_id' => $threadId]],
   706→            ['$lookup' => [
   707→                'from' => 'contacts',
   708→                'localField' => 'contact_id',
   709→                'foreignField' => 'id',
   710→                'as' => 'contact'
   711→            ]],
   712→            ['$unwind' => ['path' => '$contact', 'preserveNullAndEmptyArrays' => true]],
   713→            ['$sort' => ['date' => 1]],  // ASC
   714→            ['$project' => [
   715→                '_id' => 0,
   716→                'id' => 1,
   717→                'gmail_id' => 1,
   718→                'thread_id' => 1,
   719→                'contact_id' => 1,
   720→                'subject' => 1,
   721→                'from_email' => 1,
   722→                'to_email' => 1,
   723→                'date' => 1,
   724→                'direction' => 1,
   725→                'snippet' => 1,
   726→                'body_text' => 1,
   727→                'body_html' => 1,
   728→                'has_attachments' => 1,
   729→                'label_ids' => 1,
   730→                'contact_name' => '$contact.name',
   731→                'contact_email' => '$contact.email'
   732→            ]]
   733→        ];
   734→
   735→        $emails = $mongo->aggregate('emails', $emailPipeline);
   736→
   737→        // Get thread analysis with email analysis
   738→        $analysisPipeline = [
   739→            ['$match' => ['thread_id' => $threadId]],
   740→            ['$lookup' => [
   741→                'from' => 'email_analysis',
   742→                'localField' => 'last_email_id',
   743→                'foreignField' => 'email_id',
   744→                'as' => 'email_analysis'
   745→            ]],
   746→            ['$unwind' => ['path' => '$email_analysis', 'preserveNullAndEmptyArrays' => true]],
   747→            ['$project' => [
   748→                '_id' => 0,
   749→                'thread_id' => 1,
   750→                'contact_id' => 1,
   751→                'subject' => 1,
   752→                'email_count' => 1,
   753→                'first_date' => 1,
   754→                'last_date' => 1,
   755→                'last_email_id' => 1,
   756→                'trust_score' => 1,
   757→                'deception_score' => 1,
   758→                'risk_level' => 1,
   759→                'summary' => ['$ifNull' => ['$email_analysis.summary', '$summary']],
   760→                'sentiment_label' => ['$ifNull' => ['$email_analysis.sentiment_label', null]],
   761→                'raw_analysis' => ['$ifNull' => ['$email_analysis.raw_analysis', '$raw_analysis']],
   762→                'key_insights' => 1,
   763→                'key_points' => ['$ifNull' => ['$email_analysis.key_points', '$key_points']],
   764→                'action_items' => 1,
   765→                'warnings' => 1,
   766→                'analyzed_at' => 1,
   767→                'tokens_used' => 1
   768→            ]],
   769→            ['$limit' => 1]
   770→        ];
   771→
   772→        $analysisResults = $mongo->aggregate('thread_analysis', $analysisPipeline);
   773→        $analysis = !empty($analysisResults) ? $analysisResults[0] : null;
   774→
   775→        return [
   776→            'thread_id' => $threadId,
   777→            'emails' => $emails,
   778→            'email_count' => count($emails),
   779→            'analysis' => $analysis
   780→        ];
   781→    }
   782→
   783→    /**
   784→     * Get thread participants with aggregated statistics
   785→     *
   786→     * @param string $threadId Gmail thread ID
   787→     * @param string $userEmail Current user's email for 'self' detection
   788→     * @return array ['participants' => [...], 'subject' => string]
   789→     */
   790→    public function getThreadParticipants(string $threadId, string $userEmail): array
   791→    {
   792→
   793→        if ($this->useMongo) {
   794→            return $this->getThreadParticipantsMongo($threadId, $userEmail);
   795→        }
   796→
   797→        return $this->getThreadParticipantsSQL($threadId, $userEmail);
   798→    }
   799→
   800→    private function getThreadParticipantsSQL(string $threadId, string $userEmail): array
   801→    {
   802→        $userEmail = strtolower($userEmail);
   803→
   804→        // Fetch ALL emails in the thread to extract participants from FROM, TO, and CC fields
   805→        $emails = $this->db->fetchAll(
   806→            "SELECT from_email, to_email, cc_email, date, direction, subject
   807→             FROM emails
   808→             WHERE thread_id = ?
   809→             ORDER BY date ASC",
   810→            [$threadId]
   811→        );
   812→
   813→        // Collect all unique participants with their stats
   814→        $participantStats = [];
   815→        $threadSubject = '';
   816→
   817→        foreach ($emails as $email) {
   818→            // Get subject from first email
   819→            if (empty($threadSubject) && !empty($email['subject'])) {
   820→                $threadSubject = $email['subject'];
   821→            }
   822→
   823→            $date = $email['date'] ?? null;
   824→            $direction = $email['direction'] ?? '';
   825→
   826→            // Extract FROM participant
   827→            if (!empty($email['from_email'])) {
   828→                $fromEmail = strtolower(trim($email['from_email']));
   829→                if (!isset($participantStats[$fromEmail])) {
   830→                    $participantStats[$fromEmail] = [
   831→                        'email' => $fromEmail,
   832→                        'sent_count' => 0,
   833→                        'received_count' => 0,
   834→                        'total_count' => 0,
   835→                        'first_email' => $date,
   836→                        'last_email' => $date
   837→                    ];
   838→                }
   839→                $participantStats[$fromEmail]['sent_count']++;
   840→                $participantStats[$fromEmail]['total_count']++;
   841→                if ($date) {
   842→                    if (!$participantStats[$fromEmail]['first_email'] || $date < $participantStats[$fromEmail]['first_email']) {
   843→                        $participantStats[$fromEmail]['first_email'] = $date;
   844→                    }
   845→                    if (!$participantStats[$fromEmail]['last_email'] || $date > $participantStats[$fromEmail]['last_email']) {
   846→                        $participantStats[$fromEmail]['last_email'] = $date;
   847→                    }
   848→                }
   849→            }
   850→
   851→            // Extract TO participant(s)
   852→            if (!empty($email['to_email'])) {
   853→                $toEmail = strtolower(trim($email['to_email']));
   854→                if (!isset($participantStats[$toEmail])) {
   855→                    $participantStats[$toEmail] = [
   856→                        'email' => $toEmail,
   857→                        'sent_count' => 0,
   858→                        'received_count' => 0,
   859→                        'total_count' => 0,
   860→                        'first_email' => $date,
   861→                        'last_email' => $date
   862→                    ];
   863→                }
   864→                $participantStats[$toEmail]['received_count']++;
   865→                $participantStats[$toEmail]['total_count']++;
   866→                if ($date) {
   867→                    if (!$participantStats[$toEmail]['first_email'] || $date < $participantStats[$toEmail]['first_email']) {
   868→                        $participantStats[$toEmail]['first_email'] = $date;
   869→                    }
   870→                    if (!$participantStats[$toEmail]['last_email'] || $date > $participantStats[$toEmail]['last_email']) {
   871→                        $participantStats[$toEmail]['last_email'] = $date;
   872→                    }
   873→                }
   874→            }
   875→
   876→            // Extract CC participant(s)
   877→            if (!empty($email['cc_email'])) {
   878→                // CC might be comma-separated, split it
   879→                $ccEmails = array_map('trim', explode(',', $email['cc_email']));
   880→                foreach ($ccEmails as $ccEmail) {
   881→                    $ccEmail = strtolower($ccEmail);
   882→                    if (empty($ccEmail)) continue;
   883→
   884→                    if (!isset($participantStats[$ccEmail])) {
   885→                        $participantStats[$ccEmail] = [
   886→                            'email' => $ccEmail,
   887→                            'sent_count' => 0,
   888→                            'received_count' => 0,
   889→                            'total_count' => 0,
   890→                            'first_email' => $date,
   891→                            'last_email' => $date
   892→                        ];
   893→                    }
   894→                    $participantStats[$ccEmail]['received_count']++;
   895→                    $participantStats[$ccEmail]['total_count']++;
   896→                    if ($date) {
   897→                        if (!$participantStats[$ccEmail]['first_email'] || $date < $participantStats[$ccEmail]['first_email']) {
   898→                            $participantStats[$ccEmail]['first_email'] = $date;
   899→                        }
   900→                        if (!$participantStats[$ccEmail]['last_email'] || $date > $participantStats[$ccEmail]['last_email']) {
   901→                            $participantStats[$ccEmail]['last_email'] = $date;
   902→                        }
   903→                    }
   904→                }
   905→            }
   906→        }
   907→
   908→        // Convert to array and sort by total_count
   909→        $participants = array_values($participantStats);
   910→        usort($participants, function($a, $b) {
   911→            return $b['total_count'] - $a['total_count'];
   912→        });
   913→
   914→        // Enrich with contact data and determine participant type
   915→        foreach ($participants as &$p) {
   916→            // Determine participant type
   917→            $p['participant_type'] = (strtolower($p['email']) === $userEmail) ? 'self' : 'external';
   918→
   919→            // Calculate inbound/outbound based on direction and participant type
   920→            if ($p['participant_type'] === 'self') {
   921→                $p['inbound_count'] = $p['received_count'];
   922→                $p['outbound_count'] = $p['sent_count'];
   923→            } else {
   924→                $p['inbound_count'] = $p['sent_count'];  // External sent TO us = inbound
   925→                $p['outbound_count'] = $p['received_count'];  // We sent TO external = outbound (for us)
   926→            }
   927→
   928→            $p['email_count'] = $p['total_count'];
   929→
   930→            // Get name guess from email
   931→            $emailParts = explode('@', $p['email']);
   932→            $namePart = $emailParts[0] ?? '';
   933→            $nameGuess = ucfirst(explode('.', $namePart)[0]);
   934→
   935→            // Fetch contact data from contacts table
   936→            $contact = $this->db->fetchOne(
   937→                "SELECT id, name, email, company, type FROM contacts WHERE email = ?",
   938→                [$p['email']]
   939→            );
   940→
   941→            // Fetch enrichment data from contact_enrichment table
   942→            $enrichment = null;
   943→            if ($contact && !empty($contact['id'])) {
   944→                $enrichment = $this->db->fetchOne(
   945→                    "SELECT job_title, phone, mobile_phone, linkedin_url
   946→                     FROM contact_enrichment
   947→                     WHERE contact_id = ?",
   948→                    [$contact['id']]
   949→                );
   950→            }
   951→
   952→            if ($p['participant_type'] === 'external') {
   953→                if ($contact) {
   954→                    $p['contact_id'] = $contact['id'];
   955→                    $p['name'] = $contact['name'] ?? $nameGuess;
   956→                    $p['company'] = $contact['company'] ?? '';
   957→                    $p['contact_type'] = $contact['type'] ?? null;
   958→
   959→                    // Add enriched data
   960→                    if ($enrichment) {
   961→                        $p['job_title'] = $enrichment['job_title'] ?? null;
   962→                        $p['phone'] = $enrichment['phone'] ?? $enrichment['mobile_phone'] ?? null;
   963→                        $p['linkedin_url'] = $enrichment['linkedin_url'] ?? null;
   964→                    }
   965→                } else {
   966→                    $p['contact_id'] = null;
   967→                    $p['name'] = $nameGuess;
   968→                    $p['company'] = $emailParts[1] ?? '';
   969→                    $p['contact_type'] = null;
   970→                }
   971→            } else {
   972→                $p['name'] = 'You';
   973→                $p['company'] = '';
   974→                $p['contact_id'] = $contact ? $contact['id'] : null;
   975→                $p['contact_type'] = null;
   976→            }
   977→
   978→            // Remove temporary fields
   979→            unset($p['sent_count'], $p['received_count'], $p['total_count']);
   980→        }
   981→
   982→        return [
   983→            'participants' => $participants,
   984→            'subject' => $threadSubject
   985→        ];
   986→    }
   987→
   988→    private function getThreadParticipantsMongo(string $threadId, string $userEmail): array
   989→    {
   990→
   991→        $mongo = MongoDatabase::getInstance();
   992→        $userEmail = strtolower($userEmail);
   993→
   994→        // Fetch ALL emails in the thread to extract participants from FROM, TO, and CC fields
   995→        $emails = $mongo->findAll('emails', ['thread_id' => $threadId]);
   996→
   997→        // Collect all unique participants with their stats
   998→        $participantStats = [];
   999→        $threadSubject = '';
  1000→
  1001→        foreach ($emails as $email) {
  1002→            // Get subject from first email
  1003→            if (empty($threadSubject) && !empty($email['subject'])) {
  1004→                $threadSubject = $email['subject'];
  1005→            }
  1006→
  1007→            $date = $email['date'] ?? null;
  1008→            $direction = $email['direction'] ?? '';
  1009→
  1010→            // Extract FROM participant
  1011→            if (!empty($email['from_email'])) {
  1012→                $fromEmail = strtolower(trim($email['from_email']));
  1013→                if (!isset($participantStats[$fromEmail])) {
  1014→                    $participantStats[$fromEmail] = [
  1015→                        'email' => $fromEmail,
  1016→                        'sent_count' => 0,
  1017→                        'received_count' => 0,
  1018→                        'total_count' => 0,
  1019→                        'first_email' => $date,
  1020→                        'last_email' => $date
  1021→                    ];
  1022→                }
  1023→                $participantStats[$fromEmail]['sent_count']++;
  1024→                $participantStats[$fromEmail]['total_count']++;
  1025→                if ($date) {
  1026→                    if (!$participantStats[$fromEmail]['first_email'] || $date < $participantStats[$fromEmail]['first_email']) {
  1027→                        $participantStats[$fromEmail]['first_email'] = $date;
  1028→                    }
  1029→                    if (!$participantStats[$fromEmail]['last_email'] || $date > $participantStats[$fromEmail]['last_email']) {
  1030→                        $participantStats[$fromEmail]['last_email'] = $date;
  1031→                    }
  1032→                }
  1033→            }
  1034→
  1035→            // Extract TO participant(s)
  1036→            if (!empty($email['to_email'])) {
  1037→                $toEmail = strtolower(trim($email['to_email']));
  1038→                if (!isset($participantStats[$toEmail])) {
  1039→                    $participantStats[$toEmail] = [
  1040→                        'email' => $toEmail,
  1041→                        'sent_count' => 0,
  1042→                        'received_count' => 0,
  1043→                        'total_count' => 0,
  1044→                        'first_email' => $date,
  1045→                        'last_email' => $date
  1046→                    ];
  1047→                }
  1048→                $participantStats[$toEmail]['received_count']++;
  1049→                $participantStats[$toEmail]['total_count']++;
  1050→                if ($date) {
  1051→                    if (!$participantStats[$toEmail]['first_email'] || $date < $participantStats[$toEmail]['first_email']) {
  1052→                        $participantStats[$toEmail]['first_email'] = $date;
  1053→                    }
  1054→                    if (!$participantStats[$toEmail]['last_email'] || $date > $participantStats[$toEmail]['last_email']) {
  1055→                        $participantStats[$toEmail]['last_email'] = $date;
  1056→                    }
  1057→                }
  1058→            }
  1059→
  1060→            // Extract CC participant(s)
  1061→            if (!empty($email['cc_email'])) {
  1062→                // CC might be comma-separated, split it
  1063→                $ccEmails = array_map('trim', explode(',', $email['cc_email']));
  1064→                foreach ($ccEmails as $ccEmail) {
  1065→                    $ccEmail = strtolower($ccEmail);
  1066→                    if (empty($ccEmail)) continue;
  1067→
  1068→                    if (!isset($participantStats[$ccEmail])) {
  1069→                        $participantStats[$ccEmail] = [
  1070→                            'email' => $ccEmail,
  1071→                            'sent_count' => 0,
  1072→                            'received_count' => 0,
  1073→                            'total_count' => 0,
  1074→                            'first_email' => $date,
  1075→                            'last_email' => $date
  1076→                        ];
  1077→                    }
  1078→                    $participantStats[$ccEmail]['received_count']++;
  1079→                    $participantStats[$ccEmail]['total_count']++;
  1080→                    if ($date) {
  1081→                        if (!$participantStats[$ccEmail]['first_email'] || $date < $participantStats[$ccEmail]['first_email']) {
  1082→                            $participantStats[$ccEmail]['first_email'] = $date;
  1083→                        }
  1084→                        if (!$participantStats[$ccEmail]['last_email'] || $date > $participantStats[$ccEmail]['last_email']) {
  1085→                            $participantStats[$ccEmail]['last_email'] = $date;
  1086→                        }
  1087→                    }
  1088→                }
  1089→            }
  1090→        }
  1091→
  1092→        // Convert to array and sort by total_count
  1093→        $participants = array_values($participantStats);
  1094→        usort($participants, function($a, $b) {
  1095→            return $b['total_count'] - $a['total_count'];
  1096→        });
  1097→
  1098→        // Enrich with contact data and determine participant type
  1099→        foreach ($participants as &$p) {
  1100→            // Determine participant type
  1101→            $p['participant_type'] = (strtolower($p['email']) === $userEmail) ? 'self' : 'external';
  1102→
  1103→            // Calculate inbound/outbound based on direction and participant type
  1104→            if ($p['participant_type'] === 'self') {
  1105→                $p['inbound_count'] = $p['received_count'];
  1106→                $p['outbound_count'] = $p['sent_count'];
  1107→            } else {
  1108→                $p['inbound_count'] = $p['sent_count'];  // External sent TO us = inbound
  1109→                $p['outbound_count'] = $p['received_count'];  // We sent TO external = outbound (for us)
  1110→            }
  1111→
  1112→            $p['email_count'] = $p['total_count'];
  1113→
  1114→            // Get name guess from email
  1115→            $emailParts = explode('@', $p['email']);
  1116→            $namePart = $emailParts[0] ?? '';
  1117→            $nameGuess = ucfirst(explode('.', $namePart)[0]);
  1118→
  1119→            // Fetch contact data from contacts collection
  1120→            $contact = $mongo->findOne('contacts', ['email' => $p['email']]);
  1121→
  1122→            // Fetch enrichment data from contact_enrichment collection
  1123→            $enrichment = null;
  1124→            if ($contact && !empty($contact['id'])) {
  1125→                $enrichment = $mongo->findOne('contact_enrichment', ['contact_id' => $contact['id']]);
  1126→            }
  1127→
  1128→            if ($p['participant_type'] === 'external') {
  1129→                if ($contact) {
  1130→                    $p['contact_id'] = $contact['id'] ?? null;
  1131→                    $p['name'] = $contact['name'] ?? $nameGuess;
  1132→                    $p['company'] = $contact['company'] ?? '';
  1133→                    $p['contact_type'] = $contact['type'] ?? null;
  1134→
  1135→                    // Add enriched data
  1136→                    if ($enrichment) {
  1137→                        $p['job_title'] = $enrichment['job_title'] ?? null;
  1138→                        $p['phone'] = $enrichment['phone'] ?? $enrichment['mobile_phone'] ?? null;
  1139→                        $p['linkedin_url'] = $enrichment['linkedin_url'] ?? null;
  1140→                    }
  1141→                } else {
  1142→                    $p['contact_id'] = null;
  1143→                    $p['name'] = $nameGuess;
  1144→                    $p['company'] = $emailParts[1] ?? '';
  1145→                    $p['contact_type'] = null;
  1146→                }
  1147→            } else {
  1148→                $p['name'] = 'You';
  1149→                $p['company'] = '';
  1150→                $p['contact_id'] = $contact['id'] ?? null;
  1151→                $p['contact_type'] = null;
  1152→            }
  1153→
  1154→            // Remove temporary fields
  1155→            unset($p['sent_count'], $p['received_count'], $p['total_count']);
  1156→        }
  1157→
  1158→        return [
  1159→            'participants' => $participants,
  1160→            'subject' => $threadSubject
  1161→        ];
  1162→    }
  1163→
  1164→    // =========================================================================
  1165→    // LOGS
  1166→    // =========================================================================
  1167→
  1168→    /**
  1169→     * Get API logs with optional filtering
  1170→     *
  1171→     * @param string|null $threadId Filter by thread ID
  1172→     * @param string|null $level Filter by log level (info, warning, error)
  1173→     * @param int $limit Maximum number of logs to return
  1174→     * @return array ['logs' => [...], 'count' => int]
  1175→     */
  1176→    public function getLogs(?string $threadId = null, ?string $level = null, int $limit = 50): array
  1177→    {
  1178→        if ($this->useMongo) {
  1179→            return $this->getLogsMongo($threadId, $level, $limit);
  1180→        }
  1181→
  1182→        return $this->getLogsSQL($threadId, $level, $limit);
  1183→    }
  1184→
  1185→    private function getLogsSQL(?string $threadId, ?string $level, int $limit): array
  1186→    {
  1187→        $where = '1=1';
  1188→        $params = [];
  1189→
  1190→        if ($threadId) {
  1191→            $where .= ' AND thread_id = ?';
  1192→            $params[] = $threadId;
  1193→        }
  1194→        if ($level) {
  1195→            $where .= ' AND log_level = ?';
  1196→            $params[] = $level;
  1197→        }
  1198→
  1199→        $logs = $this->db->fetchAll(
  1200→            "SELECT id, log_level, source, message, context, thread_id, created_at
  1201→             FROM api_logs
  1202→             WHERE {$where}
  1203→             ORDER BY created_at DESC
  1204→             LIMIT ?",
  1205→            array_merge($params, [$limit])
  1206→        );
  1207→
  1208→        // Parse context JSON (MySQL stores as string)
  1209→        foreach ($logs as &$log) {
  1210→            if (isset($log['context']) && is_string($log['context'])) {
  1211→                $log['context'] = json_decode($log['context'], true);
  1212→            }
  1213→        }
  1214→
  1215→        return [
  1216→            'logs' => $logs,
  1217→            'count' => count($logs)
  1218→        ];
  1219→    }
  1220→
  1221→    private function getLogsMongo(?string $threadId, ?string $level, int $limit): array
  1222→    {
  1223→        $mongo = MongoDatabase::getInstance();
  1224→
  1225→        $filter = [];
  1226→
  1227→        if ($threadId) {
  1228→            $filter['thread_id'] = $threadId;
  1229→        }
  1230→        if ($level) {
  1231→            $filter['log_level'] = $level;
  1232→        }
  1233→
  1234→        $logs = $mongo->findAll('api_logs', $filter, [
  1235→            'sort' => ['created_at' => -1],
  1236→            'limit' => $limit
  1237→        ]);
  1238→
  1239→        // Context should already be an array in MongoDB, but handle string case for safety
  1240→        foreach ($logs as &$log) {
  1241→            if (isset($log['context']) && is_string($log['context'])) {
  1242→                $log['context'] = json_decode($log['context'], true);
  1243→            }
  1244→        }
  1245→
  1246→        return [
  1247→            'logs' => $logs,
  1248→            'count' => count($logs)
  1249→        ];
  1250→    }
  1251→
  1252→    // =========================================================================
  1253→    // ALERTS
  1254→    // =========================================================================
  1255→
  1256→    /**
  1257→     * Get alerts with contact info
  1258→     */
  1259→    public function getAlertsWithContact(
  1260→        ?string $status = null,
  1261→        int $limit = 50
  1262→    ): array {
  1263→        if ($this->useMongo) {
  1264→            $filter = $status ? ['status' => $status] : [];
  1265→            $repo = new MongoRepository();
  1266→            return $repo->getAlertsWithContact($filter, ['limit' => $limit]);
  1267→        }
  1268→
  1269→        $whereClause = $status ? "a.status = ?" : "1=1";
  1270→        $params = $status ? [$status, $limit] : [$limit];
  1271→
  1272→        return $this->db->fetchAll(
  1273→            "SELECT a.*, c.name as contact_name, c.email as contact_email
  1274→             FROM alerts a
  1275→             LEFT JOIN contacts c ON a.contact_id = c.id
  1276→             WHERE {$whereClause}
  1277→             ORDER BY a.created_at DESC
  1278→             LIMIT ?",
  1279→            $params
  1280→        );
  1281→    }
  1282→
  1283→    // =========================================================================
  1284→    // BUSINESS INTELLIGENCE
  1285→    // =========================================================================
  1286→
  1287→    /**
  1288→     * Get churn risk indicators with contacts
  1289→     */
  1290→    public function getChurnRisk(int $limit = 20): array
  1291→    {
  1292→        if ($this->useMongo) {
  1293→            $repo = new MongoRepository();
  1294→            return $repo->getChurnRiskWithContacts(['limit' => $limit]);
  1295→        }
  1296→
  1297→        return $this->db->fetchAll(
  1298→            "SELECT cri.*, c.name, c.email, c.company
  1299→             FROM churn_risk_indicators cri
  1300→             JOIN contacts c ON cri.contact_id = c.id
  1301→             ORDER BY cri.risk_score DESC
  1302→             LIMIT ?",
  1303→            [$limit]
  1304→        );
  1305→    }
  1306→
  1307→    /**
  1308→     * Get business opportunities with contacts
  1309→     */
  1310→    public function getBusinessOpportunities(int $limit = 20): array
  1311→    {
  1312→        if ($this->useMongo) {
  1313→            $repo = new MongoRepository();
  1314→            return $repo->getBusinessOpportunities([], ['limit' => $limit]);
  1315→        }
  1316→
  1317→        return $this->db->fetchAll(
  1318→            "SELECT bo.*, c.name, c.email, c.company
  1319→             FROM business_opportunities bo
  1320→             JOIN contacts c ON bo.contact_id = c.id
  1321→             ORDER BY bo.created_at DESC
  1322→             LIMIT ?",
  1323→            [$limit]
  1324→        );
  1325→    }
  1326→
  1327→    /**
  1328→     * Get leverage/bargaining chips
  1329→     */
  1330→    public function getLeverage(int $contactId, int $limit = 10): array
  1331→    {
  1332→        if ($this->useMongo) {
  1333→            $repo = new MongoRepository();
  1334→            return $repo->getLeverageWithContacts(
  1335→                ['contact_id' => $contactId],
  1336→                ['limit' => $limit]
  1337→            );
  1338→        }
  1339→
  1340→        return $this->db->fetchAll(
  1341→            "SELECT lb.*, c.name as contact_name
  1342→             FROM leverage_bargaining lb
  1343→             LEFT JOIN contacts c ON lb.contact_id = c.id
  1344→             WHERE lb.contact_id = ?
  1345→             ORDER BY lb.strength DESC
  1346→             LIMIT ?",
  1347→            [$contactId, $limit]
  1348→        );
  1349→    }
  1350→
  1351→    // =========================================================================
  1352→    // STATISTICS
  1353→    // =========================================================================
  1354→
  1355→    /**
  1356→     * Get contact email statistics
  1357→     */
  1358→    public function getContactStats(int $contactId): array
  1359→    {
  1360→        if ($this->useMongo) {
  1361→            $repo = new MongoRepository();
  1362→            return $repo->getContactEmailStats($contactId);
  1363→        }
  1364→
  1365→        return $this->db->fetchOne(
  1366→            "SELECT
  1367→                COUNT(*) as email_count,
  1368→                SUM(CASE WHEN direction = 'sent' THEN 1 ELSE 0 END) as sent_count,
  1369→                SUM(CASE WHEN direction = 'received' THEN 1 ELSE 0 END) as received_count,
  1370→                MIN(date) as first_email,
  1371→                MAX(date) as last_email
  1372→             FROM emails
  1373→             WHERE contact_id = ?",
  1374→            [$contactId]
  1375→        ) ?? ['email_count' => 0, 'sent_count' => 0, 'received_count' => 0];
  1376→    }
  1377→
  1378→    // =========================================================================
  1379→    // RAW ACCESS (for custom queries)
  1380→    // =========================================================================
  1381→
  1382→    /**
  1383→     * Get the underlying Database instance (for SQL)
  1384→     */
  1385→    public function getDatabase(): Database
  1386→    {
  1387→        return $this->db;
  1388→    }
  1389→
  1390→    /**
  1391→     * Get the MongoRepository instance (for custom MongoDB queries)
  1392→     */
  1393→    public function getMongoRepository(): ?MongoRepository
  1394→    {
  1395→        return $this->mongoRepo;
  1396→    }
  1397→}
  1398→

Prompt:
Explore how contacts and threads are searched/queried in the backend. Look at:

1. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/QueryAdapter.php` - how threads are queried, filtered by contact
2. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/ThreadAnalyzer.php` - how thread analysis works
3. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/ContactProfiler.php` - how contact profiles are built
4. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/MongoDatabase.php` and `MongoRepository.php` - how email threads are stored/queried in MongoDB
5. `/var/www/mssql.danmarcrm.com/dev1/extension2/public/api.php` - search for endpoints related to search, contacts, threads, filtering

I need to understand: how can we query all threads for a specific contact email? How are threads stored? What data structure do they have? How does the existing search work?


Prompt:
Explore how contacts and threads are searched/queried in the backend. Look at:

1. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/QueryAdapter.php` - how threads are queried, filtered by contact
2. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/ThreadAnalyzer.php` - how thread analysis works
3. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/ContactProfiler.php` - how contact profiles are built
4. `/var/www/mssql.danmarcrm.com/dev1/extension2/src/MongoDatabase.php` and `MongoRepository.php` - how email threads are stored/queried in MongoDB
5. `/var/www/mssql.danmarcrm.com/dev1/extension2/public/api.php` - search for endpoints related to search, contacts, threads, filtering

I need to understand: how can we query all threads for a specific contact email? How are threads stored? What data structure do they have? How does the existing search work?