TASK 2 done. Now TASK 3: Fix the thread contact name bug.


TodoWrite

TASK 1: Hide AI Thread Analysis section (index.php + app.js)

TASK 2: Speed up main page - reduce thread/contact limits

TASK 3: Fix thread contact name bug in QueryAdapter.php

TASK 4A: Full-width layout for thread_view.php

TASK 4B: Agent buttons with quick prompts

TASK 4C: Context builder (search + add threads)

Building context search and thread picker

TASK 4D: Token estimation display

TASK 4E: System prompt injection per agent

TASK 4F: Debug file logging endpoint + JS calls

TASK 4G: Contact memory injection

Verify PHP syntax and test readiness


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

TASK 1: Hide AI Thread Analysis section (index.php + app.js)

TASK 2: Speed up main page - reduce thread/contact limits

TASK 3: Fix thread contact name bug in QueryAdapter.php

TASK 4A: Full-width layout for thread_view.php

TASK 4B: Agent buttons with quick prompts

TASK 4C: Context builder (search + add threads)

Building context search and thread picker

TASK 4D: Token estimation display

TASK 4E: System prompt injection per agent

TASK 4F: Debug file logging endpoint + JS calls

TASK 4G: Contact memory injection

Verify PHP syntax and test readiness

TASK 4D: Token estimation display

TASK 4G: Contact memory injection

Verify PHP syntax and test readiness


Let me read the full MongoDB getThreadsMongo method again carefully to craft the fix.


   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→    }

    private function getThreadsMongo(?int $contactId, string $search, int $limit, int $offset): array
    {
        $mongo = MongoDatabase::getInstance();

        // Build match filter
        $matchFilter = [
            'thread_id' => ['$ne' => null, '$ne' => '']
        ];

        if ($contactId) {
            $matchFilter['contact_id'] = $contactId;
        }

        if ($search) {
            $matchFilter['subject'] = ['$regex' => $search, '$options' => 'i'];
        }

        // Count distinct threads for total
        $countPipeline = [
            ['$match' => $matchFilter],
            ['$group' => ['_id' => '$thread_id']],
            ['$count' => 'total']
        ];

        $countResult = $mongo->aggregate('emails', $countPipeline);
        $total = !empty($countResult) ? $countResult[0]['total'] : 0;

        // Get threads with aggregated data
        $pipeline = [
            ['$match' => $matchFilter],
            ['$group' => [
                '_id' => '$thread_id',
                'first_email_id' => ['$min' => '$id'],
                'last_email_id' => ['$max' => '$id'],
                'subject' => ['$first' => '$subject'],
                'first_date' => ['$min' => '$date'],
                'last_date' => ['$max' => '$date'],
                'email_count' => ['$sum' => 1],
                'inbound_count' => ['$sum' => ['$cond' => [['$eq' => ['$direction', 'inbound']], 1, 0]]],
                'outbound_count' => ['$sum' => ['$cond' => [['$eq' => ['$direction', 'outbound']], 1, 0]]],
                'contact_id' => ['$max' => '$contact_id']
            ]],
            ['$lookup' => [
                'from' => 'contacts',
                'localField' => 'contact_id',
                'foreignField' => 'id',
                'as' => 'contact'
            ]],
            ['$lookup' => [
                'from' => 'thread_analysis',
                'localField' => '_id',
                'foreignField' => 'thread_id',
                'as' => 'analysis'
            ]],
            ['$project' => [
                '_id' => 0,
                'thread_id' => '$_id',
                'first_email_id' => 1,
                'last_email_id' => 1,
                'subject' => 1,
                'first_date' => 1,
                'last_date' => 1,
                'email_count' => 1,
                'inbound_count' => 1,
                'outbound_count' => 1,
                'contact_id' => 1,
                'contact_name' => ['$arrayElemAt' => ['$contact.name', 0]],
                'is_analyzed' => ['$arrayElemAt' => ['$analysis.is_analyzed', 0]],
                'needs_reanalysis' => ['$arrayElemAt' => ['$analysis.needs_reanalysis', 0]]
            ]],
            ['$sort' => ['last_date' => -1]],
            ['$skip' => $offset],
            ['$limit' => $limit]
        ];

        $threads = $mongo->aggregate('emails', $pipeline);

        return [
            'threads' => $threads,
            'total' => $total
        ];
    }


The approach for fixing the MongoDB method:

1. Instead of `'contact_id' => ['$max' => '$contact_id']`, I'll collect all contacts with `$push`
2. After aggregation, I'll post-process to find the most frequent inbound sender (excluding user's email)
3. Then do a separate contact name lookup

Actually, let me think about this more carefully. The MongoDB aggregation pipeline approach of collecting all contacts via `$push` and then post-processing in PHP is the cleanest approach. However, I need to consider:

1. The `$lookup` for contacts depends on `contact_id` — if I'm now collecting all contacts, I need to move the lookup to after post-processing, or do it differently.

2. I can restructure the pipeline to push contact data directly (contact_id, email, direction) instead of just the ID, then remove the contacts lookup and handle the resolution in PHP with a batch lookup.

Writing the MongoDB aggregation... Building the aggregation pipeline... Completing the pipeline stages and then processing the results to identify the most frequent external contact for each thread. I'm sorting the contact counts to find the most frequent one, then assigning that contact ID to the thread. If there are no weighted contacts, I fall back to grabbing the first non-null contact ID from the thread's contacts. After collecting all the contact IDs, I'm cleaning up the temporary data and preparing to batch lookup the contact names.

Now I'm querying the database to fetch all the contact records for those IDs, building a lookup map of contact names. Then I'm iterating through the threads again to assign each one its corresponding contact name, and finally returning the threads array along with the total count.

For the SQL version, I'm using a correlated subquery approach instead since that's simpler and more straightforward, even though this legacy path isn't really being used anymore since everything's moved to MongoDB.

Writing SQL subqueries...


Now let me implement the MongoDB fix. I'll replace the entire `getThreadsMongo` method.


Edit

    private function getThreadsMongo(?int $contactId, string $search, int $limit, int $offset): array
    {
        $mongo = MongoDatabase::getInstance();

        // Build match filter
        $matchFilter = [
            'thread_id' => ['$ne' => null, '$ne' => '']
        ];

        if ($contactId) {
            $matchFilter['contact_id'] = $contactId;
        }

        if ($search) {
            $matchFilter['subject'] = ['$regex' => $search, '$options' => 'i'];
        }

        // Count distinct threads for total
        $countPipeline = [
            ['$match' => $matchFilter],
            ['$group' => ['_id' => '$thread_id']],
            ['$count' => 'total']
        ];

        $countResult = $mongo->aggregate('emails', $countPipeline);
        $total = !empty($countResult) ? $countResult[0]['total'] : 0;

        // Get threads with aggregated data
        $pipeline = [
            ['$match' => $matchFilter],
            ['$group' => [
                '_id' => '$thread_id',
                'first_email_id' => ['$min' => '$id'],
                'last_email_id' => ['$max' => '$id'],
                'subject' => ['$first' => '$subject'],
                'first_date' => ['$min' => '$date'],
                'last_date' => ['$max' => '$date'],
                'email_count' => ['$sum' => 1],
                'inbound_count' => ['$sum' => ['$cond' => [['$eq' => ['$direction', 'inbound']], 1, 0]]],
                'outbound_count' => ['$sum' => ['$cond' => [['$eq' => ['$direction', 'outbound']], 1, 0]]],
                'contact_id' => ['$max' => '$contact_id']
            ]],
            ['$lookup' => [
                'from' => 'contacts',
                'localField' => 'contact_id',
                'foreignField' => 'id',
                'as' => 'contact'
            ]],
            ['$lookup' => [
                'from' => 'thread_analysis',
                'localField' => '_id',
                'foreignField' => 'thread_id',
                'as' => 'analysis'
            ]],
            ['$project' => [
                '_id' => 0,
                'thread_id' => '$_id',
                'first_email_id' => 1,
                'last_email_id' => 1,
                'subject' => 1,
                'first_date' => 1,
                'last_date' => 1,
                'email_count' => 1,
                'inbound_count' => 1,
                'outbound_count' => 1,
                'contact_id' => 1,
                'contact_name' => ['$arrayElemAt' => ['$contact.name', 0]],
                'is_analyzed' => ['$arrayElemAt' => ['$analysis.is_analyzed', 0]],
                'needs_reanalysis' => ['$arrayElemAt' => ['$analysis.needs_reanalysis', 0]]
            ]],
            ['$sort' => ['last_date' => -1]],
            ['$skip' => $offset],
            ['$limit' => $limit]
        ];

        $threads = $mongo->aggregate('emails', $pipeline);

        return [
            'threads' => $threads,
            'total' => $total
        ];
    }

    private function getThreadsMongo(?int $contactId, string $search, int $limit, int $offset): array
    {
        $mongo = MongoDatabase::getInstance();
        $userEmail = defined('GMAIL_USER_EMAIL') ? strtolower(GMAIL_USER_EMAIL) : '';

        // Build match filter
        $matchFilter = [
            'thread_id' => ['$ne' => null, '$ne' => '']
        ];

        if ($contactId) {
            $matchFilter['contact_id'] = $contactId;
        }

        if ($search) {
            $matchFilter['subject'] = ['$regex' => $search, '$options' => 'i'];
        }

        // Count distinct threads for total
        $countPipeline = [
            ['$match' => $matchFilter],
            ['$group' => ['_id' => '$thread_id']],
            ['$count' => 'total']
        ];

        $countResult = $mongo->aggregate('emails', $countPipeline);
        $total = !empty($countResult) ? $countResult[0]['total'] : 0;

        // Get threads with aggregated data
        // Collect all contacts per thread to find the most relevant one (not just $max)
        $pipeline = [
            ['$match' => $matchFilter],
            ['$group' => [
                '_id' => '$thread_id',
                'first_email_id' => ['$min' => '$id'],
                'last_email_id' => ['$max' => '$id'],
                'subject' => ['$first' => '$subject'],
                'first_date' => ['$min' => '$date'],
                'last_date' => ['$max' => '$date'],
                'email_count' => ['$sum' => 1],
                'inbound_count' => ['$sum' => ['$cond' => [['$eq' => ['$direction', 'inbound']], 1, 0]]],
                'outbound_count' => ['$sum' => ['$cond' => [['$eq' => ['$direction', 'outbound']], 1, 0]]],
                'all_contacts' => ['$push' => [
                    'cid' => '$contact_id',
                    'email' => '$from_email',
                    'dir' => '$direction'
                ]]
            ]],
            ['$lookup' => [
                'from' => 'thread_analysis',
                'localField' => '_id',
                'foreignField' => 'thread_id',
                'as' => 'analysis'
            ]],
            ['$project' => [
                '_id' => 0,
                'thread_id' => '$_id',
                'first_email_id' => 1,
                'last_email_id' => 1,
                'subject' => 1,
                'first_date' => 1,
                'last_date' => 1,
                'email_count' => 1,
                'inbound_count' => 1,
                'outbound_count' => 1,
                'all_contacts' => 1,
                'is_analyzed' => ['$arrayElemAt' => ['$analysis.is_analyzed', 0]],
                'needs_reanalysis' => ['$arrayElemAt' => ['$analysis.needs_reanalysis', 0]]
            ]],
            ['$sort' => ['last_date' => -1]],
            ['$skip' => $offset],
            ['$limit' => $limit]
        ];

        $threads = $mongo->aggregate('emails', $pipeline);

        // Post-process: find the most frequent external contact per thread
        $contactIds = [];
        foreach ($threads as &$thread) {
            $contactCounts = [];

            foreach ($thread['all_contacts'] ?? [] as $c) {
                $cid = $c['cid'] ?? null;
                $email = strtolower($c['email'] ?? '');
                $dir = $c['dir'] ?? '';

                // Skip user's own emails and null contacts
                if (!$cid || $email === $userEmail) continue;

                // Weight inbound contacts higher (they're the external sender)
                if ($dir === 'inbound') {
                    $contactCounts[$cid] = ($contactCounts[$cid] ?? 0) + 2;
                } else {
                    $contactCounts[$cid] = ($contactCounts[$cid] ?? 0) + 1;
                }
            }

            // Pick the most frequent contact
            if (!empty($contactCounts)) {
                arsort($contactCounts);
                $thread['contact_id'] = (int)array_key_first($contactCounts);
            } else {
                // Fallback: first non-null contact_id
                $thread['contact_id'] = null;
                foreach ($thread['all_contacts'] ?? [] as $c) {
                    if (!empty($c['cid'])) {
                        $thread['contact_id'] = (int)$c['cid'];
                        break;
                    }
                }
            }

            if ($thread['contact_id']) {
                $contactIds[] = $thread['contact_id'];
            }

            unset($thread['all_contacts']);
        }
        unset($thread);

        // Batch lookup contact names
        $contactNames = [];
        if (!empty($contactIds)) {
            $contacts = $mongo->findAll('contacts', [
                'id' => ['$in' => array_values(array_unique($contactIds))]
            ]);
            foreach ($contacts as $c) {
                $contactNames[$c['id']] = $c['name'] ?? '';
            }
        }

        // Assign contact names
        foreach ($threads as &$thread) {
            $thread['contact_name'] = $contactNames[$thread['contact_id'] ?? 0] ?? null;
        }
        unset($thread);

        return [
            'threads' => $threads,
            'total' => $total
        ];
    }


The file /var/www/mssql.danmarcrm.com/dev1/extension2v1/src/QueryAdapter.php has been updated successfully.

    private function getThreadsMongo(?int $contactId, string $search, int $limit, int $offset): array
    {
        $mongo = MongoDatabase::getInstance();
        $userEmail = defined('GMAIL_USER_EMAIL') ? strtolower(GMAIL_USER_EMAIL) : '';

        // Build match filter
        $matchFilter = [
            'thread_id' => ['$ne' => null, '$ne' => '']
        ];

        if ($contactId) {
            $matchFilter['contact_id'] = $contactId;
        }

        if ($search) {
            $matchFilter['subject'] = ['$regex' => $search, '$options' => 'i'];
        }

        // Count distinct threads for total
        $countPipeline = [
            ['$match' => $matchFilter],
            ['$group' => ['_id' => '$thread_id']],
            ['$count' => 'total']
        ];

        $countResult = $mongo->aggregate('emails', $countPipeline);
        $total = !empty($countResult) ? $countResult[0]['total'] : 0;

        // Get threads with aggregated data
        // Collect all contacts per thread to find the most relevant one (not just $max)
        $pipeline = [
            ['$match' => $matchFilter],
            ['$group' => [
                '_id' => '$thread_id',
                'first_email_id' => ['$min' => '$id'],
                'last_email_id' => ['$max' => '$id'],
                'subject' => ['$first' => '$subject'],
                'first_date' => ['$min' => '$date'],
                'last_date' => ['$max' => '$date'],
                'email_count' => ['$sum' => 1],
                'inbound_count' => ['$sum' => ['$cond' => [['$eq' => ['$direction', 'inbound']], 1, 0]]],
                'outbound_count' => ['$sum' => ['$cond' => [['$eq' => ['$direction', 'outbound']], 1, 0]]],
                'all_contacts' => ['$push' => [
                    'cid' => '$contact_id',
                    'email' => '$from_email',
                    'dir' => '$direction'
                ]]
            ]],
            ['$lookup' => [
                'from' => 'thread_analysis',
                'localField' => '_id',
                'foreignField' => 'thread_id',
                'as' => 'analysis'
            ]],
            ['$project' => [
                '_id' => 0,
                'thread_id' => '$_id',
                'first_email_id' => 1,
                'last_email_id' => 1,
                'subject' => 1,
                'first_date' => 1,
                'last_date' => 1,
                'email_count' => 1,
                'inbound_count' => 1,
                'outbound_count' => 1,
                'all_contacts' => 1,
                'is_analyzed' => ['$arrayElemAt' => ['$analysis.is_analyzed', 0]],
                'needs_reanalysis' => ['$arrayElemAt' => ['$analysis.needs_reanalysis', 0]]
            ]],
            ['$sort' => ['last_date' => -1]],
            ['$skip' => $offset],
            ['$limit' => $limit]
        ];

        $threads = $mongo->aggregate('emails', $pipeline);

        // Post-process: find the most frequent external contact per thread
        $contactIds = [];
        foreach ($threads as &$thread) {
            $contactCounts = [];

            foreach ($thread['all_contacts'] ?? [] as $c) {
                $cid = $c['cid'] ?? null;
                $email = strtolower($c['email'] ?? '');
                $dir = $c['dir'] ?? '';

                // Skip user's own emails and null contacts
                if (!$cid || $email === $userEmail) continue;

                // Weight inbound contacts higher (they're the external sender)
                if ($dir === 'inbound') {
                    $contactCounts[$cid] = ($contactCounts[$cid] ?? 0) + 2;
                } else {
                    $contactCounts[$cid] = ($contactCounts[$cid] ?? 0) + 1;
                }
            }

            // Pick the most frequent contact
            if (!empty($contactCounts)) {
                arsort($contactCounts);
                $thread['contact_id'] = (int)array_key_first($contactCounts);
            } else {
                // Fallback: first non-null contact_id
                $thread['contact_id'] = null;
                foreach ($thread['all_contacts'] ?? [] as $c) {
                    if (!empty($c['cid'])) {
                        $thread['contact_id'] = (int)$c['cid'];
                        break;
                    }
                }
            }

            if ($thread['contact_id']) {
                $contactIds[] = $thread['contact_id'];
            }

            unset($thread['all_contacts']);
        }
        unset($thread);

        // Batch lookup contact names
        $contactNames = [];
        if (!empty($contactIds)) {
            $contacts = $mongo->findAll('contacts', [
                'id' => ['$in' => array_values(array_unique($contactIds))]
            ]);
            foreach ($contacts as $c) {
                $contactNames[$c['id']] = $c['name'] ?? '';
            }
        }

        // Assign contact names
        foreach ($threads as &$thread) {
            $thread['contact_name'] = $contactNames[$thread['contact_id'] ?? 0] ?? null;
        }
        unset($thread);

        return [
            'threads' => $threads,
            'total' => $total
        ];
    }

<?php

namespace EmailIntelligence;

/**
 * QueryAdapter - Automatically routes queries to MySQL or MongoDB
 *
 * This class provides a unified interface that works with both databases.
 * When MySQL is enabled, it uses SQL queries.
 * When MySQL is disabled, it routes to MongoRepository equivalents.
 *
 * Usage:
 *   $adapter = QueryAdapter::getInstance();
 *   $contacts = $adapter->getContactsWithProfiles($search, $limit, $offset, $sort);
 */
class QueryAdapter
{
    private static ?QueryAdapter $instance = null;
    private Database $db;
    private ?MongoRepository $mongoRepo = null;
    private bool $useMongo = true;

    private function __construct()
    {
        $this->db = Database::getInstance();

        // Use MongoDB if MySQL is disabled
        $this->useMongo = !defined('MYSQL_ENABLED') || !MYSQL_ENABLED;

        if ($this->useMongo) {
            $this->mongoRepo = new MongoRepository();
        }
    }

    public static function getInstance(): QueryAdapter
    {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Check which database is being used
     */
    public function getActiveDatabase(): string
    {
        return $this->useMongo ? 'mongodb' : 'mysql';
    }

    // =========================================================================
    // CONTACTS
    // =========================================================================

    /**
     * Get contacts with profiles (paginated)
     *
     * @param string $search Search term (name, email, company)
     * @param int $limit Results per page
     * @param int $offset Pagination offset
     * @param string $sortColumn Column to sort by
     * @param string $sortOrder ASC or DESC
     * @return array ['contacts' => [...], 'total' => int]
     */
    public function getContactsWithProfiles(
        string $search = '',
        int $limit = 50,
        int $offset = 0,
        string $sortColumn = 'name',
        string $sortOrder = 'ASC'
    ): array {
        if ($this->useMongo) {
            return $this->getContactsWithProfilesMongo($search, $limit, $offset, $sortColumn, $sortOrder);
        }

        return $this->getContactsWithProfilesSQL($search, $limit, $offset, $sortColumn, $sortOrder);
    }

    private function getContactsWithProfilesSQL(
        string $search,
        int $limit,
        int $offset,
        string $sortColumn,
        string $sortOrder
    ): array {
        $whereClause = '1=1';
        $params = [];

        if ($search) {
            $whereClause = "(c.name LIKE ? OR c.email LIKE ? OR c.company LIKE ?)";
            $searchParam = "%{$search}%";
            $params = [$searchParam, $searchParam, $searchParam];
        }

        // Validate sort column
        $allowedColumns = ['name', 'email', 'company', 'created_at', 'reliability_score', 'risk_level'];
        if (!in_array($sortColumn, $allowedColumns)) {
            $sortColumn = 'name';
        }
        $sortOrder = strtoupper($sortOrder) === 'DESC' ? 'DESC' : 'ASC';

        $total = $this->db->fetchColumn(
            "SELECT COUNT(*) FROM contacts c WHERE {$whereClause}",
            $params
        );

        $contacts = $this->db->fetchAll(
            "SELECT c.*,
                cp.relationship_health, cp.reliability_score, cp.risk_level,
                cp.pricing_sensitivity, cp.ai_recommendations
             FROM contacts c
             LEFT JOIN contact_profiles cp ON c.id = cp.contact_id
             WHERE {$whereClause}
             ORDER BY {$sortColumn} {$sortOrder}
             LIMIT ? OFFSET ?",
            array_merge($params, [$limit, $offset])
        );

        return ['contacts' => $contacts, 'total' => (int)$total];
    }

    private function getContactsWithProfilesMongo(
        string $search,
        int $limit,
        int $offset,
        string $sortColumn,
        string $sortOrder
    ): array {
        $filter = [];
        if ($search) {
            $filter = [
                '$or' => [
                    ['name' => ['$regex' => $search, '$options' => 'i']],
                    ['email' => ['$regex' => $search, '$options' => 'i']],
                    ['company' => ['$regex' => $search, '$options' => 'i']]
                ]
            ];
        }

        // Get total count
        $mongo = MongoDatabase::getInstance();
        $total = $mongo->count('contacts', $filter);

        // Map sort column to MongoDB field
        $sortDir = strtoupper($sortOrder) === 'DESC' ? -1 : 1;

        // Get contacts with profiles via aggregation
        $pipeline = [];

        if (!empty($filter)) {
            $pipeline[] = ['$match' => $filter];
        }

        $pipeline[] = ['$lookup' => [
            'from' => 'contact_profiles',
            'localField' => 'id',
            'foreignField' => 'contact_id',
            'as' => 'profile'
        ]];

        $pipeline[] = ['$unwind' => [
            'path' => '$profile',
            'preserveNullAndEmptyArrays' => true
        ]];

        $pipeline[] = ['$sort' => [$sortColumn => $sortDir]];
        $pipeline[] = ['$skip' => $offset];
        $pipeline[] = ['$limit' => $limit];

        $results = $mongo->aggregate('contacts', $pipeline);

        // Flatten profile fields
        $contacts = array_map(function($doc) {
            if (isset($doc['profile'])) {
                $doc['relationship_health'] = $doc['profile']['relationship_health'] ?? null;
                $doc['reliability_score'] = $doc['profile']['reliability_score'] ?? null;
                $doc['risk_level'] = $doc['profile']['risk_level'] ?? null;
                $doc['pricing_sensitivity'] = $doc['profile']['pricing_sensitivity'] ?? null;
                $doc['ai_recommendations'] = $doc['profile']['ai_recommendations'] ?? null;
                unset($doc['profile']);
            }
            return $doc;
        }, $results);

        return ['contacts' => $contacts, 'total' => $total];
    }

    /**
     * Get single contact with full details
     */
    public function getContactWithDetails(int $contactId): ?array
    {
        if ($this->useMongo) {
            return $this->getContactWithDetailsMongo($contactId);
        }

        return $this->getContactWithDetailsSQL($contactId);
    }

    private function getContactWithDetailsSQL(int $contactId): ?array
    {
        $contact = $this->db->fetchOne(
            "SELECT c.*, cp.*
             FROM contacts c
             LEFT JOIN contact_profiles cp ON c.id = cp.contact_id
             WHERE c.id = ?",
            [$contactId]
        );

        if (!$contact) return null;

        // Get recent emails
        $recentEmails = $this->db->fetchAll(
            "SELECT e.id, e.subject, e.date, e.direction, e.snippet,
                    ea.sentiment_label, ea.urgency_level, ea.summary
             FROM emails e
             LEFT JOIN email_analysis ea ON e.id = ea.email_id
             WHERE e.contact_id = ?
             ORDER BY e.date DESC
             LIMIT 20",
            [$contactId]
        );

        // Get quotes
        $quotes = $this->db->fetchAll(
            "SELECT * FROM quotes WHERE contact_id = ? ORDER BY quote_date DESC",
            [$contactId]
        );

        // Parse JSON fields
        $jsonFields = ['personality_traits', 'detected_inconsistencies', 'risk_factors',
                      'best_approach', 'topics_to_avoid', 'quote_history'];
        foreach ($jsonFields as $field) {
            if (isset($contact[$field]) && is_string($contact[$field])) {
                $contact[$field] = json_decode($contact[$field], true);
            }
        }

        return [
            'contact' => $contact,
            'recent_emails' => $recentEmails,
            'quotes' => $quotes
        ];
    }

    private function getContactWithDetailsMongo(int $contactId): ?array
    {
        $mongo = MongoDatabase::getInstance();

        // Get contact with profile
        $pipeline = [
            ['$match' => ['id' => $contactId]],
            ['$lookup' => [
                'from' => 'contact_profiles',
                'localField' => 'id',
                'foreignField' => 'contact_id',
                'as' => 'profile'
            ]],
            ['$unwind' => ['path' => '$profile', 'preserveNullAndEmptyArrays' => true]]
        ];

        $results = $mongo->aggregate('contacts', $pipeline);
        if (empty($results)) return null;

        $contact = $results[0];

        // Flatten profile
        if (isset($contact['profile'])) {
            foreach ($contact['profile'] as $key => $value) {
                if ($key !== '_id' && $key !== 'contact_id') {
                    $contact[$key] = $value;
                }
            }
            unset($contact['profile']);
        }

        // Get recent emails with analysis
        $emailPipeline = [
            ['$match' => ['contact_id' => $contactId]],
            ['$lookup' => [
                'from' => 'email_analysis',
                'localField' => 'id',
                'foreignField' => 'email_id',
                'as' => 'analysis'
            ]],
            ['$unwind' => ['path' => '$analysis', 'preserveNullAndEmptyArrays' => true]],
            ['$sort' => ['date' => -1]],
            ['$limit' => 20],
            ['$project' => [
                'id' => 1,
                'subject' => 1,
                'date' => 1,
                'direction' => 1,
                'snippet' => 1,
                'sentiment_label' => '$analysis.sentiment_label',
                'urgency_level' => '$analysis.urgency_level',
                'summary' => '$analysis.summary'
            ]]
        ];

        $recentEmails = $mongo->aggregate('emails', $emailPipeline);

        // Get quotes
        $quotes = $mongo->findAll('quotes',
            ['contact_id' => $contactId],
            ['sort' => ['quote_date' => -1]]
        );

        return [
            'contact' => $contact,
            'recent_emails' => $recentEmails,
            'quotes' => $quotes
        ];
    }

    // =========================================================================
    // EMAILS
    // =========================================================================

    /**
     * Get emails with analysis for a contact
     */
    public function getEmailsWithAnalysis(
        int $contactId,
        int $limit = 50,
        int $offset = 0,
        string $sortColumn = 'date',
        string $sortOrder = 'DESC'
    ): array {
        if ($this->useMongo) {
            return $this->getEmailsWithAnalysisMongo($contactId, $limit, $offset, $sortColumn, $sortOrder);
        }

        return $this->getEmailsWithAnalysisSQL($contactId, $limit, $offset, $sortColumn, $sortOrder);
    }

    private function getEmailsWithAnalysisSQL(
        int $contactId,
        int $limit,
        int $offset,
        string $sortColumn,
        string $sortOrder
    ): array {
        $total = $this->db->fetchColumn(
            "SELECT COUNT(*) FROM emails WHERE contact_id = ?",
            [$contactId]
        );

        $sortOrder = strtoupper($sortOrder) === 'DESC' ? 'DESC' : 'ASC';

        $emails = $this->db->fetchAll(
            "SELECT e.*, ea.sentiment_label, ea.urgency_level, ea.summary, ea.key_points
             FROM emails e
             LEFT JOIN email_analysis ea ON e.id = ea.email_id
             WHERE e.contact_id = ?
             ORDER BY e.{$sortColumn} {$sortOrder}
             LIMIT ? OFFSET ?",
            [$contactId, $limit, $offset]
        );

        return ['emails' => $emails, 'total' => (int)$total];
    }

    private function getEmailsWithAnalysisMongo(
        int $contactId,
        int $limit,
        int $offset,
        string $sortColumn,
        string $sortOrder
    ): array {
        $mongo = MongoDatabase::getInstance();

        $total = $mongo->count('emails', ['contact_id' => $contactId]);

        $sortDir = strtoupper($sortOrder) === 'DESC' ? -1 : 1;

        $pipeline = [
            ['$match' => ['contact_id' => $contactId]],
            ['$lookup' => [
                'from' => 'email_analysis',
                'localField' => 'id',
                'foreignField' => 'email_id',
                'as' => 'analysis'
            ]],
            ['$unwind' => ['path' => '$analysis', 'preserveNullAndEmptyArrays' => true]],
            ['$sort' => [$sortColumn => $sortDir]],
            ['$skip' => $offset],
            ['$limit' => $limit]
        ];

        $results = $mongo->aggregate('emails', $pipeline);

        // Flatten analysis
        $emails = array_map(function($doc) {
            if (isset($doc['analysis'])) {
                $doc['sentiment_label'] = $doc['analysis']['sentiment_label'] ?? null;
                $doc['urgency_level'] = $doc['analysis']['urgency_level'] ?? null;
                $doc['summary'] = $doc['analysis']['summary'] ?? null;
                $doc['key_points'] = $doc['analysis']['key_points'] ?? null;
                unset($doc['analysis']);
            }
            return $doc;
        }, $results);

        return ['emails' => $emails, 'total' => $total];
    }

    /**
     * Get single email with full details
     */
    public function getEmailWithDetails(int $emailId): ?array
    {
        if ($this->useMongo) {
            $repo = new MongoRepository();
            return $repo->getEmailWithDetails($emailId);
        }

        return $this->db->fetchOne(
            "SELECT e.*, c.name as contact_name, c.email as contact_email,
                    ea.*, ta.summary as thread_summary, ta.key_insights
             FROM emails e
             LEFT JOIN contacts c ON e.contact_id = c.id
             LEFT JOIN email_analysis ea ON e.id = ea.email_id
             LEFT JOIN thread_analysis ta ON e.thread_id = ta.thread_id
             WHERE e.id = ?",
            [$emailId]
        );
    }

    /**
     * Search emails
     */
    public function searchEmails(
        string $query,
        ?int $contactId = null,
        int $limit = 50
    ): array {
        if ($this->useMongo) {
            $repo = new MongoRepository();
            return $repo->searchEmails($query, [
                'contact_id' => $contactId,
                'limit' => $limit
            ]);
        }

        $whereClause = "(e.subject LIKE ? OR e.body_text LIKE ?)";
        $params = ["%{$query}%", "%{$query}%"];

        if ($contactId) {
            $whereClause .= " AND e.contact_id = ?";
            $params[] = $contactId;
        }

        return $this->db->fetchAll(
            "SELECT e.*, c.name as contact_name, c.email as contact_email
             FROM emails e
             LEFT JOIN contacts c ON e.contact_id = c.id
             WHERE {$whereClause}
             ORDER BY e.date DESC
             LIMIT ?",
            array_merge($params, [$limit])
        );
    }

    // =========================================================================
    // THREADS
    // =========================================================================

    /**
     * Get thread analysis with contact info
     */
    public function getThreadsWithContact(
        ?int $contactId = null,
        int $limit = 50
    ): array {
        if ($this->useMongo) {
            $filter = $contactId ? ['contact_id' => $contactId] : [];
            $repo = new MongoRepository();
            return $repo->getThreadsWithContact($filter, ['limit' => $limit]);
        }

        $whereClause = $contactId ? "ta.contact_id = ?" : "1=1";
        $params = $contactId ? [$contactId, $limit] : [$limit];

        return $this->db->fetchAll(
            "SELECT ta.*, c.name as contact_name, c.email as contact_email
             FROM thread_analysis ta
             LEFT JOIN contacts c ON ta.contact_id = c.id
             WHERE {$whereClause}
             ORDER BY ta.analyzed_at DESC
             LIMIT ?",
            $params
        );
    }

    /**
     * Get list of threads with summary information (paginated)
     *
     * @param int|null $contactId Filter by contact ID
     * @param string $search Search term for subject
     * @param int $limit Results per page
     * @param int $offset Pagination offset
     * @return array ['threads' => [...], 'total' => int]
     */
    public function getThreads(?int $contactId = null, string $search = '', int $limit = 20, int $offset = 0): array
    {
        if ($this->useMongo) {
            return $this->getThreadsMongo($contactId, $search, $limit, $offset);
        }

        return $this->getThreadsSQL($contactId, $search, $limit, $offset);
    }

    private function getThreadsSQL(?int $contactId, string $search, int $limit, int $offset): array
    {
        $where = 'e.thread_id IS NOT NULL AND e.thread_id != ""';
        $params = [];

        if ($contactId) {
            $where .= ' AND e.contact_id = ?';
            $params[] = $contactId;
        }

        if ($search) {
            $where .= ' AND e.subject LIKE ?';
            $params[] = '%' . $search . '%';
        }

        // Get total thread count
        $total = $this->db->fetchColumn(
            "SELECT COUNT(DISTINCT e.thread_id)
             FROM emails e
             WHERE {$where}",
            $params
        );

        // Get threads with summary info
        $threads = $this->db->fetchAll(
            "SELECT
                e.thread_id,
                MIN(e.id) as first_email_id,
                MAX(e.id) as last_email_id,
                MIN(e.subject) as subject,
                MIN(e.date) as first_date,
                MAX(e.date) as last_date,
                COUNT(*) as email_count,
                SUM(CASE WHEN e.direction = 'inbound' THEN 1 ELSE 0 END) as inbound_count,
                SUM(CASE WHEN e.direction = 'outbound' THEN 1 ELSE 0 END) as outbound_count,
                MAX(e.contact_id) as contact_id,
                MAX(c.name) as contact_name,
                ta.is_analyzed,
                ta.needs_reanalysis
             FROM emails e
             LEFT JOIN contacts c ON e.contact_id = c.id
             LEFT JOIN thread_analysis ta ON e.thread_id = ta.thread_id
             WHERE {$where}
             GROUP BY e.thread_id
             ORDER BY MAX(e.date) DESC
             LIMIT ? OFFSET ?",
            array_merge($params, [$limit, $offset])
        );

        return [
            'threads' => $threads,
            'total' => (int)$total
        ];
    }

    private function getThreadsMongo(?int $contactId, string $search, int $limit, int $offset): array
    {
        $mongo = MongoDatabase::getInstance();

        // Build match filter
        $matchFilter = [
            'thread_id' => ['$ne' => null, '$ne' => '']
        ];

        if ($contactId) {
            $matchFilter['contact_id'] = $contactId;
        }

        if ($search) {
            $matchFilter['subject'] = ['$regex' => $search, '$options' => 'i'];
        }

        // Count distinct threads for total
        $countPipeline = [
            ['$match' => $matchFilter],
            ['$group' => ['_id' => '$thread_id']],
            ['$count' => 'total']
        ];

        $countResult = $mongo->aggregate('emails', $countPipeline);
        $total = !empty($countResult) ? $countResult[0]['total'] : 0;

        // Get threads with aggregated data
        $pipeline = [
            ['$match' => $matchFilter],
            ['$group' => [
                '_id' => '$thread_id',
                'first_email_id' => ['$min' => '$id'],
                'last_email_id' => ['$max' => '$id'],
                'subject' => ['$first' => '$subject'],
                'first_date' => ['$min' => '$date'],
                'last_date' => ['$max' => '$date'],
                'email_count' => ['$sum' => 1],
                'inbound_count' => ['$sum' => ['$cond' => [['$eq' => ['$direction', 'inbound']], 1, 0]]],
                'outbound_count' => ['$sum' => ['$cond' => [['$eq' => ['$direction', 'outbound']], 1, 0]]],
                'contact_id' => ['$max' => '$contact_id']
            ]],
            ['$lookup' => [
                'from' => 'contacts',
                'localField' => 'contact_id',
                'foreignField' => 'id',
                'as' => 'contact'
            ]],
            ['$lookup' => [
                'from' => 'thread_analysis',
                'localField' => '_id',
                'foreignField' => 'thread_id',
                'as' => 'analysis'
            ]],
            ['$project' => [
                '_id' => 0,
                'thread_id' => '$_id',
                'first_email_id' => 1,
                'last_email_id' => 1,
                'subject' => 1,
                'first_date' => 1,
                'last_date' => 1,
                'email_count' => 1,
                'inbound_count' => 1,
                'outbound_count' => 1,
                'contact_id' => 1,
                'contact_name' => ['$arrayElemAt' => ['$contact.name', 0]],
                'is_analyzed' => ['$arrayElemAt' => ['$analysis.is_analyzed', 0]],
                'needs_reanalysis' => ['$arrayElemAt' => ['$analysis.needs_reanalysis', 0]]
            ]],
            ['$sort' => ['last_date' => -1]],
            ['$skip' => $offset],
            ['$limit' => $limit]
        ];

        $threads = $mongo->aggregate('emails', $pipeline);

        return [
            'threads' => $threads,
            'total' => $total
        ];
    }

    /**
     * Get full thread details with all emails and analysis
     *
     * @param string $threadId Gmail thread ID
     * @return array ['thread_id', 'emails', 'analysis', 'email_count']
     */
    public function getThreadDetails(string $threadId): array
    {
        if ($this->useMongo) {
            return $this->getThreadDetailsMongo($threadId);
        }

        return $this->getThreadDetailsSQL($threadId);
    }

    private function getThreadDetailsSQL(string $threadId): array
    {
        // Get all emails in thread with contact info
        $emails = $this->db->fetchAll(
            "SELECT e.*, c.name as contact_name, c.email as contact_email
             FROM emails e
             LEFT JOIN contacts c ON e.contact_id = c.id
             WHERE e.thread_id = ?
             ORDER BY e.date ASC",
            [$threadId]
        );

        // Get thread analysis with email analysis
        $analysis = $this->db->fetchOne(
            "SELECT ta.*, ea.summary, ea.sentiment_label, ea.deception_score,
                    ea.raw_analysis, ea.key_points
             FROM thread_analysis ta
             LEFT JOIN email_analysis ea ON ta.last_email_id = ea.email_id
             WHERE ta.thread_id = ?",
            [$threadId]
        );

        return [
            'thread_id' => $threadId,
            'emails' => $emails,
            'email_count' => count($emails),
            'analysis' => $analysis
        ];
    }

    private function getThreadDetailsMongo(string $threadId): array
    {
        $mongo = MongoDatabase::getInstance();

        // Get all emails in thread with contact info using aggregation
        $emailPipeline = [
            ['$match' => ['thread_id' => $threadId]],
            ['$lookup' => [
                'from' => 'contacts',
                'localField' => 'contact_id',
                'foreignField' => 'id',
                'as' => 'contact'
            ]],
            ['$unwind' => ['path' => '$contact', 'preserveNullAndEmptyArrays' => true]],
            ['$sort' => ['date' => 1]],  // ASC
            ['$project' => [
                '_id' => 0,
                'id' => 1,
                'gmail_id' => 1,
                'thread_id' => 1,
                'contact_id' => 1,
                'subject' => 1,
                'from_email' => 1,
                'to_email' => 1,
                'date' => 1,
                'direction' => 1,
                'snippet' => 1,
                'body_text' => 1,
                'body_html' => 1,
                'has_attachments' => 1,
                'label_ids' => 1,
                'contact_name' => '$contact.name',
                'contact_email' => '$contact.email'
            ]]
        ];

        $emails = $mongo->aggregate('emails', $emailPipeline);

        // Get thread analysis with email analysis
        $analysisPipeline = [
            ['$match' => ['thread_id' => $threadId]],
            ['$lookup' => [
                'from' => 'email_analysis',
                'localField' => 'last_email_id',
                'foreignField' => 'email_id',
                'as' => 'email_analysis'
            ]],
            ['$unwind' => ['path' => '$email_analysis', 'preserveNullAndEmptyArrays' => true]],
            ['$project' => [
                '_id' => 0,
                'thread_id' => 1,
                'contact_id' => 1,
                'subject' => 1,
                'email_count' => 1,
                'first_date' => 1,
                'last_date' => 1,
                'last_email_id' => 1,
                'trust_score' => 1,
                'deception_score' => 1,
                'risk_level' => 1,
                'summary' => ['$ifNull' => ['$email_analysis.summary', null]],
                'sentiment_label' => ['$ifNull' => ['$email_analysis.sentiment_label', null]],
                'raw_analysis' => 1,
                'key_insights' => 1,
                'key_points' => ['$ifNull' => ['$email_analysis.key_points', null]],
                'action_items' => 1,
                'warnings' => 1,
                'analyzed_at' => 1,
                'tokens_used' => 1
            ]],
            ['$limit' => 1]
        ];

        $analysisResults = $mongo->aggregate('thread_analysis', $analysisPipeline);
        $analysis = !empty($analysisResults) ? $analysisResults[0] : null;

        return [
            'thread_id' => $threadId,
            'emails' => $emails,
            'email_count' => count($emails),
            'analysis' => $analysis
        ];
    }

    /**
     * Get thread participants with aggregated statistics
     *
     * @param string $threadId Gmail thread ID
     * @param string $userEmail Current user's email for 'self' detection
     * @return array ['participants' => [...], 'subject' => string]
     */
    public function getThreadParticipants(string $threadId, string $userEmail): array
    {

        if ($this->useMongo) {
            return $this->getThreadParticipantsMongo($threadId, $userEmail);
        }

        return $this->getThreadParticipantsSQL($threadId, $userEmail);
    }

    private function getThreadParticipantsSQL(string $threadId, string $userEmail): array
    {
        $userEmail = strtolower($userEmail);

        // Fetch ALL emails in the thread to extract participants from FROM, TO, and CC fields
        $emails = $this->db->fetchAll(
            "SELECT from_email, to_email, cc_email, date, direction, subject
             FROM emails
             WHERE thread_id = ?
             ORDER BY date ASC",
            [$threadId]
        );

        // Collect all unique participants with their stats
        $participantStats = [];
        $threadSubject = '';

        foreach ($emails as $email) {
            // Get subject from first email
            if (empty($threadSubject) && !empty($email['subject'])) {
                $threadSubject = $email['subject'];
            }

            $date = $email['date'] ?? null;
            $direction = $email['direction'] ?? '';

            // Extract FROM participant
            if (!empty($email['from_email'])) {
                $fromEmail = strtolower(trim($email['from_email']));
                if (!isset($participantStats[$fromEmail])) {
                    $participantStats[$fromEmail] = [
                        'email' => $fromEmail,
                        'sent_count' => 0,
                        'received_count' => 0,
                        'total_count' => 0,
                        'first_email' => $date,
                        'last_email' => $date
                    ];
                }
                $participantStats[$fromEmail]['sent_count']++;
                $participantStats[$fromEmail]['total_count']++;
                if ($date) {
                    if (!$participantStats[$fromEmail]['first_email'] || $date < $participantStats[$fromEmail]['first_email']) {
                        $participantStats[$fromEmail]['first_email'] = $date;
                    }
                    if (!$participantStats[$fromEmail]['last_email'] || $date > $participantStats[$fromEmail]['last_email']) {
                        $participantStats[$fromEmail]['last_email'] = $date;
                    }
                }
            }

            // Extract TO participant(s)
            if (!empty($email['to_email'])) {
                $toEmail = strtolower(trim($email['to_email']));
                if (!isset($participantStats[$toEmail])) {
                    $participantStats[$toEmail] = [
                        'email' => $toEmail,
                        'sent_count' => 0,
                        'received_count' => 0,
                        'total_count' => 0,
                        'first_email' => $date,
                        'last_email' => $date
                    ];
                }
                $participantStats[$toEmail]['received_count']++;
                $participantStats[$toEmail]['total_count']++;
                if ($date) {
                    if (!$participantStats[$toEmail]['first_email'] || $date < $participantStats[$toEmail]['first_email']) {
                        $participantStats[$toEmail]['first_email'] = $date;
                    }
                    if (!$participantStats[$toEmail]['last_email'] || $date > $participantStats[$toEmail]['last_email']) {
                        $participantStats[$toEmail]['last_email'] = $date;
                    }
                }
            }

            // Extract CC participant(s)
            if (!empty($email['cc_email'])) {
                // CC might be comma-separated, split it
                $ccEmails = array_map('trim', explode(',', $email['cc_email']));
                foreach ($ccEmails as $ccEmail) {
                    $ccEmail = strtolower($ccEmail);
                    if (empty($ccEmail)) continue;

                    if (!isset($participantStats[$ccEmail])) {
                        $participantStats[$ccEmail] = [
                            'email' => $ccEmail,
                            'sent_count' => 0,
                            'received_count' => 0,
                            'total_count' => 0,
                            'first_email' => $date,
                            'last_email' => $date
                        ];
                    }
                    $participantStats[$ccEmail]['received_count']++;
                    $participantStats[$ccEmail]['total_count']++;
                    if ($date) {
                        if (!$participantStats[$ccEmail]['first_email'] || $date < $participantStats[$ccEmail]['first_email']) {
                            $participantStats[$ccEmail]['first_email'] = $date;
                        }
                        if (!$participantStats[$ccEmail]['last_email'] || $date > $participantStats[$ccEmail]['last_email']) {
                            $participantStats[$ccEmail]['last_email'] = $date;
                        }
                    }
                }
            }
        }

        // Convert to array and sort by total_count
        $participants = array_values($participantStats);
        usort($participants, function($a, $b) {
            return $b['total_count'] - $a['total_count'];
        });

        // Enrich with contact data and determine participant type
        foreach ($participants as &$p) {
            // Determine participant type
            $p['participant_type'] = (strtolower($p['email']) === $userEmail) ? 'self' : 'external';

            // Calculate inbound/outbound based on direction and participant type
            if ($p['participant_type'] === 'self') {
                $p['inbound_count'] = $p['received_count'];
                $p['outbound_count'] = $p['sent_count'];
            } else {
                $p['inbound_count'] = $p['sent_count'];  // External sent TO us = inbound
                $p['outbound_count'] = $p['received_count'];  // We sent TO external = outbound (for us)
            }

            $p['email_count'] = $p['total_count'];

            // Get name guess from email
            $emailParts = explode('@', $p['email']);
            $namePart = $emailParts[0] ?? '';
            $nameGuess = ucfirst(explode('.', $namePart)[0]);

            // Fetch contact data from contacts table
            $contact = $this->db->fetchOne(
                "SELECT id, name, email, company, type FROM contacts WHERE email = ?",
                [$p['email']]
            );

            // Fetch enrichment data from contact_enrichment table
            $enrichment = null;
            if ($contact && !empty($contact['id'])) {
                $enrichment = $this->db->fetchOne(
                    "SELECT job_title, phone, mobile_phone, linkedin_url
                     FROM contact_enrichment
                     WHERE contact_id = ?",
                    [$contact['id']]
                );
            }

            if ($p['participant_type'] === 'external') {
                if ($contact) {
                    $p['contact_id'] = $contact['id'];
                    $p['name'] = $contact['name'] ?? $nameGuess;
                    $p['company'] = $contact['company'] ?? '';
                    $p['contact_type'] = $contact['type'] ?? null;

                    // Add enriched data
                    if ($enrichment) {
                        $p['job_title'] = $enrichment['job_title'] ?? null;
                        $p['phone'] = $enrichment['phone'] ?? $enrichment['mobile_phone'] ?? null;
                        $p['linkedin_url'] = $enrichment['linkedin_url'] ?? null;
                    }
                } else {
                    $p['contact_id'] = null;
                    $p['name'] = $nameGuess;
                    $p['company'] = $emailParts[1] ?? '';
                    $p['contact_type'] = null;
                }
            } else {
                $p['name'] = 'You';
                $p['company'] = '';
                $p['contact_id'] = $contact ? $contact['id'] : null;
                $p['contact_type'] = null;
            }

            // Remove temporary fields
            unset($p['sent_count'], $p['received_count'], $p['total_count']);
        }

        return [
            'participants' => $participants,
            'subject' => $threadSubject
        ];
    }

    private function getThreadParticipantsMongo(string $threadId, string $userEmail): array
    {

        $mongo = MongoDatabase::getInstance();
        $userEmail = strtolower($userEmail);

        // Fetch ALL emails in the thread to extract participants from FROM, TO, and CC fields
        $emails = $mongo->findAll('emails', ['thread_id' => $threadId]);

        // Collect all unique participants with their stats
        $participantStats = [];
        $threadSubject = '';

        foreach ($emails as $email) {
            // Get subject from first email
            if (empty($threadSubject) && !empty($email['subject'])) {
                $threadSubject = $email['subject'];
            }

            $date = $email['date'] ?? null;
            $direction = $email['direction'] ?? '';

            // Extract FROM participant
            if (!empty($email['from_email'])) {
                $fromEmail = strtolower(trim($email['from_email']));
                if (!isset($participantStats[$fromEmail])) {
                    $participantStats[$fromEmail] = [
                        'email' => $fromEmail,
                        'sent_count' => 0,
                        'received_count' => 0,
                        'total_count' => 0,
                        'first_email' => $date,
                        'last_email' => $date
                    ];
                }
                $participantStats[$fromEmail]['sent_count']++;
                $participantStats[$fromEmail]['total_count']++;
                if ($date) {
                    if (!$participantStats[$fromEmail]['first_email'] || $date < $participantStats[$fromEmail]['first_email']) {
                        $participantStats[$fromEmail]['first_email'] = $date;
                    }
                    if (!$participantStats[$fromEmail]['last_email'] || $date > $participantStats[$fromEmail]['last_email']) {
                        $participantStats[$fromEmail]['last_email'] = $date;
                    }
                }
            }

            // Extract TO participant(s)
            if (!empty($email['to_email'])) {
                $toEmail = strtolower(trim($email['to_email']));
                if (!isset($participantStats[$toEmail])) {
                    $participantStats[$toEmail] = [
                        'email' => $toEmail,
                        'sent_count' => 0,
                        'received_count' => 0,
                        'total_count' => 0,
                        'first_email' => $date,
                        'last_email' => $date
                    ];
                }
                $participantStats[$toEmail]['received_count']++;
                $participantStats[$toEmail]['total_count']++;
                if ($date) {
                    if (!$participantStats[$toEmail]['first_email'] || $date < $participantStats[$toEmail]['first_email']) {
                        $participantStats[$toEmail]['first_email'] = $date;
                    }
                    if (!$participantStats[$toEmail]['last_email'] || $date > $participantStats[$toEmail]['last_email']) {
                        $participantStats[$toEmail]['last_email'] = $date;
                    }
                }
            }

            // Extract CC participant(s)
            if (!empty($email['cc_email'])) {
                // CC might be comma-separated, split it
                $ccEmails = array_map('trim', explode(',', $email['cc_email']));
                foreach ($ccEmails as $ccEmail) {
                    $ccEmail = strtolower($ccEmail);
                    if (empty($ccEmail)) continue;

                    if (!isset($participantStats[$ccEmail])) {
                        $participantStats[$ccEmail] = [
                            'email' => $ccEmail,
                            'sent_count' => 0,
                            'received_count' => 0,
                            'total_count' => 0,
                            'first_email' => $date,
                            'last_email' => $date
                        ];
                    }
                    $participantStats[$ccEmail]['received_count']++;
                    $participantStats[$ccEmail]['total_count']++;
                    if ($date) {
                        if (!$participantStats[$ccEmail]['first_email'] || $date < $participantStats[$ccEmail]['first_email']) {
                            $participantStats[$ccEmail]['first_email'] = $date;
                        }
                        if (!$participantStats[$ccEmail]['last_email'] || $date > $participantStats[$ccEmail]['last_email']) {
                            $participantStats[$ccEmail]['last_email'] = $date;
                        }
                    }
                }
            }
        }

        // Convert to array and sort by total_count
        $participants = array_values($participantStats);
        usort($participants, function($a, $b) {
            return $b['total_count'] - $a['total_count'];
        });

        // Enrich with contact data and determine participant type
        foreach ($participants as &$p) {
            // Determine participant type
            $p['participant_type'] = (strtolower($p['email']) === $userEmail) ? 'self' : 'external';

            // Calculate inbound/outbound based on direction and participant type
            if ($p['participant_type'] === 'self') {
                $p['inbound_count'] = $p['received_count'];
                $p['outbound_count'] = $p['sent_count'];
            } else {
                $p['inbound_count'] = $p['sent_count'];  // External sent TO us = inbound
                $p['outbound_count'] = $p['received_count'];  // We sent TO external = outbound (for us)
            }

            $p['email_count'] = $p['total_count'];

            // Get name guess from email
            $emailParts = explode('@', $p['email']);
            $namePart = $emailParts[0] ?? '';
            $nameGuess = ucfirst(explode('.', $namePart)[0]);

            // Fetch contact data from contacts collection
            $contact = $mongo->findOne('contacts', ['email' => $p['email']]);

            // Fetch enrichment data from contact_enrichment collection
            $enrichment = null;
            if ($contact && !empty($contact['id'])) {
                $enrichment = $mongo->findOne('contact_enrichment', ['contact_id' => $contact['id']]);
            }

            if ($p['participant_type'] === 'external') {
                if ($contact) {
                    $p['contact_id'] = $contact['id'] ?? null;
                    $p['name'] = $contact['name'] ?? $nameGuess;
                    $p['company'] = $contact['company'] ?? '';
                    $p['contact_type'] = $contact['type'] ?? null;

                    // Add enriched data
                    if ($enrichment) {
                        $p['job_title'] = $enrichment['job_title'] ?? null;
                        $p['phone'] = $enrichment['phone'] ?? $enrichment['mobile_phone'] ?? null;
                        $p['linkedin_url'] = $enrichment['linkedin_url'] ?? null;
                    }
                } else {
                    $p['contact_id'] = null;
                    $p['name'] = $nameGuess;
                    $p['company'] = $emailParts[1] ?? '';
                    $p['contact_type'] = null;
                }
            } else {
                $p['name'] = 'You';
                $p['company'] = '';
                $p['contact_id'] = $contact['id'] ?? null;
                $p['contact_type'] = null;
            }

            // Remove temporary fields
            unset($p['sent_count'], $p['received_count'], $p['total_count']);
        }

        return [
            'participants' => $participants,
            'subject' => $threadSubject
        ];
    }

    // =========================================================================
    // LOGS
    // =========================================================================

    /**
     * Get API logs with optional filtering
     *
     * @param string|null $threadId Filter by thread ID
     * @param string|null $level Filter by log level (info, warning, error)
     * @param int $limit Maximum number of logs to return
     * @return array ['logs' => [...], 'count' => int]
     */
    public function getLogs(?string $threadId = null, ?string $level = null, int $limit = 50): array
    {
        if ($this->useMongo) {
            return $this->getLogsMongo($threadId, $level, $limit);
        }

        return $this->getLogsSQL($threadId, $level, $limit);
    }

    private function getLogsSQL(?string $threadId, ?string $level, int $limit): array
    {
        $where = '1=1';
        $params = [];

        if ($threadId) {
            $where .= ' AND thread_id = ?';
            $params[] = $threadId;
        }
        if ($level) {
            $where .= ' AND log_level = ?';
            $params[] = $level;
        }

        $logs = $this->db->fetchAll(
            "SELECT id, log_level, source, message, context, thread_id, created_at
             FROM api_logs
             WHERE {$where}
             ORDER BY created_at DESC
             LIMIT ?",
            array_merge($params, [$limit])
        );

        // Parse context JSON (MySQL stores as string)
        foreach ($logs as &$log) {
            if (isset($log['context']) && is_string($log['context'])) {
                $log['context'] = json_decode($log['context'], true);
            }
        }

        return [
            'logs' => $logs,
            'count' => count($logs)
        ];
    }

    private function getLogsMongo(?string $threadId, ?string $level, int $limit): array
    {
        $mongo = MongoDatabase::getInstance();

        $filter = [];

        if ($threadId) {
            $filter['thread_id'] = $threadId;
        }
        if ($level) {
            $filter['log_level'] = $level;
        }

        $logs = $mongo->findAll('api_logs', $filter, [
            'sort' => ['created_at' => -1],
            'limit' => $limit
        ]);

        // Context should already be an array in MongoDB, but handle string case for safety
        foreach ($logs as &$log) {
            if (isset($log['context']) && is_string($log['context'])) {
                $log['context'] = json_decode($log['context'], true);
            }
        }

        return [
            'logs' => $logs,
            'count' => count($logs)
        ];
    }

    // =========================================================================
    // ALERTS
    // =========================================================================

    /**
     * Get alerts with contact info
     */
    public function getAlertsWithContact(
        ?string $status = null,
        int $limit = 50
    ): array {
        if ($this->useMongo) {
            $filter = $status ? ['status' => $status] : [];
            $repo = new MongoRepository();
            return $repo->getAlertsWithContact($filter, ['limit' => $limit]);
        }

        $whereClause = $status ? "a.status = ?" : "1=1";
        $params = $status ? [$status, $limit] : [$limit];

        return $this->db->fetchAll(
            "SELECT a.*, c.name as contact_name, c.email as contact_email
             FROM alerts a
             LEFT JOIN contacts c ON a.contact_id = c.id
             WHERE {$whereClause}
             ORDER BY a.created_at DESC
             LIMIT ?",
            $params
        );
    }

    // =========================================================================
    // BUSINESS INTELLIGENCE
    // =========================================================================

    /**
     * Get churn risk indicators with contacts
     */
    public function getChurnRisk(int $limit = 20): array
    {
        if ($this->useMongo) {
            $repo = new MongoRepository();
            return $repo->getChurnRiskWithContacts(['limit' => $limit]);
        }

        return $this->db->fetchAll(
            "SELECT cri.*, c.name, c.email, c.company
             FROM churn_risk_indicators cri
             JOIN contacts c ON cri.contact_id = c.id
             ORDER BY cri.risk_score DESC
             LIMIT ?",
            [$limit]
        );
    }

    /**
     * Get business opportunities with contacts
     */
    public function getBusinessOpportunities(int $limit = 20): array
    {
        if ($this->useMongo) {
            $repo = new MongoRepository();
            return $repo->getBusinessOpportunities([], ['limit' => $limit]);
        }

        return $this->db->fetchAll(
            "SELECT bo.*, c.name, c.email, c.company
             FROM business_opportunities bo
             JOIN contacts c ON bo.contact_id = c.id
             ORDER BY bo.created_at DESC
             LIMIT ?",
            [$limit]
        );
    }

    /**
     * Get leverage/bargaining chips
     */
    public function getLeverage(int $contactId, int $limit = 10): array
    {
        if ($this->useMongo) {
            $repo = new MongoRepository();
            return $repo->getLeverageWithContacts(
                ['contact_id' => $contactId],
                ['limit' => $limit]
            );
        }

        return $this->db->fetchAll(
            "SELECT lb.*, c.name as contact_name
             FROM leverage_bargaining lb
             LEFT JOIN contacts c ON lb.contact_id = c.id
             WHERE lb.contact_id = ?
             ORDER BY lb.strength DESC
             LIMIT ?",
            [$contactId, $limit]
        );
    }

    // =========================================================================
    // STATISTICS
    // =========================================================================

    /**
     * Get contact email statistics
     */
    public function getContactStats(int $contactId): array
    {
        if ($this->useMongo) {
            $repo = new MongoRepository();
            return $repo->getContactEmailStats($contactId);
        }

        return $this->db->fetchOne(
            "SELECT
                COUNT(*) as email_count,
                SUM(CASE WHEN direction = 'sent' THEN 1 ELSE 0 END) as sent_count,
                SUM(CASE WHEN direction = 'received' THEN 1 ELSE 0 END) as received_count,
                MIN(date) as first_email,
                MAX(date) as last_email
             FROM emails
             WHERE contact_id = ?",
            [$contactId]
        ) ?? ['email_count' => 0, 'sent_count' => 0, 'received_count' => 0];
    }

    // =========================================================================
    // RAW ACCESS (for custom queries)
    // =========================================================================

    /**
     * Get the underlying Database instance (for SQL)
     */
    public function getDatabase(): Database
    {
        return $this->db;
    }

    /**
     * Get the MongoRepository instance (for custom MongoDB queries)
     */
    public function getMongoRepository(): ?MongoRepository
    {
        return $this->mongoRepo;
    }
}

+        $userEmail = defined('GMAIL_USER_EMAIL') ? strtolower(GMAIL_USER_EMAIL) : '';

+        // Collect all contacts per thread to find the most relevant one (not just $max)

-                'contact_id' => ['$max' => '$contact_id']

+                'all_contacts' => ['$push' => [

+                    'cid' => '$contact_id',

+                    'email' => '$from_email',

+                    'dir' => '$direction'

-                'localField' => 'contact_id',

-                'foreignField' => 'id',

-                'contact_name' => ['$arrayElemAt' => ['$contact.name', 0]],

+        // Post-process: find the most frequent external contact per thread

+        foreach ($threads as &$thread) {

+            foreach ($thread['all_contacts'] ?? [] as $c) {

+                $cid = $c['cid'] ?? null;

+                $email = strtolower($c['email'] ?? '');

+                // Skip user's own emails and null contacts

+                if (!$cid || $email === $userEmail) continue;

+                // Weight inbound contacts higher (they're the external sender)

+                if ($dir === 'inbound') {

+                    $contactCounts[$cid] = ($contactCounts[$cid] ?? 0) + 2;

+                    $contactCounts[$cid] = ($contactCounts[$cid] ?? 0) + 1;

+            // Pick the most frequent contact

+            if (!empty($contactCounts)) {

+                arsort($contactCounts);

+                $thread['contact_id'] = (int)array_key_first($contactCounts);

+                // Fallback: first non-null contact_id

+                $thread['contact_id'] = null;

+                        $thread['contact_id'] = (int)$c['cid'];

+            if ($thread['contact_id']) {

+                $contactIds[] = $thread['contact_id'];

+            unset($thread['all_contacts']);

+            $contacts = $mongo->findAll('contacts', [

+                'id' => ['$in' => array_values(array_unique($contactIds))]

+            foreach ($contacts as $c) {

+                $contactNames[$c['id']] = $c['name'] ?? '';

+        foreach ($threads as &$thread) {

+            $thread['contact_name'] = $contactNames[$thread['contact_id'] ?? 0] ?? null;


Now fix the SQL version too.


    private function getThreadsSQL(?int $contactId, string $search, int $limit, int $offset): array
    {
        $where = 'e.thread_id IS NOT NULL AND e.thread_id != ""';
        $params = [];

        if ($contactId) {
            $where .= ' AND e.contact_id = ?';
            $params[] = $contactId;
        }

        if ($search) {
            $where .= ' AND e.subject LIKE ?';
            $params[] = '%' . $search . '%';
        }

        // Get total thread count
        $total = $this->db->fetchColumn(
            "SELECT COUNT(DISTINCT e.thread_id)
             FROM emails e
             WHERE {$where}",
            $params
        );

        // Get threads with summary info
        $threads = $this->db->fetchAll(
            "SELECT
                e.thread_id,
                MIN(e.id) as first_email_id,
                MAX(e.id) as last_email_id,
                MIN(e.subject) as subject,
                MIN(e.date) as first_date,
                MAX(e.date) as last_date,
                COUNT(*) as email_count,
                SUM(CASE WHEN e.direction = 'inbound' THEN 1 ELSE 0 END) as inbound_count,
                SUM(CASE WHEN e.direction = 'outbound' THEN 1 ELSE 0 END) as outbound_count,
                MAX(e.contact_id) as contact_id,
                MAX(c.name) as contact_name,
                ta.is_analyzed,
                ta.needs_reanalysis
             FROM emails e
             LEFT JOIN contacts c ON e.contact_id = c.id
             LEFT JOIN thread_analysis ta ON e.thread_id = ta.thread_id
             WHERE {$where}
             GROUP BY e.thread_id
             ORDER BY MAX(e.date) DESC
             LIMIT ? OFFSET ?",
            array_merge($params, [$limit, $offset])
        );

        return [
            'threads' => $threads,
            'total' => (int)$total
        ];
    }

    private function getThreadsSQL(?int $contactId, string $search, int $limit, int $offset): array
    {
        $userEmail = defined('GMAIL_USER_EMAIL') ? strtolower(GMAIL_USER_EMAIL) : '';
        $where = 'e.thread_id IS NOT NULL AND e.thread_id != ""';
        $params = [];

        if ($contactId) {
            $where .= ' AND e.contact_id = ?';
            $params[] = $contactId;
        }

        if ($search) {
            $where .= ' AND e.subject LIKE ?';
            $params[] = '%' . $search . '%';
        }

        // Get total thread count
        $total = $this->db->fetchColumn(
            "SELECT COUNT(DISTINCT e.thread_id)
             FROM emails e
             WHERE {$where}",
            $params
        );

        // Get threads with summary info
        // Use subquery for contact_id to find most frequent inbound sender (not MAX)
        $threads = $this->db->fetchAll(
            "SELECT
                e.thread_id,
                MIN(e.id) as first_email_id,
                MAX(e.id) as last_email_id,
                MIN(e.subject) as subject,
                MIN(e.date) as first_date,
                MAX(e.date) as last_date,
                COUNT(*) as email_count,
                SUM(CASE WHEN e.direction = 'inbound' THEN 1 ELSE 0 END) as inbound_count,
                SUM(CASE WHEN e.direction = 'outbound' THEN 1 ELSE 0 END) as outbound_count,
                (SELECT e2.contact_id FROM emails e2
                 WHERE e2.thread_id = e.thread_id
                 AND e2.direction = 'inbound'
                 AND LOWER(e2.from_email) != ?
                 GROUP BY e2.contact_id
                 ORDER BY COUNT(*) DESC LIMIT 1) as contact_id,
                (SELECT c2.name FROM emails e2
                 JOIN contacts c2 ON e2.contact_id = c2.id
                 WHERE e2.thread_id = e.thread_id
                 AND e2.direction = 'inbound'
                 AND LOWER(e2.from_email) != ?
                 GROUP BY e2.contact_id, c2.name
                 ORDER BY COUNT(*) DESC LIMIT 1) as contact_name,
                ta.is_analyzed,
                ta.needs_reanalysis
             FROM emails e
             LEFT JOIN thread_analysis ta ON e.thread_id = ta.thread_id
             WHERE {$where}
             GROUP BY e.thread_id
             ORDER BY MAX(e.date) DESC
             LIMIT ? OFFSET ?",
            array_merge([$userEmail, $userEmail], $params, [$limit, $offset])
        );

        return [
            'threads' => $threads,
            'total' => (int)$total
        ];
    }