     1→<?php
     2→/**
     3→ * Thread Analyzer - Analyzes Complete Email Threads
     4→ *
     5→ * Unlike AIAnalyzer which processes individual emails,
     6→ * this analyzes ENTIRE email threads (conversations) together.
     7→ *
     8→ * WHY THREADS?
     9→ * - Detects inconsistencies across multiple emails
    10→ * - Understands negotiation flow (quote → counter → agreement)
    11→ * - Sees how tone/sentiment changes through conversation
    12→ * - Better psychological profiling with full context
    13→ *
    14→ * CALLING MAP:
    15→ * - Called by: cli/analyze_threads.php (main entry)
    16→ * - Called by: cli/cron_hybrid.php (scheduled)
    17→ * - Uses: Database.php, calls Claude API
    18→ * - Reads from: emails table (grouped by thread_id)
    19→ * - Writes to: thread_analysis, email_analysis tables
    20→ *
    21→ * @package EmailIntelligence
    22→ */
    23→
    24→namespace EmailIntelligence;
    25→
    26→use Exception;
    27→use GuzzleHttp\Client as HttpClient;
    28→use GuzzleHttp\HandlerStack;
    29→use GuzzleHttp\Handler\CurlHandler;
    30→
    31→class ThreadAnalyzer
    32→{
    33→    private Database $db;
    34→    private HttpClient $http;
    35→    private array $config;
    36→    private $progressCallback = null;
    37→
    38→    private array $stats = [
    39→        'threads_analyzed' => 0,
    40→        'threads_failed' => 0,
    41→        'emails_processed' => 0,
    42→        'quotes_extracted' => 0,
    43→        'inconsistencies_found' => 0,
    44→        'tokens_used' => 0,
    45→        'start_time' => null
    46→    ];
    47→
    48→    public function __construct(?string $provider = null, ?string $model = null)
    49→    {
    50→        $this->db = Database::getInstance();
    51→
    52→        // Determine which AI provider to use
    53→        // Use passed parameter, fall back to constant, then default to 'claude'
    54→        if ($provider === null) {
    55→            $provider = defined('AI_PROVIDER') ? AI_PROVIDER : 'claude';
    56→        }
    57→
    58→        if ($provider === 'openai' && defined('OPENAI_API_KEY') && !empty(OPENAI_API_KEY)) {
    59→            // Determine OpenAI model
    60→            // If model is passed, use it; otherwise use OPENAI_MODEL constant or default
    61→            $selectedModel = $model ?? (defined('OPENAI_MODEL') ? OPENAI_MODEL : 'gpt-4o');
    62→
    63→            $this->config = [
    64→                'provider' => 'openai',
    65→                'api_key' => OPENAI_API_KEY,
    66→                'model' => $selectedModel,
    67→                'max_tokens' => defined('OPENAI_MAX_TOKENS') ? OPENAI_MAX_TOKENS : 16000,
    68→                'endpoint' => 'https://api.openai.com/v1/chat/completions'
    69→            ];
    70→
    71→            // Create clean handler stack to avoid New Relic Guzzle infinite loop
    72→            $stack = HandlerStack::create(new CurlHandler());
    73→
    74→            $this->http = new HttpClient([
    75→                'timeout' => 180,
    76→                'handler' => $stack,
    77→                'headers' => [
    78→                    'Content-Type' => 'application/json',
    79→                    'Authorization' => 'Bearer ' . $this->config['api_key']
    80→                ]
    81→            ]);
    82→        } else {
    83→            // Default to Claude
    84→            // Determine Claude model
    85→            // If model is passed, use it; otherwise use CLAUDE_MODEL constant or default
    86→            $selectedModel = $model ?? (defined('CLAUDE_MODEL') ? CLAUDE_MODEL : 'claude-sonnet-4-5-20250929');
    87→
    88→            $this->config = [
    89→                'provider' => 'claude',
    90→                'api_key' => defined('CLAUDE_API_KEY') ? CLAUDE_API_KEY : '',
    91→                'model' => $selectedModel,
    92→                'max_tokens' => defined('CLAUDE_MAX_TOKENS') ? CLAUDE_MAX_TOKENS : 16000,
    93→                'endpoint' => 'https://api.anthropic.com/v1/messages'
    94→            ];
    95→
    96→            // Create clean handler stack to avoid New Relic Guzzle infinite loop
    97→            $stack = HandlerStack::create(new CurlHandler());
    98→
    99→            $this->http = new HttpClient([
   100→                'timeout' => 180,
   101→                'handler' => $stack,
   102→                'headers' => [
   103→                    'Content-Type' => 'application/json',
   104→                    'x-api-key' => $this->config['api_key'],
   105→                    'anthropic-version' => '2023-06-01'
   106→                ]
   107→            ]);
   108→        }
   109→    }
   110→
   111→    public function isConfigured(): bool
   112→    {
   113→        return !empty($this->config['api_key']);
   114→    }
   115→
   116→    public function getProvider(): string
   117→    {
   118→        return $this->config['provider'] ?? 'claude';
   119→    }
   120→
   121→    public function setProgressCallback(callable $callback): void
   122→    {
   123→        $this->progressCallback = $callback;
   124→    }
   125→
   126→    private function reportProgress(string $message, ?int $percent = null): void
   127→    {
   128→        $this->logMessage($message);
   129→        if ($this->progressCallback) {
   130→            call_user_func($this->progressCallback, $message, $percent, $this->stats);
   131→        }
   132→    }
   133→
   134→    /**
   135→     * Analyze threads that need analysis
   136→     *
   137→     * @param int $batchSize Number of threads to analyze
   138→     * @param int $delayMs Delay between API calls
   139→     * @return array Statistics
   140→     */
   141→    public function analyzeBatch(int $batchSize = 10, int $delayMs = 1000): array
   142→    {
   143→        if (!$this->isConfigured()) {
   144→            throw new Exception("Claude API key not configured");
   145→        }
   146→
   147→        $this->stats['start_time'] = microtime(true);
   148→        $this->reportProgress("Starting thread analysis batch", 0);
   149→
   150→        // Get threads that need analysis
   151→        $threads = $this->getThreadsNeedingAnalysis($batchSize);
   152→
   153→        if (empty($threads)) {
   154→            $this->reportProgress("No threads need analysis", 100);
   155→            return $this->stats;
   156→        }
   157→
   158→        $total = count($threads);
   159→        $this->reportProgress("Found {$total} threads to analyze", 5);
   160→
   161→        foreach ($threads as $index => $thread) {
   162→            $percent = (int)((($index + 1) / $total) * 100);
   163→
   164→            try {
   165→                $this->reportProgress(
   166→                    "Analyzing thread " . ($index + 1) . "/{$total}: " . substr($thread['subject'] ?? 'No subject', 0, 50),
   167→                    $percent
   168→                );
   169→
   170→                $this->analyzeThread($thread['thread_id']);
   171→                $this->stats['threads_analyzed']++;
   172→
   173→                // Rate limiting
   174→                if ($index < $total - 1 && $delayMs > 0) {
   175→                    usleep($delayMs * 1000);
   176→                }
   177→
   178→            } catch (Exception $e) {
   179→                $this->stats['threads_failed']++;
   180→                $this->logMessage("Failed to analyze thread {$thread['thread_id']}: " . $e->getMessage(), 'error');
   181→            }
   182→        }
   183→
   184→        $duration = round(microtime(true) - $this->stats['start_time'], 2);
   185→        $this->reportProgress(
   186→            "Batch complete in {$duration}s. Analyzed: {$this->stats['threads_analyzed']}, Failed: {$this->stats['threads_failed']}",
   187→            100
   188→        );
   189→
   190→        return $this->stats;
   191→    }
   192→
   193→    /**
   194→     * Get threads that need analysis
   195→     *
   196→     * @param int $limit
   197→     * @return array
   198→     */
   199→    private function getThreadsNeedingAnalysis(int $limit): array
   200→    {
   201→        // Get threads that either:
   202→        // 1. Have never been analyzed (not in thread_analysis)
   203→        // 2. Need re-analysis (needs_reanalysis = 1)
   204→        return $this->db->fetchAll(
   205→            "SELECT
   206→                e.thread_id,
   207→                MIN(e.subject) as subject,
   208→                MAX(e.date) as last_email_date,
   209→                COUNT(*) as email_count,
   210→                MAX(e.contact_id) as contact_id
   211→             FROM emails e
   212→             LEFT JOIN thread_analysis ta ON e.thread_id = ta.thread_id
   213→             WHERE e.thread_id IS NOT NULL
   214→               AND e.thread_id != ''
   215→               AND (ta.id IS NULL OR ta.needs_reanalysis = 1)
   216→             GROUP BY e.thread_id
   217→             HAVING email_count >= 1
   218→             ORDER BY last_email_date DESC
   219→             LIMIT ?",
   220→            [$limit]
   221→        );
   222→    }
   223→
   224→    /**
   225→     * Analyze a complete email thread
   226→     *
   227→     * @param string $threadId Gmail thread ID
   228→     * @return array Analysis results
   229→     */
   230→    public function analyzeThread(string $threadId): array
   231→    {
   232→        // Get all emails in thread
   233→
   234→        if (isset($_REQUEST['threadID'])){
   235→            $extraSQL = " WHERE thread_id = '{$_REQUEST['threadID']}'";
   236→            $emails = $this->db->fetchAll(
   237→                "SELECT * FROM emails
   238→                 {$extraSQL}
   239→                 ORDER BY date ASC"
   240→            );
   241→        }else{
   242→            $emails = $this->db->fetchAll(
   243→                "SELECT * FROM emails
   244→                 WHERE thread_id = ?
   245→                 ORDER BY date ASC",
   246→                [$threadId]
   247→            );
   248→        }
   249→        //echo $threadId;
   250→        //exit();
   251→
   252→
   253→        if (empty($emails)) {
   254→            throw new Exception("No emails found for thread: {$threadId}");
   255→        }
   256→
   257→        $this->stats['emails_processed'] += count($emails);
   258→
   259→        // RAG: Retrieve similar past threads for context
   260→        $ragContext = $this->retrieveSimilarThreads($emails);
   261→
   262→        // Build thread context for Claude with RAG context
   263→        $prompt = $this->buildThreadAnalysisPrompt($emails, $ragContext);
   264→
   265→        //file_put_contents("logs/__ddbgA.log","\n emails: ".print_r($emails, true), FILE_APPEND);
   266→        //file_put_contents("logs/__ddbgA.log","\n argContext: ".print_r($ragContext, true), FILE_APPEND);
   267→        //echo 222;
   268→        //print_r($this->db);
   269→        //exit();
   270→        // Call AI API (Claude or OpenAI)
   271→        $this->logToDb('info', 'ThreadAnalyzer', "Starting analysis for thread {$threadId} with " . count($emails) . " emails", [
   272→            'thread_id' => $threadId,
   273→            'email_count' => count($emails),
   274→            'provider' => $this->config['provider'] ?? 'claude'
   275→        ], $threadId);
   276→
   277→        $response = $this->callClaudeAPI($prompt, $threadId);
   278→        //print_r($response);
   279→        //exit();
   280→        file_put_contents("../logs/__ddbgA.log","\n prompt: ".print_r($prompt, true), FILE_APPEND);
   281→        file_put_contents("../logs/__ddbgA.log","\n resp: ".print_r($response, true), FILE_APPEND);
   282→        //exit();
   283→        // Parse response
   284→        $analysis = $this->parseAnalysisResponse($response);
   285→
   286→        // Store analysis
   287→        $this->storeThreadAnalysis($threadId, $emails, $analysis, $response);
   288→
   289→        // Extract and store quotes
   290→        if (!empty($analysis['quotes'])) {
   291→            $this->stats['quotes_extracted'] += count($analysis['quotes']);
   292→            $this->storeQuotes($emails, $analysis['quotes']);
   293→        }
   294→
   295→        // Count inconsistencies
   296→        if (!empty($analysis['inconsistencies'])) {
   297→            $this->stats['inconsistencies_found'] += count($analysis['inconsistencies']);
   298→        }
   299→
   300→        // Mark all emails in thread as analyzed
   301→        $this->db->execute(
   302→            "UPDATE emails SET is_analyzed = 1 WHERE thread_id = ?",
   303→            [$threadId]
   304→        );
   305→
   306→        return $analysis;
   307→    }
   308→
   309→    /**
   310→     * Retrieve similar past threads for RAG context
   311→     *
   312→     * @param array $emails Current thread emails
   313→     * @return array Similar threads with their analyses
   314→     */
   315→    private function retrieveSimilarThreads(array $emails): array
   316→    {
   317→        try {
   318→            // Build search query from thread content
   319→            $searchQuery = $this->buildThreadSearchQuery($emails);
   320→
   321→            // Search vector memory for similar threads
   322→            $vectorMemory = new VectorMemory();
   323→            $similarResults = $vectorMemory->search($searchQuery, 3, ['type' => 'thread_summary']);
   324→
   325→            $similarThreads = [];
   326→            foreach ($similarResults as $result) {
   327→                // Get thread analysis from database
   328→                $threadId = $result['metadata']['thread_id'] ?? null;
   329→                if ($threadId) {
   330→                    $analysis = $this->db->fetchOne(
   331→                        "SELECT ta.*, c.name as contact_name
   332→                         FROM thread_analysis ta
   333→                         LEFT JOIN contacts c ON ta.contact_id = c.id
   334→                         WHERE ta.thread_id = ?",
   335→                        [$threadId]
   336→                    );
   337→
   338→                    if ($analysis) {
   339→                        $similarThreads[] = [
   340→                            'similarity' => $result['similarity'] ?? 0,
   341→                            'thread_id' => $threadId,
   342→                            'contact_name' => $analysis['contact_name'],
   343→                            'summary' => $result['content'] ?? '',
   344→                            'deception_score' => $result['metadata']['deception_score'] ?? null,
   345→                            'trust_score' => $result['metadata']['trust_score'] ?? null,
   346→                            'risk_level' => $result['metadata']['risk_level'] ?? null
   347→                        ];
   348→                    }
   349→                }
   350→            }
   351→
   352→            return $similarThreads;
   353→
   354→        } catch (Exception $e) {
   355→            error_log("RAG retrieval failed: " . $e->getMessage());
   356→            return []; // Continue without RAG context if it fails
   357→        }
   358→    }
   359→
   360→    /**
   361→     * Build search query from thread emails
   362→     *
   363→     * @param array $emails Thread emails
   364→     * @return string Search query
   365→     */
   366→    private function buildThreadSearchQuery(array $emails): string
   367→    {
   368→        // Extract key topics from thread
   369→        $subjects = array_unique(array_column($emails, 'subject'));
   370→        $mainSubject = $subjects[0] ?? 'discussion';
   371→
   372→        // Get snippet from first and last email
   373→        $firstBody = strip_tags($emails[0]['body_text'] ?? $emails[0]['body_html'] ?? '');
   374→        $lastBody = strip_tags($emails[count($emails) - 1]['body_text'] ?? '');
   375→
   376→        $snippet = substr($firstBody, 0, 200) . ' ... ' . substr($lastBody, 0, 200);
   377→
   378→        return "Thread about: {$mainSubject}. {$snippet}";
   379→    }
   380→
   381→    /**
   382→     * Build analysis prompt for complete thread
   383→     *
   384→     * @param array $emails All emails in thread
   385→     * @param array $ragContext Similar past threads for context
   386→     * @return string
   387→     */
   388→    private function buildThreadAnalysisPrompt(array $emails, array $ragContext = []): string
   389→    {
   390→        $emailCount = count($emails);
   391→        $firstEmail = $emails[0];
   392→        $lastEmail = $emails[$emailCount - 1];
   393→
   394→        // Build conversation timeline
   395→        $timeline = "";
   396→        foreach ($emails as $index => $email) {
   397→            $num = $index + 1;
   398→            $direction = $email['direction'] === 'inbound' ? 'FROM THEM' : 'FROM US';
   399→            $date = date('Y-m-d H:i', strtotime($email['date']));
   400→            $subject = $email['subject'] ?? '(No Subject)';
   401→
   402→            // Get email body and strip quoted content
   403→            $body = $email['body_text'] ?? $email['body_html'] ?? '';
   404→            $body = $this->stripQuotedContent($body);
   405→
   406→            // Truncate long emails
   407→            if (strlen($body) > 2000) {
   408→                $body = substr($body, 0, 2000) . "\n[... email truncated ...]";
   409→            }
   410→
   411→            $timeline .= "━━━ EMAIL #{$num} ({$direction}) ━━━\n";
   412→            $timeline .= "Date: {$date}\n";
   413→            $timeline .= "Subject: {$subject}\n";
   414→            $timeline .= "Content:\n{$body}\n\n";
   415→        }
   416→
   417→        // Build RAG context section
   418→        $ragSection = "";
   419→        if (!empty($ragContext)) {
   420→            $ragSection = "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n";
   421→            $ragSection .= "SIMILAR PAST CONVERSATIONS (for context):\n";
   422→            $ragSection .= "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n";
   423→            $ragSection .= "We found " . count($ragContext) . " similar past situations. Use these as reference:\n\n";
   424→
   425→            foreach ($ragContext as $i => $similar) {
   426→                $num = $i + 1;
   427→                $similarity = round($similar['similarity'] * 100, 1);
   428→                $ragSection .= "SIMILAR #{$num} (Similarity: {$similarity}%):\n";
   429→                $ragSection .= "Contact: " . ($similar['contact_name'] ?? 'Unknown') . "\n";
   430→                $ragSection .= "Summary: " . substr($similar['summary'], 0, 300) . "\n";
   431→
   432→                if ($similar['deception_score'] !== null) {
   433→                    $ragSection .= "Deception Score: " . round($similar['deception_score'] * 100) . "%\n";
   434→                }
   435→                if ($similar['trust_score'] !== null) {
   436→                    $ragSection .= "Trust Score: " . round($similar['trust_score'] * 100) . "%\n";
   437→                }
   438→                if ($similar['risk_level']) {
   439→                    $ragSection .= "Risk Level: {$similar['risk_level']}\n";
   440→                }
   441→                $ragSection .= "\n";
   442→            }
   443→
   444→            $ragSection .= "Use the patterns from these similar situations to inform your analysis.\n";
   445→            $ragSection .= "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n";
   446→        }
   447→
   448→        // Build participant list
   449→        $participantStats = [];
   450→        foreach ($emails as $email) {
   451→            $from = $email['from_email'];
   452→            $fromName = $email['from_name'] ?? '';
   453→            if (!isset($participantStats[$from])) {
   454→                $participantStats[$from] = [
   455→                    'name' => $fromName,
   456→                    'email_count' => 0,
   457→                    'direction' => $email['direction'] === 'inbound' ? 'external' : 'self'
   458→                ];
   459→            }
   460→            $participantStats[$from]['email_count']++;
   461→        }
   462→
   463→        $participantList = "";
   464→        foreach ($participantStats as $email => $stats) {
   465→            $role = $stats['direction'] === 'self' ? '(US)' : '(THEM)';
   466→            $participantList .= "- {$stats['name']} <{$email}> {$role}: {$stats['email_count']} emails\n";
   467→        }
   468→
   469→        // Get owner email from config
   470→        $ownerEmail = defined('GMAIL_USER_EMAIL') ? GMAIL_USER_EMAIL : 'dan@danmarcrm.com';
   471→        $ownerPersona = defined('AI_PERSONA') ? AI_PERSONA : 'Dan runs DanmarCRM for 22 years, developing CRM software and integrations.';
   472→
   473→        return <<<PROMPT
   474→You are a behavioral analyst, business strategist, and protective advisor for Dan.
   475→
   476→OWNER CONTEXT:
   477→- Owner email: {$ownerEmail}
   478→- {$ownerPersona}
   479→- Dan is the GOOD GUY who must be protected. Identify any threats, manipulation, or harmful patterns from clients.
   480→- Your job: Analyze emails, suggest business opportunities, generate reply options, and protect Dan's interests.
   481→
   482→THREAD INFO:
   483→- Total emails: {$emailCount}
   484→- First email: {$firstEmail['date']}
   485→- Last email: {$lastEmail['date']}
   486→- Thread duration: From {$firstEmail['date']} to {$lastEmail['date']}
   487→
   488→PARTICIPANTS IN THIS CONVERSATION:
   489→{$participantList}
   490→{$ragSection}
   491→CONVERSATION TIMELINE:
   492→{$timeline}
   493→
   494→Analyze this ENTIRE conversation and respond with JSON:
   495→
   496→{
   497→  "thread_summary": "<2-3 sentence summary of what this conversation is about and its outcome>",
   498→
   499→  "participants_analysis": [
   500→    {
   501→      "email": "<participant's email address>",
   502→      "name": "<participant's name>",
   503→      "role": "<external|self - external=client/supplier, self=us>",
   504→      "email_count": <number of emails they sent>,
   505→
   506→      "psychological_profile": {
   507→        "trust_score": <float 0.0-1.0, how trustworthy this person appears>,
   508→        "deception_score": <float 0.0-1.0, likelihood they are being deceptive>,
   509→        "manipulation_score": <float 0.0-1.0, how manipulative their communication is>,
   510→        "reliability_score": <float 0.0-1.0, do they follow through on commitments>,
   511→        "aggression_score": <float 0.0-1.0, level of aggressive/demanding behavior>
   512→      },
   513→
   514→      "behavioral_indicators": {
   515→        "communication_style": "<direct|evasive|assertive|passive-aggressive|professional|casual>",
   516→        "consistency": "<consistent|inconsistent|contradictory> - do they change their story?",
   517→        "transparency": "<open|guarded|secretive|deceptive> - how forthcoming are they?",
   518→        "emotional_state": "<calm|anxious|frustrated|angry|desperate|confident>",
   519→        "negotiation_tactics": ["<tactic1>", "<tactic2>"],
   520→        "pressure_tactics": ["<any pressure/manipulation tactics used>"]
   521→      },
   522→
   523→      "red_flags": [
   524→        {
   525→          "flag": "<description of concerning behavior>",
   526→          "severity": "<low|medium|high|critical>",
   527→          "evidence": "<quote or reference from their emails>"
   528→        }
   529→      ],
   530→
   531→      "positive_signals": [
   532→        {
   533→          "signal": "<positive behavior observed>",
   534→          "evidence": "<quote or reference>"
   535→        }
   536→      ],
   537→
   538→      "lies_or_inconsistencies": [
   539→        {
   540→          "claim": "<what they claimed>",
   541→          "contradiction": "<how it contradicts other statements or facts>",
   542→          "emails_involved": "<email numbers where this appears>"
   543→        }
   544→      ],
   545→
   546→      "overall_assessment": "<1-2 sentence assessment of this person's trustworthiness and intentions>",
   547→      "risk_level": "<low|medium|high|critical>",
   548→      "verdict": "<trustworthy|cautious|suspicious|untrustworthy>"
   549→    }
   550→  ],
   551→
   552→  "sentiment_analysis": {
   553→    "overall_sentiment": "<positive|neutral|negative>",
   554→    "sentiment_score": <float -1.0 to 1.0>,
   555→    "sentiment_trend": "<improving|stable|declining> - how sentiment changed from start to end",
   556→    "tone_progression": "<how the tone evolved through the conversation>"
   557→  },
   558→
   559→  "key_points": [
   560→    "<main point 1>",
   561→    "<main point 2>",
   562→    "<main point 3>"
   563→  ],
   564→
   565→  "negotiation_analysis": {
   566→    "type": "<price_negotiation|service_request|support|information|complaint|other>",
   567→    "their_stance": "<aggressive|collaborative|passive|demanding|flexible>",
   568→    "our_stance": "<accommodating|firm|flexible|defensive>",
   569→    "outcome": "<resolved|ongoing|abandoned|escalated>",
   570→    "who_initiated": "<them|us>",
   571→    "resolution_summary": "<how it was resolved or current status>"
   572→  },
   573→
   574→  "quotes_and_pricing": [
   575→    {
   576→      "email_number": <which email it appeared in>,
   577→      "amount": <number>,
   578→      "currency": "<EUR|USD|etc>",
   579→      "description": "<what it's for>",
   580→      "direction": "<sent_by_us|received_from_them>",
   581→      "type": "<initial|counter|final|discount_request>",
   582→      "context": "<why this quote was given>"
   583→    }
   584→  ],
   585→
   586→  "commitments_made": [
   587→    {
   588→      "by": "<them|us>",
   589→      "commitment": "<what was promised>",
   590→      "email_number": <which email>,
   591→      "deadline": "<if mentioned>",
   592→      "was_fulfilled": "<yes|no|unknown|pending> - look if later emails show it was done"
   593→    }
   594→  ],
   595→
   596→  "inconsistencies": [
   597→    {
   598→      "type": "<contradiction|changed_story|avoided_question|false_claim>",
   599→      "description": "<what the inconsistency is>",
   600→      "evidence": "<compare email #X where they said A vs email #Y where they said B>",
   601→      "severity": "<low|medium|high>"
   602→    }
   603→  ],
   604→
   605→  "deception_indicators": {
   606→    "deception_score": <float 0.0 to 1.0>,
   607→    "trust_score": <float 0.0 to 1.0>,
   608→    "shady_behavior_score": <float 0.0 to 1.0>,
   609→    "red_flags": ["<flag1>", "<flag2>"],
   610→    "hiding_information": "<evidence of withholding relevant information>",
   611→    "manipulation_tactics": ["<any attempts to trick, deceive, or manipulate>"],
   612→    "reasoning": "<why you assigned these scores>"
   613→  },
   614→
   615→  "business_risks": {
   616→    "payment_risk": {
   617→      "score": <float 0.0 to 1.0>,
   618→      "payment_delays": "<evidence of delaying or avoiding payments>",
   619→      "excuses_used": ["<list of excuses given for non-payment or delays>"],
   620→      "payment_history_issues": "<any mentioned payment problems>"
   621→    },
   622→    "project_theft_risk": {
   623→      "score": <float 0.0 to 1.0>,
   624→      "knowledge_extraction": "<attempts to extract info to give to competitors>",
   625→      "competitor_mentions": ["<any mentions of other companies doing similar work>"],
   626→      "suspicious_questions": ["<questions that suggest they want to replicate our work elsewhere>"]
   627→    },
   628→    "relationship_risks": [
   629→      {
   630→        "type": "<delay_tactics|scope_creep|blame_shifting|ghosting|false_promises>",
   631→        "description": "<what happened>",
   632→        "evidence": "<email numbers and quotes>"
   633→      }
   634→    ]
   635→  },
   636→
   637→  "entities_mentioned": {
   638→    "amounts": [{"value": <num>, "currency": "<curr>", "context": "<what for>"}],
   639→    "dates": [{"date": "<date>", "context": "<what for>"}],
   640→    "products_services": ["<item1>", "<item2>"],
   641→    "companies": ["<company1>"],
   642→    "people": ["<person1>"]
   643→  },
   644→
   645→  "action_items": [
   646→    {
   647→      "for": "<them|us>",
   648→      "action": "<what needs to be done>",
   649→      "priority": "<high|medium|low>",
   650→      "deadline": "<if mentioned>"
   651→    }
   652→  ],
   653→
   654→  "psychological_insights": {
   655→    "communication_style": "<direct|indirect|formal|casual|aggressive|passive>",
   656→    "decision_making": "<fast|slow|indecisive|confident>",
   657→    "price_sensitivity": "<very_high|high|medium|low> - based on their reactions to quotes",
   658→    "negotiation_skill": "<novice|average|skilled|expert>",
   659→    "reliability_indicators": "<do they follow through? do they change their story?>",
   660→    "pressure_tactics": ["<any tactics they used>"]
   661→  },
   662→
   663→  "recommendations": {
   664→    "next_steps": ["<what we should do next>"],
   665→    "how_to_approach": "<advice on how to handle this client/supplier>",
   666→    "pricing_strategy": "<should we quote higher/lower, any insights on their budget>",
   667→    "warnings": ["<things to watch out for with this contact>"],
   668→    "overall_risk_level": "<low|medium|high|critical>",
   669→    "client_verdict": "<trustworthy|cautious|risky|avoid>"
   670→  },
   671→
   672→  "reply_suggestions": [
   673→    {
   674→      "label": "<short label like 'Agree & Proceed' or 'Ask Clarification'>",
   675→      "tone": "<positive|neutral|firm|friendly|professional>",
   676→      "preview": "<first 60 chars of reply>",
   677→      "full_text": "<complete ready-to-send email reply with greeting and sign-off>"
   678→    }
   679→  ],
   680→
   681→  "business_opportunities": {
   682→    "cross_sell_products": [
   683→      {
   684→        "product_idea": "<what product/service we could offer based on their needs mentioned in emails>",
   685→        "why": "<evidence from emails showing they need this>",
   686→        "how_to_pitch": "<suggested approach to bring this up>"
   687→      }
   688→    ],
   689→    "software_development_ideas": [
   690→      {
   691→        "idea": "<custom software we could build for them based on their problems/needs>",
   692→        "problem_they_have": "<what problem from the emails this solves>",
   693→        "scope_estimate": "<small|medium|large>",
   694→        "pitch_approach": "<how to suggest this to them>"
   695→      }
   696→    ],
   697→    "quote_analysis": {
   698→      "is_quote_related": "<true if this thread is about pricing/quotes, false otherwise>",
   699→      "current_situation": "<summary of quote status from emails>",
   700→      "recommended_action": "<what to propose - accept/counter/hold>",
   701→      "suggested_price_adjustment": "<higher/lower/same and why>",
   702→      "negotiation_leverage": "<what leverage do we have based on emails>",
   703→      "closing_strategy": "<how to close this deal>"
   704→    },
   705→    "persuasion_strategy": {
   706→      "client_motivation": "<what motivates this client based on their emails>",
   707→      "pain_points": ["<pain point 1 from emails>", "<pain point 2>"],
   708→      "questions_to_ask": [
   709→        {
   710→          "question": "<strategic question to ask them>",
   711→          "why_ask": "<what info this reveals>",
   712→          "expected_answers": ["<possible answer 1>", "<possible answer 2>"],
   713→          "follow_up_actions": ["<what to do based on answer 1>", "<what to do based on answer 2>"]
   714→        }
   715→      ],
   716→      "objections_to_expect": ["<likely objection based on emails>"],
   717→      "rebuttals": ["<how to counter each objection>"],
   718→      "buying_signals": ["<positive signals from emails indicating interest>"],
   719→      "recommended_approach": "<overall strategy to win them over>"
   720→    }
   721→  },
   722→
   723→  "protective_analysis": {
   724→    "threats_to_owner": [
   725→      {
   726→        "threat_type": "<payment_risk|scope_creep|manipulation|knowledge_theft|reputation_risk|time_waste>",
   727→        "description": "<what the threat is>",
   728→        "severity": "<low|medium|high|critical>",
   729→        "evidence": "<quote from emails showing this threat>",
   730→        "recommended_action": "<how to protect against this>"
   731→      }
   732→    ],
   733→    "harmful_patterns": [
   734→      {
   735→        "pattern": "<pattern name like 'Playing vendors against each other' or 'Endless revision requests'>",
   736→        "evidence": "<proof from emails>",
   737→        "risk_to_dan": "<how this could hurt Dan>"
   738→      }
   739→    ],
   740→    "grammar_analysis": [
   741→      {
   742→        "participant_email": "<email address>",
   743→        "participant_name": "<name>",
   744→        "spelling_errors": "<count of spelling mistakes noticed>",
   745→        "grammar_errors": "<count of grammar issues>",
   746→        "professionalism_score": "<float 0.0-1.0, how professional their writing is>",
   747→        "writing_quality": "<poor|average|good|excellent>",
   748→        "notes": "<observations about their writing style>"
   749→      }
   750→    ],
   751→    "overall_safety_score": "<float 0.0-1.0, how safe is this client for Dan>",
   752→    "protection_alerts": ["<alert 1 if any serious concerns>", "<alert 2>"]
   753→  },
   754→
   755→  "churn_risk_signals": {
   756→    "exit_intent_score": <float 0.0-1.0, likelihood they are planning to leave>,
   757→    "exit_signals": [
   758→      {
   759→        "signal": "<what they said or did suggesting they might leave>",
   760→        "severity": "<low|medium|high|critical>",
   761→        "evidence": "<exact quote from email>",
   762→        "email_number": <which email>
   763→      }
   764→    ],
   765→    "competitor_mentions": [
   766→      {
   767→        "competitor": "<name of competitor or alternative mentioned>",
   768→        "context": "<how they mentioned it - comparing, considering, already using?>",
   769→        "threat_level": "<curious|comparing|considering|decided>"
   770→      }
   771→    ],
   772→    "disengagement_signals": {
   773→      "response_slowing": "<true|false - are their responses getting slower?>",
   774→      "enthusiasm_declining": "<true|false - less enthusiastic than before?>",
   775→      "communication_frequency_drop": "<true|false - contacting less often?>",
   776→      "short_replies": "<true|false - replies getting shorter/curt?>"
   777→    },
   778→    "goodbye_language": {
   779→      "detected": <true|false>,
   780→      "examples": ["<farewell phrases used>"],
   781→      "context": "<was it project end, relationship end, or just email sign-off?>"
   782→    },
   783→    "contract_end_signals": {
   784→      "mentioned_ending": <true|false>,
   785→      "mentioned_not_renewing": <true|false>,
   786→      "asked_about_data_export": <true|false>,
   787→      "requested_documentation": <true|false - requesting docs before leaving?>,
   788→      "mentioned_budget_cuts": <true|false>,
   789→      "mentioned_change_in_direction": <true|false>
   790→    },
   791→    "retention_opportunities": ["<what could we do to keep them?>"],
   792→    "churn_risk_summary": "<1-2 sentence summary of churn risk>",
   793→    "recommended_retention_action": "<specific action to prevent churn>"
   794→  },
   795→
   796→  "price_perception_analysis": {
   797→    "quote_reaction_type": "<immediate_accept|accept|negotiate|hesitate|reject|shock|ignore|not_applicable>",
   798→    "perceived_value": "<bargain|fair|acceptable|high|very_high|outrageous|unknown>",
   799→    "price_sensitivity_score": <float 0.0-1.0, how sensitive to pricing they are>,
   800→    "willingness_to_pay_more": <float 0.0-1.0, signals they would accept higher prices>,
   801→    "negotiation_aggressiveness": "<passive|mild|moderate|aggressive|very_aggressive>",
   802→    "price_objections": [
   803→      {
   804→        "objection": "<what they said about price>",
   805→        "severity": "<mild|moderate|strong>",
   806→        "email_number": <which email>,
   807→        "response_strategy": "<how to address this objection>"
   808→      }
   809→    ],
   810→    "budget_signals": {
   811→      "budget_mentioned": <true|false>,
   812→      "budget_constraints": "<what constraints they mentioned if any>",
   813→      "budget_flexibility": "<tight|moderate|flexible|generous|unknown>",
   814→      "approval_process_mentioned": <true|false - do they need approval?>
   815→    },
   816→    "value_perception": {
   817→      "understands_value": <true|false>,
   818→      "questioned_value": <true|false>,
   819→      "compared_to_competitors": <true|false>,
   820→      "asked_for_justification": <true|false>
   821→    },
   822→    "discount_behavior": {
   823→      "requested_discount": <true|false>,
   824→      "discount_percent_requested": <number or null>,
   825→      "discount_justification": "<reason they gave for wanting discount>",
   826→      "frequency_of_discount_requests": "<never|rare|occasional|frequent|always>"
   827→    },
   828→    "pricing_strategy_recommendation": "<recommend higher/lower/same pricing and why>",
   829→    "maximum_they_would_pay": "<estimate based on signals, or 'unknown'>"
   830→  },
   831→
   832→  "support_satisfaction_analysis": {
   833→    "satisfaction_score": <float 0.0-1.0, overall support satisfaction>,
   834→    "frustration_level": "<none|mild|moderate|high|extreme>",
   835→    "frustration_incidents": [
   836→      {
   837→        "issue": "<what frustrated them>",
   838→        "severity": "<low|medium|high>",
   839→        "our_response": "<how we handled it>",
   840→        "resolution": "<resolved|unresolved|partial>",
   841→        "email_number": <which email>
   842→      }
   843→    ],
   844→    "complaint_patterns": {
   845→      "frequency": "<never|rare|occasional|frequent|constant>",
   846→      "types": ["<type of complaint 1>", "<type 2>"],
   847→      "escalation_tendency": <float 0.0-1.0, likelihood to escalate>
   848→    },
   849→    "patience_level": "<very_patient|patient|neutral|impatient|very_impatient>",
   850→    "appreciation_expressed": <true|false - did they thank us or express appreciation?>,
   851→    "would_recommend_us": "<likely|unlikely|unknown>",
   852→    "relationship_repair_needed": <true|false>,
   853→    "repair_suggestions": ["<how to improve the relationship>"]
   854→  },
   855→
   856→  "deep_client_psychology": {
   857→    "decision_maker_analysis": {
   858→      "decision_maker_type": "<primary|influencer|gatekeeper|user|unknown>",
   859→      "authority_level": "<final_authority|needs_approval|recommender|no_authority|unknown>",
   860→      "reports_to_whom": "<if mentioned, who do they need to convince?>",
   861→      "decision_speed": "<impulsive|fast|moderate|slow|very_slow>",
   862→      "decision_criteria": ["<what factors drive their decisions>"]
   863→    },
   864→    "personality_traits": ["<trait1>", "<trait2>", "<trait3>"],
   865→    "communication_preferences": {
   866→      "preferred_channel": "<email|phone|video|in_person|mixed>",
   867→      "response_urgency_expected": "<immediate|same_day|next_day|within_week|flexible>",
   868→      "detail_orientation": "<big_picture|balanced|detail_oriented|extremely_detailed>",
   869→      "emotional_vs_rational": "<very_emotional|emotional|balanced|rational|very_rational>",
   870→      "formality_preference": "<very_casual|casual|neutral|formal|very_formal>"
   871→    },
   872→    "motivators": ["<what motivates them - efficiency, cost savings, innovation, etc>"],
   873→    "fears_and_concerns": ["<what worries them>"],
   874→    "trust_building_opportunities": ["<how to build more trust>"],
   875→    "loyalty_indicators": {
   876→      "loyalty_score": <float 0.0-1.0>,
   877→      "long_term_potential": "<low|medium|high>",
   878→      "referral_likelihood": <float 0.0-1.0>,
   879→      "relationship_investment_level": "<minimal|low|moderate|high|very_high>"
   880→    }
   881→  },
   882→
   883→  "software_opportunity_mining": {
   884→    "pain_points_detected": [
   885→      {
   886→        "pain_point": "<specific problem they mentioned>",
   887→        "evidence": "<quote from email>",
   888→        "frequency_mentioned": <how many times mentioned>,
   889→        "urgency": "<low|medium|high|critical>",
   890→        "current_solution": "<how they handle it now, if mentioned>"
   891→      }
   892→    ],
   893→    "automation_opportunities": [
   894→      {
   895→        "process": "<manual process they do that could be automated>",
   896→        "evidence": "<how you know this from emails>",
   897→        "estimated_time_savings": "<low|medium|high>",
   898→        "complexity": "<simple|moderate|complex>",
   899→        "recommended_solution": "<what to build>"
   900→      }
   901→    ],
   902→    "integration_needs": [
   903→      {
   904→        "system_a": "<one system they use>",
   905→        "system_b": "<another system that needs connecting>",
   906→        "evidence": "<how you know from emails>",
   907→        "complexity": "<simple|moderate|complex>"
   908→      }
   909→    ],
   910→    "data_problems": [
   911→      {
   912→        "problem": "<data issue they have - scattered, duplicates, manual entry, etc>",
   913→        "evidence": "<from emails>",
   914→        "solution_type": "<migration|cleanup|integration|dashboard>"
   915→      }
   916→    ],
   917→    "industry_context": {
   918→      "client_industry": "<their industry based on emails>",
   919→      "industry_specific_needs": ["<common needs in this industry>"],
   920→      "regulatory_requirements": ["<if any compliance needs mentioned>"]
   921→    },
   922→    "specific_software_proposals": [
   923→      {
   924→        "title": "<name of software/tool to propose>",
   925→        "problem_solved": "<what it solves for them>",
   926→        "scope": "<tiny|small|medium|large|enterprise>",
   927→        "estimated_hours": "<range like 40-80>",
   928→        "pitch_approach": "<how to bring this up>",
   929→        "win_probability": <float 0.0-1.0>
   930→      }
   931→    ]
   932→  }
   933→
   934→}
   935→
   936→CRITICAL INSTRUCTIONS:
   937→- BE CONCISE. Keep all text fields SHORT (1-2 sentences max).
   938→- Profile EACH participant separately in participants_analysis
   939→- SKIP per_email_analysis - too verbose
   940→- Only include 3-5 most important red_flags, key_points, action_items
   941→- Only include quotes_and_pricing if actual prices are mentioned
   942→- Look for deception, payment delays, manipulation, knowledge extraction attempts
   943→- Omit empty arrays - if no red_flags found, use empty array []
   944→
   945→REPLY SUGGESTIONS (reply_suggestions):
   946→- Generate exactly 5 different reply options based on the conversation
   947→- Each reply must be COMPLETE and ready to copy-paste and send
   948→- Include proper greeting (Hi [name],) and sign-off (Best regards, Dan)
   949→- Options should cover: positive response, clarifying question, boundary-setting, solution-focused, brief acknowledgment
   950→- Base the replies on ACTUAL email content - reference specific things they mentioned
   951→
   952→BUSINESS OPPORTUNITIES (business_opportunities):
   953→- Cross-sell: Look at what they're discussing - what OTHER products/services could help them?
   954→- Software ideas: What custom software could solve problems they mentioned in emails?
   955→- Quote analysis: Only fill if this is about pricing/quotes, otherwise set is_quote_related to false
   956→- Persuasion: What questions should Dan ask? What objections might come? How to close?
   957→- ALL insights must come from the EMAIL CONTENT - not generic advice
   958→
   959→PROTECTIVE ANALYSIS (protective_analysis):
   960→- Dan (owner) is the GOOD GUY - protect his interests
   961→- Look for: payment risks, scope creep, manipulation, knowledge theft, time wasters
   962→- Grammar analysis: Count actual errors you see in their emails, assess professionalism
   963→- Be SPECIFIC with evidence - quote the emails
   964→- overall_safety_score: 1.0 = completely safe, 0.0 = avoid this client
   965→
   966→CHURN RISK SIGNALS (churn_risk_signals) - CRITICAL FOR BUSINESS SURVIVAL:
   967→- Look for ANY hint they might leave: competitor mentions, budget issues, dissatisfaction
   968→- "Goodbye language" = farewell phrases beyond normal email sign-offs
   969→- Disengagement = slower responses, shorter replies, less enthusiasm
   970→- Contract signals = asking about data export, documentation, ending dates
   971→- BE SENSITIVE to subtle signals - even small hints of leaving are valuable
   972→- exit_intent_score: 0.0 = loyal, 1.0 = definitely leaving soon
   973→
   974→PRICE PERCEPTION ANALYSIS (price_perception_analysis) - CRITICAL FOR PRICING STRATEGY:
   975→- How did they react to quotes? Shock, acceptance, negotiation?
   976→- Did they ask for discounts? How aggressively?
   977→- Do they understand the value we provide?
   978→- Could we charge MORE? (high willingness_to_pay_more = we're underpricing)
   979→- What is their budget reality? Tight or flexible?
   980→- Maximum they would pay = your best estimate based on all signals
   981→
   982→SUPPORT SATISFACTION (support_satisfaction_analysis):
   983→- Any frustration expressed? How severe?
   984→- Did they complain? About what? How often?
   985→- Did they escalate issues?
   986→- Express appreciation or just demands?
   987→- Would they recommend us to others?
   988→
   989→DEEP CLIENT PSYCHOLOGY (deep_client_psychology):
   990→- Are they the decision maker or need approval?
   991→- What personality traits are evident?
   992→- What motivates them? Fears?
   993→- How loyal are they? Referral potential?
   994→
   995→SOFTWARE OPPORTUNITY MINING (software_opportunity_mining) - REVENUE OPPORTUNITY:
   996→- What PAIN POINTS did they mention? (manual processes, spreadsheet hell, data problems)
   997→- What could be AUTOMATED for them?
   998→- What systems need INTEGRATION?
   999→- What SOFTWARE could Dan build to solve their problems?
  1000→- Be SPECIFIC - propose actual software with scope estimates
  1001→- This is how Dan finds new work to stay in business!
  1002→
  1003→BE A SKEPTICAL INVESTIGATOR - examine each person's behavior critically.
  1004→Look for evidence of deception, manipulation, and hidden agendas in each participant.
  1005→Be objective and evidence-based but err on the side of caution.
  1006→Respond ONLY with the JSON, no additional text
  1007→
  1008→PROMPT;
  1009→    }
  1010→
  1011→    /**
  1012→     * Public method to ask Claude a question and get text response
  1013→     * Used by API endpoints for ask_ai and generate_reply features
  1014→     */
  1015→    public function askClaude(string $prompt): string
  1016→    {
  1017→        $response = $this->callClaudeAPI($prompt);
  1018→        return $response['content'] ?? '';
  1019→    }
  1020→
  1021→    /**
  1022→     * Call AI API (supports both Claude and OpenAI)
  1023→     */
  1024→    private function callClaudeAPI(string $prompt, ?string $threadId = null): array
  1025→    {
  1026→        try {
  1027→            // Clean UTF-8 in prompt to avoid json_encode errors
  1028→            $cleanPrompt = $this->cleanUtf8($prompt);
  1029→
  1030→            $provider = $this->config['provider'] ?? 'claude';
  1031→            // Generate unique request ID to track duplicate calls
  1032→            $ddbg = debug_backtrace(2);
  1033→            $requestId = uniqid('req_', true);
  1034→            file_put_contents(__DIR__ . '/../logs/prompt.log', "\n\nDate: " . date('Y-m-d H:i:s') . " [RequestID: {$requestId}] [ThreadID: " . ($threadId ?? 'N/A') . "] - Prompt: " . $cleanPrompt . "\n", FILE_APPEND);
  1035→            file_put_contents(__DIR__ . '/../logs/prompt.log', "\n\nDebug Backtrace: " . print_r($ddbg, true) . "\n", FILE_APPEND);
  1036→            // Log the request
  1037→            $this->logToDb('info', 'AI-Request', "Sending request to {$provider} ({$this->config['model']})", [
  1038→                'provider' => $provider,
  1039→                'model' => $this->config['model'],
  1040→                'max_tokens' => $this->config['max_tokens'],
  1041→                'prompt_length' => strlen($cleanPrompt),
  1042→                'prompt_preview' => substr($cleanPrompt, 0, 500) . '...',
  1043→                'prompt' => $cleanPrompt  // Full prompt sent to AI
  1044→            ], $threadId);
  1045→
  1046→            if ($provider === 'openai') {
  1047→                // OpenAI request format
  1048→                $requestBody = [
  1049→                    'model' => $this->config['model'],
  1050→                    'max_tokens' => $this->config['max_tokens'],
  1051→                    'messages' => [
  1052→                        ['role' => 'user', 'content' => $cleanPrompt]
  1053→                    ]
  1054→                ];
  1055→            } else {
  1056→                // Claude request format
  1057→                $requestBody = [
  1058→                    'model' => $this->config['model'],
  1059→                    'max_tokens' => $this->config['max_tokens'],
  1060→                    'messages' => [
  1061→                        ['role' => 'user', 'content' => $cleanPrompt]
  1062→                    ]
  1063→                ];
  1064→            }
  1065→
  1066→            $jsonBody = $this->safeJsonEncode($requestBody);
  1067→            $startTime = microtime(true);
  1068→
  1069→            $response = $this->http->post($this->config['endpoint'], [
  1070→                'headers' => [
  1071→                    'Content-Type' => 'application/json'
  1072→                ],
  1073→                'body' => $jsonBody
  1074→            ]);
  1075→
  1076→            $elapsedTime = round(microtime(true) - $startTime, 2);
  1077→            $body = json_decode($response->getBody()->getContents(), true);
  1078→
  1079→            // Parse response based on provider
  1080→            if ($provider === 'openai') {
  1081→                if (!isset($body['choices'][0]['message']['content'])) {
  1082→                    throw new Exception("Invalid OpenAI response structure: " . json_encode($body));
  1083→                }
  1084→                $content = $body['choices'][0]['message']['content'];
  1085→                $usage = $body['usage'] ?? [];
  1086→                if (isset($usage['total_tokens'])) {
  1087→                    $this->stats['tokens_used'] += $usage['total_tokens'];
  1088→                }
  1089→            } else {
  1090→                if (!isset($body['content'][0]['text'])) {
  1091→                    throw new Exception("Invalid Claude response structure: " . json_encode($body));
  1092→                }
  1093→                $content = $body['content'][0]['text'];
  1094→                $usage = $body['usage'] ?? [];
  1095→                if (isset($usage)) {
  1096→                    $this->stats['tokens_used'] += ($usage['input_tokens'] ?? 0) + ($usage['output_tokens'] ?? 0);
  1097→                }
  1098→            }
  1099→
  1100→            // Log the response
  1101→            $this->logToDb('info', 'AI-Response', "Received response from {$provider} in {$elapsedTime}s", [
  1102→                'provider' => $provider,
  1103→                'model' => $body['model'] ?? $this->config['model'],
  1104→                'elapsed_seconds' => $elapsedTime,
  1105→                'usage' => $usage,
  1106→                'response_length' => strlen($content),
  1107→                'response_preview' => substr($content, 0, 500) . '...',
  1108→                'response' => $content  // Full response from AI
  1109→            ], $threadId);
  1110→
  1111→            // Save full request/response to disk for debugging
  1112→            $this->saveAnalysisLog($threadId, $cleanPrompt, $content, $usage, $elapsedTime, $provider);
  1113→
  1114→            return [
  1115→                'content' => $content,
  1116→                'model' => $body['model'] ?? $this->config['model'],
  1117→                'usage' => $usage,
  1118→                'provider' => $provider
  1119→            ];
  1120→
  1121→        } catch (Exception $e) {
  1122→            $this->logMessage("API request failed ({$this->config['provider']}): " . $e->getMessage(), 'error');
  1123→            $this->logToDb('error', 'AI-Error', "API request failed: " . $e->getMessage(), [
  1124→                'provider' => $this->config['provider'] ?? 'unknown',
  1125→                'error' => $e->getMessage()
  1126→            ], $threadId);
  1127→            throw new Exception("{$this->config['provider']} API error: " . $e->getMessage());
  1128→        }
  1129→    }
  1130→
  1131→    /**
  1132→     * Parse JSON response from Claude
  1133→     */
  1134→    private function parseAnalysisResponse(array $response): array
  1135→    {
  1136→        $content = $response['content'];
  1137→
  1138→        // Strip markdown code block wrappers if present
  1139→        $content = preg_replace('/^```(?:json)?\s*/i', '', $content);
  1140→        $content = preg_replace('/\s*```\s*$/', '', $content);
  1141→        $content = trim($content);
  1142→
  1143→        // Detect AI refusal responses (content moderation)
  1144→        $refusalPatterns = [
  1145→            '/I\'m sorry.*can\'t assist/i',
  1146→            '/I cannot help with/i',
  1147→            '/I\'m not able to/i',
  1148→            '/I apologize.*cannot/i',
  1149→            '/I\'m unable to/i',
  1150→            '/This request.*violates/i',
  1151→            '/I can\'t provide/i',
  1152→            '/I cannot provide/i'
  1153→        ];
  1154→
  1155→        foreach ($refusalPatterns as $pattern) {
  1156→            if (preg_match($pattern, $content)) {
  1157→                $this->logMessage("AI refused to analyze. Response: " . substr($content, 0, 500), 'warning');
  1158→                throw new Exception("AI declined to analyze this thread. This may happen with sensitive content. Try again or contact support.");
  1159→            }
  1160→        }
  1161→
  1162→        $jsonStart = strpos($content, '{');
  1163→        $jsonEnd = strrpos($content, '}');
  1164→
  1165→        if ($jsonStart === false || $jsonEnd === false) {
  1166→            $this->logMessage("No JSON found in response. Content: " . substr($content, 0, 500), 'error');
  1167→            throw new Exception("No JSON found in API response. The AI may have returned an unexpected response format.");
  1168→        }
  1169→
  1170→        $jsonString = substr($content, $jsonStart, $jsonEnd - $jsonStart + 1);
  1171→
  1172→        // Try to fix common JSON issues
  1173→        // Remove trailing commas before } or ]
  1174→        $jsonString = preg_replace('/,\s*([\}\]])/', '$1', $jsonString);
  1175→
  1176→        $analysis = json_decode($jsonString, true);
  1177→
  1178→        if (json_last_error() !== JSON_ERROR_NONE) {
  1179→            // Log the full JSON to file for debugging
  1180→            file_put_contents(__DIR__ . '/../logs/failed_json.log', "[" . date('Y-m-d H:i:s') . "]\n" . $jsonString . "\n\n", FILE_APPEND);
  1181→            $this->logMessage("JSON parse failed. Error: " . json_last_error_msg() . ". Full JSON logged to failed_json.log", 'error');
  1182→            throw new Exception("Failed to parse JSON: " . json_last_error_msg());
  1183→        }
  1184→
  1185→        return $analysis;
  1186→    }
  1187→
  1188→    /**
  1189→     * Store thread analysis results
  1190→     */
  1191→    private function storeThreadAnalysis(string $threadId, array $emails, array $analysis, array $rawResponse): void
  1192→    {
  1193→        $firstEmail = $emails[0];
  1194→        $lastEmail = $emails[count($emails) - 1];
  1195→        $sentiment = $analysis['sentiment_analysis'] ?? [];
  1196→        $negotiation = $analysis['negotiation_analysis'] ?? [];
  1197→        $deception = $analysis['deception_indicators'] ?? [];
  1198→
  1199→        // Store in thread_analysis table with full analysis
  1200→        // Ensure summary is a string (Claude might return it as an array)
  1201→        $summary = $analysis['thread_summary'] ?? null;
  1202→        if (is_array($summary)) {
  1203→            $summary = implode(' ', $summary);
  1204→        }
  1205→
  1206→        $this->db->insertOrUpdate('thread_analysis', [
  1207→            'thread_id' => $threadId,
  1208→            'contact_id' => $lastEmail['contact_id'] ?? null,
  1209→            'email_count' => count($emails),
  1210→            'last_email_date' => $lastEmail['date'],
  1211→            'last_email_id' => $lastEmail['id'],
  1212→            'is_analyzed' => 1,
  1213→            'needs_reanalysis' => 0,
  1214→            'analyzed_at' => date('Y-m-d H:i:s'),
  1215→            'summary' => $summary,
  1216→            'raw_analysis' => $this->safeJsonEncode($analysis),
  1217→            'key_points' => isset($analysis['key_points']) ? $this->safeJsonEncode($analysis['key_points']) : null,
  1218→            'deception_score' => $deception['deception_score'] ?? 0
  1219→        ], ['contact_id', 'email_count', 'last_email_date', 'last_email_id', 'is_analyzed', 'needs_reanalysis', 'analyzed_at', 'summary', 'raw_analysis', 'key_points', 'deception_score']);
  1220→
  1221→        // Also update MongoDB to keep dashboards in sync
  1222→        try {
  1223→            $mongo = MongoDatabase::getInstance();
  1224→            $mongo->insertOrUpdate('thread_analysis', [
  1225→                'thread_id' => $threadId,
  1226→                'is_analyzed' => 1,
  1227→                'needs_reanalysis' => 0,
  1228→                'analyzed_at' => date('Y-m-d H:i:s'),
  1229→                'summary' => $summary,
  1230→                'raw_analysis' => $this->safeJsonEncode($analysis),
  1231→                'key_points' => isset($analysis['key_points']) ? $this->safeJsonEncode($analysis['key_points']) : null,
  1232→                'deception_score' => $deception['deception_score'] ?? 0,
  1233→                'contact_id' => $lastEmail['contact_id'] ?? null,
  1234→                'email_count' => count($emails),
  1235→                'last_email_date' => $lastEmail['date'],
  1236→                'last_email_id' => $lastEmail['id']
  1237→            ], ['thread_id']);
  1238→        } catch (Exception $mongoEx) {
  1239→            // Log but don't fail the analysis if MongoDB sync fails
  1240→            error_log("MongoDB sync failed for thread {$threadId}: " . $mongoEx->getMessage());
  1241→        }
  1242→
  1243→        // Store detailed analysis in email_analysis (linked to last email)
  1244→        // Ensure string fields are not arrays
  1245→        $tone = $negotiation['their_stance'] ?? null;
  1246→        if (is_array($tone)) {
  1247→            $tone = implode(', ', $tone);
  1248→        }
  1249→        $sentimentLabel = $sentiment['overall_sentiment'] ?? null;
  1250→        if (is_array($sentimentLabel)) {
  1251→            $sentimentLabel = is_array($sentimentLabel) ? implode(', ', $sentimentLabel) : $sentimentLabel;
  1252→        }
  1253→
  1254→        $this->db->insertOrUpdate('email_analysis', [
  1255→            'email_id' => $lastEmail['id'],
  1256→            'sentiment_score' => $sentiment['sentiment_score'] ?? null,
  1257→            'sentiment_label' => $sentimentLabel,
  1258→            'tone' => $tone,
  1259→            'urgency_level' => null,
  1260→            'key_entities' => isset($analysis['entities_mentioned']) ? $this->safeJsonEncode($analysis['entities_mentioned']) : null,
  1261→            'commitments' => isset($analysis['commitments_made']) ? $this->safeJsonEncode($analysis['commitments_made']) : null,
  1262→            'requests' => null,
  1263→            'action_items' => isset($analysis['action_items']) ? $this->safeJsonEncode($analysis['action_items']) : null,
  1264→            'deception_flags' => isset($analysis['inconsistencies']) ? $this->safeJsonEncode($analysis['inconsistencies']) : null,
  1265→            'deception_score' => $deception['deception_score'] ?? null,
  1266→            'contains_quote' => !empty($analysis['quotes_and_pricing']) ? 1 : 0,
  1267→            'quote_amounts' => isset($analysis['quotes_and_pricing']) ? $this->safeJsonEncode($analysis['quotes_and_pricing']) : null,
  1268→            'summary' => $summary,
  1269→            'key_points' => isset($analysis['key_points']) ? $this->safeJsonEncode($analysis['key_points']) : null,
  1270→            'raw_analysis' => $this->safeJsonEncode($analysis),
  1271→            'model_used' => $rawResponse['model'] ?? $this->config['model'],
  1272→            'tokens_used' => ($rawResponse['usage']['input_tokens'] ?? 0) + ($rawResponse['usage']['output_tokens'] ?? 0),
  1273→            'analyzed_at' => date('Y-m-d H:i:s')
  1274→        ], ['email_id']);
  1275→
  1276→        // Store all participants with their analysis (non-fatal - don't let this kill the response)
  1277→        try {
  1278→            $this->storeThreadParticipants($threadId, $analysis);
  1279→        } catch (\Throwable $e) {
  1280→            $this->logMessage("storeThreadParticipants failed (non-fatal): " . $e->getMessage(), 'warning');
  1281→        }
  1282→
  1283→        // Log success to DB
  1284→        $this->logToDb('info', 'Thread analysis completed', $threadId, [
  1285→            'email_count' => count($emails),
  1286→            'participants' => count($analysis['participants_analysis'] ?? []),
  1287→            'deception_score' => $deception['deception_score'] ?? 0
  1288→        ]);
  1289→    }
  1290→
  1291→    /**
  1292→     * Store extracted quotes
  1293→     */
  1294→    private function storeQuotes(array $emails, array $quotes): void
  1295→    {
  1296→        foreach ($quotes as $quote) {
  1297→            if (empty($quote['amount'])) continue;
  1298→
  1299→            // Find which email in thread this quote came from
  1300→            $emailIndex = ($quote['email_number'] ?? 1) - 1;
  1301→            $email = $emails[$emailIndex] ?? $emails[count($emails) - 1];
  1302→
  1303→            try {
  1304→                $this->db->insert('quotes', [
  1305→                    'contact_id' => $email['contact_id'],
  1306→                    'email_id' => $email['id'],
  1307→                    'amount' => $quote['amount'],
  1308→                    'currency' => $quote['currency'] ?? 'EUR',
  1309→                    'description' => $quote['description'] ?? $quote['context'] ?? null,
  1310→                    'direction' => $quote['direction'] === 'sent_by_us' ? 'sent' : 'received',
  1311→                    'quote_type' => $quote['type'] ?? 'initial',
  1312→                    'outcome' => 'pending',
  1313→                    'confidence_score' => 0.8,
  1314→                    'quote_date' => $email['date']
  1315→                ]);
  1316→            } catch (Exception $e) {
  1317→                $this->logMessage("Failed to store quote: " . $e->getMessage(), 'warning');
  1318→            }
  1319→        }
  1320→    }
  1321→
  1322→    /**
  1323→     * Store thread participants (all contacts in the thread with their analysis)
  1324→     */
  1325→    private function storeThreadParticipants(string $threadId, array $analysis): void
  1326→    {
  1327→        $participants = $analysis['participants_analysis'] ?? [];
  1328→
  1329→        foreach ($participants as $participant) {
  1330→            $email = $participant['email'] ?? null;
  1331→            if (!$email) continue;
  1332→
  1333→            // Find or create contact
  1334→            $contact = $this->db->fetchOne("SELECT id FROM contacts WHERE email = ?", [$email]);
  1335→            if (!$contact) {
  1336→                // Create new contact
  1337→                $this->db->insert('contacts', [
  1338→                    'email' => $email,
  1339→                    'name' => $participant['name'] ?? null,
  1340→                    'type' => $participant['role'] === 'self' ? 'internal' : 'client',
  1341→                    'created_at' => date('Y-m-d H:i:s')
  1342→                ]);
  1343→                // lastInsertId() requires PDO (MySQL). Fall back to SELECT when using MongoDB.
  1344→                try {
  1345→                    $contactId = $this->db->lastInsertId();
  1346→                } catch (\Throwable $e) {
  1347→                    $newContact = $this->db->fetchOne("SELECT id FROM contacts WHERE email = ?", [$email]);
  1348→                    $contactId = $newContact['id'] ?? null;
  1349→                }
  1350→            } else {
  1351→                $contactId = $contact['id'];
  1352→            }
  1353→            if (!$contactId) continue; // Skip if we couldn't get a contact ID
  1354→
  1355→            $profile = $participant['psychological_profile'] ?? [];
  1356→            $behavioral = $participant['behavioral_indicators'] ?? [];
  1357→
  1358→            // Store participant in thread_participants table
  1359→            try {
  1360→                $this->db->insertOrUpdate('thread_participants', [
  1361→                    'thread_id' => $threadId,
  1362→                    'contact_id' => $contactId,
  1363→                    'email_count' => $participant['email_count'] ?? 0,
  1364→                    'role' => $participant['role'] === 'self' ? 'self' : 'external',
  1365→                    'trust_score' => $profile['trust_score'] ?? null,
  1366→                    'deception_score' => $profile['deception_score'] ?? null,
  1367→                    'manipulation_score' => $profile['manipulation_score'] ?? null,
  1368→                    'reliability_score' => $profile['reliability_score'] ?? null,
  1369→                    'risk_level' => $participant['risk_level'] ?? null,
  1370→                    'verdict' => $participant['verdict'] ?? null,
  1371→                    'psychological_profile' => $this->safeJsonEncode($profile),
  1372→                    'behavioral_indicators' => $this->safeJsonEncode($behavioral),
  1373→                    'red_flags' => $this->safeJsonEncode($participant['red_flags'] ?? []),
  1374→                    'positive_signals' => $this->safeJsonEncode($participant['positive_signals'] ?? []),
  1375→                    'overall_assessment' => $participant['overall_assessment'] ?? null
  1376→                ], ['email_count', 'role', 'trust_score', 'deception_score', 'manipulation_score',
  1377→                    'reliability_score', 'risk_level', 'verdict', 'psychological_profile',
  1378→                    'behavioral_indicators', 'red_flags', 'positive_signals', 'overall_assessment']);
  1379→
  1380→                // Store learned behaviors from this participant
  1381→                $this->storeLearnedBehaviors($contactId, $threadId, $participant);
  1382→
  1383→            } catch (Exception $e) {
  1384→                $this->logMessage("Failed to store participant {$email}: " . $e->getMessage(), 'warning');
  1385→            }
  1386→        }
  1387→    }
  1388→
  1389→    /**
  1390→     * Store learned behaviors for RAG context in future analyses
  1391→     */
  1392→    private function storeLearnedBehaviors(int $contactId, string $threadId, array $participant): void
  1393→    {
  1394→        // Store red flags as learned behaviors
  1395→        $redFlags = $participant['red_flags'] ?? [];
  1396→        foreach ($redFlags as $flag) {
  1397→            $this->addLearnedBehavior($contactId, $threadId, 'red_flag', $flag['flag'] ?? '', $flag['evidence'] ?? '', $flag['severity'] ?? 'medium');
  1398→        }
  1399→
  1400→        // Store positive signals
  1401→        $positives = $participant['positive_signals'] ?? [];
  1402→        foreach ($positives as $signal) {
  1403→            $this->addLearnedBehavior($contactId, $threadId, 'positive_signal', $signal['signal'] ?? '', $signal['evidence'] ?? '', 'low');
  1404→        }
  1405→
  1406→        // Store negotiation tactics
  1407→        $tactics = $participant['behavioral_indicators']['negotiation_tactics'] ?? [];
  1408→        foreach ($tactics as $tactic) {
  1409→            if (is_string($tactic)) {
  1410→                $this->addLearnedBehavior($contactId, $threadId, 'negotiation_tactic', $tactic, '', 'medium');
  1411→            }
  1412→        }
  1413→
  1414→        // Store pressure tactics as potential deception indicators
  1415→        $pressure = $participant['behavioral_indicators']['pressure_tactics'] ?? [];
  1416→        foreach ($pressure as $tactic) {
  1417→            if (is_string($tactic)) {
  1418→                $this->addLearnedBehavior($contactId, $threadId, 'deception_indicator', $tactic, '', 'medium');
  1419→            }
  1420→        }
  1421→
  1422→        // Store communication pattern
  1423→        $style = $participant['behavioral_indicators']['communication_style'] ?? null;
  1424→        if ($style) {
  1425→            $this->addLearnedBehavior($contactId, $threadId, 'communication_pattern', $style, '', 'low');
  1426→        }
  1427→    }
  1428→
  1429→    /**
  1430→     * Add or update a learned behavior
  1431→     */
  1432→    private function addLearnedBehavior(int $contactId, string $threadId, string $type, string $description, string $evidence, string $severity): void
  1433→    {
  1434→        if (empty($description)) return;
  1435→
  1436→        // Check if similar behavior already exists
  1437→        $existing = $this->db->fetchOne(
  1438→            "SELECT id, frequency FROM learned_behaviors WHERE contact_id = ? AND behavior_type = ? AND description = ?",
  1439→            [$contactId, $type, $description]
  1440→        );
  1441→
  1442→        if ($existing) {
  1443→            // Update frequency and last seen
  1444→            $this->db->execute(
  1445→                "UPDATE learned_behaviors SET frequency = frequency + 1, last_seen_thread = ?, updated_at = NOW() WHERE id = ?",
  1446→                [$threadId, $existing['id']]
  1447→            );
  1448→        } else {
  1449→            // Insert new behavior
  1450→            try {
  1451→                $this->db->insert('learned_behaviors', [
  1452→                    'contact_id' => $contactId,
  1453→                    'behavior_type' => $type,
  1454→                    'description' => $description,
  1455→                    'evidence' => $evidence,
  1456→                    'severity' => $severity,
  1457→                    'frequency' => 1,
  1458→                    'first_seen_thread' => $threadId,
  1459→                    'last_seen_thread' => $threadId,
  1460→                    'confidence_score' => 0.8,
  1461→                    'is_active' => 1
  1462→                ]);
  1463→            } catch (Exception $e) {
  1464→                // Ignore duplicate errors
  1465→            }
  1466→        }
  1467→    }
  1468→
  1469→    /**
  1470→     * Log to api_logs table for extension visibility
  1471→     */
  1472→    public function logToDb(string $level, string $source, string $message, array $context = [], ?string $threadId = null): void
  1473→    {
  1474→        try {
  1475→            $this->db->insert('api_logs', [
  1476→                'log_level' => $level,
  1477→                'source' => $source,
  1478→                'message' => $message,
  1479→                'context' => !empty($context) ? $this->safeJsonEncode($context) : null,
  1480→                'thread_id' => $threadId,
  1481→                'created_at' => date('Y-m-d H:i:s')
  1482→            ]);
  1483→        } catch (Exception $e) {
  1484→            // Fallback to file log
  1485→            $this->logMessage("DB log failed: " . $e->getMessage(), 'warning');
  1486→        }
  1487→    }
  1488→
  1489→    /**
  1490→     * Mark thread for re-analysis (when new emails arrive)
  1491→     *
  1492→     * @param string $threadId
  1493→     */
  1494→    public function markThreadForReanalysis(string $threadId): void
  1495→    {
  1496→        $this->db->execute(
  1497→            "UPDATE thread_analysis SET needs_reanalysis = 1 WHERE thread_id = ?",
  1498→            [$threadId]
  1499→        );
  1500→    }
  1501→
  1502→    /**
  1503→     * Get statistics
  1504→     */
  1505→    public function getStats(): array
  1506→    {
  1507→        return $this->stats;
  1508→    }
  1509→
  1510→    private function logMessage(string $message, string $level = 'info'): void
  1511→    {
  1512→        $logFile = defined('LOG_PATH') ? LOG_PATH . 'thread_analyzer.log' : __DIR__ . '/../logs/thread_analyzer.log';
  1513→        $timestamp = date('Y-m-d H:i:s');
  1514→        $levelUpper = strtoupper($level);
  1515→        file_put_contents($logFile, "[{$timestamp}] [{$levelUpper}] {$message}\n", FILE_APPEND);
  1516→
  1517→        if (php_sapi_name() === 'cli') {
  1518→            echo "[{$levelUpper}] {$message}\n";
  1519→        }
  1520→    }
  1521→
  1522→    /**
  1523→     * Safely encode data to JSON, fixing UTF-8 issues
  1524→     */
  1525→    private function safeJsonEncode($data): string
  1526→    {
  1527→        $cleaned = $this->cleanUtf8($data);
  1528→        $json = json_encode($cleaned, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
  1529→        if ($json === false) {
  1530→            $this->logMessage("json_encode error: " . json_last_error_msg(), 'error');
  1531→            return '{}';
  1532→        }
  1533→        return $json;
  1534→    }
  1535→
  1536→    /**
  1537→     * Recursively clean UTF-8 strings in data
  1538→     */
  1539→    private function cleanUtf8($data)
  1540→    {
  1541→        if (is_string($data)) {
  1542→            // Remove invalid UTF-8 sequences
  1543→            $data = mb_convert_encoding($data, 'UTF-8', 'UTF-8');
  1544→            // Remove null bytes and other problematic characters
  1545→            $data = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $data);
  1546→            return $data;
  1547→        }
  1548→        if (is_array($data)) {
  1549→            return array_map([$this, 'cleanUtf8'], $data);
  1550→        }
  1551→        return $data;
  1552→    }
  1553→
  1554→    /**
  1555→     * Save full AI request/response to disk for debugging and review
  1556→     * Files are saved in logs/ai_analysis/ organized by date
  1557→     */
  1558→    private function saveAnalysisLog(?string $threadId, string $prompt, string $response, array $usage, float $elapsedTime, string $provider): void
  1559→    {
  1560→        try {
  1561→            $logDir = dirname(__DIR__) . '/logs/ai_analysis/' . date('Y-m-d');
  1562→            if (!is_dir($logDir)) {
  1563→                mkdir($logDir, 0755, true);
  1564→            }
  1565→
  1566→            $timestamp = date('H-i-s');
  1567→            $safeThreadId = preg_replace('/[^a-zA-Z0-9_-]/', '_', $threadId ?? 'unknown');
  1568→            $filename = "{$logDir}/{$timestamp}_{$safeThreadId}.txt";
  1569→
  1570→            $tokenInfo = "Input tokens: " . ($usage['input_tokens'] ?? 'N/A') .
  1571→                        ", Output tokens: " . ($usage['output_tokens'] ?? 'N/A') .
  1572→                        ", Total: " . (($usage['input_tokens'] ?? 0) + ($usage['output_tokens'] ?? 0));
  1573→
  1574→            $logContent = "=" . str_repeat("=", 79) . "\n";
  1575→            $logContent .= "THREAD ANALYSIS LOG\n";
  1576→            $logContent .= "=" . str_repeat("=", 79) . "\n\n";
  1577→            $logContent .= "Thread ID: {$threadId}\n";
  1578→            $logContent .= "Timestamp: " . date('Y-m-d H:i:s') . "\n";
  1579→            $logContent .= "Provider: {$provider}\n";
  1580→            $logContent .= "Elapsed Time: {$elapsedTime}s\n";
  1581→            $logContent .= "Token Usage: {$tokenInfo}\n";
  1582→            $logContent .= "Prompt Length: " . strlen($prompt) . " chars\n";
  1583→            $logContent .= "Response Length: " . strlen($response) . " chars\n";
  1584→            $logContent .= "\n" . str_repeat("-", 80) . "\n";
  1585→            $logContent .= "PROMPT SENT TO AI:\n";
  1586→            $logContent .= str_repeat("-", 80) . "\n\n";
  1587→            $logContent .= $prompt;
  1588→            $logContent .= "\n\n" . str_repeat("-", 80) . "\n";
  1589→            $logContent .= "AI RESPONSE:\n";
  1590→            $logContent .= str_repeat("-", 80) . "\n\n";
  1591→            $logContent .= $response;
  1592→            $logContent .= "\n\n" . str_repeat("=", 80) . "\n";
  1593→            $logContent .= "END OF LOG\n";
  1594→            $logContent .= str_repeat("=", 80) . "\n";
  1595→
  1596→            file_put_contents($filename, $logContent);
  1597→
  1598→            // Also keep a summary log for quick reference
  1599→            $summaryFile = dirname(__DIR__) . '/logs/ai_analysis/summary.log';
  1600→            $summaryLine = date('Y-m-d H:i:s') . " | Thread: {$threadId} | Provider: {$provider} | Time: {$elapsedTime}s | Tokens: " . (($usage['input_tokens'] ?? 0) + ($usage['output_tokens'] ?? 0)) . " | File: {$filename}\n";
  1601→            file_put_contents($summaryFile, $summaryLine, FILE_APPEND);
  1602→
  1603→        } catch (Exception $e) {
  1604→            // Don't fail the analysis if logging fails
  1605→            error_log("Failed to save analysis log: " . $e->getMessage());
  1606→        }
  1607→    }
  1608→
  1609→    /**
  1610→     * Strip quoted content from email body to avoid duplication
  1611→     * Removes:
  1612→     * - Lines starting with > (quoted replies)
  1613→     * - "On X date, Y wrote:" patterns and everything after
  1614→     * - "From: ... Sent: ... To: ... Subject: ..." forwarded headers
  1615→     */
  1616→    private function stripQuotedContent(string $body): string
  1617→    {
  1618→        // Remove lines starting with > (quoted text)
  1619→        $body = preg_replace('/^>+.*$/m', '', $body);
  1620→
  1621→        // Remove "On [date], [person] wrote:" and everything after
  1622→        $patterns = [
  1623→            '/On\s+.{10,60}\s+wrote:\s*$/mi',  // "On Monday, Jan 1, John wrote:"
  1624→            '/On\s+\d{1,2}[\/-]\d{1,2}[\/-]\d{2,4}.+wrote:\s*$/mi',  // "On 01/02/2023, John wrote:"
  1625→            '/-{2,}.*Original Message.*-{2,}/i',  // "-----Original Message-----"
  1626→            '/From:\s+.+\nSent:\s+.+\nTo:\s+.+\nSubject:\s+.+/i',  // Outlook forwarded header
  1627→            '/_{5,}.*$/m',  // _____ separator lines
  1628→        ];
  1629→
  1630→        foreach ($patterns as $pattern) {
  1631→            if (preg_match($pattern, $body, $matches, PREG_OFFSET_CAPTURE)) {
  1632→                // Cut everything from the match onwards
  1633→                $body = substr($body, 0, $matches[0][1]);
  1634→            }
  1635→        }
  1636→
  1637→        // Clean up multiple consecutive newlines
  1638→        $body = preg_replace('/\n{3,}/', "\n\n", $body);
  1639→
  1640→        return trim($body);
  1641→    }
  1642→}
  1643→

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?