<?php

namespace App\Controllers;

use App\Libraries\CloudTalkService;
use App\Libraries\DocumentProcessor;
use App\Libraries\QuizGeneratorService;
use App\Libraries\VapiService;
use App\Libraries\BookingSlotGenerator;
use App\Libraries\BookingMailer;
use App\Models\BookingScheduleModel;
use App\Models\BookingShiftModel;
use App\Models\BookingScheduleExceptionModel;
use App\Models\BookingModel;
use App\Models\BookingSmtpConfigModel;
use App\Models\CategoryModel;
use App\Models\ChatbotConfigModel;
use App\Models\CloudTalkConfigModel;
use App\Models\DocumentChunkModel;
use App\Models\DocumentModel;
use App\Models\LeadCallModel;
use App\Models\LeadCallRecordingModel;
use App\Models\LeadCallTranscriptModel;
use App\Models\LeadCustomFieldModel;
use App\Models\LeadCustomValueModel;
use App\Models\LeadModel;
use App\Models\LeadNoteModel;
use App\Models\LeadStatusModel;
use App\Models\QuizAttemptModel;
use App\Models\QuizAttemptQuestionModel;
use App\Models\QuizSettingModel;
use App\Models\UserModel;
use App\Models\VapiConfigModel;
use App\Models\VapiAssistantBindingModel;
use App\Models\VapiCallLogModel;
use App\Models\VapiCallScoreModel;
use Throwable;

class Admin extends BaseController
{
    protected $categoryModel;
    protected $chatbotConfigModel;
    protected $cloudtalkConfigModel;
    protected $documentModel;
    protected $leadModel;
    protected $leadCallModel;
    protected $leadCallTranscriptModel;
    protected $leadCallRecordingModel;
    protected $leadStatusModel;
    protected $leadCustomFieldModel;
    protected $leadCustomValueModel;
    protected $leadNoteModel;
    protected $quizSettingModel;
    protected $quizAttemptModel;
    protected $quizQuestionModel;
    protected $userModel;
    protected $vapiConfigModel;
    protected $vapiAssistantBindingModel;
    protected $vapiCallLogModel;
    protected $vapiCallScoreModel;
    protected $vapiService;

    public function __construct()
    {
        $this->categoryModel              = new CategoryModel();
        $this->chatbotConfigModel         = new ChatbotConfigModel();
        $this->cloudtalkConfigModel       = new CloudTalkConfigModel();
        $this->documentModel              = new DocumentModel();
        $this->leadModel                  = new LeadModel();
        $this->leadCallModel              = new LeadCallModel();
        $this->leadCallTranscriptModel    = new LeadCallTranscriptModel();
        $this->leadCallRecordingModel     = new LeadCallRecordingModel();
        $this->leadStatusModel            = new LeadStatusModel();
        $this->leadCustomFieldModel       = new LeadCustomFieldModel();
        $this->leadCustomValueModel       = new LeadCustomValueModel();
        $this->leadNoteModel              = new LeadNoteModel();
        $this->quizSettingModel           = new QuizSettingModel();
        $this->quizAttemptModel           = new QuizAttemptModel();
        $this->quizQuestionModel          = new QuizAttemptQuestionModel();
        $this->userModel                  = new UserModel();
        $this->vapiConfigModel            = new VapiConfigModel();
        $this->vapiAssistantBindingModel  = new VapiAssistantBindingModel();
        $this->vapiCallLogModel           = new VapiCallLogModel();
        $this->vapiCallScoreModel         = new VapiCallScoreModel();
        $this->vapiService                = new VapiService();
    }

    public function dashboard()
    {
        $totalLeads     = $this->leadModel->countAll();
        $totalDocuments = $this->documentModel->countAll();
        $totalAgents    = $this->userModel->where('role', 'agent')->countAllResults();
        $totalStatuses  = $this->leadStatusModel->countAll();

        $recentLeads = $this->leadModel
            ->select('leads.*, lead_statuses.name as status_name, lead_statuses.color as status_color')
            ->join('lead_statuses', 'lead_statuses.id = leads.status_id', 'left')
            ->orderBy('leads.created_at', 'DESC')
            ->limit(5)
            ->get()
            ->getResultArray();

        $data = [
            'totalLeads'     => $totalLeads,
            'totalDocuments' => $totalDocuments,
            'totalAgents'    => $totalAgents,
            'totalStatuses'  => $totalStatuses,
            'recentLeads'    => $recentLeads,
        ];

        return view('layout/app', [
            'title'     => 'Dashboard',
            'pageTitle' => 'Dashboard',
            'content'   => view('admin/dashboard', $data),
        ]);
    }

    public function categories()
    {
        if ($this->request->getMethod(true) === 'POST') {
            $rules = [
                'name'        => 'required',
                'sort_order'  => 'permit_empty|integer',
            ];

            if (!$this->validate($rules)) {
                session()->setFlashdata('error', 'Please fix the validation errors.');
                return redirect()->to('/admin/categories')->withInput()->with('errors', $this->validator->getErrors());
            }

            $this->categoryModel->insert([
                'name'        => $this->request->getPost('name'),
                'description' => $this->request->getPost('description'),
                'sort_order'  => $this->request->getPost('sort_order') ?? 0,
            ]);

            session()->setFlashdata('success', 'Category added successfully.');
            return redirect()->to('/admin/categories');
        }

        $categories = $this->categoryModel->orderBy('sort_order', 'ASC')->findAll();

        return view('layout/app', [
            'title'     => 'Categories',
            'pageTitle' => 'Categories',
            'content'   => view('admin/categories', ['categories' => $categories]),
        ]);
    }

    public function deleteCategory($id)
    {
        $this->categoryModel->delete($id);
        session()->setFlashdata('success', 'Category deleted successfully.');
        return redirect()->to('/admin/categories');
    }

    public function updateCategory($id)
    {
        $id = (int) $id;
        $category = $this->categoryModel->find($id);
        if (!$category) {
            session()->setFlashdata('error', 'Category not found.');
            return redirect()->to(site_url('/admin/categories'));
        }

        if ($this->request->getMethod(true) !== 'POST') {
            return redirect()->to(site_url('/admin/categories'));
        }

        $rules = [
            'name'        => 'required',
            'sort_order'  => 'permit_empty|integer',
        ];

        if (!$this->validate($rules)) {
            session()->setFlashdata('error', 'Please fix the validation errors.');
            return redirect()->to(site_url('/admin/categories'))->withInput();
        }

        $this->categoryModel->update($id, [
            'name'        => $this->request->getPost('name'),
            'description' => $this->request->getPost('description'),
            'sort_order'  => $this->request->getPost('sort_order') ?? 0,
        ]);

        session()->setFlashdata('success', 'Category updated successfully.');
        return redirect()->to(site_url('/admin/categories'));
    }

    public function documents()
    {
        $categories = $this->categoryModel->orderBy('sort_order', 'ASC')->findAll();
        $documents  = $this->documentModel->getWithCategory();

        $chunkCountById = [];
        $pendingCount = 0;
        try {
            $chunkModel = new DocumentChunkModel();
            $rows = $chunkModel
                ->select('document_id, COUNT(*) as cnt')
                ->groupBy('document_id')
                ->get()
                ->getResultArray();
            foreach ($rows as $r) {
                $chunkCountById[(int) $r['document_id']] = (int) $r['cnt'];
            }
            $pendingCount = (int) $this->documentModel
                ->whereIn('embedding_status', ['pending', 'processing', 'error'])
                ->countAllResults();
        } catch (Throwable $e) {
            log_message('error', 'documents() chunk stats failed: ' . $e->getMessage());
        }

        foreach ($documents as &$d) {
            $id = (int) ($d['id'] ?? 0);
            $contentText = (string) ($d['content_text'] ?? '');
            $title = trim((string) ($d['title'] ?? ''));
            $desc  = trim((string) ($d['description'] ?? ''));
            $type  = strtolower(trim((string) ($d['type'] ?? '')));
            $url   = trim((string) ($d['url'] ?? ''));
            $titleHuman = (string) preg_replace('/[\s\-_.]+/u', ' ', $title);
            $titleHuman = trim((string) preg_replace('/\s{2,}/u', ' ', $titleHuman));
            $prefixLines = [];
            if ($titleHuman !== '') {
                $prefixLines[] = 'Document title: ' . $titleHuman;
                if ($titleHuman !== $title && $title !== '') {
                    $prefixLines[] = 'Document filename (original): ' . $title;
                }
            } elseif ($title !== '') {
                $prefixLines[] = 'Document title: ' . $title;
            }
            if ($desc  !== '') $prefixLines[] = 'Summary: ' . $desc;
            if ($type  !== '') $prefixLines[] = 'Type: ' . $type;
            if ($url   !== '') $prefixLines[] = 'Source URL: ' . $url;
            $prefixEstimate = mb_strlen(count($prefixLines) ? implode("\n", $prefixLines) . "\n\n" : '');
            $d['chunk_count'] = $chunkCountById[$id] ?? 0;
            $d['content_length'] = mb_strlen($contentText);
            $d['prefix_estimate_chars'] = $prefixEstimate;
            $bodyOnly = max(0, $d['content_length'] - $prefixEstimate);
            $d['body_extracted_chars'] = $bodyOnly;
            $d['word_count_est'] = $contentText === '' ? 0 : (int) preg_match_all('/\S+/u', $contentText);
            $d['body_word_estimate'] = $bodyOnly < 50 ? 0 : max(0, (int) preg_match_all('/\S+/u', $contentText) - (int) preg_match_all('/\S+/u', implode(' ', $prefixLines)));
            $d['chunk_estimate_low'] = ($d['chunk_count'] <= 1 && $bodyOnly > 500) || ($bodyOnly < 50 && ($d['file_size'] ?? 0) > 20000) || ($bodyOnly < 50 && in_array($type, ['pdf', 'docx', 'doc', 'txt', 'csv', 'link', 'url'], true) && ($title !== ''));
        }
        unset($d);

        return view('layout/app', [
            'title'        => 'Documents',
            'pageTitle'    => 'Documents',
            'content'      => view('admin/documents', [
                'categories'   => $categories,
                'documents'    => $documents,
                'pendingCount' => $pendingCount,
            ]),
        ]);
    }

    public function trainDocument($id)
    {
        $id = (int) $id;
        $doc = $this->documentModel->find($id);
        if ($doc === null) {
            session()->setFlashdata('error', 'Document not found.');
            return redirect()->to(site_url('/admin/documents'));
        }
        try {
            $processor = new DocumentProcessor();
            $result = $processor->processDocumentId($id, true);
            if ($result['ok']) {
                session()->setFlashdata('success', $result['message'] ?? 'Trained successfully.');
            } else {
                session()->setFlashdata('error', 'Training failed: ' . ($result['message'] ?? 'Unknown error'));
            }
        } catch (Throwable $e) {
            log_message('error', 'trainDocument(' . $id . '): ' . $e->getMessage());
            session()->setFlashdata('error', 'Training failed: ' . $e->getMessage());
        }
        return redirect()->to(site_url('/admin/documents'));
    }

    public function trainAllPending()
    {
        $pendingDocs = $this->documentModel
            ->whereIn('embedding_status', ['pending', 'error'])
            ->orderBy('id', 'ASC')
            ->findAll();
        $total = count($pendingDocs);
        $succeeded = 0;
        $failed = 0;
        $lastMsg = '';
        $processor = new DocumentProcessor();
        foreach ($pendingDocs as $d) {
            $res = $processor->processDocumentId((int) $d['id'], true);
            if (!empty($res['ok'])) {
                $succeeded++;
            } else {
                $failed++;
                if ($lastMsg === '') $lastMsg = ($res['message'] ?? 'unknown');
            }
        }
        if ($total === 0) {
            session()->setFlashdata('info', 'No pending documents to train.');
        } else {
            $msg = "Trained {$succeeded}/{$total} documents successfully.";
            if ($failed > 0) {
                $msg .= " {$failed} failed (last: {$lastMsg})";
                session()->setFlashdata('warning', $msg);
            } else {
                session()->setFlashdata('success', $msg);
            }
        }
        return redirect()->to(site_url('/admin/documents'));
    }

    public function uploadDocument()
    {
        $categories = $this->categoryModel->orderBy('sort_order', 'ASC')->findAll();

        return view('layout/app', [
            'title'     => 'Upload Document',
            'pageTitle' => 'Upload Document',
            'content'   => view('admin/document_upload', ['categories' => $categories]),
        ]);
    }

    public function storeDocument()
    {
        $docType   = strtolower(trim((string) $this->request->getPost('type') ?? ''));
        $urlInput  = trim((string) $this->request->getPost('url') ?? '');
        $urlTypes  = ['image', 'video', 'link', 'url'];
        $fileTypes = ['pdf', 'docx', 'txt', 'csv', 'png', 'jpg', 'jpeg', 'gif', 'mp4', 'mov', 'webm', 'webp', 'doc'];
        $isUrlMode = ($urlInput !== '') || in_array($docType, $urlTypes, true);

        $visibility = strtolower(trim((string) $this->request->getPost('visibility') ?? 'agents'));
        if (!in_array($visibility, ['agents', 'private'], true)) {
            $visibility = 'agents';
        }

        $rules = [
            'title'       => 'required',
            'category_id' => 'required|integer',
            'visibility'  => 'permit_empty|in_list[agents,private]',
        ];

        if ($isUrlMode) {
            $rules['url'] = 'required|valid_url_strict';
        } else {
            $rules['document'] = 'uploaded[document]|max_size[document,10240]';
        }

        if (!$this->validate($rules)) {
            session()->setFlashdata('error', 'Please fix the validation errors.');
            return redirect()->to(site_url('/admin/documents/upload'))->withInput()->with('errors', $this->validator->getErrors());
        }

        $redirectBack = site_url('/admin/documents/upload');
        $redirectDone = site_url('/admin/documents');

        $type      = $docType;
        $filePath  = null;
        $fileSize  = 0;
        $mimeType  = null;
        $url       = null;
        $contentText = null;

        if ($isUrlMode) {
            $url      = $urlInput;
            if (!in_array($docType, $urlTypes, true)) {
                $host = parse_url($url, PHP_URL_HOST) ?? '';
                $pathExt = strtolower(pathinfo((string) parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION));
                $videoExts = ['mp4', 'mov', 'webm', 'avi', 'mkv'];
                $imageExts = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico'];
                if (in_array($pathExt, $videoExts, true) || stripos($host, 'youtube') !== false || stripos($host, 'vimeo') !== false || stripos($host, 'loom') !== false) {
                    $type = 'video';
                } elseif (in_array($pathExt, $imageExts, true) || stripos($host, 'imgur') !== false || stripos($host, 'unsplash') !== false || stripos($host, 'cloudinary') !== false) {
                    $type = 'image';
                } else {
                    $type = 'link';
                }
            }
            $mimeMap = [
                'image' => 'image/*',
                'video' => 'video/*',
                'link'  => 'text/uri-list',
                'url'   => 'text/uri-list',
            ];
            $mimeType = $mimeMap[$type] ?? 'text/uri-list';
        } else {
            $file = $this->request->getFile('document');
            if ($file === null) {
                $file = $this->request->getFile('file');
            }

            if ($file === null || !$file->isValid() || $file->hasMoved() || $file->getError() !== UPLOAD_ERR_OK) {
                $errCode = $file ? $file->getError() : UPLOAD_ERR_NO_FILE;
                $errMessages = [
                    UPLOAD_ERR_INI_SIZE   => 'The uploaded file exceeds the server upload_max_filesize directive.',
                    UPLOAD_ERR_FORM_SIZE  => 'The uploaded file exceeds the MAX_FILE_SIZE directive specified in the HTML form.',
                    UPLOAD_ERR_PARTIAL    => 'The uploaded file was only partially uploaded.',
                    UPLOAD_ERR_NO_FILE    => 'No file was uploaded.',
                    UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder on the server.',
                    UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk (check writable/uploads permissions).',
                    UPLOAD_ERR_EXTENSION  => 'A PHP extension stopped the file upload.',
                ];
                $msg = $errMessages[$errCode] ?? 'Failed to upload document.';
                session()->setFlashdata('error', $msg);
                return redirect()->to($redirectBack);
            }

            $uploadDir = WRITEPATH . 'uploads';
            if (!is_dir($uploadDir)) {
                @mkdir($uploadDir, 0755, true);
            }
            if (!is_writable($uploadDir)) {
                session()->setFlashdata('error', 'Uploads directory is not writable: ' . $uploadDir);
                return redirect()->to($redirectBack);
            }

            $originalName = $file->getClientName();
            $fileExt      = strtolower($file->getClientExtension() ?: (string) pathinfo($originalName, PATHINFO_EXTENSION));
            $fileSize     = $file->getSize() ?: 0;
            try {
                $mimeType = $file->getMimeType();
            } catch (\Throwable $e) {
                log_message('error', 'getMimeType() failed: ' . $e->getMessage());
                $mimeType = false;
            }
            $mimeMap = [
                'pdf'  => 'application/pdf',
                'doc'  => 'application/msword',
                'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
                'txt'  => 'text/plain',
                'csv'  => 'text/csv',
                'png'  => 'image/png',
                'jpg'  => 'image/jpeg',
                'jpeg' => 'image/jpeg',
                'gif'  => 'image/gif',
                'mp4'  => 'video/mp4',
                'mov'  => 'video/quicktime',
                'webm' => 'video/webm',
                'webp' => 'image/webp',
            ];
            if ($mimeType === false || $mimeType === null || $mimeType === '') {
                $mimeType = $mimeMap[$fileExt] ?? 'application/octet-stream';
            }
            if ($type === '' || !in_array($type, $fileTypes, true)) {
                $type = in_array($fileExt, $fileTypes, true) ? $fileExt : ($fileExt ?: 'file');
            }

            $fileName = $file->getRandomName();
            try {
                $moved = $file->move($uploadDir, $fileName);
            } catch (\Throwable $e) {
                log_message('error', 'Document upload move failed: ' . $e->getMessage());
                $moved = false;
            }

            if ($moved === false) {
                session()->setFlashdata('error', 'Failed to store uploaded file on disk.');
                return redirect()->to($redirectBack);
            }

            $filePath = $fileName;

            $finalPath = $uploadDir . DIRECTORY_SEPARATOR . $fileName;
            if ($fileSize === 0 && file_exists($finalPath)) {
                $fileSize = filesize($finalPath) ?: 0;
            }
        }

        $this->documentModel->insert([
            'category_id'      => $this->request->getPost('category_id'),
            'title'            => $this->request->getPost('title'),
            'description'      => $this->request->getPost('description'),
            'type'             => $type,
            'file_path'        => $filePath,
            'file_size'        => $fileSize ?: 0,
            'mime_type'        => $mimeType,
            'url'              => $url,
            'content_text'     => $contentText,
            'embedding_status' => $isUrlMode ? 'pending' : 'pending',
            'visibility'       => $visibility,
        ]);

        session()->setFlashdata('success', 'Document uploaded successfully.');
        return redirect()->to($redirectDone);
    }

    public function toggleVisibility($id, $target)
    {
        $id = (int) $id;
        $target = strtolower(trim((string) $target));
        if (!in_array($target, ['agents', 'private'], true)) {
            $target = 'agents';
        }

        $doc = $this->documentModel->find($id);
        if ($doc === null) {
            session()->setFlashdata('error', 'Document not found.');
            return redirect()->to(site_url('/admin/documents'));
        }

        $this->documentModel->update($id, ['visibility' => $target]);

        $label = $target === 'private' ? 'Admin Only (Private)' : 'Visible to Agents & Admin';
        session()->setFlashdata('success', 'Document visibility updated to: ' . $label);
        return redirect()->to(site_url('/admin/documents'));
    }

    public function deleteDocument($id)
    {
        $doc = $this->documentModel->find($id);
        if ($doc && !empty($doc['file_path']) && file_exists(WRITEPATH . 'uploads/' . $doc['file_path'])) {
            unlink(WRITEPATH . 'uploads/' . $doc['file_path']);
        }
        $this->documentModel->delete($id);
        session()->setFlashdata('success', 'Document deleted successfully.');
        return redirect()->to(site_url('/admin/documents'));
    }

    public function chatbotConfig()
    {
        if ($this->request->getMethod(true) === 'POST') {
            $openaiApiKey = trim((string) $this->request->getPost('openai_api_key'));
            $systemPrompt = trim((string) $this->request->getPost('system_prompt'));
            $modelName    = trim((string) ($this->request->getPost('model_name') ?: 'gpt-4o-mini'));
            $temperature  = (string) ($this->request->getPost('temperature') ?? '0.7');
            $embeddingModel = 'text-embedding-3-small';

            $this->chatbotConfigModel->setValue('openai_api_key', $openaiApiKey);
            $this->chatbotConfigModel->setValue('system_prompt', $systemPrompt);
            $this->chatbotConfigModel->setValue('model_name', $modelName);
            $this->chatbotConfigModel->setValue('temperature', $temperature);
            $this->chatbotConfigModel->setValue('openai_chat_model', $modelName);
            $this->chatbotConfigModel->setValue('openai_embedding_model', $embeddingModel);

            session()->setFlashdata('success', 'Chatbot configuration saved.');
            return redirect()->to(site_url('/admin/chatbot-config'));
        }

        $config = $this->chatbotConfigModel->getAll();
        if (empty($config['openai_chat_model']) && !empty($config['model_name'])) {
            $config['openai_chat_model'] = $config['model_name'];
        }
        if (empty($config['model_name']) && !empty($config['openai_chat_model'])) {
            $config['model_name'] = $config['openai_chat_model'];
        }
        $config['openai_embedding_model'] = $config['openai_embedding_model'] ?? 'text-embedding-3-small';

        $validationStatus = null;
        $apiKey = (string) ($config['openai_api_key'] ?? '');
        if ($apiKey !== '') {
            try {
                $openAi = new \App\Libraries\OpenAiService();
                $status = $openAi->testKey();
                $validationStatus = [
                    'ok'      => (bool) $status['ok'],
                    'message' => (string) $status['message'],
                    'models'  => $status['models'] ?? null,
                    'used_model_ok' => $status['used_model_ok'] ?? false,
                    'used_model'    => $status['used_model'] ?? '',
                ];
            } catch (\Throwable $e) {
                $msg = $e->getMessage();
                $validationStatus = ['ok' => false, 'message' => $msg === '' ? 'Unknown error while testing the API key.' : $msg];
            }
        }

        return view('layout/app', [
            'title'     => 'Chatbot Config',
            'pageTitle' => 'Chatbot Configuration',
            'content'   => view('admin/chatbot_config', [
                'config'           => $config,
                'validationStatus' => $validationStatus,
            ]),
        ]);
    }

    public function testOpenai()
    {
        $apiKey        = trim((string) $this->request->getPost('openai_api_key'));
        $chatModel     = trim((string) ($this->request->getPost('model_name') ?: 'gpt-4o-mini'));
        $fromSaved     = (string) ($this->request->getPost('use_saved') ?: '0') === '1';

        if ($fromSaved || $apiKey === '') {
            try {
                $stored = (string) $this->chatbotConfigModel->getValue('openai_api_key', '');
                if ($stored !== '') $apiKey = $stored;
                $m = (string) $this->chatbotConfigModel->getValue('openai_chat_model', '');
                $n = (string) $this->chatbotConfigModel->getValue('model_name', '');
                if ($n !== '')       $chatModel = $n;
                elseif ($m !== '')   $chatModel = $m;
            } catch (\Throwable $e) {
                return $this->response->setJSON([
                    'ok' => false,
                    'error'   => 'Could not read saved Chatbot configuration.',
                    'message' => 'Could not read saved Chatbot configuration.',
                ]);
            }
        }

        if ($apiKey === '') {
            return $this->response->setJSON([
                'ok' => false,
                'error'   => 'No API key was provided or saved yet. Paste your key into the field and click Save Configuration, or click Test Connection to validate the currently-pasted key.',
                'message' => 'No API key was provided or saved yet. Paste your key into the field and click Save Configuration, or click Test Connection to validate the currently-pasted key.',
            ]);
        }
        if ($chatModel === '') $chatModel = 'gpt-4o-mini';

        try {
            $openAi = new \App\Libraries\OpenAiService();
            $status = $openAi->testKey($apiKey, $chatModel);
            $ok = (bool) $status['ok'];
            $msg = (string) $status['message'];
            return $this->response->setJSON([
                'ok'             => $ok,
                'message'        => $msg,
                'error'          => $ok ? null : $msg,
                'models'         => $status['models'] ?? null,
                'used_model_ok'  => (bool) ($status['used_model_ok'] ?? false),
                'used_model'     => (string) ($status['used_model'] ?? $chatModel),
            ]);
        } catch (\Throwable $e) {
            $msg = $e->getMessage();
            if ($msg === '' || stripos($msg, 'api key is not configured') !== false) {
                $msg = 'Could not initialize OpenAI test client.';
            }
            return $this->response->setJSON([
                'ok'      => false,
                'error'   => $msg,
                'message' => $msg,
            ]);
        }
    }

    public function cloudtalkSettings()
    {
        if ($this->request->getMethod(true) === 'POST') {
            $this->cloudtalkConfigModel->setValue('api_key_id',            trim((string) $this->request->getPost('api_key_id')));
            $this->cloudtalkConfigModel->setValue('api_key_secret',        trim((string) $this->request->getPost('api_key_secret')));
            $this->cloudtalkConfigModel->setValue('webhook_signing_secret',trim((string) $this->request->getPost('webhook_signing_secret')));

            $agentNumberIdRaw = trim((string) $this->request->getPost('agent_number_id'));
            $agentNumberIdInt = ($agentNumberIdRaw === '' || !ctype_digit($agentNumberIdRaw)) ? 0 : (int)$agentNumberIdRaw;
            $this->cloudtalkConfigModel->setValue('agent_number_id',       (string)$agentNumberIdInt);

            $this->cloudtalkConfigModel->setValue('outbound_caller_id',    trim((string) $this->request->getPost('outbound_caller_id')));
            $this->cloudtalkConfigModel->setValue('enable_transcription',  (string) ($this->request->getPost('enable_transcription') === '1' ? '1' : '0'));
            $this->cloudtalkConfigModel->setValue('enable_recording',      (string) ($this->request->getPost('enable_recording')      === '1' ? '1' : '0'));
            $this->cloudtalkConfigModel->setValue('auto_summarize',        (string) ($this->request->getPost('auto_summarize')        === '1' ? '1' : '0'));

            session()->setFlashdata('success', 'CloudTalk settings saved.');
            return redirect()->to(site_url('/admin/cloudtalk-settings'));
        }

        $config = $this->cloudtalkConfigModel->getAll();
        $numbers = [];
        $connectionStatus = null;
        $listNumbersError = null;
        $listNumbersAttempted = false;
        $svcIsConfigured = false;
        try {
            $svc = new CloudTalkService();
            $svcIsConfigured = $svc->isConfigured();
            if ($svcIsConfigured) {
                try {
                    $listNumbersAttempted = true;
                    $numbers = $svc->listNumbers();
                    $connectionStatus = ['ok' => true, 'message' => 'Connected. Found ' . count($numbers) . ' CloudTalk number(s).'];
                } catch (Throwable $eList) {
                    $numbers = [];
                    $listNumbersError = $eList->getMessage();
                    $connectionStatus = ['ok' => true, 'warning' => 'Connected to CloudTalk but numbers list failed. Use the "Test Connection" button below or paste Number ID manually. Details: ' . $listNumbersError];
                }
            }
        } catch (Throwable $e) {
            $connectionStatus = ['ok' => false, 'error' => $e->getMessage()];
            $listNumbersError = $e->getMessage();
        }

        $webhookUrl = site_url('/callback/cloudtalk');

        return view('layout/app', [
            'title'     => 'CloudTalk Settings',
            'pageTitle' => 'CloudTalk Integration',
            'content'   => view('admin/cloudtalk_settings', [
                'config'              => $config,
                'numbers'             => $numbers,
                'connectionStatus'    => $connectionStatus,
                'webhookUrl'          => $webhookUrl,
                'listNumbersError'    => $listNumbersError,
                'listNumbersAttempted'=> $listNumbersAttempted,
                'svcIsConfigured'     => $svcIsConfigured,
            ]),
        ]);
    }

    public function cloudtalkTestConnection()
    {
        if ($this->request->getMethod(true) !== 'POST') {
            return $this->response->setStatusCode(405)->setJSON(['ok' => false, 'error' => 'Method not allowed.']);
        }
        try {
            $overrideId     = trim((string) ($this->request->getPost('api_key_id')     ?? ''));
            $overrideSecret = trim((string) ($this->request->getPost('api_key_secret') ?? ''));

            if ($overrideId !== '' && $overrideSecret !== '') {
                $svc = new CloudTalkService($overrideId, $overrideSecret);
            } else {
                $svc = new CloudTalkService();
            }

            $result = $svc->testConnection();
            return $this->response->setJSON($result);
        } catch (Throwable $e) {
            return $this->response->setStatusCode(500)->setJSON(['ok' => false, 'error' => $e->getMessage()]);
        }
    }

    public function leads()
    {
        $selectedStatus = $this->request->getGet('status_id');

        $leads = $this->leadModel->getWithStatus();

        if (!empty($selectedStatus)) {
            $leads = array_filter($leads, function ($lead) use ($selectedStatus) {
                return ($lead['status_id'] ?? null) == $selectedStatus;
            });
            $leads = array_values($leads);
        }

        $statuses = $this->leadStatusModel->orderBy('sort_order', 'ASC')->findAll();

        return view('layout/app', [
            'title'     => 'Leads',
            'pageTitle' => 'Lead Management',
            'content'   => view('admin/leads', [
                'leads'          => $leads,
                'statuses'       => $statuses,
                'selectedStatus' => $selectedStatus,
            ]),
        ]);
    }

    public function createLead()
    {
        $statuses     = $this->leadStatusModel->orderBy('sort_order', 'ASC')->findAll();
        $customFields = $this->leadCustomFieldModel->orderBy('sort_order', 'ASC')->findAll();
        $agentUsers   = $this->userModel->where('role', 'agent')->findAll();

        return view('layout/app', [
            'title'     => 'Create Lead',
            'pageTitle' => 'Create New Lead',
            'content'   => view('admin/lead_create', [
                'statuses'     => $statuses,
                'customFields' => $customFields,
                'agentUsers'   => $agentUsers,
            ]),
        ]);
    }

    public function storeLead()
    {
        $rules = [
            'name'  => 'required',
            'email' => 'permit_empty|valid_email',
            'phone' => 'required',
        ];

        if (!$this->validate($rules)) {
            session()->setFlashdata('error', 'Please fix the validation errors.');
            return redirect()->to('/admin/leads/create')->withInput()->with('errors', $this->validator->getErrors());
        }

        $leadNumber = $this->leadModel->generateLeadNumber();

        $leadId = $this->leadModel->insert([
            'lead_number'    => $leadNumber,
            'name'           => $this->request->getPost('name'),
            'email'          => $this->request->getPost('email'),
            'phone'          => $this->request->getPost('phone'),
            'company_name'   => $this->request->getPost('company_name'),
            'license_number' => $this->request->getPost('license_number'),
            'status_id'      => $this->request->getPost('status_id'),
            'assigned_to'    => $this->request->getPost('assigned_to') ?: null,
        ]);

        $customFields = $this->leadCustomFieldModel->orderBy('sort_order', 'ASC')->findAll();
        foreach ($customFields as $field) {
            $value = $this->request->getPost('custom_' . $field['id']);
            if ($value !== null && $value !== '') {
                $this->leadCustomValueModel->insert([
                    'lead_id'     => $leadId,
                    'field_id'    => $field['id'],
                    'field_value' => $value,
                ]);
            }
        }

        session()->setFlashdata('success', 'Lead created successfully.');
        return redirect()->to('/admin/leads');
    }

    public function viewLead($id)
    {
        $lead         = $this->leadModel->getWithStatus((int) $id);
        $customFields = $this->leadCustomFieldModel->orderBy('sort_order', 'ASC')->findAll();
        $customValues = $this->leadCustomValueModel->getByLeadId((int) $id);
        $notes        = $this->leadNoteModel->getByLeadId((int) $id);
        $bookingModel = new BookingModel();
        $bookings     = $bookingModel->getForLead((int) $id);
        $users        = $this->userModel->findAll();
        $userMap      = [];
        foreach ($users as $u) {
            $userMap[$u['id']] = $u;
        }

        if (!$lead) {
            session()->setFlashdata('error', 'Lead not found.');
            return redirect()->to('/admin/leads');
        }

        $calls = $this->leadCallModel->getForLead((int) $id);
        $callsHydrated = [];
        foreach ($calls as $call) {
            $cid = (int) $call['id'];
            $callsHydrated[] = [
                'call'        => $call,
                'transcript'  => $this->leadCallTranscriptModel->findByCallId($cid),
                'recording'   => $this->leadCallRecordingModel->findByCallId($cid),
            ];
        }

        $cloudtalkConfigured = false;
        try {
            $svc = new CloudTalkService();
            $cloudtalkConfigured = $svc->isConfigured();
        } catch (Throwable $e) {
        }

        return view('layout/app', [
            'title'     => 'View Lead',
            'pageTitle' => 'Lead: ' . esc($lead['name']),
            'content'   => view('admin/lead_view', [
                'lead'                 => $lead,
                'customFields'         => $customFields,
                'customValues'         => $customValues,
                'notes'                => $notes,
                'bookings'             => $bookings,
                'userMap'              => $userMap,
                'calls'                => $callsHydrated,
                'cloudtalkConfigured'  => $cloudtalkConfigured,
                'syncCallsUrl'         => site_url('/admin/leads/sync-calls/' . (int)$id),
                'agentBookUrl'         => site_url('/admin/leads/book/' . (int)$id),
            ]),
        ]);
    }

    public function editLead($id)
    {
        $lead = $this->leadModel->find($id);
        if (!$lead) {
            session()->setFlashdata('error', 'Lead not found.');
            return redirect()->to('/admin/leads');
        }

        $statuses     = $this->leadStatusModel->orderBy('sort_order', 'ASC')->findAll();
        $customFields = $this->leadCustomFieldModel->orderBy('sort_order', 'ASC')->findAll();
        $customValues = $this->leadCustomValueModel->getByLeadId((int) $id);
        $agentUsers   = $this->userModel->where('role', 'agent')->findAll();

        return view('layout/app', [
            'title'     => 'Edit Lead',
            'pageTitle' => 'Edit Lead: ' . esc($lead['name']),
            'content'   => view('admin/lead_edit', [
                'lead'         => $lead,
                'statuses'     => $statuses,
                'customFields' => $customFields,
                'customValues' => $customValues,
                'agentUsers'   => $agentUsers,
            ]),
        ]);
    }

    public function updateLead($id)
    {
        $lead = $this->leadModel->find($id);
        if (!$lead) {
            session()->setFlashdata('error', 'Lead not found.');
            return redirect()->to('/admin/leads');
        }

        $rules = [
            'name'  => 'required',
            'email' => 'permit_empty|valid_email',
            'phone' => 'required',
        ];

        if (!$this->validate($rules)) {
            session()->setFlashdata('error', 'Please fix the validation errors.');
            return redirect()->to('/admin/leads/edit/' . $id)->withInput()->with('errors', $this->validator->getErrors());
        }

        $this->leadModel->update($id, [
            'name'           => $this->request->getPost('name'),
            'email'          => $this->request->getPost('email'),
            'phone'          => $this->request->getPost('phone'),
            'company_name'   => $this->request->getPost('company_name'),
            'license_number' => $this->request->getPost('license_number'),
            'status_id'      => $this->request->getPost('status_id'),
            'assigned_to'    => $this->request->getPost('assigned_to') ?: null,
        ]);

        $this->leadCustomValueModel->where('lead_id', $id)->delete();

        $customFields = $this->leadCustomFieldModel->orderBy('sort_order', 'ASC')->findAll();
        foreach ($customFields as $field) {
            $value = $this->request->getPost('custom_' . $field['id']);
            if ($value !== null && $value !== '') {
                $this->leadCustomValueModel->insert([
                    'lead_id'     => $id,
                    'field_id'    => $field['id'],
                    'field_value' => $value,
                ]);
            }
        }

        session()->setFlashdata('success', 'Lead updated successfully.');
        return redirect()->to('/admin/leads/view/' . $id);
    }

    public function deleteLead($id)
    {
        $this->leadCustomValueModel->where('lead_id', $id)->delete();
        $this->leadNoteModel->where('lead_id', $id)->delete();
        $this->leadModel->delete($id);

        session()->setFlashdata('success', 'Lead deleted successfully.');
        return redirect()->to('/admin/leads');
    }

    public function addLeadNote($id)
    {
        $rules = [
            'note_text' => 'required',
        ];

        if (!$this->validate($rules)) {
            session()->setFlashdata('error', 'Note text is required.');
            return redirect()->to('/admin/leads/view/' . $id)->withInput()->with('errors', $this->validator->getErrors());
        }

        $userId = session()->get('user_id');

        $this->leadNoteModel->insert([
            'lead_id'   => (int) $id,
            'user_id'   => $userId,
            'note_text' => $this->request->getPost('note_text'),
        ]);

        session()->setFlashdata('success', 'Note added successfully.');
        return redirect()->to('/admin/leads/view/' . $id);
    }

    public function leadUpdateStatus($id)
    {
        if (! $this->request->isAJAX()) {
            return $this->response->setStatusCode(405)->setJSON(['ok' => false, 'error' => 'Method not allowed.']);
        }
        if (strtolower($this->request->getMethod()) !== 'post') {
            return $this->response->setStatusCode(405)->setJSON(['ok' => false, 'error' => 'POST required.']);
        }
        $adminId = (int) session()->get('user_id');
        if ($adminId <= 0) {
            return $this->response->setStatusCode(401)->setJSON(['ok' => false, 'error' => 'Unauthenticated.']);
        }
        $leadId = (int) $id;
        if ($leadId <= 0) {
            return $this->response->setStatusCode(400)->setJSON(['ok' => false, 'error' => 'Invalid lead.']);
        }
        $newStatusId = (int) $this->request->getPost('status_id');
        if ($newStatusId <= 0) {
            return $this->response->setStatusCode(400)->setJSON(['ok' => false, 'error' => 'Invalid status.']);
        }
        $lead = $this->leadModel->find($leadId);
        if (! $lead) {
            return $this->response->setStatusCode(404)->setJSON(['ok' => false, 'error' => 'Lead not found.']);
        }
        $newStatus = $this->leadStatusModel->find($newStatusId);
        if (! $newStatus) {
            return $this->response->setStatusCode(400)->setJSON(['ok' => false, 'error' => 'Unknown status.']);
        }
        $oldStatusId = (int) ($lead['status_id'] ?? 0);
        if ($oldStatusId === $newStatusId) {
            return $this->response->setJSON(['ok' => true, 'unchanged' => true, 'status' => $newStatus]);
        }
        $db = db_connect();
        $db->transStart();
        $this->leadModel->update($leadId, ['status_id' => $newStatusId]);
        $oldName = $oldStatusId ? (($this->leadStatusModel->find($oldStatusId)['name'] ?? null) ?: 'N/A') : 'N/A';
        $noteText = 'Status changed from <b>' . esc($oldName) . '</b> → <b>' . esc($newStatus['name']) . '</b>.';
        $this->leadNoteModel->insert([
            'lead_id'   => $leadId,
            'user_id'   => $adminId,
            'note_text' => $noteText,
        ]);
        $db->transComplete();
        if (! $db->transStatus()) {
            return $this->response->setStatusCode(500)->setJSON(['ok' => false, 'error' => 'Failed to save.']);
        }
        return $this->response->setJSON([
            'ok'     => true,
            'status' => [
                'id'    => (int) $newStatus['id'],
                'name'  => (string) $newStatus['name'],
                'color' => (string) ($newStatus['color'] ?? '#64748b'),
            ],
            'note'   => $noteText,
        ]);
    }

    public function leadStatuses()
    {
        if ($this->request->getMethod(true) === 'POST') {
            $rules = [
                'name'       => 'required',
                'sort_order' => 'permit_empty|integer',
            ];

            if (!$this->validate($rules)) {
                session()->setFlashdata('error', 'Please fix the validation errors.');
                return redirect()->to('/admin/lead-statuses')->withInput()->with('errors', $this->validator->getErrors());
            }

            $this->leadStatusModel->insert([
                'name'       => $this->request->getPost('name'),
                'color'      => $this->request->getPost('color') ?? '#3b82f6',
                'sort_order' => $this->request->getPost('sort_order') ?? 0,
            ]);

            session()->setFlashdata('success', 'Lead status added successfully.');
            return redirect()->to('/admin/lead-statuses');
        }

        $statuses = $this->leadStatusModel->orderBy('sort_order', 'ASC')->findAll();

        return view('layout/app', [
            'title'     => 'Lead Statuses',
            'pageTitle' => 'Lead Statuses',
            'content'   => view('admin/lead_statuses', ['statuses' => $statuses]),
        ]);
    }

    public function updateLeadStatus($id)
    {
        $id = (int) $id;
        $status = $this->leadStatusModel->find($id);
        if (!$status) {
            session()->setFlashdata('error', 'Status not found.');
            return redirect()->to(site_url('/admin/lead-statuses'));
        }

        if ($this->request->getMethod(true) !== 'POST') {
            return redirect()->to(site_url('/admin/lead-statuses'));
        }

        $rules = [
            'name'       => 'required',
            'sort_order' => 'permit_empty|integer',
        ];

        if (!$this->validate($rules)) {
            session()->setFlashdata('error', 'Please fix the validation errors.');
            return redirect()->to(site_url('/admin/lead-statuses'))->withInput();
        }

        $this->leadStatusModel->update($id, [
            'name'       => $this->request->getPost('name'),
            'color'      => $this->request->getPost('color') ?? '#3b82f6',
            'sort_order' => $this->request->getPost('sort_order') ?? 0,
        ]);

        session()->setFlashdata('success', 'Lead status updated successfully.');
        return redirect()->to(site_url('/admin/lead-statuses'));
    }

    public function deleteLeadStatus($id)
    {
        $id = (int) $id;
        $this->leadStatusModel->delete($id);
        session()->setFlashdata('success', 'Lead status deleted successfully.');
        return redirect()->to(site_url('/admin/lead-statuses'));
    }

    public function leadCustomFields()
    {
        if ($this->request->getMethod(true) === 'POST') {
            $rules = [
                'field_name'  => 'required',
                'field_label' => 'required',
                'field_type'  => 'required|in_list[text,textarea,select,number,email,date]',
                'sort_order'  => 'permit_empty|integer',
            ];

            if (!$this->validate($rules)) {
                session()->setFlashdata('error', 'Please fix the validation errors.');
                return redirect()->to('/admin/lead-custom-fields')->withInput()->with('errors', $this->validator->getErrors());
            }

            $optionsText = trim((string) $this->request->getPost('options'));
            $optionsJson = null;
            if ($optionsText !== '') {
                $options = array_map('trim', explode(',', $optionsText));
                $options = array_filter($options, function ($v) {
                    return $v !== '';
                });
                $optionsJson = json_encode(array_values($options));
            }

            $this->leadCustomFieldModel->insert([
                'field_name'   => $this->request->getPost('field_name'),
                'field_label'  => $this->request->getPost('field_label'),
                'field_type'   => $this->request->getPost('field_type'),
                'options_json' => $optionsJson,
                'sort_order'   => $this->request->getPost('sort_order') ?? 0,
                'required'     => $this->request->getPost('required') ? 1 : 0,
            ]);

            session()->setFlashdata('success', 'Custom field added successfully.');
            return redirect()->to('/admin/lead-custom-fields');
        }

        $fields = $this->leadCustomFieldModel->orderBy('sort_order', 'ASC')->findAll();

        return view('layout/app', [
            'title'     => 'Lead Custom Fields',
            'pageTitle' => 'Lead Custom Fields',
            'content'   => view('admin/lead_custom_fields', ['fields' => $fields]),
        ]);
    }

    public function updateLeadCustomField($id)
    {
        $id = (int) $id;
        $field = $this->leadCustomFieldModel->find($id);
        if (!$field) {
            session()->setFlashdata('error', 'Custom field not found.');
            return redirect()->to(site_url('/admin/lead-custom-fields'));
        }

        if ($this->request->getMethod(true) !== 'POST') {
            return redirect()->to(site_url('/admin/lead-custom-fields'));
        }

        $rules = [
            'field_name'  => 'required',
            'field_label' => 'required',
            'field_type'  => 'required|in_list[text,textarea,select,number,email,date]',
            'sort_order'  => 'permit_empty|integer',
        ];

        if (!$this->validate($rules)) {
            session()->setFlashdata('error', 'Please fix the validation errors.');
            return redirect()->to(site_url('/admin/lead-custom-fields'))->withInput();
        }

        $optionsText = trim((string) $this->request->getPost('options'));
        $optionsJson = null;
        if ($optionsText !== '') {
            $options = array_map('trim', explode(',', $optionsText));
            $options = array_filter($options, function ($v) {
                return $v !== '';
            });
            $optionsJson = json_encode(array_values($options));
        }

        $this->leadCustomFieldModel->update($id, [
            'field_name'   => $this->request->getPost('field_name'),
            'field_label'  => $this->request->getPost('field_label'),
            'field_type'   => $this->request->getPost('field_type'),
            'options_json' => $optionsJson,
            'sort_order'   => $this->request->getPost('sort_order') ?? 0,
            'required'     => $this->request->getPost('required') ? 1 : 0,
        ]);

        session()->setFlashdata('success', 'Custom field updated successfully.');
        return redirect()->to(site_url('/admin/lead-custom-fields'));
    }

    public function deleteLeadCustomField($id)
    {
        $id = (int) $id;
        $this->leadCustomValueModel->where('field_id', $id)->delete();
        $this->leadCustomFieldModel->delete($id);
        session()->setFlashdata('success', 'Custom field deleted successfully.');
        return redirect()->to(site_url('/admin/lead-custom-fields'));
    }

    public function syncLeadCalls($id)
    {
        $id = (int) $id;
        $lead = $this->leadModel->find($id);
        if (!$lead) {
            if ($this->request->isAJAX()) {
                return $this->response->setStatusCode(404)->setJSON(['ok' => false, 'error' => 'Lead not found.']);
            }
            session()->setFlashdata('error', 'Lead not found.');
            return redirect()->to('/admin/leads');
        }

        try {
            $svc = new CloudTalkService();
            if (!$svc->isConfigured()) {
                throw new \RuntimeException('CloudTalk API credentials not configured yet. Visit Admin > CloudTalk Settings.');
            }

            $phone = trim((string) ($lead['phone'] ?? ''));
            if ($phone === '') {
                throw new \RuntimeException('This lead has no phone number — cannot match CloudTalk calls.');
            }
            $digits = preg_replace('/\D+/', '', $phone);

            $dateFrom = '';
            try {
                $createdAt = $lead['created_at'] ?? '';
                if (is_string($createdAt) && $createdAt !== '') {
                    $ts = strtotime($createdAt);
                    if ($ts !== false) {
                        $dateFrom = gmdate('Y-m-d H:i:s', $ts - 86400);
                    }
                }
            } catch (\Throwable $e) {}
            if ($dateFrom === '') {
                $dateFrom = gmdate('Y-m-d H:i:s', time() - 90 * 86400);
            }

            $listOpts = [
                'limit'     => 1000,
                'date_from' => $dateFrom,
            ];

            $fetched = $svc->listCalls($listOpts);
            $calls = $fetched['calls'] ?? [];

            $to_mysql = static function (string $v): ?string {
                if ($v === '') return null;
                $ts = strtotime($v);
                if ($ts === false) return null;
                return gmdate('Y-m-d H:i:s', $ts);
            };

            $matched = 0;
            $inserted = 0;
            $updated = 0;
            $recordings = 0;
            $skipped = 0;
            $errors = [];
            $transcripts = 0;
            $summaries = 0;
            $sentiments = 0;
            $ciErrors = 0;

            $encodeJsonOrNull = static function ($data, bool $preferEmptyArray = true): ?string {
                if ($data === null) return null;
                $flags = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE | JSON_PARTIAL_OUTPUT_ON_ERROR;
                $encoded = @json_encode($data, $flags, 512);
                if (is_string($encoded) && $encoded !== '') {
                    $check = @json_decode($encoded, true);
                    if (json_last_error() === JSON_ERROR_NONE) return $encoded;
                }
                if (is_array($data)) {
                    $fallback = $preferEmptyArray ? '[]' : '{}';
                } else {
                    $fallback = $preferEmptyArray ? '[]' : 'null';
                }
                return $fallback;
            };

            $normalizeValidJsonOrDefault = static function ($existingString, $default = null) use ($encodeJsonOrNull) {
                if ($existingString === null || $existingString === '') return $default;
                if (!is_string($existingString)) return $encodeJsonOrNull($existingString) ?? $default;
                $decoded = @json_decode($existingString, true);
                if (json_last_error() === JSON_ERROR_NONE) return $existingString;
                return $default;
            };

            $normalizeRawJson = static function ($data) use ($encodeJsonOrNull): ?string {
                $s = $encodeJsonOrNull($data, false);
                if ($s === null || $s === '') return null;
                $dec = @json_decode($s, true);
                if (json_last_error() !== JSON_ERROR_NONE) return '{}';
                return $s;
            };

            $safeSegmentsOrNull = static function (array $newSegments, $existingString) use ($encodeJsonOrNull, $normalizeValidJsonOrDefault): ?string {
                if ($newSegments !== []) {
                    $encoded = $encodeJsonOrNull(array_values($newSegments), true);
                    if ($encoded === null) $encoded = '[]';
                    $dec = @json_decode($encoded, true);
                    if (json_last_error() !== JSON_ERROR_NONE) $encoded = '[]';
                    return $encoded;
                }
                return $normalizeValidJsonOrDefault($existingString, null);
            };

            $ciHasContent = static function ($data) use (&$ciHasContent) {
                if ($data === null || $data === '') return false;
                if (is_array($data)) {
                    foreach ($data as $k => $v) {
                        if (is_int($k) && is_array($v)) { if ($ciHasContent($v)) return true; continue; }
                        if ($v !== null && $v !== '' && $v !== [] && $v !== 0 && $v !== '0') return true;
                        if (is_array($v) && $ciHasContent($v)) return true;
                    }
                    return false;
                }
                return true;
            };

            foreach ($calls as $c) {
                $a = preg_replace('/\D+/', '', (string) ($c['destination_number'] ?? ''));
                $b = preg_replace('/\D+/', '', (string) ($c['caller_number'] ?? ''));
                $callIdStr = (string) ($c['cloudtalk_call_id'] ?? '');
                $matchedThis = false;
                if ($callIdStr !== '') {
                    $existing = $this->leadCallModel->findByCloudTalkCallId($callIdStr);
                    if ($existing !== null && (int) ($existing['lead_id'] ?? 0) === $id) {
                        $matchedThis = true;
                    }
                }
                if (!$matchedThis) {
                    if ($digits !== '' && ($a === $digits || $b === $digits)) {
                        $matchedThis = true;
                    }
                }
                if (!$matchedThis) {
                    $skipped++;
                    continue;
                }
                $matched++;

                $userId = (int) ($c['user_id'] ?? 0);
                $agentEmail = '';
                if ($userId > 0) {
                    try {
                        $u = $this->userModel->find($userId);
                        if ($u) {
                            $agentEmail = (string) ($u['email'] ?? '');
                        }
                    } catch (\Throwable $e) {}
                }
                if ($agentEmail === '' && !empty($lead['assigned_to'])) {
                    $assignee = $this->userModel->find((int)$lead['assigned_to']);
                    if ($assignee) $agentEmail = (string) ($assignee['email'] ?? '');
                }

                $payload = [
                    'lead_id'              => $id,
                    'agent_id'             => $userId > 0 ? $userId : ((int)($lead['assigned_to'] ?? 0) ?: null),
                    'agent_email'          => $agentEmail !== '' ? $agentEmail : null,
                    'cloudtalk_call_id'    => $callIdStr,
                    'cloudtalk_contact_id' => !empty($c['cloudtalk_contact_id']) ? (string)$c['cloudtalk_contact_id'] : null,
                    'direction'            => (string) ($c['direction'] ?? 'outbound'),
                    'caller_number'        => !empty($c['caller_number']) ? (string)$c['caller_number'] : null,
                    'destination_number'   => !empty($c['destination_number']) ? (string)$c['destination_number'] : null,
                    'status'               => $this->leadCallModel->sanitizeCallStatus($c['status'] ?? 'ended', 'ended'),
                    'started_at'           => $to_mysql((string) ($c['started_at'] ?? '')),
                    'answered_at'          => $to_mysql((string) ($c['answered_at'] ?? '')),
                    'ended_at'             => $to_mysql((string) ($c['ended_at'] ?? '')),
                    'duration_seconds'     => (int) ($c['duration_seconds'] ?? 0),
                    'talk_time_seconds'    => (int) ($c['talk_time_seconds'] ?? 0),
                    'wrap_up_time_seconds' => (int) ($c['wrap_up_time_seconds'] ?? 0),
                    'result'               => !empty($c['status']) ? (string)$c['status'] : null,
                    'raw_json'             => json_encode($c, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
                ];

                $existing = null;
                if ($callIdStr !== '') {
                    $existing = $this->leadCallModel->findByCloudTalkCallId($callIdStr);
                }
                if ($existing === null) {
                    $st = is_string($payload['started_at'] ?? null) ? (string)$payload['started_at'] : '';
                    if ($st === '' && !empty($c['started_at'])) {
                        $st = $to_mysql((string)$c['started_at']);
                    }
                    $tryDest = (string)($c['destination_number'] ?? $payload['destination_number'] ?? '');
                    $tryCall = (string)($c['caller_number'] ?? $payload['caller_number'] ?? '');
                    if ($st !== '' && ($tryDest !== '' || $tryCall !== '')) {
                        $existing = $this->leadCallModel->findByPhoneAndTimeWindow($id, $tryDest, $tryCall, $st, 600);
                    }
                }

                if ($existing !== null) {
                    $existingId = (int)$existing['id'];
                    if ((int)($existing['lead_id'] ?? 0) !== $id) {
                        $payload['lead_id'] = $id;
                    }
                    if ((int)($existing['agent_id'] ?? 0) <= 0 && $payload['agent_id'] !== null) {
                        // keep
                    } else {
                        unset($payload['agent_id']);
                    }
                    $this->leadCallModel->update($existingId, $payload);
                    $callLocalId = $existingId;
                    $updated++;
                } else {
                    $callLocalId = (int)$this->leadCallModel->insert($payload);
                    $inserted++;
                }
                if ($callLocalId <= 0) {
                    $errors[] = 'Failed to save call id=' . $callIdStr;
                    continue;
                }

                $recUrl = trim((string) ($c['recording_link'] ?? ''));
                if ($recUrl !== '') {
                    try {
                        $existingRec = $this->leadCallRecordingModel->findByCallId($callLocalId);
                        $recPayload = [
                            'call_id'          => $callLocalId,
                            'recording_url'    => $recUrl,
                            'duration_seconds' => (int) ($c['talk_time_seconds'] ?? $c['duration_seconds'] ?? 0) ?: null,
                            'raw_json'         => json_encode($c, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
                        ];
                        if ($existingRec === null) {
                            $this->leadCallRecordingModel->insert($recPayload);
                            $recordings++;
                        } else {
                            if (empty($existingRec['recording_url']) || $existingRec['recording_url'] !== $recUrl) {
                                $this->leadCallRecordingModel->update((int)$existingRec['id'], $recPayload);
                            }
                        }
                    } catch (\Throwable $eRec) {
                        $errors[] = 'Recording save failed for call ' . $callIdStr . ': ' . $eRec->getMessage();
                    }
                }

                if ($callIdStr !== '') {
                    try {
                        $aiBundle = $svc->getAiBundle($callIdStr);
                        $ciErrs = $aiBundle['errors'] ?? [];
                        if (is_array($ciErrs) && $ciErrs !== []) {
                            $ciErrors += count($ciErrs);
                            foreach ($ciErrs as $k => $errMsg) {
                                if (stripos((string)$errMsg, '404') !== false || stripos((string)$errMsg, 'not generated') !== false) {
                                    continue;
                                }
                                $errors[] = 'CI/' . $k . ': ' . (string)$errMsg;
                            }
                        }

                        $tr = $aiBundle['transcription'] ?? null;
                        $transcriptText = '';
                        $segments = [];
                        $language = '';
                        if (is_array($tr) && !empty($tr['exists'])) {
                            $transcriptText = (string) ($tr['text'] ?? '');
                            $segments = is_array($tr['segments'] ?? null) ? $tr['segments'] : [];
                            $language = (string) ($tr['language'] ?? '');
                            $transcripts++;
                        }

                        $aiSummaryText = '';
                        $summaryStatus = 'pending';
                        $sum = $aiBundle['summary'] ?? null;
                        if (is_array($sum) && !empty($sum['exists'])) {
                            $aiSummaryText = (string) ($sum['summary'] ?? '');
                            if ($aiSummaryText !== '') {
                                $summaryStatus = 'cloudtalk';
                                $summaries++;
                            }
                        }

                        $sentimentRow = $aiBundle['sentiment'] ?? null;
                        $sentiment = is_array($sentimentRow) ? [
                            'overall_sentiment' => (string) ($sentimentRow['overall_sentiment'] ?? ''),
                            'sentiment_score'   => (int) ($sentimentRow['sentiment_score'] ?? 0),
                            'raw'               => $sentimentRow['raw'] ?? null,
                        ] : [];
                        if (!empty($sentiment['overall_sentiment'])) {
                            $sentiments++;
                        }

                        $topics = $aiBundle['topics'] ?? null;
                        $talk   = $aiBundle['talk_listen'] ?? null;
                        $notes  = $aiBundle['smart_notes'] ?? null;

                        $rawMerged = [];
                        if (is_array($topics) && !empty($topics['exists'])) {
                            if ($ciHasContent($topics['topics'] ?? null)) $rawMerged['topics'] = $topics['topics'];
                            if ($ciHasContent($topics['raw'] ?? null)) $rawMerged['topics_raw'] = $topics['raw'];
                        }
                        if (is_array($talk) && !empty($talk['exists'])) {
                            if ($ciHasContent($talk['talk_listen_ratio'] ?? null)) $rawMerged['talk_listen_ratio'] = $talk['talk_listen_ratio'];
                            if ($ciHasContent($talk['callers'] ?? null)) $rawMerged['talk_listen_callers'] = $talk['callers'];
                            if ($ciHasContent($talk['raw'] ?? null)) $rawMerged['talk_listen_raw'] = $talk['raw'];
                        }
                        if (is_array($notes) && !empty($notes['exists'])) {
                            if ($ciHasContent($notes['notes'] ?? null)) $rawMerged['smart_notes'] = $notes['notes'];
                            if ($ciHasContent($notes['raw'] ?? null)) $rawMerged['smart_notes_raw'] = $notes['raw'];
                        }
                        if ($ciHasContent($sentiment)) $rawMerged['sentiment'] = $sentiment;
                        if (is_array($tr) && $ciHasContent($tr['raw'] ?? null)) $rawMerged['transcript_raw'] = $tr['raw'];
                        if (is_array($sum) && $ciHasContent($sum['raw'] ?? null)) $rawMerged['summary_raw'] = $sum['raw'];

                        $transcriptModel = $this->leadCallTranscriptModel;
                        $existingTranscript = $transcriptModel->findByCallId($callLocalId);

                        if ($transcriptText !== '' || $segments !== [] || $aiSummaryText !== '' || $rawMerged !== []) {
                            $prevRaw = [];
                            if ($existingTranscript !== null && !empty($existingTranscript['raw_json'])) {
                                $dec = json_decode((string)$existingTranscript['raw_json'], true);
                                if (is_array($dec)) $prevRaw = $dec;
                            }
                            $mergedRaw = $rawMerged + $prevRaw;

                            $upsertPayload = [
                                'call_id'          => $callLocalId,
                                'transcript_text'  => $transcriptText !== '' ? $transcriptText : (string)($existingTranscript['transcript_text'] ?? ''),
                                'speaker_segments' => $safeSegmentsOrNull($segments, $existingTranscript['speaker_segments'] ?? null),
                                'language'         => $language !== '' ? $language : (string)($existingTranscript['language'] ?? null),
                                'raw_json'         => $normalizeRawJson($mergedRaw),
                            ];

                            $keepExistingSummary = false;
                            if ($existingTranscript !== null) {
                                $prevStatus = (string) ($existingTranscript['ai_summary_status'] ?? '');
                                $prevSummary = trim((string) ($existingTranscript['ai_summary'] ?? ''));
                                if ($aiSummaryText === '' && ($prevStatus === 'done' || $prevStatus === 'cloudtalk' || $prevStatus === 'user_edited' || $prevSummary !== '')) {
                                    $keepExistingSummary = true;
                                }
                            }
                            if ($keepExistingSummary) {
                                $upsertPayload['ai_summary']        = (string)($existingTranscript['ai_summary'] ?? '');
                                $upsertPayload['ai_summary_status'] = (string)($existingTranscript['ai_summary_status'] ?? 'pending');
                            } else {
                                $upsertPayload['ai_summary']        = $aiSummaryText !== '' ? $aiSummaryText : (string)($existingTranscript['ai_summary'] ?? '');
                                $upsertPayload['ai_summary_status'] = $summaryStatus;
                                if ($upsertPayload['ai_summary'] !== '' && in_array($upsertPayload['ai_summary_status'], ['pending',''], true)) {
                                    $upsertPayload['ai_summary_status'] = 'cloudtalk';
                                }
                            }

                            if ($existingTranscript === null) {
                                $transcriptModel->insert($upsertPayload);
                            } else {
                                $changes = false;
                                foreach ($upsertPayload as $k => $v) {
                                    if ($k === 'call_id') continue;
                                    $existingVal = $existingTranscript[$k] ?? null;
                                    if (is_string($existingVal) && is_string($v) && $existingVal === $v) continue;
                                    if ($existingVal === null && $v === '') continue;
                                    if ($existingVal === null && $v !== null && $v !== '') { $changes = true; break; }
                                    if (is_string($existingVal) && is_string($v) && trim($existingVal) !== trim($v)) { $changes = true; break; }
                                    if ($existingVal != $v) { $changes = true; break; }
                                }
                                if ($changes) {
                                    $transcriptModel->update((int)$existingTranscript['id'], $upsertPayload);
                                }
                            }
                        }
                    } catch (\Throwable $eCi) {
                        $ciErrors++;
                        $errors[] = 'CI pull failed ' . $callIdStr . ': ' . $eCi->getMessage();
                    }
                }
            }

            $result = [
                'ok'         => true,
                'searched'   => count($calls),
                'matched'    => $matched,
                'inserted'   => $inserted,
                'updated'    => $updated,
                'recordings' => $recordings,
                'transcripts'=> $transcripts,
                'summaries'  => $summaries,
                'sentiments' => $sentiments,
                'ci_errors'  => $ciErrors,
                'skipped'    => $skipped,
                'phone'      => $phone,
                'date_from'  => $dateFrom,
            ];
            if ($errors !== []) {
                $result['warnings'] = array_slice($errors, 0, 10);
            }

            if ($this->request->isAJAX()) {
                return $this->response->setJSON($result);
            }
            session()->setFlashdata('success',
                'Synced ' . $matched . ' call(s) from CloudTalk: inserted ' . $inserted . ', updated ' . $updated . ', recordings ' . $recordings . ', transcripts ' . $transcripts . ', summaries ' . $summaries . ', sentiments ' . $sentiments . ($ciErrors > 0 ? ' (CI errors: '.$ciErrors.')' : '') . '.'
            );
            return redirect()->to('/admin/leads/view/' . $id);
        } catch (\Throwable $e) {
            if ($this->request->isAJAX()) {
                return $this->response->setStatusCode(500)->setJSON(['ok' => false, 'error' => $e->getMessage()]);
            }
            session()->setFlashdata('error', 'Sync failed: ' . $e->getMessage());
            return redirect()->to('/admin/leads/view/' . $id);
        }
    }

    public function quizSettings()
    {
        $quizSettings = $this->quizSettingModel->orderBy('created_at', 'DESC')->findAll();
        $docList = $this->documentModel
            ->select('documents.*, categories.name as category_name, (SELECT COUNT(id) FROM document_chunks WHERE document_chunks.document_id = documents.id) AS chunk_count')
            ->join('categories', 'categories.id = documents.category_id', 'left')
            ->orderBy('documents.title', 'ASC')
            ->findAll();
        $docMap = [];
        foreach ($docList as $d) $docMap[(int)($d['id'] ?? 0)] = $d;

        foreach ($quizSettings as &$q) {
            $ids = !empty($q['selected_document_ids']) ? (array)json_decode((string)$q['selected_document_ids'], true) : [];
            $q['docIds'] = is_array($ids) ? array_values(array_filter(array_map('intval', $ids))) : [];
            $q['docNames'] = [];
            foreach ($q['docIds'] as $did) {
                if (isset($docMap[$did])) $q['docNames'][] = $docMap[$did]['title'] ?? "Doc #{$did}";
            }
            $attemptCount = $this->quizAttemptModel->where('quiz_setting_id', $q['id'])->countAllResults();
            $q['attempt_count'] = $attemptCount;
        }
        unset($q);

        return view('layout/app', [
            'title'     => 'Quiz Settings',
            'pageTitle' => 'Quiz Settings',
            'content'   => view('admin/quiz_settings', [
                'quizSettings' => $quizSettings,
                'documents'    => $docList,
            ]),
        ]);
    }

    public function quizSettingSave()
    {
        if ($this->request->getMethod(true) !== 'POST') {
            return redirect()->to('/admin/quiz-settings');
        }
        $id = (int) $this->request->getPost('id');
        $docIds = $this->request->getPost('document_ids') ?? [];
        if (!is_array($docIds)) $docIds = [];
        $docIds = array_values(array_filter(array_map('intval', $docIds), static fn($x) => $x > 0));

        $questionType = $this->request->getPost('question_type') ?? 'multiple_choice';
        if (!in_array($questionType, ['multiple_choice', 'fill_up'], true)) $questionType = 'multiple_choice';

        $questionCount = max(1, min(30, (int)($this->request->getPost('question_count') ?? 10)));
        $timerSeconds  = max(10, min(600, (int)($this->request->getPost('question_timer_seconds') ?? 60)));
        $passing       = max(0, min(100, (int)($this->request->getPost('passing_score_pct') ?? 70)));
        $isActive      = (int)((bool)$this->request->getPost('is_active'));
        $title         = trim((string)($this->request->getPost('title') ?? ''));
        if ($title === '') $title = 'Quiz ' . date('Y-m-d H:i');

        $payload = [
            'title'                   => $title,
            'description'             => trim((string)($this->request->getPost('description') ?? '')),
            'question_type'           => $questionType,
            'question_count'          => $questionCount,
            'question_timer_seconds'  => $timerSeconds,
            'passing_score_pct'       => $passing,
            'selected_document_ids'   => json_encode($docIds),
            'is_active'               => $isActive,
        ];

        if ($id > 0) {
            $this->quizSettingModel->update($id, $payload);
            session()->setFlashdata('success', 'Quiz updated successfully.');
        } else {
            $uid = (int)(session()->get('user_id') ?? 0);
            if ($uid > 0) $payload['created_by'] = $uid;
            $this->quizSettingModel->insert($payload);
            session()->setFlashdata('success', 'Quiz created successfully. Agents can now take this quiz.');
        }
        return redirect()->to('/admin/quiz-settings');
    }

    public function quizSettingDelete($id)
    {
        $id = (int)$id;
        if ($id > 0) $this->quizSettingModel->delete($id);
        session()->setFlashdata('success', 'Quiz deleted.');
        return redirect()->to('/admin/quiz-settings');
    }

    public function quizSettingToggle($id, $state)
    {
        $id = (int)$id;
        $state = strtolower(trim((string)$state)) === 'on' ? 1 : 0;
        if ($id > 0) $this->quizSettingModel->update($id, ['is_active' => $state]);
        session()->setFlashdata('success', 'Quiz ' . ($state ? 'activated' : 'deactivated') . '.');
        return redirect()->to('/admin/quiz-settings');
    }

    public function quizAttempts($id = null)
    {
        $quizId = $id !== null ? (int)$id : null;

        $req = $this->request;
        $filterAgentId = trim((string)($req->getGet('agent_id') ?? $req->getPost('agent_id') ?? ''));
        $filterQuizId  = trim((string)($req->getGet('quiz_id')  ?? $req->getPost('quiz_id')  ?? ''));
        $fromDate      = trim((string)($req->getGet('from')     ?? $req->getPost('from')     ?? ''));
        $toDate        = trim((string)($req->getGet('to')       ?? $req->getPost('to')       ?? ''));

        if ($quizId !== null) {
            $filterQuizId = (string)$quizId;
        }

        $agentIdInt = ($filterAgentId !== '') ? (int)$filterAgentId : null;
        $quizIdInt  = ($filterQuizId  !== '') ? (int)$filterQuizId  : null;

        $attempts = $this->quizAttemptModel->listWithSettingAndUser($quizIdInt, $agentIdInt, $fromDate, $toDate);

        $setting = null;
        if ($quizId !== null) $setting = $this->quizSettingModel->find($quizId);

        $agentUsers = $this->userModel
            ->where('role', 'agent')
            ->orderBy('full_name', 'ASC')
            ->findAll();

        $allQuizzes = $this->quizSettingModel
            ->where('is_active', 1)
            ->orderBy('title', 'ASC')
            ->findAll();

        return view('layout/app', [
            'title'     => 'Quiz Attempt History',
            'pageTitle' => 'Quiz Attempt History',
            'content'   => view('admin/quiz_attempts', [
                'attempts'        => $attempts,
                'filterQuizId'    => $quizId,
                'setting'         => $setting,
                'filterAgentId'   => $filterAgentId,
                'filterQuizSelId' => $filterQuizId,
                'fromDate'        => $fromDate,
                'toDate'          => $toDate,
                'agentUsers'      => $agentUsers,
                'allQuizzes'      => $allQuizzes,
            ]),
        ]);
    }

    public function quizAttemptView($id)
    {
        $attempt = $this->quizAttemptModel->getWithQuestions((int)$id);
        if ($attempt === null) {
            session()->setFlashdata('error', 'Attempt not found.');
            return redirect()->to('/admin/quiz-attempts');
        }
        return view('layout/app', [
            'title'     => 'Attempt Review',
            'pageTitle' => 'Quiz Attempt Review',
            'content'   => view('admin/quiz_attempt_view', ['attempt' => $attempt]),
        ]);
    }

    public function vapiSettings()
    {
        if ($this->request->getMethod(true) === 'POST') {
            $action = trim((string) ($this->request->getPost('action') ?? ''));

            if ($action === 'save_key') {
                $val = trim((string) $this->request->getPost('api_key') ?? '');
                if ($val === '' || strlen($val) < 8) {
                    session()->setFlashdata('error', 'API key must be at least 8 characters.');
                    return redirect()->to('/admin/vapi-settings')->withInput();
                }
                $this->vapiConfigModel->setValue('api_key', $val);
                session()->setFlashdata('success', 'Vapi API key saved.');
                return redirect()->to('/admin/vapi-settings');
            }

            if ($action === 'refresh_assistants') {
                $res = $this->vapiService->listAssistants();
                if (empty($res['ok'])) {
                    session()->setFlashdata('error', $res['msg'] ?? 'Failed to fetch assistants from Vapi.');
                    return redirect()->to('/admin/vapi-settings');
                }
                $assistants = is_array($res['data']) ? $res['data'] : [];
                $count = 0;
                $now = date('Y-m-d H:i:s');
                foreach ($assistants as $a) {
                    if (!is_array($a)) continue;
                    $vapiId = (string) ($a['id'] ?? '');
                    if ($vapiId === '') continue;
                    $vapiName = (string) ($a['name'] ?? $a['id'] ?? '');
                    $existing = $this->vapiAssistantBindingModel->findByVapiAssistantId($vapiId);
                    if ($existing === null) {
                        $this->vapiAssistantBindingModel->insert([
                            'vapi_assistant_id' => $vapiId,
                            'name'              => '',
                            'vapi_name'         => $vapiName,
                            'url'               => '',
                            'selected_doc_ids'  => '[]',
                            'assigned_agent_ids' => '[]',
                            'pass_threshold_pct' => 70,
                            'is_active'         => 0,
                            'updated_at'        => $now,
                        ]);
                    } else {
                        $this->vapiAssistantBindingModel->update((int)$existing['id'], [
                            'vapi_name'  => $vapiName,
                            'updated_at' => $now,
                        ]);
                    }
                    $count++;
                }
                session()->setFlashdata('success', "Refreshed {$count} assistant(s) from Vapi.");
                return redirect()->to('/admin/vapi-settings');
            }

            if ($action === 'save_bindings') {
                $allBindings = $this->vapiAssistantBindingModel->findAll();
                $bindingIdsInDb = [];
                foreach ($allBindings as $b) {
                    $bindingIdsInDb[(int)$b['id']] = true;
                }

                $postedIds = $this->request->getPost('binding_ids') ?? [];
                if (!is_array($postedIds)) $postedIds = [];
                $postedIds = array_values(array_filter(array_map('intval', $postedIds)));

                $urlErrors = [];
                foreach ($postedIds as $bid) {
                    if (!isset($bindingIdsInDb[$bid])) continue;

                    $nameVal = trim((string) ($this->request->getPost('name_' . $bid) ?? ''));
                    $urlVal  = trim((string) ($this->request->getPost('url_' . $bid) ?? ''));
                    if ($urlVal !== '' && filter_var($urlVal, FILTER_VALIDATE_URL) === false) {
                        $urlErrors[] = "Row #{$bid}";
                    }

                    $docs = $this->request->getPost('docs_' . $bid) ?? [];
                    if (!is_array($docs)) $docs = [];
                    $docs = array_values(array_filter(array_map('intval', $docs)));

                    $agents = $this->request->getPost('agents_' . $bid) ?? [];
                    if (!is_array($agents)) $agents = [];
                    $agents = array_values(array_filter(array_map('intval', $agents)));

                    $threshold = (int) ($this->request->getPost('pass_' . $bid) ?? 70);
                    if ($threshold < 0) $threshold = 0;
                    if ($threshold > 100) $threshold = 100;

                    $isActive = (int) ((bool) ($this->request->getPost('active_' . $bid) ?? false));

                    $this->vapiAssistantBindingModel->update($bid, [
                        'name'               => $nameVal,
                        'url'                => $urlVal,
                        'selected_doc_ids'   => json_encode($docs),
                        'assigned_agent_ids' => json_encode($agents),
                        'pass_threshold_pct' => $threshold,
                        'is_active'          => $isActive,
                    ]);
                }

                if ($urlErrors !== []) {
                    session()->setFlashdata('error', 'Invalid URL in row(s): ' . implode(', ', $urlErrors) . '. URLs left unchanged but other fields were saved.');
                } else {
                    session()->setFlashdata('success', 'Bindings saved successfully.');
                }
                return redirect()->to('/admin/vapi-settings');
            }
        }

        $trainedDocs = $this->documentModel
            ->select('documents.*, categories.name as category_name, (SELECT COUNT(id) FROM document_chunks WHERE document_chunks.document_id = documents.id) AS chunk_count')
            ->join('categories', 'categories.id = documents.category_id', 'left')
            ->where('(SELECT COUNT(id) FROM document_chunks WHERE document_chunks.document_id = documents.id) > 0')
            ->orderBy('documents.title', 'ASC')
            ->findAll();

        $agentUsers = $this->userModel
            ->where('role', 'agent')
            ->orderBy('full_name', 'ASC')
            ->findAll();

        $bindings = $this->vapiAssistantBindingModel->getAllWithCounts();

        $apiKey = (string) $this->vapiConfigModel->getValue('api_key', '');

        $vapiAssistants = [];
        if ($apiKey !== '') {
            try {
                $res = $this->vapiService->listAssistants();
                if (!empty($res['ok']) && is_array($res['data'])) {
                    $vapiAssistants = $res['data'];
                }
            } catch (Throwable $e) {
                log_message('error', 'vapiSettings listAssistants: ' . $e->getMessage());
            }
        }

        return view('layout/app', [
            'title'     => 'Vapi Settings',
            'pageTitle' => 'Vapi Integration',
            'content'   => view('admin/vapi_settings', [
                'trainedDocs'   => $trainedDocs,
                'agentUsers'    => $agentUsers,
                'bindings'      => $bindings,
                'apiKey'        => $apiKey,
                'vapiAssistants' => $vapiAssistants,
            ]),
        ]);
    }

    public function vapiTestConnection()
    {
        if ($this->request->getMethod(true) !== 'POST') {
            return $this->response->setStatusCode(405)->setJSON(['ok' => false, 'msg' => 'Method not allowed.', 'count' => null]);
        }
        $key = trim((string) ($this->request->getPost('api_key') ?? ''));
        $fromSaved = (string) ($this->request->getPost('use_saved') ?: '0') === '1';
        if ($fromSaved || $key === '') {
            $stored = (string) $this->vapiConfigModel->getValue('api_key', '');
            if ($stored !== '') $key = $stored;
        }
        try {
            $result = $this->vapiService->testConnection($key);
            $count = null;
            if (!empty($result['ok'])) {
                $count = isset($result['data']['assistants_count']) ? (int)$result['data']['assistants_count'] : null;
            }
            return $this->response->setJSON([
                'ok'    => !empty($result['ok']),
                'msg'   => (string) ($result['msg'] ?? 'Unknown error'),
                'count' => $count,
            ]);
        } catch (Throwable $e) {
            return $this->response->setStatusCode(500)->setJSON([
                'ok'    => false,
                'msg'   => $e->getMessage(),
                'count' => null,
            ]);
        }
    }

    public function vapiDeleteBinding($id)
    {
        $id = (int) $id;
        if ($id > 0) {
            $this->vapiAssistantBindingModel->delete($id);
            session()->setFlashdata('success', 'Binding deleted.');
        } else {
            session()->setFlashdata('error', 'Invalid binding id.');
        }
        return redirect()->to('/admin/vapi-settings');
    }

    public function vapiScoreHistory($bindingId = null)
    {
        $bindingId = ($bindingId !== null) ? (int)$bindingId : null;

        $req = $this->request;
        $fromDate = trim((string)($req->getGet('from') ?? $req->getPost('from') ?? ''));
        $toDate   = trim((string)($req->getGet('to')   ?? $req->getPost('to')   ?? ''));

        $db = db_connect();
        $builder = $db->table('vapi_call_logs vcl')
            ->select('vcl.*, vb.name AS binding_name, vb.vapi_name AS binding_vapi_name, vb.pass_threshold_pct AS threshold_pct,
                      u.username AS agent_username, u.full_name AS agent_name,
                      vcs.score_pct AS score_pct, vcs.pass_threshold_pct AS score_threshold_pct,
                      vcs.breakdown_json AS breakdown_json, vcs.ai_feedback AS ai_feedback')
            ->join('vapi_assistant_bindings vb', 'vb.id = vcl.binding_id', 'left')
            ->join('users u', 'u.id = vcl.agent_id', 'left')
            ->join('vapi_call_scores vcs', 'vcs.call_log_id = vcl.id', 'left')
            ->orderBy('vcl.started_at', 'DESC');

        if ($bindingId !== null && $bindingId > 0) {
            $builder->where('vcl.binding_id', $bindingId);
        }
        if ($fromDate !== '') {
            $builder->where('DATE(vcl.started_at) >=', $fromDate);
        }
        if ($toDate !== '') {
            $builder->where('DATE(vcl.started_at) <=', $toDate);
        }

        $calls = $builder->get()->getResultArray();

        $allBindings = $this->vapiAssistantBindingModel->orderBy('name', 'ASC')->findAll();

        return view('layout/app', [
            'title'     => 'Vapi Score History',
            'pageTitle' => 'Vapi Call Score History',
            'content'   => view('admin/vapi_score_history', [
                'calls'        => $calls,
                'allBindings'  => $allBindings,
                'filterBindingId' => $bindingId,
            ]),
        ]);
    }

    public function vapiCallReview($id)
    {
        $call = $this->vapiCallLogModel->getWithScore((int)$id);
        if ($call === null) {
            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
        }

        $binding = null;
        if (!empty($call['binding_id'])) {
            $binding = $this->vapiAssistantBindingModel->find((int)$call['binding_id']);
        }

        $selectedDocNames = [];
        if ($binding !== null && !empty($binding['selected_doc_ids'])) {
            $docIds = json_decode($binding['selected_doc_ids'], true);
            if (is_array($docIds) && $docIds !== []) {
                $docIds = array_values(array_filter(array_map('intval', $docIds)));
                if ($docIds !== []) {
                    $docs = $this->documentModel->whereIn('id', $docIds)->findAll();
                    foreach ($docs as $d) {
                        $selectedDocNames[] = [
                            'id'    => (int)$d['id'],
                            'title' => (string)($d['title'] ?? ''),
                        ];
                    }
                }
            }
        }

        $breakdown = [];
        if (!empty($call['breakdown_json'])) {
            $dec = json_decode($call['breakdown_json'], true);
            if (is_array($dec)) $breakdown = $dec;
        }

        return view('layout/app', [
            'title'     => 'Call Review',
            'pageTitle' => 'Vapi Call Review',
            'content'   => view('admin/vapi_call_review', [
                'call'             => $call,
                'binding'          => $binding,
                'selectedDocNames' => $selectedDocNames,
                'breakdown'        => $breakdown,
            ]),
        ]);
    }

    public function bookingSchedules()
    {
        $scheduleModel = new BookingScheduleModel();
        $shiftModel    = new BookingShiftModel();
        $userModel     = new UserModel();

        $agents = $userModel->whereIn('role', ['agent', 'admin'])->orderBy('role', 'DESC')->orderBy('full_name', 'ASC')->findAll();

        if ($this->request->getMethod(true) === 'POST') {
            $action = (string) ($this->request->getPost('action') ?? '');

            if ($action === 'save' || $action === 'update') {
                $daysRaw = $this->request->getPost('days') ?? [];
                if (!is_array($daysRaw)) $daysRaw = [];
                $daysInts = array_values(array_filter(array_map('intval', $daysRaw), static fn($v) => $v >= 0 && $v <= 6));

                $agentIdsRaw = $this->request->getPost('assigned_agent_ids') ?? [];
                if (!is_array($agentIdsRaw)) $agentIdsRaw = [];
                $agentIdsInts = array_values(array_filter(array_map('intval', $agentIdsRaw)));

                $scheduleId = 0;
                if ($action === 'update') {
                    $scheduleId = (int) $this->request->getPost('schedule_id');
                }

                $title = trim((string) $this->request->getPost('title'));
                $timezone = trim((string) $this->request->getPost('timezone'));
                $meetingMins = (int) $this->request->getPost('meeting_duration_minutes');
                $breakMins   = (int) $this->request->getPost('break_duration_minutes');
                if ($title === '' || $timezone === '' || $meetingMins <= 0 || $breakMins < 0) {
                    session()->setFlashdata('error', 'All fields are required.');
                    return redirect()->to(site_url('/admin/booking-schedules'));
                }

                $payload = [
                    'title'                     => $title,
                    'timezone'                  => $timezone,
                    'meeting_duration_minutes'  => $meetingMins,
                    'break_duration_minutes'    => $breakMins,
                    'days_json'                 => json_encode($daysInts),
                    'assigned_agent_ids_json'   => json_encode($agentIdsInts),
                    'is_active'                 => $this->request->getPost('is_active') ? 1 : 0,
                ];

                if ($action === 'update' && $scheduleId > 0) {
                    $existing = $scheduleModel->find($scheduleId);
                    if (!$existing) {
                        session()->setFlashdata('error', 'Schedule not found.');
                    } else {
                        $scheduleModel->update($scheduleId, $payload);
                        session()->setFlashdata('success', 'Schedule updated successfully.');
                    }
                } else {
                    $scheduleModel->insert($payload);
                    session()->setFlashdata('success', 'Schedule added successfully.');
                }
                return redirect()->to(site_url('/admin/booking-schedules'));
            }

            if ($action === 'save-shift') {
                $scheduleId = (int) $this->request->getPost('schedule_id');
                $start      = trim((string) $this->request->getPost('shift_start'));
                $end        = trim((string) $this->request->getPost('shift_end'));
                if ($scheduleId > 0 && $start !== '' && $end !== '') {
                    $maxOrder = (int) $shiftModel->where('schedule_id', $scheduleId)->selectMax('sort_order', 'so')->first()['so'] ?? 0;
                    $shiftModel->insert([
                        'schedule_id' => $scheduleId,
                        'shift_start' => $start,
                        'shift_end'   => $end,
                        'sort_order'  => $maxOrder + 1,
                    ]);
                    session()->setFlashdata('success', 'Shift added.');
                } else {
                    session()->setFlashdata('error', 'Invalid shift parameters.');
                }
                return redirect()->to(site_url('/admin/booking-schedules'));
            }

            if ($action === 'delete-shift') {
                $id = (int) $this->request->getPost('id');
                if ($id > 0) {
                    $shiftModel->delete($id);
                    session()->setFlashdata('success', 'Shift deleted.');
                }
                return redirect()->to(site_url('/admin/booking-schedules'));
            }

            if ($action === 'delete-schedule') {
                $id = (int) $this->request->getPost('id');
                if ($id > 0) {
                    $shiftModel->where('schedule_id', $id)->delete();
                    $scheduleModel->delete($id);
                    session()->setFlashdata('success', 'Schedule deleted.');
                }
                return redirect()->to(site_url('/admin/booking-schedules'));
            }
        }

        $schedules = [];
        try {
            $rows = method_exists($scheduleModel, 'listActiveWithCounts')
                ? $scheduleModel->listActiveWithCounts()
                : $scheduleModel->orderBy('title', 'ASC')->findAll();
            foreach ($rows as $s) {
                $sid = (int) ($s['id'] ?? 0);
                $s['shifts']        = $shiftModel->where('schedule_id', $sid)->orderBy('sort_order', 'ASC')->orderBy('shift_start', 'ASC')->findAll();
                $s['shifts_count'] = $s['shifts_count'] ?? count($s['shifts']);
                $s['bookings_count'] = $s['bookings_count'] ?? 0;
                $schedules[] = $s;
            }
            $inactive = $scheduleModel->where('is_active', 0)->orderBy('title', 'ASC')->findAll();
            foreach ($inactive as $s) {
                $sid = (int) ($s['id'] ?? 0);
                $s['shifts'] = $shiftModel->where('schedule_id', $sid)->orderBy('sort_order', 'ASC')->orderBy('shift_start', 'ASC')->findAll();
                $s['shifts_count'] = count($s['shifts']);
                $s['bookings_count'] = 0;
                $schedules[] = $s;
            }
        } catch (Throwable $e) {
            log_message('error', 'bookingSchedules load: ' . $e->getMessage());
        }

        return view('layout/app', [
            'title'     => 'Booking Schedules',
            'pageTitle' => 'Booking Schedules',
            'content'   => view('admin/booking_schedules', [
                'schedules' => $schedules,
                'agents'    => $agents,
            ]),
        ]);
    }

    public function bookingScheduleDelete($id)
    {
        $id = (int) $id;
        $scheduleModel = new BookingScheduleModel();
        $shiftModel    = new BookingShiftModel();
        $schedule = $scheduleModel->find($id);
        if (!$schedule) {
            session()->setFlashdata('error', 'Schedule not found.');
            return redirect()->to(site_url('/admin/booking-schedules'));
        }
        try {
            $shiftModel->where('schedule_id', $id)->delete();
            $scheduleModel->delete($id);
            session()->setFlashdata('success', 'Schedule and shifts deleted.');
        } catch (Throwable $e) {
            session()->setFlashdata('error', 'Delete failed: ' . $e->getMessage());
        }
        return redirect()->to(site_url('/admin/booking-schedules'));
    }

    public function scheduleShiftDelete($id)
    {
        $id = (int) $id;
        $shiftModel = new BookingShiftModel();
        $shift = $shiftModel->find($id);
        if (!$shift) {
            session()->setFlashdata('error', 'Shift not found.');
            return redirect()->to(site_url('/admin/booking-schedules'));
        }
        $shiftModel->delete($id);
        session()->setFlashdata('success', 'Shift deleted.');
        return redirect()->to(site_url('/admin/booking-schedules'));
    }

    public function bookingSlotsAJAX($sid = 0)
    {
        if ($this->request->getMethod(true) !== 'POST' || !$this->request->isAJAX()) {
            return $this->response->setStatusCode(405)->setJSON(['ok' => false, 'error' => 'Method not allowed.', 'slots' => []]);
        }
        $scheduleId = (int) ($this->request->getPost('schedule_id') ?: $sid);
        $dateRaw    = trim((string) $this->request->getPost('date'));

        if ($scheduleId <= 0 || $dateRaw === '' || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateRaw)) {
            return $this->response->setJSON(['ok' => false, 'error' => 'Invalid schedule_id or date (YYYY-MM-DD required).', 'slots' => []]);
        }

        $scheduleModel  = new BookingScheduleModel();
        $shiftModel     = new BookingShiftModel();
        $exModel        = new BookingScheduleExceptionModel();
        $bookingModel   = new BookingModel();

        $schedule = $scheduleModel->find($scheduleId);
        if (!$schedule) {
            return $this->response->setJSON(['ok' => false, 'error' => 'Schedule not found.', 'slots' => []]);
        }

        $tzName = !empty($schedule['timezone']) ? (string)$schedule['timezone'] : 'America/Los_Angeles';
        try {
            $tz = new \DateTimeZone($tzName);
        } catch (\Throwable $e) {
            $tz = new \DateTimeZone('America/Los_Angeles');
        }
        $dateObj = new \DateTimeImmutable($dateRaw . ' 00:00:00', $tz);

        $shifts     = $shiftModel->where('schedule_id', $scheduleId)->orderBy('sort_order', 'ASC')->orderBy('shift_start', 'ASC')->findAll();
        $exceptions = $exModel->where('schedule_id', $scheduleId)->where('exception_date', $dateRaw)->findAll();
        $bookings   = $bookingModel
            ->where('schedule_id', $scheduleId)
            ->where("DATE(CONVERT_TZ(starts_at, '+00:00', '" . $tzName . "'))", $dateRaw)
            ->whereIn('status', ['booked', 'rescheduled'])
            ->findAll();
        $bookedStarts = [];
        foreach ($bookings as $b) {
            $st = (string)($b['starts_at'] ?? '');
            $et = (string)($b['ends_at']   ?? '');
            if ($st === '') continue;
            $bookedStarts[] = [
                'start' => $st,
                'end'   => $et !== '' ? $et : null,
            ];
        }

        try {
            $gen = new BookingSlotGenerator();
            $slots = $gen->generateSlotsForDate($schedule, $shifts, $exceptions, $dateObj, $bookedStarts);
            $out = [];
            foreach ($slots as $s) {
                $out[] = [
                    'start_str' => date('H:i', (int)($s['start_ut'] ?? 0)),
                    'end_str'   => date('H:i', (int)($s['end_ut'] ?? 0)),
                    'start_iso' => (string)($s['start_str'] ?? ''),
                    'end_iso'   => (string)($s['end_str'] ?? ''),
                    'start_ut'  => (int)($s['start_ut'] ?? 0),
                    'end_ut'    => (int)($s['end_ut'] ?? 0),
                    'available' => !empty($s['available']),
                ];
            }
            return $this->response->setJSON(['ok' => true, 'timezone' => $tzName, 'slots' => $out]);
        } catch (Throwable $e) {
            log_message('error', 'bookingSlotsAJAX: ' . $e->getMessage());
            return $this->response->setJSON(['ok' => false, 'error' => $e->getMessage() ?: 'Slot generation failed.', 'slots' => []]);
        }
    }

    public function leadBook($id)
    {
        $leadId  = (int)$id;
        $adminId = (int) session()->get('user_id');
        if ($adminId <= 0) {
            return redirect()->to('/login');
        }
        $leadModel       = new LeadModel();
        $bookingModel    = new BookingModel();
        $statusModel     = new LeadStatusModel();
        $noteModel       = new LeadNoteModel();
        $scheduleModel   = new BookingScheduleModel();
        $smtpConfigModel = new BookingSmtpConfigModel();

        $lead = $leadModel->find($leadId);
        if (!$lead) {
            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
        }

        $error = '';
        if ($this->request->getMethod(true) === 'POST') {
            $scheduleId   = (int) $this->request->getPost('schedule_id');
            $startsAtStr  = trim((string) $this->request->getPost('starts_at'));
            $endsAtStr    = trim((string) $this->request->getPost('ends_at'));
            $timezone     = trim((string) $this->request->getPost('timezone'));
            $durationStr  = (int) $this->request->getPost('duration_minutes');
            $notes        = trim((string) $this->request->getPost('notes'));
            $guestName    = trim((string) $this->request->getPost('guest_name'));
            $guestEmail   = trim((string) $this->request->getPost('guest_email'));
            $guestPhone   = trim((string) $this->request->getPost('guest_phone'));
            $locationUrl  = trim((string) $this->request->getPost('location_url'));
            $assignedAgentPost = (int) $this->request->getPost('assigned_agent_id');
            if ($assignedAgentPost <= 0) $assignedAgentPost = $adminId;

            if ($scheduleId <= 0 || $startsAtStr === '' || $endsAtStr === '') {
                $error = 'All booking fields are required.';
            }

            $schedule = $error ? null : $scheduleModel->find($scheduleId);
            if ($error === '' && (!$schedule || empty($schedule['is_active']))) {
                $error = 'Schedule not found or inactive.';
            }

            if ($error === '') {
                try {
                    $startsUTC = new \DateTimeImmutable($startsAtStr, new \DateTimeZone('UTC'));
                    $endsUTC   = new \DateTimeImmutable($endsAtStr, new \DateTimeZone('UTC'));
                    if ($endsUTC <= $startsUTC) {
                        $error = 'End time must be after start time.';
                    }
                } catch (\Throwable $e) {
                    $error = 'Invalid start/end datetime format.';
                }
            }

            if ($error === '' && $durationStr <= 0) {
                $duration = $startsUTC->diff($endsUTC);
                $durationStr = ($duration->h * 60) + $duration->i;
                if ($durationStr <= 0) $durationStr = (int)($schedule['meeting_duration_minutes'] ?? 60);
            }

            if ($error === '') {
                $existing = $bookingModel->getBetweenRange($startsAtStr, $endsAtStr, $assignedAgentPost, $scheduleId);
                foreach ($existing as $b) {
                    if (($b['status'] ?? 'booked') === 'cancelled') continue;
                    $bS = (string)($b['starts_at'] ?? '');
                    $bE = (string)($b['ends_at']   ?? '');
                    if ($bS === '' || $bE === '') continue;
                    if ($startsAtStr < $bE && $endsAtStr > $bS) {
                        $error = 'This time slot is already booked for the assigned agent. Please select another.';
                        break;
                    }
                }
            }

            if ($error === '') {
                $db = db_connect();
                $db->transStart();

                $booked = [
                    'lead_id'            => $leadId,
                    'schedule_id'        => $scheduleId,
                    'booked_by_user_id'  => $adminId,
                    'assigned_agent_id'  => $assignedAgentPost,
                    'starts_at'          => $startsAtStr,
                    'ends_at'            => $endsAtStr,
                    'timezone'           => $timezone,
                    'duration_minutes'   => $durationStr,
                    'notes'              => $notes,
                    'status'             => 'booked',
                    'guest_name'         => $guestName,
                    'guest_email'        => $guestEmail,
                    'guest_phone'        => $guestPhone,
                    'cal_link'           => $locationUrl,
                ];
                $bookingId = (int)$bookingModel->insert($booked);

                $assessmentStatus = $statusModel->where('name', 'Assessment booked')->first();
                if (!$assessmentStatus) {
                    $assessmentStatus = $statusModel->where('name LIKE', '%Assessment%booked%')->first();
                }
                if ($assessmentStatus) {
                    $leadModel->update($leadId, ['status_id' => (int)$assessmentStatus['id']]);
                }

                $displayDate = $startsAtStr . ' UTC';
                try {
                    $tzStr = (string)($schedule['timezone'] ?? '');
                    if ($tzStr !== '') {
                        $d = new \DateTimeImmutable($startsAtStr, new \DateTimeZone('UTC'));
                        $d = $d->setTimezone(new \DateTimeZone($tzStr));
                        $displayDate = $d->format('l, F j, Y g:ia') . ' ' . $tzStr;
                    }
                } catch (\Throwable $e) {
                    $displayDate = $startsAtStr . ' UTC';
                }
                $userName = '';
                $um = new UserModel();
                $actor = $um->find($adminId);
                if ($actor) {
                    $userName = (string)($actor['full_name'] ?? $actor['username'] ?? '');
                }
                $assignedName = '';
                if ($assignedAgentPost !== $adminId) {
                    $ass = $um->find($assignedAgentPost);
                    if ($ass) {
                        $assignedName = ' (assigned: ' . (string)($ass['full_name'] ?? $ass['username'] ?? '') . ')';
                    }
                }
                $noteModel->insert([
                    'lead_id'    => $leadId,
                    'user_id'    => $adminId,
                    'note_text'  => 'Booked Assessment: ' . $displayDate . ' by ' . $userName . $assignedName . ($notes ? ' | Notes: ' . $notes : ''),
                    'created_at' => date('Y-m-d H:i:s'),
                ]);

                $db->transComplete();

                if ($db->transStatus() === false) {
                    $error = 'Failed to save booking. Please try again.';
                } else {
                    try {
                        if ((bool)$smtpConfigModel->getValue('notify_enabled', '1')) {
                            $mailer = new BookingMailer();
                            $fullBooking = $bookingModel->find($bookingId);
                            $notifyAgentIds = (array) json_decode((string)$smtpConfigModel->getValue('notify_agent_ids', '[]'), true);
                            $notifyAdminIds = (array) json_decode((string)$smtpConfigModel->getValue('notify_admin_ids', '[]'), true);
                            $leadEnabled = (bool)$smtpConfigModel->getValue('notify_lead_enabled', '0');
                            $mailer->notifyBookingCreated($fullBooking, $notifyAgentIds, $notifyAdminIds, $leadEnabled);
                        }
                    } catch (\Throwable $e) {
                        log_message('error', 'Admin leadBook notify created failed: ' . $e->getMessage());
                    }
                    session()->setFlashdata('success', 'Assessment booked successfully.');
                    return redirect()->to('/admin/leads/view/' . $leadId);
                }
            }
        }

        $schedules = $scheduleModel->where('is_active', 1)->orderBy('title', 'ASC')->findAll();
        $bookings  = $bookingModel->getForLead($leadId);
        $userModel = new UserModel();
        $agents    = $userModel->whereIn('role', ['agent', 'admin'])->orderBy('full_name', 'ASC')->findAll();
        $defaultGuestName  = (string)($lead['name'] ?? '');
        $leadDisplayName   = $defaultGuestName !== '' ? $defaultGuestName : 'Lead #' . $leadId;

        return view('layout/app', [
            'title'     => 'Book Assessment - ' . $leadDisplayName,
            'pageTitle' => 'Book Assessment',
            'content'   => view('agent/lead_booking_form', [
                'lead'              => $lead,
                'leadId'            => $leadId,
                'schedules'         => $schedules,
                'bookings'          => $bookings,
                'agents'            => $agents,
                'defaultGuestName'  => $defaultGuestName,
                'error'             => $error,
                'bookEndpoint'      => site_url('/admin/leads/book/' . $leadId),
                'leadViewUrl'       => site_url('/admin/leads/view/' . $leadId),
                'slotsEndpoint'     => site_url('/admin/bookings/slots/'),
                'bookingsViewUrl'   => site_url('/admin/bookings/view/'),
            ]),
        ]);
    }

    public function bookingList()
    {
        $scheduleModel = new BookingScheduleModel();
        $bookingModel  = new BookingModel();
        $userModel     = new UserModel();

        $reqScheduleId = $this->request->getGet('scheduleId');
        $reqAgentId    = $this->request->getGet('agentId');
        $reqFrom       = $this->request->getGet('from');
        $reqTo         = $this->request->getGet('to');
        $reqStatus     = $this->request->getGet('status');

        $scheduleId = $reqScheduleId !== null && $reqScheduleId !== '' ? (int)$reqScheduleId : null;
        $agentId    = $reqAgentId    !== null && $reqAgentId    !== '' ? (int)$reqAgentId    : null;
        $from       = is_string($reqFrom) && $reqFrom !== '' ? $reqFrom : null;
        $to         = is_string($reqTo)   && $reqTo   !== '' ? $reqTo   : null;
        $status     = is_string($reqStatus) && $reqStatus !== '' && in_array($reqStatus, ['booked','cancelled','rescheduled','pending','completed'], true) ? $reqStatus : null;

        $schedules = $scheduleModel->orderBy('title', 'ASC')->findAll();
        $agents    = $userModel->where('role', 'agent')->orderBy('full_name', 'ASC')->findAll();

        $db = \Config\Database::connect();
        $builder = $db->table('bookings b')
            ->select('b.*, s.title as schedule_title, s.timezone as tz,
                      l.name as lead_name, l.name as lead_first, NULL as lead_last, l.email as lead_email, l.phone as lead_phone,
                      u.full_name as agent_name')
            ->join('booking_schedules s', 's.id = b.schedule_id', 'left')
            ->join('leads l', 'l.id = b.lead_id', 'left')
            ->join('users u', 'u.id = b.assigned_agent_id', 'left');

        if ($scheduleId !== null) $builder->where('b.schedule_id', $scheduleId);
        if ($agentId    !== null) $builder->where('b.assigned_agent_id', $agentId);
        if ($from       !== null) $builder->where("DATE(b.starts_at) >=", $from);
        if ($to         !== null) $builder->where("DATE(b.starts_at) <=", $to);
        if ($status     !== null) $builder->where('b.status', $status);

        $bookings = $builder->orderBy('b.starts_at', 'DESC')->limit(500)->get()->getResultArray();

        $totalBooked     = 0;
        $totalCancelled  = 0;
        $totalRevenue    = 0;
        $upcoming7d      = 0;
        $nowTs           = time();
        $sevenDaysLater  = $nowTs + (7 * 86400);
        foreach ($bookings as $b) {
            $bs = $b['status'] ?? '';
            if ($bs === 'booked')    $totalBooked++;
            if ($bs === 'cancelled') $totalCancelled++;
            $st = !empty($b['starts_at']) ? strtotime($b['starts_at']) : null;
            if ($st !== null && $st >= $nowTs && $st <= $sevenDaysLater && $bs === 'booked') $upcoming7d++;
        }
        $totalLeadsRaw = $db->table('leads')->countAllResults();
        $leadRatePct = $totalLeadsRaw > 0 ? (($totalBooked / $totalLeadsRaw) * 100) : 0;

        $analytics = [
            'total_booked'    => $totalBooked,
            'total_cancelled' => $totalCancelled,
            'total_revenue'   => 0,
            'lead_rate_pct'   => $leadRatePct,
            'upcoming_7d'     => $upcoming7d,
        ];

        return view('layout/app', [
            'title'     => 'Bookings',
            'pageTitle' => 'Bookings',
            'content'   => view('admin/bookings_list', [
                'bookings'   => $bookings,
                'schedules'  => $schedules,
                'agents'     => $agents,
                'scheduleId' => $scheduleId,
                'agentId'    => $agentId,
                'from'       => (string)$from,
                'to'         => (string)$to,
                'status'     => (string)$status,
                'analytics'  => $analytics,
            ]),
        ]);
    }

    public function bookingView($id)
    {
        $id = (int) $id;
        $db = \Config\Database::connect();

        $booking = $db->table('bookings b')
            ->select('b.*, s.title as schedule_title, s.timezone as tz,
                      l.name as lead_name, l.name as lead_first, NULL as lead_last, l.email as lead_email, l.phone as lead_phone,
                      u.full_name as agent_name, u.email as agent_email')
            ->join('booking_schedules s', 's.id = b.schedule_id', 'left')
            ->join('leads l', 'l.id = b.lead_id', 'left')
            ->join('users u', 'u.id = b.assigned_agent_id', 'left')
            ->where('b.id', $id)
            ->get()
            ->getRowArray();

        if (!$booking) {
            session()->setFlashdata('error', 'Booking not found.');
            return redirect()->to(site_url('/admin/bookings'));
        }

        return view('layout/app', [
            'title'     => 'View Booking',
            'pageTitle' => 'Booking #' . $id,
            'content'   => view('admin/booking_view', ['booking' => $booking]),
        ]);
    }

    public function bookingCancel($id)
    {
        if ($this->request->getMethod(true) !== 'POST' || !$this->request->isAJAX()) {
            return $this->response->setStatusCode(405)->setJSON(['ok' => false, 'message' => 'Method not allowed.']);
        }
        $id = (int) $id;
        $bookingModel = new BookingModel();
        $booking = $bookingModel->find($id);
        if (!$booking) {
            return $this->response->setJSON(['ok' => false, 'message' => 'Booking not found.']);
        }
        if (($booking['status'] ?? '') === 'cancelled') {
            return $this->response->setJSON(['ok' => true, 'message' => 'Booking is already cancelled.']);
        }

        try {
            $bookingModel->update($id, ['status' => 'cancelled']);
        } catch (Throwable $e) {
            return $this->response->setJSON(['ok' => false, 'message' => 'Failed to update status: ' . $e->getMessage()]);
        }

        $notifyMsg = '';
        try {
            $mailer = new BookingMailer();
            $smtp = new BookingSmtpConfigModel();
            $notifyAgentIds = json_decode((string)($smtp->getValue('notify_agent_ids', '[]') ?? '[]'), true) ?: [];
            $notifyAdminIds = json_decode((string)($smtp->getValue('notify_admin_ids', '[]') ?? '[]'), true) ?: [];
            $notifyLeadEnabled = (bool) $smtp->getValue('notify_lead_enabled', '0');
            $agent = null;
            if (!empty($booking['assigned_agent_id'])) {
                $u = new UserModel();
                $agent = $u->find((int)$booking['assigned_agent_id']);
            }
            $lead = null;
            if (!empty($booking['lead_id'])) {
                $lm = new LeadModel();
                $lead = $lm->find((int)$booking['lead_id']);
            }
            $res = $mailer->notifyBookingCancelled($booking, $lead, $agent, null, $notifyAgentIds, $notifyAdminIds, $notifyLeadEnabled);
            $notifyMsg = !empty($res['ok']) ? ' Notifications sent.' : ' (Notification: ' . ($res['users_msg'] ?? 'issue') . ')';
        } catch (Throwable $e) {
            $notifyMsg = ' (Notification issue: ' . $e->getMessage() . ')';
            log_message('error', 'bookingCancel notify: ' . $e->getMessage());
        }

        return $this->response->setJSON(['ok' => true, 'message' => 'Booking cancelled successfully.' . $notifyMsg]);
    }

    public function bookingSMTP()
    {
        $smtpModel = new BookingSmtpConfigModel();
        $userModel = new UserModel();

        $agents = $userModel->where('role', 'agent')->orderBy('full_name', 'ASC')->findAll();
        $admins = $userModel->where('role', 'admin')->orderBy('full_name', 'ASC')->findAll();

        if ($this->request->getMethod(true) === 'POST') {
            $action = (string) ($this->request->getPost('action') ?? '');

            if ($action === 'save') {
                $fields = [
                    'smtp_host'       => trim((string) $this->request->getPost('smtp_host')),
                    'smtp_port'       => trim((string) $this->request->getPost('smtp_port')),
                    'smtp_user'       => trim((string) $this->request->getPost('smtp_user')),
                    'smtp_pass'       => trim((string) $this->request->getPost('smtp_pass')),
                    'smtp_from_email' => trim((string) $this->request->getPost('smtp_from_email')),
                    'smtp_from_name'  => trim((string) $this->request->getPost('smtp_from_name')),
                    'smtp_secure'     => trim((string) $this->request->getPost('smtp_encryption')),
                ];
                $authVal = $this->request->getPost('smtp_auth') ? '1' : '0';
                foreach ($fields as $k => $v) {
                    $smtpModel->setValue($k, $v);
                }
                $smtpModel->setValue('smtp_auth', $authVal);
                session()->setFlashdata('success', 'SMTP settings saved.');
                return redirect()->to(site_url('/admin/booking-smtp'));
            }

            if ($action === 'save_notifications') {
                $smtpModel->setValue('notify_enabled',      $this->request->getPost('enable_notifications') ? '1' : '0');
                $smtpModel->setValue('notify_lead_enabled', $this->request->getPost('enable_lead_notification') ? '1' : '0');

                $agentIdsRaw = $this->request->getPost('notify_agent_ids') ?? [];
                if (!is_array($agentIdsRaw)) $agentIdsRaw = [];
                $adminIdsRaw = $this->request->getPost('notify_admin_ids') ?? [];
                if (!is_array($adminIdsRaw)) $adminIdsRaw = [];

                $smtpModel->setValue('notify_agent_ids',      json_encode(array_values(array_filter(array_map('intval', $agentIdsRaw)))));
                $smtpModel->setValue('notify_admin_ids',      json_encode(array_values(array_filter(array_map('intval', $adminIdsRaw)))));
                $smtpModel->setValue('notify_lead_from_email', trim((string) $this->request->getPost('lead_from_email')));
                $smtpModel->setValue('booking_default_location_url', trim((string) $this->request->getPost('booking_location_url')));
                $smtpModel->setValue('email_logo_url',       trim((string) $this->request->getPost('email_logo_url')));
                session()->setFlashdata('success', 'Notification settings saved.');
                return redirect()->to(site_url('/admin/booking-smtp'));
            }
        }

        $config = method_exists($smtpModel, 'getAll') ? $smtpModel->getAll() : [];

        return view('layout/app', [
            'title'     => 'Booking SMTP',
            'pageTitle' => 'SMTP & Notifications',
            'content'   => view('admin/booking_smtp', [
                'config' => $config,
                'agents' => $agents,
                'admins' => $admins,
            ]),
        ]);
    }

    public function bookingSMTPTestConnection()
    {
        if ($this->request->getMethod(true) !== 'POST' || !$this->request->isAJAX()) {
            return $this->response->setStatusCode(405)->setJSON(['ok' => false, 'message' => 'Method not allowed.']);
        }
        $cfg = [
            'smtp_host'       => trim((string) $this->request->getPost('smtp_host')),
            'smtp_port'       => trim((string) $this->request->getPost('smtp_port')),
            'smtp_user'       => trim((string) $this->request->getPost('smtp_user')),
            'smtp_pass'       => trim((string) $this->request->getPost('smtp_pass')),
            'smtp_from_email' => trim((string) $this->request->getPost('smtp_from_email')),
            'smtp_from_name'  => trim((string) $this->request->getPost('smtp_from_name')),
            'smtp_secure'     => trim((string) $this->request->getPost('smtp_encryption')),
            'smtp_auth'       => $this->request->getPost('smtp_auth') ? '1' : '0',
        ];
        if ($cfg['smtp_host'] === '') {
            return $this->response->setJSON(['ok' => false, 'message' => 'SMTP host is required.']);
        }
        try {
            $mailer = new BookingMailer($cfg);
            $res = $mailer->testConnection();
            return $this->response->setJSON([
                'ok'      => !empty($res['ok']),
                'message' => (string)($res['msg'] ?? ''),
            ]);
        } catch (Throwable $e) {
            return $this->response->setJSON(['ok' => false, 'message' => $e->getMessage() ?: 'SMTP connection error.']);
        }
    }

    public function bookingSMTPTestMail()
    {
        if ($this->request->getMethod(true) !== 'POST' || !$this->request->isAJAX()) {
            return $this->response->setStatusCode(405)->setJSON(['ok' => false, 'message' => 'Method not allowed.']);
        }
        $userId = (int) session()->get('user_id');
        $user   = null;
        if ($userId > 0) {
            $um = new UserModel();
            $user = $um->find($userId);
        }
        $toEmail = $user['email'] ?? '';
        if ($toEmail === '') {
            return $this->response->setJSON(['ok' => false, 'message' => 'Current admin user has no email address configured.']);
        }
        $cfg = [
            'smtp_host'       => trim((string) $this->request->getPost('smtp_host')),
            'smtp_port'       => trim((string) $this->request->getPost('smtp_port')),
            'smtp_user'       => trim((string) $this->request->getPost('smtp_user')),
            'smtp_pass'       => trim((string) $this->request->getPost('smtp_pass')),
            'smtp_from_email' => trim((string) $this->request->getPost('smtp_from_email')),
            'smtp_from_name'  => trim((string) $this->request->getPost('smtp_from_name')),
            'smtp_secure'     => trim((string) $this->request->getPost('smtp_encryption')),
            'smtp_auth'       => $this->request->getPost('smtp_auth') ? '1' : '0',
        ];
        try {
            $mailer = new BookingMailer($cfg);
            $res = $mailer->sendTestMail($toEmail);
            return $this->response->setJSON([
                'ok'      => !empty($res['ok']),
                'message' => (string)($res['msg'] ?? ''),
            ]);
        } catch (Throwable $e) {
            return $this->response->setJSON(['ok' => false, 'message' => $e->getMessage() ?: 'Mailer error.']);
        }
    }
}
