<?php

namespace App\Controllers;

use App\Libraries\CloudTalkService;
use App\Libraries\OpenAiService;
use App\Models\CloudTalkConfigModel;
use App\Models\DocumentModel;
use App\Models\DocumentChunkModel;
use App\Models\ChatHistoryModel;
use App\Models\ChatbotConfigModel;
use App\Models\LeadCallModel;
use App\Models\LeadCallRecordingModel;
use App\Models\LeadCallTranscriptModel;
use App\Models\LeadModel;
use Throwable;

class ChatController extends BaseController
{
    public function index()
    {
        $docModel = new DocumentModel();
        $role = (string) (session()->get('role') ?? '');
        $isAdmin = ($role === 'admin');

        $docBuilder = $docModel->select('id, title, embedding_status');
        if (!$isAdmin) {
            $docBuilder->where('visibility', 'agents');
        }
        $documents = $docBuilder
            ->orderBy('created_at', 'DESC')
            ->findAll();

        $trainedCount = 0;
        foreach ($documents as $d) {
            if (($d['embedding_status'] ?? '') === 'trained') {
                $trainedCount++;
            }
        }

        $apiKey = '';
        $apiValidation = null;
        try {
            $apiKey = getenv('OPENAI_API_KEY') ?: '';
            if ($apiKey === '') {
                $cfgModel = new ChatbotConfigModel();
                $apiKey = (string) $cfgModel->getValue('openai_api_key', '');
            }
            if ($apiKey !== '') {
                try {
                    $svc = new \App\Libraries\OpenAiService();
                    $apiValidation = $svc->testKey();
                } catch (\Throwable $e) {
                    $apiValidation = ['ok' => false, 'message' => $e->getMessage() ?: 'Unknown error while testing OpenAI key.'];
                }
            }
        } catch (\Throwable $e) {
            $apiKey = '';
        }
        $apiConfigured = (
            $apiKey !== ''
            && stripos($apiKey, 'your-') === false
            && stripos($apiKey, 'sk-xxx') === false
            && stripos($apiKey, 'sk-your') === false
            && ($apiValidation === null || $apiValidation['ok'] !== false || !empty($apiValidation['used_model_ok']))
        );
        if ($apiConfigured && $apiValidation !== null && isset($apiValidation['ok']) && $apiValidation['ok'] === false) {
            $apiConfigured = (stripos((string)($apiValidation['message'] ?? ''), 'network') === false);
        }

        return view('layout/app', [
            'title'     => 'AI Assistant',
            'pageTitle' => 'AI Assistant',
            'content'   => view('admin/ai_assistant', [
                'documents'     => $documents,
                'trainedCount'  => $trainedCount,
                'apiConfigured' => $apiConfigured,
                'apiKeySaved'   => ($apiKey !== ''),
                'apiValidation' => $apiValidation,
            ]),
        ]);
    }

    private function classifyApiError(\Throwable $e): array
    {
        $msg = $e->getMessage() ?? '';
        $lower = strtolower($msg);

        $kind = 'unknown';
        $title = 'AI request failed';
        $body = $msg;
        $action = null;

        if ($lower === '' || strpos($lower, 'api key is not configured') !== false) {
            $kind = 'no_api_key';
            $title = 'OpenAI API key is missing';
            $body = 'No valid API key was found. Go to Chatbot Configuration and paste your OpenAI key (starts with sk-).';
            $action = ['label' => 'Open Chatbot Config', 'url' => site_url('/admin/chatbot-config')];
        } elseif (strpos($lower, 'http 401') !== false || strpos($lower, 'invalid_api_key') !== false || strpos($lower, 'incorrect api key provided') !== false) {
            $kind = 'invalid_api_key';
            $title = 'OpenAI API key is invalid';
            $body = 'The configured API key was rejected by OpenAI (HTTP 401). Double-check that it starts with sk- and is copied correctly without extra spaces.';
            $action = ['label' => 'Fix in Chatbot Config', 'url' => site_url('/admin/chatbot-config')];
        } elseif (strpos($lower, 'http 429') !== false || strpos($lower, 'rate limit') !== false || strpos($lower, 'quota') !== false) {
            $kind = 'rate_limit';
            $title = 'OpenAI rate limit or billing quota reached';
            $body = 'You hit a rate limit or have exhausted your OpenAI credit balance. Wait a moment, or add billing details in your OpenAI dashboard, then retry.';
        } elseif (strpos($lower, 'http 404') !== false || strpos($lower, 'model_not_found') !== false) {
            $kind = 'model_not_found';
            $title = 'Model not available for this API key';
            $body = 'The configured model name cannot be used with this API key. Check that the model in Chatbot Configuration matches what your OpenAI account can access (e.g. gpt-4o-mini or gpt-3.5-turbo).';
            $action = ['label' => 'Change model', 'url' => site_url('/admin/chatbot-config')];
        } elseif (strpos($lower, 'curl') !== false || strpos($lower, 'ssl') !== false || strpos($lower, 'timed out') !== false) {
            $kind = 'network';
            $title = 'Could not reach OpenAI';
            $body = 'There was a network/SSL problem connecting to api.openai.com. Check your internet connection or server outbound firewall.';
        } elseif (
            strpos($lower, 'undefined method') !== false
            || strpos($lower, 'call to undefined') !== false
            || strpos($lower, 'not found in') !== false
            || preg_match('/::\w+\(\)/', $msg)
            || strpos($lower, 'error: uncaught') !== false
            || strpos($lower, 'stack trace') !== false
            || strpos($lower, '.php on line') !== false
        ) {
            $kind = 'internal';
            $title = 'AI generation had a temporary issue';
            $body = 'Default questions are shown while the service is fixed.';
        }

        return [
            'kind'   => $kind,
            'title'  => $title,
            'body'   => $body,
            'action' => $action,
        ];
    }

    private function friendlyWarn(\Throwable $e, string $prefix = 'AI generation failed'): string
    {
        try {
            $info = $this->classifyApiError($e);
            if (($info['kind'] ?? '') !== 'unknown') {
                return $prefix . ': ' . $info['body'] . '; falling back to defaults.';
            }
        } catch (\Throwable $ignored) { /* fall through to generic */ }
        log_message('error', '[ChatController][friendlyWarn] ' . ($prefix ?? 'warn') . ' :: ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
        return $prefix . '; falling back to default questions.';
    }

    public function downloadDocument($id)
    {
        $id = (int) $id;
        if ($id <= 0) {
            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
        }

        $docModel = new DocumentModel();
        $document = $docModel->find($id);
        if (!$document) {
            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
        }

        $role = (string) (session()->get('role') ?? '');
        $isAdmin = ($role === 'admin');
        if (!$isAdmin) {
            $dv = strtolower(trim((string) ($document['visibility'] ?? 'agents')));
            if ($dv !== 'agents') {
                throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
            }
        }

        $type = strtolower(trim((string) ($document['type'] ?? 'txt')));
        $title = trim((string) ($document['title'] ?? 'document'));
        if ($title === '') $title = 'document';

        if (!empty($document['url']) && $type === 'link') {
            return redirect()->to($document['url']);
        }

        $filePath = trim((string) ($document['file_path'] ?? ''));
        if ($filePath === '') {
            $contentText = (string) ($document['content_text'] ?? '');
            if ($contentText !== '' && in_array($type, ['txt', 'docx', 'pdf', 'md'], true)) {
                $ext = in_array($type, ['txt', 'docx', 'pdf', 'md'], true) ? $type : 'txt';
                $name = preg_replace('/[^A-Za-z0-9_\- ]+/', '', $title) . '.' . $ext;
                return $this->response->setBody($contentText)
                    ->setContentType('text/plain; charset=utf-8')
                    ->setHeader('Content-Disposition', 'attachment; filename="' . $name . '"');
            }
            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
        }

        $fullPath = WRITEPATH . 'uploads/' . ltrim($filePath, '/\\');
        if (!is_file($fullPath) || !is_readable($fullPath)) {
            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
        }

        $ext = strtolower(pathinfo($fullPath, PATHINFO_EXTENSION));
        if ($ext === '') $ext = $type;
        $safeTitle = preg_replace('/[^A-Za-z0-9_\- ]+/', '', $title);
        if ($safeTitle === '') $safeTitle = 'document';
        $downloadName = $safeTitle . '.' . $ext;

        $mime = '';
        if (function_exists('mime_content_type')) {
            try { $mime = mime_content_type($fullPath); } catch (\Throwable $e) { $mime = ''; }
        }
        if (!$mime) {
            $map = [
                'pdf'  => 'application/pdf',
                'txt'  => 'text/plain',
                'md'   => 'text/markdown',
                'csv'  => 'text/csv',
                'doc'  => 'application/msword',
                'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
                'xls'  => 'application/vnd.ms-excel',
                'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
                'png'  => 'image/png',
                'jpg'  => 'image/jpeg',
                'jpeg' => 'image/jpeg',
                'gif'  => 'image/gif',
                'webp' => 'image/webp',
                'svg'  => 'image/svg+xml',
                'mp4'  => 'video/mp4',
                'webm' => 'video/webm',
                'mov'  => 'video/quicktime',
                'mp3'  => 'audio/mpeg',
                'zip'  => 'application/zip',
            ];
            $mime = $map[strtolower($ext)] ?? 'application/octet-stream';
        }

        try {
            $disposition = 'attachment';
            if (isset($_GET['inline']) && $_GET['inline'] === '1') {
                $inlineable = ['pdf', 'txt', 'md', 'csv', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'mp4', 'webm', 'mp3'];
                if (in_array(strtolower($ext), $inlineable, true)) {
                    $disposition = 'inline';
                }
            }
            return $this->response
                ->setHeader('Content-Type', $mime)
                ->setHeader('Content-Disposition', $disposition . '; filename="' . $downloadName . '"')
                ->setHeader('Content-Length', (string) filesize($fullPath))
                ->setBody(file_get_contents($fullPath));
        } catch (\Throwable $e) {
            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
        }
    }

    public function summarize()
    {
        $documentId = (int) $this->request->getPost('document_id');
        if (!$documentId) {
            return $this->response->setJSON(['success' => false, 'error' => 'Invalid document ID']);
        }

        $role = (string) (session()->get('role') ?? '');
        $isAdmin = ($role === 'admin');

        $docModel = new DocumentModel();
        $document = $docModel->find($documentId);
        if (!$document) {
            return $this->response->setJSON(['success' => false, 'error' => 'Document not found']);
        }
        if (!$isAdmin) {
            $dv = strtolower(trim((string) ($document['visibility'] ?? 'agents')));
            if ($dv !== 'agents') {
                return $this->response->setJSON(['success' => false, 'error' => 'Document not found']);
            }
        }

        $summary = $document['summary'] ?? '';

        if (empty($summary)) {
            try {
                $openAi = new OpenAiService();
                $contentText = $document['content_text'] ?? '';
                $summary = $openAi->summarizeDocument($contentText);

                $docModel->update($documentId, ['summary' => $summary]);
            } catch (\Exception $e) {
                $info = $this->classifyApiError($e);
                return $this->response->setJSON([
                    'success'   => false,
                    'error'     => $info['body'],
                    'errorInfo' => $info,
                ]);
            }
        }

        $user = $this->currentUser();
        $userId = $user['user_id'] ?? null;

        if ($userId) {
            $chatHistoryModel = new ChatHistoryModel();
            $chatHistoryModel->insert([
                'user_id'      => $userId,
                'document_id'  => $documentId,
                'message_role' => 'assistant',
                'message_text' => $summary,
            ]);
        }

        try {
            $openAi = new OpenAiService();
            $questions = $openAi->generateQuestions($summary, 3);
        } catch (\Exception $e) {
            $questions = [];
        }

        return $this->response->setJSON([
            'success'   => true,
            'summary'   => $summary,
            'questions' => $questions,
        ]);
    }

    public function generateQuestions()
    {
        $documentId = (int) $this->request->getPost('document_id');
        $summary = (string) $this->request->getPost('summary');

        if (empty($summary) && $documentId) {
            $role = (string) (session()->get('role') ?? '');
            $isAdmin = ($role === 'admin');
            $docModel = new DocumentModel();
            $document = $docModel->find($documentId);
            if ($document) {
                if (!$isAdmin) {
                    $dv = strtolower(trim((string) ($document['visibility'] ?? 'agents')));
                    if ($dv !== 'agents') $document = null;
                }
            }
            if ($document) {
                $summary = $document['summary'] ?? '';
            }
        }

        if (empty($summary)) {
            return $this->response->setJSON(['success' => false, 'error' => 'No summary available for question generation']);
        }

        try {
            $openAi = new OpenAiService();
            $questions = $openAi->generateQuestions($summary, 3);
        } catch (\Exception $e) {
            $info = $this->classifyApiError($e);
            return $this->response->setJSON([
                'success'   => false,
                'error'     => $info['body'],
                'errorInfo' => $info,
            ]);
        }

        return $this->response->setJSON([
            'success'   => true,
            'questions' => $questions,
        ]);
    }

    public function suggestQuestions()
    {
        $role = (string) (session()->get('role') ?? '');
        $isAdmin = ($role === 'admin');

        $rawDocId = $this->request->getPost('document_id');
        $documentId = (is_numeric($rawDocId) && (int) $rawDocId > 0) ? (int) $rawDocId : null;
        $count = max(3, min(8, (int) ($this->request->getPost('count') ?? 5)));
        $regenSeed = (string) ($this->request->getPost('seed') ?? (string) mt_rand(1000, 999999));

        $docModel = new DocumentModel();
        $chunkModel = new DocumentChunkModel();

        $selectFields = 'documents.id, documents.title, documents.description, documents.summary, documents.embedding_status, documents.visibility, documents.type';

        $buildTrainedWhere = static function ($builder) {
            $builder->groupStart()
                ->where('documents.embedding_status', 'trained')
                ->orWhere('documents.embedding_status', 'completed')
                ->orWhere('documents.embedding_status', 'success')
                ->orLike('documents.embedding_status', 'trained', 'both')
                ->orLike('documents.embedding_status', 'embed', 'both')
                ->groupEnd();
        };

        $builder = $docModel->builder();
        $builder->select($selectFields);
        if (!$isAdmin) {
            $builder->where('documents.visibility', 'agents');
        }
        $builder->where('documents.embedding_status IS NOT NULL', null, false);
        $buildTrainedWhere($builder);
        if ($documentId !== null) {
            $builder->where('documents.id', $documentId);
            $scopeType = 'single';
        } else {
            $scopeType = 'all';
        }
        $builder->orderBy('documents.updated_at', 'DESC');
        $builder->limit(15);
        $docs = $builder->get()->getResultArray();

        if ($docs === []) {
            $builder2 = $docModel->builder();
            $builder2->select($selectFields);
            if (!$isAdmin) {
                $builder2->where('documents.visibility', 'agents');
            }
            if ($documentId !== null) {
                $builder2->where('documents.id', $documentId);
            }
            $builder2->orderBy('documents.updated_at', 'DESC');
            $builder2->limit(15);
            $docs = $builder2->get()->getResultArray();
        }

        $docLabels = [];
        $perDocSnippets = [];
        foreach ($docs as $d) {
            $did = (int) $d['id'];
            $label = (string) $d['title'];
            if ($label === '') $label = 'Document #' . $did;
            $docLabels[] = $label;

            $parts = [];
            $parts[] = 'DOCUMENT #' . $did . ' — TYPE: ' . ($d['type'] ?? 'unknown') . ' — STATUS: ' . ($d['embedding_status'] ?? 'n/a');
            $parts[] = 'TITLE: ' . $label;
            if (!empty($d['description'])) $parts[] = 'DESCRIPTION: ' . trim((string) $d['description']);
            if (!empty($d['summary']))       $parts[] = 'SUMMARY: ' . trim((string) $d['summary']);

            try {
                $totalChunks = (int) $chunkModel->where('document_id', $did)->countAllResults();
                $chunksHead = $chunkModel
                    ->where('document_id', $did)
                    ->orderBy('chunk_index', 'ASC')
                    ->limit(5)
                    ->findAll();
                $chunksMid = [];
                if ($totalChunks > 10) {
                    $midOffset = (int) floor($totalChunks * 0.35);
                    if ($midOffset < 3) $midOffset = 3;
                    $chunksMid = $chunkModel
                        ->where('document_id', $did)
                        ->orderBy('chunk_index', 'ASC')
                        ->limit(4, $midOffset)
                        ->findAll();
                }
                $chunksTail = [];
                if ($totalChunks > 15) {
                    $tailOffset = max(0, $totalChunks - 4);
                    $chunksTail = $chunkModel
                        ->where('document_id', $did)
                        ->orderBy('chunk_index', 'ASC')
                        ->limit(4, $tailOffset)
                        ->findAll();
                }
                $chunks = array_merge(
                    is_array($chunksHead) ? $chunksHead : [],
                    is_array($chunksMid)  ? $chunksMid  : [],
                    is_array($chunksTail) ? $chunksTail : []
                );
                usort($chunks, static function ($a, $b) {
                    $ai = (int) ($a['chunk_index'] ?? 0);
                    $bi = (int) ($b['chunk_index'] ?? 0);
                    return $ai <=> $bi;
                });
                if ($chunks !== []) {
                    $chunkSnippets = [];
                    $headWordSet = [];
                    foreach ($chunks as $ci => $c) {
                        $snippet = trim((string) ($c['content_text'] ?? ''));
                        if ($snippet === '') continue;
                        if (mb_strlen($snippet) > 700) $snippet = mb_substr($snippet, 0, 680) . '…';
                        $chunkSnippets[] = 'CHUNK ' . ((int) ($c['chunk_index'] ?? $ci) + 1) . ' of ' . $totalChunks . ': ' . $snippet;
                        preg_match_all('/\b[A-Z][A-Za-z\-]{3,}\b/', $snippet, $wm);
                        if (!empty($wm[0])) {
                            foreach ($wm[0] as $w) {
                                $wL = mb_strtolower($w);
                                if (isset($headWordSet[$wL])) $headWordSet[$wL]++; else $headWordSet[$wL] = 1;
                            }
                        }
                    }
                    if ($chunkSnippets !== []) {
                        $parts[] = 'TOTAL CHUNKS IN DOC: ' . $totalChunks . ' · sampled head+middle+tail for context.';
                        $parts[] = implode("\n", $chunkSnippets);
                        arsort($headWordSet);
                        $topWords = array_slice(array_keys($headWordSet), 0, 20);
                        if ($topWords !== []) $parts[] = 'KEY TOPICS DETECTED (use these words in questions to match answerable content): ' . implode(', ', $topWords) . '.';
                    }
                } elseif (!empty($d['content_text'])) {
                    $snippet = trim((string) $d['content_text']);
                    if (mb_strlen($snippet) > 1400) $snippet = mb_substr($snippet, 0, 1380) . '…';
                    if ($snippet !== '') $parts[] = 'RAW CONTENT SNIPPET: ' . $snippet;
                }
            } catch (\Throwable $e) {
                $parts[] = 'CHUNKS: unavailable';
            }
            $perDocSnippets[] = implode("\n", $parts);
        }

        $scopeLabel = 'Scope: ' . ($scopeType === 'single'
            ? ('Single document = ' . implode(', ', $docLabels))
            : ('All trained documents (' . count($docLabels) . ') = ' . implode('; ', array_slice($docLabels, 0, 8))));

        $contextBlock = '';
        if ($perDocSnippets !== []) {
            $contextBlock = implode("\n\n" . str_repeat('=', 40) . "\n\n", $perDocSnippets);
        }
        if (mb_strlen($contextBlock) > 18000) {
            $contextBlock = mb_substr($contextBlock, 0, 17950) . "\n[TRUNCATED to fit context window]";
        }

        $fallbackQuestions = [
            $scopeType === 'single'
                ? 'Summarize the key points of this document'
                : 'Summarize all key information from the knowledge base',
            'What are the most important talking points?',
            'List any objections and suggested rebuttals',
            'What are the key pricing details?',
            'Provide qualifying questions for leads',
        ];

        $promptSummary = $scopeLabel . "\n\n"
            . "Generate {$count} starter questions a sales agent can ask this RAG assistant.\n"
            . "CRITICAL RULES — VIOLATE THESE AND YOUR OUTPUT IS USELESS:\n"
            . "1. EVERY QUESTION MUST BE DIRECTLY ANSWERABLE using ONLY the quoted document context snippets and detected key topics shown below.\n"
            . "   ✅ GOOD: 'What are the key components of the Revenue Recovery Plan?' (phrase 'Revenue Recovery Plan' appears in key topics / title).\n"
            . "   ❌ BAD:  'Are there any training materials available?' (training materials are NEVER mentioned in this doc → DO NOT INVENT).\n"
            . "   ❌ BAD:  'Where can I find more resources?' (this doc IS the resource; asking for 'more' is external).\n"
            . "   ❌ BAD:  Meta-comparison questions like 'does doc A cover topic B?' or 'what info is missing?'.\n"
            . "2. Use the DETECTED KEY TOPICS list and the exact phrases you see inside the CHUNK snippets. Questions that match those exact phrases will answer correctly.\n"
            . "3. Mix: 1 broad summary/overview question, 1-2 tactical talking-points/objection questions, 1-2 drill-down questions about specific sections you can SEE in the chunks.\n"
            . "4. 8-28 words each. Natural sales-agent phrasing. No repeats. No meta questions.\n"
            . "5. If you genuinely cannot find enough answerable content, produce the shortest factual 'what is in this document' questions only.\n"
            . "6. Optional randomization seed = {$regenSeed}.\n"
            . "7. Output ONLY a JSON array of strings like [\"q1?\",\"q2?\",\"q3?\"]. No commentary.\n\n"
            . "Document/knowledge base context:\n" . ($contextBlock !== '' ? $contextBlock : '(no documents in scope yet, send 5 generic guided questions)');

        try {
            $openAi = new OpenAiService();
            if (!$openAi->isConfigured()) {
                return $this->response->setJSON([
                    'success' => true,
                    'questions' => array_slice($fallbackQuestions, 0, $count),
                    'scope_type' => $scopeType,
                    'scope_document_id' => $documentId,
                    'doc_labels_used' => $docLabels,
                    'context_chars' => 0,
                    'seed' => $regenSeed,
                    'warn' => 'OpenAI API not configured; showing default fallback questions.',
                ]);
            }
            $questions = $openAi->generateQuestions($promptSummary, $count);
            $finalQuestions = is_array($questions) && $questions !== []
                ? array_values(array_slice($questions, 0, $count))
                : array_slice($fallbackQuestions, 0, $count);
        } catch (\Throwable $e) {
            $finalQuestions = array_slice($fallbackQuestions, 0, $count);
            return $this->response->setJSON([
                'success'   => true,
                'questions' => $finalQuestions,
                'scope_type' => $scopeType,
                'scope_document_id' => $documentId,
                'doc_labels_used' => $docLabels,
                'context_chars' => mb_strlen($contextBlock),
                'seed' => $regenSeed,
                'warn' => $this->friendlyWarn($e, 'AI generation failed'),
            ]);
        }

        return $this->response->setJSON([
            'success' => true,
            'questions' => $finalQuestions,
            'scope_type' => $scopeType,
            'scope_document_id' => $documentId,
            'doc_labels_used' => $docLabels,
            'context_chars' => mb_strlen($contextBlock),
            'seed' => $regenSeed,
        ]);
    }

    public function ask()
    {
        $question = trim((string) $this->request->getPost('question'));
        $documentId = $this->request->getPost('document_id');
        $documentId = $documentId ? (int) $documentId : null;

        if (empty($question)) {
            return $this->response->setJSON(['success' => false, 'error' => 'Question is required']);
        }

        $role = (string) (session()->get('role') ?? '');
        $isAdmin = ($role === 'admin');
        $visibility = $isAdmin ? null : 'agents';

        if ($documentId > 0 && !$isAdmin) {
            $docModel2 = new DocumentModel();
            $scopedDoc = $docModel2->find($documentId);
            if ($scopedDoc === null) {
                return $this->response->setJSON(['success' => false, 'error' => 'Document not found']);
            }
            $dv = strtolower(trim((string) ($scopedDoc['visibility'] ?? 'agents')));
            if ($dv !== 'agents') {
                return $this->response->setJSON(['success' => false, 'error' => 'Document not found']);
            }
        }

        $user = $this->currentUser();
        $userId = $user['user_id'] ?? null;

        $chatHistoryModel = new ChatHistoryModel();
        $userMsgId = null;

        if ($userId) {
            $chatHistoryModel->insert([
                'user_id'      => $userId,
                'document_id'  => $documentId,
                'message_role' => 'user',
                'message_text' => $question,
            ]);
            try {
                $userMsgId = (int) $chatHistoryModel->getInsertID();
            } catch (\Throwable $e) {
                $userMsgId = null;
            }
        }

        try {
            $openAi = new OpenAiService();
            $similarResults = $openAi->searchSimilarDocuments($question, 10, $documentId, $visibility);
        } catch (\Exception $e) {
            $info = $this->classifyApiError($e);
            if ($userMsgId > 0) {
                try { $chatHistoryModel->delete($userMsgId); } catch (\Throwable $e2) {}
            }
            return $this->response->setJSON([
                'success'   => false,
                'error'     => $info['body'],
                'errorInfo' => $info,
            ]);
        }

        $scopedForcedLoaded = false;
        $scopedForcedDocTitle = '';
        if ($documentId > 0) {
            try {
                $dmF = new DocumentModel();
                $dF = $dmF->find($documentId);
                if ($dF !== null && !empty($dF)) {
                    $visOk = $visibility === null || strtolower(trim((string)($dF['visibility'] ?? 'agents'))) === $visibility;
                    if ($visOk) {
                        $scopedForcedDocTitle = trim((string) ($dF['title'] ?? ''));
                        if ($scopedForcedDocTitle === '') $scopedForcedDocTitle = 'Document #' . $documentId;
                        $cmF = new DocumentChunkModel();
                        $forcedChunks = $cmF
                            ->where('document_id', (int)$documentId)
                            ->orderBy('chunk_index', 'ASC')
                            ->limit(35)
                            ->findAll();
                        if (is_array($forcedChunks) && $forcedChunks !== []) {
                            $simKeys = [];
                            foreach ($similarResults as $sr) {
                                if (empty($sr['chunk']['id'])) continue;
                                $kk = (int)($sr['chunk']['id'] ?? 0) . '_' . (int)($sr['chunk']['document_id'] ?? 0) . '_' . (int)($sr['chunk']['chunk_index'] ?? 0);
                                $simKeys[$kk] = true;
                            }
                            $prepend = [];
                            $fDocRow = [
                                'id' => (int)($dF['id'] ?? $documentId),
                                'title' => $scopedForcedDocTitle,
                                'type'  => (string)($dF['type'] ?? 'unknown'),
                                'visibility' => (string)($dF['visibility'] ?? 'agents'),
                            ];
                            $fAdded = 0;
                            foreach ($forcedChunks as $fc) {
                                $fcId = (int)($fc['id'] ?? 0);
                                $fcIdx = (int)($fc['chunk_index'] ?? 0);
                                $kK = $fcId . '_' . (int)$documentId . '_' . $fcIdx;
                                if (isset($simKeys[$kK])) continue;
                                $prepend[] = [
                                    'document' => $fDocRow,
                                    'chunk'    => $fc,
                                    'similarity' => null,
                                    'padded'   => true,
                                    'title_matched' => false,
                                    'scope_forced' => true,
                                ];
                                $fAdded++;
                            }
                            if ($fAdded > 0) {
                                $similarResults = array_merge($prepend, $similarResults);
                                $scopedForcedLoaded = true;
                            }
                        }
                    }
                }
            } catch (\Throwable $eF) {
                log_message('error', 'ask() scoped-forced inject failed: ' . $eF->getMessage());
            }
        }

        $contextParts = [];
        $sources = [];
        $chunkRefs = [];
        $docChunksByTitle = [];
        $docByIdCache = [];
        $chunkCountTotal = 0;
        $paddedCount = 0;
        $similarityCount = 0;
        $titleMatchedCount = 0;
        $scopeForcedCount = 0;
        $contextRawChars = 0;

        foreach ($similarResults as $idx => $result) {
            $doc = $result['document'] ?? null;
            $chunk = $result['chunk'] ?? null;
            if (!$doc || !$chunk) continue;

            $docId = (int) ($doc['id'] ?? 0);
            if ($docId > 0) $docByIdCache[$docId] = $doc;

            $docTitle = trim((string) ($doc['title'] ?? 'Unknown Document'));
            if ($docTitle === '') $docTitle = 'Untitled Document ' . ((int)($doc['id'] ?? 0));
            $chunkIndex = (int) ($chunk['chunk_index'] ?? 0);
            $chunkText = trim((string) ($chunk['content_text'] ?? ''));
            if ($chunkText === '') continue;

            if (!in_array($docTitle, $sources, true)) $sources[] = $docTitle;
            if (!isset($docChunksByTitle[$docTitle])) $docChunksByTitle[$docTitle] = 0;
            $ordinal = ++$docChunksByTitle[$docTitle];
            $sim = $result['similarity'] ?? null;
            $padded = !empty($result['padded']);
            $titleMatched = !empty($result['title_matched']);
            $scopeForced = !empty($result['scope_forced']);
            if ($titleMatched) $titleMatchedCount++;
            elseif ($scopeForced) $scopeForcedCount++;
            elseif ($padded) $paddedCount++;
            else $similarityCount++;
            $chunkCountTotal++;
            $contextRawChars += strlen($chunkText);
            $chunkRefs[] = [
                'doc'   => $docTitle,
                'doc_id'=> $docId,
                'c'     => $chunkIndex,
                'ord'   => $ordinal,
                'sim'   => $sim === null ? null : round((float)$sim, 4),
                'pad'   => $padded,
                'tm'    => $titleMatched,
                'sf'    => $scopeForced,
            ];
            $extraTag = [];
            if ($sim !== null) $extraTag[] = 'score=' . round((float)$sim, 3);
            if ($titleMatched) $extraTag[] = 'title_matched';
            elseif ($scopeForced) $extraTag[] = 'scope_forced';
            elseif ($padded) $extraTag[] = 'fallback_chunk';
            $tag = "[Document: {$docTitle} · Passage {$ordinal}" . ($chunkIndex > 0 ? " · Chunk {$chunkIndex}" : '') . ($extraTag !== [] ? " · " . implode(' · ', $extraTag) : '') . "]";
            $contextParts[] = $tag . "\n" . $chunkText;
        }

        $titleMatchedForcedDocs = [];
        try {
            $normQ = \App\Libraries\OpenAiService::normalizeTitleKey($question);
            if ($normQ !== '') {
                $documentModel = new \App\Models\DocumentModel();
                $candBuilder = $documentModel->builder();
                $candBuilder->select('id, title, visibility, type');
                if ($documentId > 0) {
                    $candBuilder->where('id', (int)$documentId);
                }
                if ($visibility !== null) {
                    $candBuilder->where('visibility', $visibility);
                }
                $candBuilder->whereIn('embedding_status', ['trained', 'processing']);
                $candidates = $candBuilder->get()->getResultArray();
                $forcedInject = [];
                $seenKeys = [];
                foreach ($similarResults as $sr) {
                    if (empty($sr['chunk']['id'])) continue;
                    $k = (int)($sr['chunk']['id'] ?? 0) . '_' . (int)($sr['chunk']['document_id'] ?? 0) . '_' . (int)($sr['chunk']['chunk_index'] ?? 0);
                    $seenKeys[$k] = true;
                }
                foreach ($candidates as $cand) {
                    $candId = (int) ($cand['id'] ?? 0);
                    $candTitle = trim((string) ($cand['title'] ?? ''));
                    if ($candId <= 0 || $candTitle === '') continue;
                    if (!\App\Libraries\OpenAiService::titleMatchesQuestion($question, $candTitle)) continue;
                    $alreadyLoaded = false;
                    foreach (array_keys($docChunksByTitle) as $loadedTitle) {
                        if (\App\Libraries\OpenAiService::titleMatchesQuestion($loadedTitle, $candTitle)) { $alreadyLoaded = true; break; }
                    }
                    if ($alreadyLoaded) continue;
                    $chunkModel = new \App\Models\DocumentChunkModel();
                    $topChunks = $chunkModel
                        ->where('document_id', $candId)
                        ->orderBy('chunk_index', 'ASC')
                        ->limit(25)
                        ->findAll();
                    if ($topChunks === []) continue;
                    $docByIdCache[$candId] = $cand;
                    $titleMatchedForcedDocs[] = $candTitle;
                    $perOrd = $docChunksByTitle[$candTitle] ?? 0;
                    foreach ($topChunks as $tc) {
                        $ct = trim((string) ($tc['content_text'] ?? ''));
                        if ($ct === '') continue;
                        $chunkIdx = (int) ($tc['chunk_index'] ?? 0);
                        $k = (int)($tc['id'] ?? 0) . '_' . $candId . '_' . $chunkIdx;
                        if (isset($seenKeys[$k])) continue;
                        $perOrd++;
                        $docChunksByTitle[$candTitle] = $perOrd;
                        if (!in_array($candTitle, $sources, true)) $sources[] = $candTitle;
                        $tag = "[Document: {$candTitle} · Passage {$perOrd}" . ($chunkIdx > 0 ? " · Chunk {$chunkIdx}" : '') . " · title_match_injected]";
                        $contextParts[] = $tag . "\n" . $ct;
                        $chunkRefs[] = [
                            'doc' => $candTitle,
                            'doc_id' => $candId,
                            'c' => $chunkIdx,
                            'ord' => $perOrd,
                            'sim' => null,
                            'pad' => true,
                            'tm'  => true,
                            'injected' => true,
                        ];
                        $chunkCountTotal++;
                        $contextRawChars += strlen($ct);
                        $titleMatchedCount++;
                        $seenKeys[$k] = true;
                        $forcedInject[] = true;
                    }
                }
            }
        } catch (\Throwable $e) {
            log_message('error', 'ask() title-match meta-inject failed: ' . $e->getMessage());
        }

        $contextHeader = "DOCUMENT CONTEXT BLOCK — {$chunkCountTotal} passage(s) across " . count($sources) . " document(s). Character length of context below: {$contextRawChars}. ";
        if ($scopeForcedCount > 0) $contextHeader .= "{$scopeForcedCount} passage(s) were loaded directly from the user-selected scoped document '{$scopedForcedDocTitle}' (semantic similarity ignored — user chose to search within this document so all its content is authoritative). ";
        if ($titleMatchedCount > 0) $contextHeader .= "{$titleMatchedCount} passage(s) were matched by explicit document title mentioned in the user question and loaded directly from the document index (semantic similarity ignored). ";
        if ($paddedCount > 0) $contextHeader .= "Broad-question fallback was used because the query asked for a summary/overview; {$paddedCount} passages were pulled directly from trained documents to guarantee context coverage. ";
        else if ($similarityCount > 0) $contextHeader .= "Passages ordered by semantic similarity vs the user question. ";
        $contextHeader .= "Use ONLY the information inside DOCUMENT CONTEXT BLOCK to answer the user question. If the answer is not present, SAY SO CLEARLY rather than inventing it — BUT prefer answering with a related truthful summary USING THE MATERIAL YOU DO HAVE in the context block rather than immediately saying 'answer not present'. IMPORTANT RULE: If the user's question explicitly references a specific document by name (title), that specific document IS available for you to answer from — do NOT claim 'Document X does not contain information about Document Y' or similar meta-statements. Instead, answer using the passages from the named document(s) in the context block, or if the specific detail genuinely is missing even after reading that document's passages, then say 'the detail you asked for is not written in the currently trained version of [document name]' and then summarize what the document DOES say on the broader topic. Always cite the document title(s) the answer came from at the end of your response.";
        $context = $contextHeader . "\n\n" . implode("\n\n", $contextParts) . "\n\n[END DOCUMENT CONTEXT BLOCK]";

        if (count($contextParts) === 0) {
            $context = "DOCUMENT CONTEXT BLOCK — EMPTY. No trained document passages matched the scope requested by the user. Tell the user politely that no matching documents were found in the knowledge base, and suggest: (1) Train documents first via Admin > Documents > Train All Pending, (2) Confirm the selected scoped document under 'Search within' is correct for their question, (3) Ask a question using keywords that appear in a specific trained document. Do NOT attempt to answer the user's factual question from outside this context block.";
        }

        $messages = [];
        $sysPersona = 'You are a knowledgeable AI assistant for outbound sales agents working inside Caller Suite. Answer strictly using the information provided in the DOCUMENT CONTEXT BLOCK. Never invent facts, prices, policies, or talking points. If the answer is missing from the context block, first try to ANSWER WITH THE NEAREST RELATED INFORMATION THAT IS in the context block (and note that the exact phrasing asked was not covered), and only if truly zero related information exists then say that the answer is not in the knowledge base and suggest training documents or narrowing the scope. Never meta-compare documents ("X does not talk about Y")—if the question names a document, that document is authoritative; if the specific detail is missing even in that document\'s passages, specifically say the named document does not cover that detail in its trained version AND THEN summarize what the document DOES say on adjacent topics. When the question asks for a summary, talking points, objections/rebuttals, pricing or qualifying questions, synthesize those directly from the provided context passages, then cite which document(s) you used.';
        if ($scopedForcedLoaded && $scopedForcedDocTitle !== '') {
            $sysPersona .= ' CRITICAL SCOPE RULE: This question is scoped by the user to the single document "' . $scopedForcedDocTitle . '". You MUST attempt an answer using ONLY "' . $scopedForcedDocTitle . '" passages from the context block. Do not claim "this document does not contain information about X" unless after carefully reading every loaded passage of "' . $scopedForcedDocTitle . '" you truly find ZERO related content. If the user phrased their question poorly (e.g. asked for "training materials" when the document IS the training material), respond with a truthful synthesized answer using what IS in the document and explain how the document itself covers that topic. Never give up on a scoped question without at least producing a reasonable synthesis of the scoped document content that is closest to what the user asked about.';
        }
        if ($titleMatchedForcedDocs !== []) {
            $sysPersona .= ' IMPORTANT: The user question explicitly references the document(s): ' . implode(', ', array_map(static function ($t) { return "'{$t}'"; }, $titleMatchedForcedDocs)) . '. You MUST answer using those document(s) first. Do not suggest they contain no information about themselves. If you cannot find the exact answer inside the loaded passages of ' . implode(' / ', $titleMatchedForcedDocs) . ', respond with a truthful narrow statement of what IS written in those passages on the topic, and note that the specific detail was not covered in those passages.';
        }
        $messages[] = [
            'role'    => 'system',
            'content' => $sysPersona,
        ];

        if ($chunkCountTotal > 0 || true) {
            $messages[] = [
                'role'    => 'system',
                'content' => $context,
            ];
        }

        if ($userId) {
            $recentHistory = $chatHistoryModel
                ->where('user_id', $userId)
                ->orderBy('id', 'DESC')
                ->limit(10)
                ->findAll();

            $recentHistory = array_reverse($recentHistory);
            foreach ($recentHistory as $row) {
                $r = $row['message_role'] ?? 'user';
                $text = $row['message_text'] ?? '';
                if (in_array($r, ['user', 'assistant'], true) && $text !== '') {
                    $messages[] = [
                        'role'    => $r,
                        'content' => $text,
                    ];
                }
            }
        }

        $scopeNote = '';
        if ($documentId > 0) {
            try {
                $dm = new DocumentModel();
                $d = $dm->find($documentId);
                if ($d) {
                    $t = trim((string) ($d['title'] ?? ''));
                    if ($t !== '') $scopeNote = "\n\n[Search Scope: user restricted this question to the single document titled '{$t}'. Use only passages from '{$t}' in your answer.]";
                }
            } catch (\Throwable $e) { $scopeNote = ''; }
        }

        $messages[] = [
            'role'    => 'user',
            'content' => "User question:\n" . $question . $scopeNote,
        ];

        try {
            $openAi2 = new OpenAiService();
            $answer = $openAi2->chatCompletions($messages, 0.35);
        } catch (\Exception $e) {
            $info = $this->classifyApiError($e);
            return $this->response->setJSON([
                'success'   => false,
                'error'     => $info['body'],
                'errorInfo' => $info,
            ]);
        }

        if ($userId) {
            $chatHistoryModel->insert([
                'user_id'      => $userId,
                'document_id'  => $documentId,
                'message_role' => 'assistant',
                'message_text' => $answer,
            ]);
        }

        $followUps = [];
        try {
            $openAi3 = new OpenAiService();
            if ($openAi3->isConfigured()) {
                $refsForFollows = [];
                foreach (array_slice($chunkRefs, 0, 10) as $cr) {
                    $t = (string) ($cr['doc_title'] ?? ($cr['document']['title'] ?? 'Document'));
                    $s = isset($cr['score']) ? (float) $cr['score'] : null;
                    $idx = (int) ($cr['chunk_index'] ?? 0);
                    $refsForFollows[] = $t . ($idx > 0 ? ' #' . $idx : '') . ($s !== null ? ' score=' . round($s, 3) : '');
                }
                if ($refsForFollows === [] && $sources !== []) {
                    $refsForFollows = $sources;
                }
                $followUps = $openAi3->generateFollowUpQuestions($question, $answer, $refsForFollows, 3);
            }
        } catch (\Throwable $e) {
            $followUps = [];
        }

        return $this->response->setJSON([
            'success'    => true,
            'answer'     => $answer,
            'sources'    => $sources,
            'follow_ups' => array_values($followUps),
            'debug'      => [
                'chunks'       => $chunkCountTotal,
                'chunks_sim'   => $similarityCount,
                'chunks_pad'   => $paddedCount,
                'docs'         => count($sources),
                'context_chars'=> $contextRawChars,
                'document_id'  => $documentId,
                'refs'         => array_slice($chunkRefs, 0, 20),
            ],
        ]);
    }

    public function initiateLeadCall()
    {
        $user = $this->currentUser();
        if ($user === null || empty($user['user_id'])) {
            return $this->response->setStatusCode(401)->setJSON(['ok' => false, 'error' => 'Not authenticated.']);
        }

        $leadId  = (int) ($this->request->getPost('lead_id') ?: 0);
        $phone   = trim((string) ($this->request->getPost('phone') ?: ''));
        $name    = trim((string) ($this->request->getPost('name') ?: ''));
        $email   = trim((string) ($this->request->getPost('email') ?: ''));
        $company = trim((string) ($this->request->getPost('company') ?: ''));

        if ($phone === '') {
            return $this->response->setJSON(['ok' => false, 'error' => 'Phone number is required.']);
        }

        try {
            $leadModel = new LeadModel();
            $lead = $leadId > 0 ? $leadModel->find($leadId) : null;
            if ($lead) {
                if ($phone === '') $phone = (string) ($lead['phone'] ?? '');
                if ($name  === '') $name  = (string) ($lead['name'] ?? '');
                if ($email === '') $email = (string) ($lead['email'] ?? '');
                if ($company === '') $company = (string) ($lead['company_name'] ?? '');
            }

            if ($phone === '') {
                return $this->response->setJSON(['ok' => false, 'error' => 'Lead has no phone number.']);
            }

            $svc = new CloudTalkService();
            if (!$svc->isConfigured()) {
                return $this->response->setJSON(['ok' => false, 'error' => 'CloudTalk is not configured yet. Ask an admin to save the API Key ID and Secret in CloudTalk Settings.']);
            }

            $cfg = new CloudTalkConfigModel();
            $agentNumberId    = (int) $cfg->getValue('agent_number_id', 0);
            $outboundCallerId = (string) $cfg->getValue('outbound_caller_id', '');

            $contactId = 0;
            try {
                $contactId = $svc->createOrGetContact([
                    'phone'       => $phone,
                    'name'        => $name,
                    'email'       => $email,
                    'company'     => $company,
                    'external_id' => $leadId > 0 ? (string) $leadId : '',
                ]);
            } catch (Throwable $e) {
                $contactId = 0;
            }

            $opts = [];
            if ($agentNumberId > 0)    $opts['number_id']   = $agentNumberId;
            if ($outboundCallerId !== '') $opts['from_number'] = $outboundCallerId;
            if ($contactId > 0)         $opts['contact_id']  = $contactId;
            if ($leadId > 0)            $opts['external_id'] = (string) $leadId;

            $agentEmail = '';
            $agentId    = (int) ($user['user_id'] ?? 0);
            try {
                if ($leadId > 0) {
                    $assignedTo = (int) ($lead['assigned_to'] ?? 0);
                    if ($assignedTo > 0) {
                        $userModel = new \App\Models\UserModel();
                        $assigned  = $userModel->find($assignedTo);
                        if ($assigned && !empty($assigned['email'])) {
                            $agentEmail = (string) $assigned['email'];
                            $agentId    = (int) ($assigned['id'] ?? $agentId);
                        }
                    }
                }
            } catch (Throwable $e) {
                $agentEmail = '';
            }
            if ($agentEmail === '') {
                $agentEmail = trim((string) ($user['email'] ?? ''));
            }
            if ($agentEmail === '') {
                return $this->response->setJSON([
                    'ok' => false,
                    'error' => 'Agent email is empty. CloudTalk click-to-call requires a valid agent_email. Either assign the lead to a real Agent with an accepted CloudTalk workspace invite (email matches CloudTalk dashboard user), or fill the email field on your own user account.',
                ]);
            }

            $resp = $svc->initiateCall($agentEmail, $phone, $opts);

            $callModel = new LeadCallModel();
            $rawStatus = (string) ($resp['status'] ?? 'initiated');
            $sanitizedStatus = $callModel->sanitizeCallStatus($rawStatus, 'initiated');
            $callId = $callModel->insert([
                'lead_id'              => $leadId > 0 ? $leadId : null,
                'agent_id'             => $agentId > 0 ? $agentId : null,
                'agent_email'          => $agentEmail,
                'cloudtalk_call_id'    => (string) ($resp['cloudtalk_call_id'] ?? ''),
                'cloudtalk_contact_id' => $contactId > 0 ? (string) $contactId : null,
                'direction'            => 'outbound',
                'caller_number'        => $outboundCallerId ?: null,
                'destination_number'   => (string) ($resp['normalized_phone'] ?? $phone),
                'status'               => $sanitizedStatus,
                'started_at'           => date('Y-m-d H:i:s'),
                'raw_json'             => json_encode($resp, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
            ]);

            return $this->response->setJSON([
                'ok'                   => true,
                'cloudtalk_call_id'    => (string) ($resp['cloudtalk_call_id'] ?? ''),
                'local_call_id'        => (int) $callId,
                'status'               => (string) ($resp['status'] ?? 'initiated'),
                'agent_email_used'     => $agentEmail,
                'normalized_phone'     => (string) ($resp['normalized_phone'] ?? $phone),
                'tried_variants'       => $resp['tried_variants'] ?? null,
            ]);
        } catch (Throwable $e) {
            return $this->response->setStatusCode(500)->setJSON(['ok' => false, 'error' => $e->getMessage()]);
        }
    }

    public function askCallTranscript()
    {
        $user = $this->currentUser();
        if ($user === null || empty($user['user_id'])) {
            return $this->response->setStatusCode(401)->setJSON(['success' => false, 'error' => 'Not authenticated.']);
        }

        $callId   = (int) ($this->request->getPost('call_id') ?: 0);
        $question = trim((string) ($this->request->getPost('question') ?: ''));
        $preset   = trim((string) ($this->request->getPost('preset') ?: ''));

        if ($callId <= 0) {
            return $this->response->setJSON(['success' => false, 'error' => 'Call ID is required.']);
        }

        $callModel = new LeadCallModel();
        $call = $callModel->find($callId);
        if (!$call) {
            return $this->response->setJSON(['success' => false, 'error' => 'Call not found.']);
        }

        $role = (string) ($user['role'] ?? '');
        $isAdmin = ($role === 'admin');
        if (!$isAdmin) {
            if ((int) ($call['agent_id'] ?? 0) !== (int) $user['user_id']) {
                return $this->response->setStatusCode(403)->setJSON(['success' => false, 'error' => 'You do not have access to this call transcript.']);
            }
        }

        $transcriptModel = new LeadCallTranscriptModel();
        $transcript = $transcriptModel->findByCallId($callId);
        if (!$transcript || trim((string) ($transcript['transcript_text'] ?? '')) === '') {
            return $this->response->setJSON(['success' => false, 'error' => 'Transcript not available for this call yet.']);
        }
        $transcriptText = (string) $transcript['transcript_text'];

        if ($question === '') {
            switch ($preset) {
                case 'summarize':
                    $question = 'Summarize this call in 5-7 bullet points. Include: customer sentiment, key topics discussed, objections, commitments made, and overall outcome.';
                    break;
                case 'what_happened':
                    $question = 'Explain what happened on this call, chronologically. Who spoke? What was discussed? What was the final outcome?';
                    break;
                case 'next_steps':
                    $question = 'List concrete, actionable next steps each party should take after this call. Include who is responsible and suggested deadlines where inferred.';
                    break;
                case 'sentiment':
                    $question = 'Analyze the customer sentiment on this call: label Overall Sentiment (Positive / Neutral / Negative), give a brief rationale, and note any risk flags or follow-up needs.';
                    break;
                default:
                    return $this->response->setJSON(['success' => false, 'error' => 'Question or preset is required.']);
            }
        }

        $systemPrompt = 'You are a helpful sales intelligence assistant reading a call transcript between an outbound agent and a lead. Answer the user\'s question precisely, referencing specific moments or lines from the transcript whenever it helps. If a question cannot be answered from the transcript, say so clearly rather than guessing.';

        $messages = [
            ['role' => 'system', 'content' => $systemPrompt],
            ['role' => 'user',   'content' => "Call Transcript:\n" . $transcriptText . "\n\nUser question: " . $question],
        ];

        try {
            $openAi = new OpenAiService();
            $answer = $openAi->chatCompletions($messages);
        } catch (Throwable $e) {
            $info = $this->classifyApiError($e);
            return $this->response->setJSON([
                'success'   => false,
                'error'     => $info['body'],
                'errorInfo' => $info,
            ]);
        }

        if ($preset === 'summarize' && !empty($answer)) {
            try {
                $existingSummary = trim((string) ($transcript['ai_summary'] ?? ''));
                if ($existingSummary === '' || $transcript['ai_summary_status'] === 'error') {
                    $transcriptModel->update((int) $transcript['id'], [
                        'ai_summary'        => $answer,
                        'ai_summary_status' => 'done',
                    ]);
                }
            } catch (Throwable $e) {}
        }

        return $this->response->setJSON([
            'success'    => true,
            'answer'     => $answer,
            'preset'     => $preset,
            'question'   => $question,
        ]);
    }

    public function downloadCallRecording($id = null, $ctId = null)
    {
        $recordingModel = new LeadCallRecordingModel();
        $callModel      = new LeadCallModel();
        $user           = $this->currentUser();
        $userId         = (int) ($user['user_id'] ?? 0);
        $role           = (string) ($user['role'] ?? '');

        $recording = null;
        $call = null;
        $source = '';

        $idLocal = (int) $id;
        if ($idLocal > 0) {
            $recording = $recordingModel->find($idLocal);
            if ($recording) {
                $call = $callModel->find((int) ($recording['call_id'] ?? 0));
                $source = 'db_recording_id';
            }
        }
        if (!$call && $ctId !== null && trim((string)$ctId) !== '') {
            $ctIdStr = trim((string)$ctId);
            $call = $callModel->findByCloudTalkCallId($ctIdStr);
            if ($call) {
                $recording = $recordingModel->findByCallId((int) ($call['id'] ?? 0));
                $source = 'cloudtalk_call_id';
            }
        }

        if (!$call) {
            return $this->response->setStatusCode(404)->setJSON(['error' => 'Call not found', 'source' => $source]);
        }
        $agentId = (int) ($call['agent_id'] ?? 0);
        $leadId  = (int) ($call['lead_id'] ?? 0);
        if ($role !== 'admin' && $userId !== 0) {
            if ($agentId !== 0 && $agentId !== $userId) {
                try {
                    $leadModel = new LeadModel();
                    $lead = $leadModel->find($leadId);
                    $assignedTo = (int) ($lead['assigned_to'] ?? 0);
                    if ($assignedTo !== 0 && $assignedTo !== $userId) {
                        return $this->response->setStatusCode(403)->setJSON(['error' => 'Forbidden']);
                    } elseif ($assignedTo === 0) {
                        return $this->response->setStatusCode(403)->setJSON(['error' => 'Forbidden']);
                    }
                } catch (Throwable $e) {
                    return $this->response->setStatusCode(403)->setJSON(['error' => 'Forbidden']);
                }
            }
        }

        $downloadFallback = static function ($targetUrl, $authHeader = null, $timeout = 90) {
            if ($targetUrl === '') return false;
            if (function_exists('set_time_limit')) @set_time_limit(0);
            if (function_exists('ini_set')) @ini_set('max_execution_time', '0');

            header('Content-Type: audio/x-wav');
            header('Content-Transfer-Encoding: binary');
            header('Accept-Ranges: bytes');
            header('Cache-Control: private, max-age=31536000');
            $fname = 'recording_' . date('Ymd_His') . '.wav';
            header('Content-Disposition: inline; filename="' . $fname . '"');
            if (session_id()) session_write_close();
            while (ob_get_level() > 0) @ob_end_clean();

            $ch = curl_init($targetUrl);
            if ($ch === false) {
                http_response_code(500);
                echo 'curl_init_failed';
                return true;
            }
            $headers = [];
            if ($authHeader !== null && $authHeader !== '') $headers[] = $authHeader;
            $headers[] = 'Accept: audio/*, application/octet-stream, */*';
            if (!empty($_SERVER['HTTP_RANGE'])) {
                $headers[] = 'Range: ' . $_SERVER['HTTP_RANGE'];
            }
            curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
            curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
            curl_setopt($ch, CURLOPT_MAXREDIRS, 10);
            curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
            curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Outbound Recording Proxy)');
            curl_setopt(
                $ch,
                CURLOPT_HEADERFUNCTION,
                static function ($ch2, $hdr) use (&$headersOut) {
                    if (!is_array($headersOut)) $headersOut = [];
                    $len = strlen($hdr);
                    $hdrTrim = trim($hdr);
                    if ($hdrTrim === '') return $len;
                    $colon = strpos($hdrTrim, ':');
                    if ($colon !== false) {
                        $k = strtolower(trim(substr($hdrTrim, 0, $colon)));
                        $v = trim(substr($hdrTrim, $colon + 1));
                        $headersOut[$k] = $v;
                        if (in_array($k, ['content-type','content-length','content-disposition','content-range','accept-ranges','last-modified','etag'], true)) {
                            if ($k === 'content-type') {
                                $pass = stripos($v, 'text/html') === false
                                    && stripos($v, 'text/plain') === false
                                    && stripos($v, 'application/json') === false;
                                if ($pass) header($hdrTrim);
                            } else {
                                header($hdrTrim);
                            }
                        }
                    }
                    return $len;
                }
            );
            $httpCode = 0;
            curl_setopt(
                $ch,
                CURLOPT_WRITEFUNCTION,
                static function ($ch2, $bodyPart) use (&$httpCode) {
                    if ($httpCode === 0) {
                        $httpCode = (int) curl_getinfo($ch2, CURLINFO_HTTP_CODE);
                        if ($httpCode >= 400) {
                            return 0;
                        }
                        $ct = (string) curl_getinfo($ch2, CURLINFO_CONTENT_TYPE);
                        if (stripos($ct, 'text/') !== false || stripos($ct, 'application/json') !== false) {
                            return 0;
                        }
                    }
                    echo $bodyPart;
                    flush();
                    return strlen($bodyPart);
                }
            );
            $ok = curl_exec($ch);
            $httpCodeFinal = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $err = curl_error($ch);
            curl_close($ch);
            if ($ok === false) {
                if (!headers_sent()) {
                    http_response_code(502);
                }
                echo ' /* proxy_error: ' . $err . ' */';
            } elseif ($httpCodeFinal >= 400) {
                if (!headers_sent()) {
                    http_response_code((int)$httpCodeFinal);
                }
            }
            return true;
        };

        $storedUrl = $recording ? trim((string) ($recording['recording_url'] ?? '')) : '';
        $storedIsUsable = false;
        if ($storedUrl !== '') {
            $host = (string) parse_url($storedUrl, PHP_URL_HOST);
            $path = (string) parse_url($storedUrl, PHP_URL_PATH);
            $isHtmlShortlink = $host !== '' && preg_match('#/r/(play|download)/#', $path);
            $endsWithAudio = preg_match('#\.(wav|mp3|ogg|m4a|webm)(\?|$)#i', $path);
            $isApiEndpoint = $host !== '' && str_ends_with($host, 'cloudtalk.io') && str_contains($path, '/calls/recording/');
            $storedIsUsable = !$isHtmlShortlink && ($endsWithAudio || $isApiEndpoint);
        }

        try {
            $svc = new CloudTalkService();
            $ctCallId = (string) ($call['cloudtalk_call_id'] ?? '');
            $streamInfo = $ctCallId !== '' ? $svc->getRecordingStreamInfo($ctCallId) : ['url' => '', 'basic_auth' => ''];
            $auth = (string) ($streamInfo['basic_auth'] ?? '');
        } catch (\Throwable $e) {
            $auth = '';
            $streamInfo = ['url' => '', 'basic_auth' => ''];
        }

        if ($storedUrl !== '' && $storedIsUsable) {
            $handled = $downloadFallback($storedUrl, $auth !== '' ? $auth : null, 90);
            if ($handled) return $this->response;
        }

        if (!empty($streamInfo['url'])) {
            $directUrl = (string) $streamInfo['url'];
            $handled = $downloadFallback($directUrl, $auth !== '' ? $auth : null, 120);
            if ($handled) return $this->response;
        }

        if ($storedUrl !== '' && !$storedIsUsable) {
            $handled = $downloadFallback($storedUrl, $auth !== '' ? $auth : null, 90);
            if ($handled) return $this->response;
        }

        return $this->response->setStatusCode(404)->setJSON(['error' => 'Recording not available.']);
    }
}
