<?php

namespace App\Controllers;

use App\Libraries\CloudTalkService;
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\DocumentModel;
use App\Models\LeadCallModel;
use App\Models\LeadCallRecordingModel;
use App\Models\LeadCallTranscriptModel;
use App\Models\LeadModel;
use App\Models\LeadStatusModel;
use App\Models\LeadCustomFieldModel;
use App\Models\LeadCustomValueModel;
use App\Models\LeadNoteModel;
use App\Models\QuizAttemptModel;
use App\Models\QuizAttemptQuestionModel;
use App\Models\QuizSettingModel;
use App\Models\UserModel;
use App\Models\VapiAssistantBindingModel;
use App\Models\VapiCallLogModel;
use App\Models\VapiCallScoreModel;
use App\Models\VapiConfigModel;
use Throwable;

class Agent extends BaseController
{
    protected $vapiConfigModel;
    protected $bindingModel;
    protected $callLogModel;
    protected $callScoreModel;
    protected $vapiService;
    protected $documentModel;

    public function __construct()
    {
        $this->vapiConfigModel = new VapiConfigModel();
        $this->bindingModel = new VapiAssistantBindingModel();
        $this->callLogModel = new VapiCallLogModel();
        $this->callScoreModel = new VapiCallScoreModel();
        $this->vapiService = new VapiService();
        $this->documentModel = new DocumentModel();
    }
    public function dashboard()
    {
        $docModel = new DocumentModel();
        $leadModel = new LeadModel();
        $user = $this->currentUser();
        $userId = $user['user_id'] ?? null;

        $totalDocs = $docModel->where('visibility', 'agents')->countAllResults();
        $leadsAssigned = $userId ? $leadModel->where('assigned_to', $userId)->countAllResults() : 0;
        $totalLeads = $leadModel->countAll();
        $recentDocs = $docModel
            ->where('visibility', 'agents')
            ->orderBy('created_at', 'DESC')
            ->limit(5)
            ->findAll();

        return view('agent/dashboard', [
            'totalDocs'      => $totalDocs,
            'leadsAssigned'  => $leadsAssigned,
            'totalLeads'     => $totalLeads,
            'recentDocs'     => $recentDocs,
        ]);
    }

    public function documents()
    {
        $categoryModel = new CategoryModel();
        $docModel = new DocumentModel();

        $categories = $categoryModel->orderBy('sort_order', 'ASC')->findAll();
        $documents = $docModel
            ->select('documents.*, categories.name as category_name')
            ->join('categories', 'categories.id = documents.category_id', 'left')
            ->where('documents.visibility', 'agents')
            ->orderBy('documents.created_at', 'DESC')
            ->findAll();

        return view('agent/documents', [
            'categories'       => $categories,
            'documents'        => $documents,
            'selectedCategory' => null,
        ]);
    }

    public function documentsByCategory($categoryId)
    {
        $categoryModel = new CategoryModel();
        $docModel = new DocumentModel();

        $categories = $categoryModel->orderBy('sort_order', 'ASC')->findAll();
        $documents = $docModel
            ->select('documents.*, categories.name as category_name')
            ->join('categories', 'categories.id = documents.category_id', 'left')
            ->where('documents.category_id', $categoryId)
            ->where('documents.visibility', 'agents')
            ->orderBy('documents.created_at', 'DESC')
            ->findAll();

        return view('agent/documents', [
            'categories'       => $categories,
            'documents'        => $documents,
            'selectedCategory' => (int) $categoryId,
        ]);
    }

    public function viewDocument($id)
    {
        $docModel = new DocumentModel();
        $categoryModel = new CategoryModel();

        $document = $docModel->getWithCategory((int) $id);
        if (!$document) {
            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
        }
        $docVis = strtolower(trim((string) ($document['visibility'] ?? 'agents')));
        if (!in_array($docVis, ['agents'], true)) {
            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
        }

        $categories = $categoryModel->orderBy('sort_order', 'ASC')->findAll();
        $needsSummary = empty($document['summary']);

        return view('agent/document_view', [
            'document'    => $document,
            'categories'  => $categories,
            'needsSummary' => $needsSummary,
        ]);
    }

    public function leads()
    {
        $leadModel = new LeadModel();
        $statusModel = new LeadStatusModel();
        $user = $this->currentUser();
        $userId = $user['user_id'] ?? null;

        $statusId = $this->request->getGet('status_id');

        $builder = $leadModel->builder();
        $builder->select('leads.*, lead_statuses.name as status_name, lead_statuses.color as status_color, users.full_name as assigned_name');
        $builder->join('lead_statuses', 'lead_statuses.id = leads.status_id', 'left');
        $builder->join('users', 'users.id = leads.assigned_to', 'left');

        if ($userId) {
            $builder->groupStart()
                ->where('leads.assigned_to', $userId)
                ->orWhere('leads.assigned_to IS NULL', null, false)
                ->groupEnd();
        }

        if ($statusId) {
            $builder->where('leads.status_id', $statusId);
        }

        $builder->orderBy('leads.created_at', 'DESC');
        $leads = $builder->get()->getResultArray();

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

        return view('agent/leads', [
            'leads'    => $leads,
            'statuses' => $statuses,
        ]);
    }

    public function viewLead($id)
    {
        $leadModel = new LeadModel();
        $customFieldModel = new LeadCustomFieldModel();
        $customValueModel = new LeadCustomValueModel();
        $noteModel = new LeadNoteModel();
        $callModel = new LeadCallModel();
        $callTranscriptModel = new LeadCallTranscriptModel();
        $callRecordingModel = new LeadCallRecordingModel();
        $bookingModel = new BookingModel();

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

        $user = $this->currentUser();
        $userId = (int) ($user['user_id'] ?? 0);
        $role   = (string) ($user['role'] ?? 'agent');

        if ($role !== 'admin') {
            $assignedTo = (int) ($lead['assigned_to'] ?? 0);
            if ($assignedTo !== 0 && $assignedTo !== $userId) {
                session()->setFlashdata('error', 'You do not have permission to view this lead.');
                return redirect()->to(site_url('/agent/leads'));
            }
        }

        $customFields = $customFieldModel->orderBy('sort_order', 'ASC')->findAll();
        $customValues = $customValueModel->getByLeadId((int) $id);
        $notes = $noteModel->getByLeadId((int) $id);
        $bookings = $bookingModel->getForLead((int) $id);

        $rawCalls = $callModel->getForLead((int) $id);
        $calls = [];
        foreach ($rawCalls as $c) {
            $cid = (int) ($c['id'] ?? 0);
            $calls[] = [
                'call'        => $c,
                'transcript'  => $callTranscriptModel->findByCallId($cid),
                'recording'   => $callRecordingModel->findByCallId($cid),
            ];
        }

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

        return view('agent/lead_view', [
            'lead'                => $lead,
            'customFields'        => $customFields,
            'customValues'        => $customValues,
            'notes'               => $notes,
            'calls'               => $calls,
            'bookings'            => $bookings,
            'cloudtalkConfigured' => $cloudtalkConfigured,
            'syncCallsUrl'        => site_url('/agent/leads/sync-calls/' . (int)$id),
            'bookUrl'             => site_url('/agent/leads/book/' . (int)$id),
        ]);
    }

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

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

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

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

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

    public function syncLeadCalls($id)
    {
        $id = (int) $id;
        $leadModel = new LeadModel();
        $userModel = new \App\Models\UserModel();
        $callModel = new LeadCallModel();
        $callRecordingModel = new LeadCallRecordingModel();

        $lead = $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('/agent/leads');
        }

        $user = $this->currentUser();
        $userId = (int) ($user['user_id'] ?? 0);
        $role = (string) ($user['role'] ?? 'agent');
        $assignedTo = (int) ($lead['assigned_to'] ?? 0);
        if ($role !== 'admin' && $assignedTo !== 0 && $assignedTo !== $userId) {
            if ($this->request->isAJAX()) {
                return $this->response->setStatusCode(403)->setJSON(['ok' => false, 'error' => 'You are not assigned to this lead.']);
            }
            session()->setFlashdata('error', 'You are not assigned to this lead.');
            return redirect()->to('/agent/leads/view/' . $id);
        }

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

            $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);
            }

            $fetched = $svc->listCalls([
                'limit'     => 1000,
                'date_from' => $dateFrom,
            ]);
            $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 = $callModel->findByCloudTalkCallId($callIdStr);
                    if ($existing !== null && (int) ($existing['lead_id'] ?? 0) === $id) {
                        $matchedThis = true;
                    }
                }
                if (!$matchedThis && $digits !== '' && ($a === $digits || $b === $digits)) {
                    $matchedThis = true;
                }
                if (!$matchedThis) {
                    $skipped++;
                    continue;
                }
                $matched++;

                $ctUserId = (int) ($c['user_id'] ?? 0);
                $agentEmail = '';
                if ($ctUserId > 0) {
                    try {
                        $u = $userModel->find($ctUserId);
                        if ($u) $agentEmail = (string) ($u['email'] ?? '');
                    } catch (\Throwable $e) {}
                }
                if ($agentEmail === '' && $assignedTo > 0) {
                    $assignee = $userModel->find($assignedTo);
                    if ($assignee) $agentEmail = (string) ($assignee['email'] ?? '');
                }

                $payload = [
                    'lead_id'              => $id,
                    'agent_id'             => $ctUserId > 0 ? $ctUserId : ($assignedTo > 0 ? $assignedTo : 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'               => $callModel->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 = $callIdStr !== '' ? $callModel->findByCloudTalkCallId($callIdStr) : null;
                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 = $callModel->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) {
                        unset($payload['agent_id']);
                    }
                    $callModel->update($existingId, $payload);
                    $callLocalId = $existingId;
                    $updated++;
                } else {
                    $callLocalId = (int)$callModel->insert($payload);
                    $inserted++;
                }
                if ($callLocalId <= 0) {
                    $errors[] = 'Failed to save call id=' . $callIdStr;
                    continue;
                }

                $recUrl = trim((string) ($c['recording_link'] ?? ''));
                if ($recUrl !== '') {
                    try {
                        $existingRec = $callRecordingModel->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) {
                            $callRecordingModel->insert($recPayload);
                            $recordings++;
                        } else {
                            if (empty($existingRec['recording_url']) || $existingRec['recording_url'] !== $recUrl) {
                                $callRecordingModel->update((int)$existingRec['id'], $recPayload);
                            }
                        }
                    } catch (\Throwable $eRec) {
                        $errors[] = 'Recording save failed: ' . $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'];

                        try {
                            $transcriptModel = new \App\Models\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 $eTr) {
                            $errors[] = 'Transcript save failed ' . $callIdStr . ': ' . $eTr->getMessage();
                        }
                    } 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('/agent/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('/agent/leads/view/' . $id);
        }
    }

    public function quizzes()
    {
        $user = $this->currentUser();
        $userId = (int)($user['user_id'] ?? 0);

        $settingModel = new QuizSettingModel();
        $attemptModel = new QuizAttemptModel();

        $quizList = $settingModel
            ->where('is_active', 1)
            ->orderBy('created_at', 'DESC')
            ->findAll();

        $docModel = new DocumentModel();
        $docsById = [];
        foreach ($docModel->findAll() as $d) $docsById[(int)($d['id'] ?? 0)] = $d;

        $out = [];
        foreach ($quizList as $q) {
            $ids = !empty($q['selected_document_ids']) ? (array)json_decode((string)$q['selected_document_ids'], true) : [];
            if (!is_array($ids)) $ids = [];
            $ids = array_values(array_filter(array_map('intval', $ids), static fn($x) => $x > 0));
            $q['docIds'] = $ids;
            $q['docNames'] = [];
            foreach ($ids as $did) if (isset($docsById[$did])) $q['docNames'][] = $docsById[$did]['title'] ?? "Doc #{$did}";
            $myAttempts = $attemptModel
                ->where('quiz_setting_id', (int)$q['id'])
                ->where('user_id', $userId)
                ->orderBy('created_at', 'DESC')
                ->findAll();
            $q['my_attempts'] = $myAttempts;
            $q['my_best_score'] = null;
            $q['my_last_score'] = null;
            $q['attempt_count'] = count($myAttempts);
            if ($myAttempts !== []) {
                $best = null; $last = null;
                foreach ($myAttempts as $ma) {
                    if ($ma['score_pct'] !== null && $ma['status'] === 'completed') {
                        $s = (float)$ma['score_pct'];
                        if ($best === null || $s > $best) $best = $s;
                    }
                }
                $lastRow = $myAttempts[0];
                if ($lastRow['score_pct'] !== null) $last = (float)$lastRow['score_pct'];
                $q['my_best_score'] = $best;
                $q['my_last_score'] = $last;
            }
            $out[] = $q;
        }

        return view('agent/quizzes', ['quizzes' => $out]);
    }

    public function quizStart($quizSettingId)
    {
        $user = $this->currentUser();
        $userId = (int)($user['user_id'] ?? 0);
        if ($userId <= 0) return redirect()->to('/login');

        $settingModel = new QuizSettingModel();
        $setting = $settingModel->find((int)$quizSettingId);
        if (empty($setting) || empty($setting['is_active'])) {
            session()->setFlashdata('error', 'Quiz not found or not active.');
            return redirect()->to('/agent/quizzes');
        }

        $docIds = !empty($setting['selected_document_ids']) ? (array)json_decode((string)$setting['selected_document_ids'], true) : [];
        if (!is_array($docIds)) $docIds = [];
        $docIds = array_values(array_filter(array_map('intval', $docIds), static fn($x) => $x > 0));
        if ($docIds === []) {
            session()->setFlashdata('error', 'Quiz has no documents assigned. Ask admin to add trained documents first.');
            return redirect()->to('/agent/quizzes');
        }

        $attemptModel = new QuizAttemptModel();
        $questionModel = new QuizAttemptQuestionModel();
        $prevAttempts = $attemptModel
            ->where('quiz_setting_id', (int)$setting['id'])
            ->where('user_id', $userId)
            ->findColumn('id') ?: [];
        $nextAttemptNo = count($prevAttempts) + 1;

        $excludeSigs = $questionModel->usedQuestionSignatures((int)$setting['id'], $userId);
        $need = max(1, (int)($setting['question_count'] ?? 5));
        $timer = max(10, (int)($setting['question_timer_seconds'] ?? 60));
        $type = in_array($setting['question_type'], ['multiple_choice', 'fill_up'], true) ? $setting['question_type'] : 'multiple_choice';

        try {
            $svc = new QuizGeneratorService();
            $generated = $svc->generate($docIds, $need, $type, $excludeSigs);
        } catch (\Throwable $e) {
            session()->setFlashdata('error', 'Quiz generator error: ' . $e->getMessage());
            return redirect()->to('/agent/quizzes');
        }
        if ($generated === []) {
            session()->setFlashdata('error', 'Could not generate questions. Try again or ask admin to check the configured documents.');
            return redirect()->to('/agent/quizzes');
        }
        shuffle($generated);

        $now = date('Y-m-d H:i:s');
        $attemptId = (int)$attemptModel->insert([
            'quiz_setting_id'     => (int)$setting['id'],
            'user_id'             => $userId,
            'attempt_no'          => $nextAttemptNo,
            'status'              => 'in_progress',
            'started_at'          => $now,
            'total_questions'     => count($generated),
            'answered_count'      => 0,
            'correct_count'       => 0,
            'raw_questions_json'  => json_encode($generated, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
        ]);
        if ($attemptId <= 0) {
            session()->setFlashdata('error', 'Failed to create quiz attempt.');
            return redirect()->to('/agent/quizzes');
        }

        $qIndex = 0;
        foreach ($generated as $g) {
            $options = !empty($g['options']) && is_array($g['options']) ? json_encode($g['options'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null;
            $questionModel->insert([
                'quiz_attempt_id' => $attemptId,
                'question_index'  => $qIndex,
                'question_text'   => (string)($g['question_text'] ?? ''),
                'question_type'   => $type,
                'options_json'    => $options,
                'correct_answer'  => (string)($g['correct_answer'] ?? ''),
                'timer_seconds'   => $timer,
                'ai_rationale'    => (string)($g['rationale'] ?? ''),
            ]);
            $qIndex++;
        }

        return redirect()->to('/agent/quizzes/take/' . $attemptId);
    }

    public function quizTake($attemptId)
    {
        $user = $this->currentUser();
        $userId = (int)($user['user_id'] ?? 0);
        if ($userId <= 0) return redirect()->to('/login');

        $attemptModel = new QuizAttemptModel();
        $attempt = $attemptModel->getWithQuestions((int)$attemptId);
        if (!$attempt || (int)($attempt['user_id'] ?? 0) !== $userId) {
            session()->setFlashdata('error', 'Attempt not found.');
            return redirect()->to('/agent/quizzes');
        }
        if (($attempt['status'] ?? '') === 'completed') {
            return redirect()->to('/agent/quizzes/result/' . $attempt['id']);
        }

        $settingModel = new QuizSettingModel();
        $setting = $settingModel->find((int)($attempt['quiz_setting_id'] ?? 0));

        $questions = $attempt['questions'] ?? [];
        $frontQs = [];
        foreach ($questions as $q) {
            $opts = !empty($q['options_json']) ? (array)json_decode((string)$q['options_json'], true) : [];
            $frontQs[] = [
                'id'             => (int)$q['id'],
                'index'          => (int)$q['question_index'],
                'text'           => (string)$q['question_text'],
                'type'           => (string)$q['question_type'],
                'options'        => is_array($opts) ? array_values($opts) : [],
                'timer_seconds'  => (int)($q['timer_seconds'] ?? 60),
                'answered'       => !empty($q['answered']),
                'timed_out'      => !empty($q['timed_out']),
                'user_answer'    => (string)($q['user_answer'] ?? ''),
                'question_start_ts' => null,
            ];
        }

        return view('agent/quiz_take', [
            'attempt'   => $attempt,
            'setting'   => $setting,
            'questions' => $frontQs,
            'total'     => count($frontQs),
        ]);
    }

    public function quizAnswerAjax()
    {
        if (!$this->request->isAJAX()) return $this->response->setStatusCode(400)->setJSON(['ok' => false, 'error' => 'ajax-only']);
        $user = $this->currentUser();
        $userId = (int)($user['user_id'] ?? 0);
        if ($userId <= 0) return $this->response->setStatusCode(401)->setJSON(['ok' => false, 'error' => 'unauth']);

        $questionId = (int)$this->request->getPost('question_id');
        $userAnswer = (string)$this->request->getPost('answer');
        $timeSpent  = max(0, (int)$this->request->getPost('time_spent'));
        $timedOut   = (bool)$this->request->getPost('timed_out');

        $questionModel = new QuizAttemptQuestionModel();
        $attemptModel  = new QuizAttemptModel();
        $q = $questionModel->find($questionId);
        if (!$q) return $this->response->setStatusCode(404)->setJSON(['ok' => false, 'error' => 'Question not found']);
        $attempt = $attemptModel->find((int)$q['quiz_attempt_id']);
        if (!$attempt || (int)($attempt['user_id'] ?? 0) !== $userId) return $this->response->setStatusCode(403)->setJSON(['ok' => false, 'error' => 'forbidden']);
        if (($attempt['status'] ?? '') !== 'in_progress') return $this->response->setJSON(['ok' => true, 'already_done' => true, 'status' => $attempt['status']]);
        if (!empty($q['answered'])) return $this->response->setJSON(['ok' => true, 'already_answered' => true]);

        $userAnswer = trim($userAnswer);
        $correctAns = trim((string)($q['correct_answer'] ?? ''));
        $type = (string)($q['question_type'] ?? '');

        if ($timedOut && $userAnswer === '') {
            $isCorrect = false;
            $answered = 0;
            $timedOutFlag = 1;
        } else {
            $answered = 1;
            $timedOutFlag = $timedOut ? 1 : 0;
            if ($type === 'multiple_choice') {
                $userLetter = strtoupper($userAnswer);
                $correctLetter = strtoupper($correctAns);
                $isCorrect = $userLetter !== '' && $userLetter === $correctLetter;
            } else {
                $norm = static function ($s) {
                    $s = preg_replace('/\s+/u', ' ', (string)$s);
                    $s = preg_replace('/[^A-Za-z0-9 ]/u', '', $s);
                    return function_exists('mb_strtolower') ? mb_strtolower(trim($s), 'UTF-8') : strtolower(trim($s));
                };
                $uN = $norm($userAnswer);
                $cN = $norm($correctAns);
                if ($uN === '' || $cN === '') {
                    $isCorrect = false;
                } else {
                    $isCorrect = $uN === $cN;
                    if (!$isCorrect) {
                        similar_text($uN, $cN, $pct);
                        if ($pct >= 88.0) $isCorrect = true;
                    }
                }
            }
        }

        $questionModel->update($questionId, [
            'user_answer'        => $answered ? $userAnswer : null,
            'answered'           => $answered,
            'timed_out'          => $timedOutFlag,
            'is_correct'         => $answered ? ($isCorrect ? 1 : 0) : 0,
            'answered_at'        => date('Y-m-d H:i:s'),
            'time_spent_seconds' => $timeSpent,
        ]);

        return $this->response->setJSON([
            'ok'         => true,
            'is_correct' => (bool)($answered ? $isCorrect : false),
            'answered'   => (bool)$answered,
            'timed_out'  => (bool)$timedOutFlag,
        ]);
    }

    public function quizFinishAjax()
    {
        if (!$this->request->isAJAX()) return $this->response->setStatusCode(400)->setJSON(['ok' => false, 'error' => 'ajax-only']);
        $user = $this->currentUser();
        $userId = (int)($user['user_id'] ?? 0);
        if ($userId <= 0) return $this->response->setStatusCode(401)->setJSON(['ok' => false, 'error' => 'unauth']);
        $attemptId = (int)$this->request->getPost('attempt_id');

        $attemptModel  = new QuizAttemptModel();
        $questionModel = new QuizAttemptQuestionModel();
        $attempt = $attemptModel->find($attemptId);
        if (!$attempt || (int)($attempt['user_id'] ?? 0) !== $userId) return $this->response->setStatusCode(403)->setJSON(['ok' => false, 'error' => 'forbidden']);
        if (($attempt['status'] ?? '') === 'completed') return $this->response->setJSON(['ok' => true, 'redirect' => site_url('/agent/quizzes/result/' . $attemptId)]);

        $settingModel = new QuizSettingModel();
        $setting = $settingModel->find((int)($attempt['quiz_setting_id'] ?? 0));
        $passing = (float)($setting['passing_score_pct'] ?? 70);
        $docIds = !empty($setting['selected_document_ids']) ? (array)json_decode((string)$setting['selected_document_ids'], true) : [];
        if (!is_array($docIds)) $docIds = [];
        $docIds = array_values(array_filter(array_map('intval', $docIds), static fn($x) => $x > 0));

        $questions = $questionModel
            ->where('quiz_attempt_id', $attemptId)
            ->orderBy('question_index', 'ASC')
            ->findAll();
        $total = count($questions);
        $answeredCount = 0;
        $correctCount = 0;
        foreach ($questions as $q) {
            if (!empty($q['answered'])) $answeredCount++;
            if (!empty($q['is_correct'])) $correctCount++;
        }
        $scorePct = $total > 0 ? round(($correctCount / $total) * 100, 2) : 0.0;
        $passed = $scorePct >= $passing;

        $aiQaForFeedback = [];
        foreach ($questions as $q) {
            $aiQaForFeedback[] = [
                'question_text'  => (string)$q['question_text'],
                'correct_answer' => (string)$q['correct_answer'],
                'user_answer'    => (string)($q['user_answer'] ?? ''),
                'is_correct'     => !empty($q['is_correct']),
            ];
        }
        try {
            $svc = new QuizGeneratorService();
            $type = ($setting['question_type'] ?? 'multiple_choice');
            $feedback = $svc->gradeAndSuggestions($aiQaForFeedback, $docIds, (float)$scorePct, $passed, $type);
        } catch (\Throwable $e) {
            $feedback = '';
        }

        $attemptModel->update($attemptId, [
            'status'         => 'completed',
            'completed_at'   => date('Y-m-d H:i:s'),
            'answered_count' => $answeredCount,
            'correct_count'  => $correctCount,
            'score_pct'      => $scorePct,
            'passed'         => $passed ? 1 : 0,
            'ai_feedback'    => $feedback,
        ]);
        return $this->response->setJSON([
            'ok'       => true,
            'redirect' => site_url('/agent/quizzes/result/' . $attemptId),
        ]);
    }

    public function quizResult($attemptId)
    {
        $user = $this->currentUser();
        $userId = (int)($user['user_id'] ?? 0);
        if ($userId <= 0) return redirect()->to('/login');

        $attemptModel  = new QuizAttemptModel();
        $questionModel = new QuizAttemptQuestionModel();
        $attempt = $attemptModel->find((int)$attemptId);
        if (!$attempt || (int)($attempt['user_id'] ?? 0) !== $userId) {
            session()->setFlashdata('error', 'Attempt not found.');
            return redirect()->to('/agent/quizzes');
        }
        $questions = $questionModel
            ->where('quiz_attempt_id', (int)$attemptId)
            ->orderBy('question_index', 'ASC')
            ->findAll();
        $questionsWithOpts = [];
        foreach ($questions as $q) {
            $q['options_arr'] = !empty($q['options_json']) ? (array)json_decode((string)$q['options_json'], true) : [];
            $questionsWithOpts[] = $q;
        }
        $attempt['questions'] = $questionsWithOpts;

        $settingModel = new QuizSettingModel();
        $setting = $settingModel->find((int)($attempt['quiz_setting_id'] ?? 0));
        return view('agent/quiz_result', ['attempt' => $attempt, 'setting' => $setting]);
    }

    public function mockCalls()
    {
        $userId = (int) session()->get('user_id');
        if ($userId <= 0) return redirect()->to('/login');

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

        if ($isAdmin) {
            $bindings = $this->bindingModel->where('is_active', 1)->orderBy('name', 'ASC')->findAll();
        } else {
            $bindings = $this->bindingModel->listForAgent($userId);
        }

        $allDocIds = [];
        foreach ($bindings as $b) {
            $docIds = !empty($b['selected_doc_ids']) ? (array)json_decode((string)$b['selected_doc_ids'], true) : [];
            if (is_array($docIds)) {
                foreach ($docIds as $did) {
                    $didInt = (int)$did;
                    if ($didInt > 0) $allDocIds[$didInt] = true;
                }
            }
        }

        $docMap = [];
        if (!empty($allDocIds)) {
            $docRows = $this->documentModel->whereIn('id', array_keys($allDocIds))->findAll();
            foreach ($docRows as $d) {
                $docMap[(int)$d['id']] = (string)($d['title'] ?? "Doc #{$d['id']}");
            }
        }

        $out = [];
        $userModel = null;
        foreach ($bindings as $b) {
            $id = (int)$b['id'];
            $assignedAgentIds = !empty($b['assigned_agent_ids']) ? (array)json_decode((string)$b['assigned_agent_ids'], true) : [];
            if (!is_array($assignedAgentIds)) $assignedAgentIds = [];
            $selectedDocIds = !empty($b['selected_doc_ids']) ? (array)json_decode((string)$b['selected_doc_ids'], true) : [];
            if (!is_array($selectedDocIds)) $selectedDocIds = [];
            $selectedDocIds = array_values(array_filter(array_map('intval', $selectedDocIds), static fn($x) => $x > 0));

            $callsRecentX = $this->callLogModel->getForBinding($id);
            $callCount = count($callsRecentX);

            $docNames = [];
            foreach ($selectedDocIds as $did) {
                $docNames[] = $docMap[$did] ?? "Doc #{$did}";
            }

            $avgScore = null;
            $scoredCount = 0;
            $scoreSum = 0;
            foreach ($callsRecentX as $c) {
                $scoreRow = $this->callScoreModel->findByCallLogId((int)($c['id'] ?? 0));
                if ($scoreRow && isset($scoreRow['score_pct'])) {
                    $scoreSum += (float)$scoreRow['score_pct'];
                    $scoredCount++;
                }
            }
            if ($scoredCount > 0) {
                $avgScore = round($scoreSum / $scoredCount, 1);
            }

            $assignedAgentNames = [];
            if ($isAdmin && !empty($assignedAgentIds)) {
                if ($userModel === null) {
                    $userModel = new UserModel();
                }
                $agentRows = $userModel->whereIn('id', array_values(array_map('intval', $assignedAgentIds)))->findAll();
                foreach ($agentRows as $ar) {
                    $assignedAgentNames[(int)($ar['id'] ?? 0)] = (string)($ar['full_name'] ?? "User #{$ar['id']}");
                }
            }

            $out[] = [
                'binding'               => $b,
                'id'                    => $id,
                'assigned_agent_ids'    => $assignedAgentIds,
                'assigned_agent_names'  => $assignedAgentNames,
                'selected_doc_ids'      => $selectedDocIds,
                'doc_names'             => $docNames,
                'total_calls'           => $callCount,
                'avg_score'             => $avgScore,
                'is_admin_view'         => $isAdmin,
            ];
        }

        return view('agent/mock_calls', [
            'bindings' => $out,
            'docMap'   => $docMap,
        ]);
    }

    public function mockCallLogs($bindingId = null)
    {
        $userId = (int) session()->get('user_id');
        if ($userId <= 0) return redirect()->to('/login');

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

        $bindingIdInt = $bindingId !== null ? (int)$bindingId : 0;

        if ($bindingIdInt > 0) {
            $binding = $this->bindingModel->find($bindingIdInt);
            if (!$binding) {
                throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
            }
            if (!$isAdmin) {
                $agentIds = !empty($binding['assigned_agent_ids']) ? (array)json_decode((string)$binding['assigned_agent_ids'], true) : [];
                if (!is_array($agentIds)) $agentIds = [];
                if (!in_array($userId, $agentIds, true)) {
                    session()->setFlashdata('error', 'You do not have permission to view these call logs.');
                    return redirect()->to(site_url('/agent/mock-calls'));
                }
            }
        }

        if ($isAdmin) {
            $agentBindings = $this->bindingModel->where('is_active', 1)->orderBy('name', 'ASC')->findAll();
        } else {
            $agentBindings = $this->bindingModel->listForAgent($userId);
        }
        $agentBindingIds = [];
        foreach ($agentBindings as $b) {
            $agentBindingIds[(int)$b['id']] = true;
        }

        if ($isAdmin) {
            $builder = $this->callLogModel->db->table('vapi_call_logs vcl')
                ->select('vcl.*, vb.name AS binding_name, vb.vapi_assistant_id, vb.url, u.full_name AS agent_name')
                ->join('vapi_assistant_bindings vb', 'vcl.binding_id = vb.id', 'left')
                ->join('users u', 'u.id = vcl.agent_id', 'left');
            if ($bindingIdInt > 0) {
                $builder->where('vcl.binding_id', $bindingIdInt);
            }
            $builder->orderBy('vcl.started_at', 'DESC');
            $callRows = $builder->get()->getResultArray();
        } else {
            $callRows = $this->callLogModel->getForAgent($userId);
            if ($bindingIdInt > 0) {
                $callRows = array_values(array_filter($callRows, static fn($r) => (int)($r['binding_id'] ?? 0) === $bindingIdInt));
            }
        }

        $scoreRows = [];
        foreach ($callRows as &$c) {
            $cid = (int)($c['id'] ?? 0);
            $score = $this->callScoreModel->findByCallLogId($cid);
            $c['score_pct'] = $score['score_pct'] ?? null;
            $c['pass_threshold_pct'] = $score['pass_threshold_pct'] ?? null;
            $c['breakdown_json'] = $score['breakdown_json'] ?? null;
            $c['ai_feedback'] = $score['ai_feedback'] ?? null;
            $scoreRows[$cid] = $score;
        }
        unset($c);

        usort($callRows, static function ($a, $b) {
            $ta = strtotime((string)($a['started_at'] ?? '')) ?: 0;
            $tb = strtotime((string)($b['started_at'] ?? '')) ?: 0;
            return $tb <=> $ta;
        });

        return view('agent/mock_call_logs', [
            'callRows'    => $callRows,
            'scoreRows'   => $scoreRows,
            'bindingId'   => $bindingIdInt,
            'agentBindings' => $agentBindings,
        ]);
    }

    public function mockCallSync($bindingId = 0)
    {
        $userId = (int) session()->get('user_id');
        if ($userId <= 0) {
            if ($this->request->isAJAX()) {
                return $this->response->setStatusCode(401)->setJSON(['ok' => false, 'error' => 'unauth']);
            }
            return redirect()->to('/login');
        }

        $bindingIdInt = (int)$bindingId;
        if ($bindingIdInt <= 0) {
            if ($this->request->isAJAX()) {
                return $this->response->setStatusCode(400)->setJSON(['ok' => false, 'error' => 'binding id required']);
            }
            session()->setFlashdata('error', 'Binding ID required.');
            return redirect()->to(site_url('/agent/mock-calls'));
        }

        $binding = $this->bindingModel->find($bindingIdInt);
        if (!$binding) {
            if ($this->request->isAJAX()) {
                return $this->response->setStatusCode(404)->setJSON(['ok' => false, 'error' => 'binding not found']);
            }
            session()->setFlashdata('error', 'Binding not found.');
            return redirect()->to(site_url('/agent/mock-calls'));
        }

        $agentIds = !empty($binding['assigned_agent_ids']) ? (array)json_decode((string)$binding['assigned_agent_ids'], true) : [];
        if (!is_array($agentIds)) $agentIds = [];
        $isAdmin = (string) (session()->get('role') ?? '') === 'admin';
        if (!$isAdmin && !in_array($userId, $agentIds, true)) {
            if ($this->request->isAJAX()) {
                return $this->response->setStatusCode(403)->setJSON(['ok' => false, 'error' => 'forbidden']);
            }
            session()->setFlashdata('error', 'You are not assigned to this assistant.');
            return redirect()->to(site_url('/agent/mock-calls'));
        }

        $inserted = 0;
        $updated = 0;
        $total = 0;

        try {
            $assistantId = (string)($binding['vapi_assistant_id'] ?? '');
            $callsResp = $this->vapiService->listCalls(['assistantId' => $assistantId, 'limit' => 100]);
            if (empty($callsResp['ok'])) {
                $msg = $callsResp['msg'] ?? 'Failed to list calls from Vapi.';
                if ($this->request->isAJAX()) {
                    return $this->response->setStatusCode(500)->setJSON(['ok' => false, 'error' => $msg, 'inserted' => 0, 'updated' => 0, 'total' => 0, 'msg' => $msg]);
                }
                session()->setFlashdata('error', $msg);
                return redirect()->to(site_url('/agent/mock-calls'));
            }

            $calls = is_array($callsResp['data'] ?? null) ? $callsResp['data'] : [];
            $total = count($calls);

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

            foreach ($calls as $call) {
                $vapiCallId = (string)($call['id'] ?? '');
                if ($vapiCallId === '') continue;

                $customer = $call['customer'] ?? null;
                $phoneNumber = null;
                if (is_array($customer)) {
                    $phoneNumber = (string)($customer['number'] ?? '');
                }
                if ($phoneNumber === '' || $phoneNumber === null) {
                    $phoneNumber = (string)($call['phoneNumber'] ?? '');
                }
                if ($phoneNumber === '') $phoneNumber = null;

                $transcript = $call['transcript'] ?? null;
                if ($transcript === null || $transcript === '') {
                    $messages = $call['messages'] ?? null;
                    if (is_array($messages)) {
                        $transcript = json_encode($messages, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
                    }
                }
                if (is_array($transcript)) {
                    $transcript = json_encode($transcript, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
                }

                $callSummary = $call['summary'] ?? null;
                if ($callSummary === null || $callSummary === '') {
                    $analysis = $call['analysis'] ?? null;
                    if (is_array($analysis)) {
                        $callSummary = $analysis['summary'] ?? null;
                    }
                }
                if (is_array($callSummary)) {
                    $callSummary = json_encode($callSummary, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
                }

                $payload = [
                    'binding_id'       => $bindingIdInt,
                    'vapi_call_id'     => $vapiCallId,
                    'agent_id'         => $userId,
                    'started_at'       => $to_mysql($call['startedAt'] ?? null),
                    'ended_at'         => $to_mysql($call['endedAt'] ?? null),
                    'duration_seconds' => (int)($call['durationSeconds'] ?? 0),
                    'direction'        => (string)($call['direction'] ?? 'outbound'),
                    'phone_number'     => $phoneNumber,
                    'transcript'       => is_string($transcript) ? $transcript : null,
                    'recording_url'    => !empty($call['recordingUrl']) ? (string)$call['recordingUrl'] : null,
                    'call_summary'     => is_string($callSummary) ? $callSummary : null,
                    'raw_json'         => json_encode($call, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
                ];

                $existing = $this->callLogModel->findByVapiCallId($vapiCallId);
                if ($existing === null) {
                    $this->callLogModel->insert($payload);
                    $inserted++;
                } else {
                    $eid = (int)$existing['id'];
                    if ((int)($existing['binding_id'] ?? 0) !== $bindingIdInt) {
                        $payload['binding_id'] = $bindingIdInt;
                    }
                    $this->callLogModel->update($eid, $payload);
                    $updated++;
                }
            }

            $msg = "Synced {$total} call(s) from Vapi: inserted {$inserted}, updated {$updated}.";
            if ($this->request->isAJAX()) {
                return $this->response->setJSON([
                    'ok'       => true,
                    'inserted' => $inserted,
                    'updated'  => $updated,
                    'total'    => $total,
                    'msg'      => $msg,
                ]);
            }
            session()->setFlashdata('success', $msg);
            return redirect()->to(site_url('/agent/mock-calls'));
        } catch (\Throwable $e) {
            $msg = 'Sync failed: ' . $e->getMessage();
            if ($this->request->isAJAX()) {
                return $this->response->setStatusCode(500)->setJSON([
                    'ok'       => false,
                    'error'    => $e->getMessage(),
                    'inserted' => $inserted,
                    'updated'  => $updated,
                    'total'    => $total,
                    'msg'      => $msg,
                ]);
            }
            session()->setFlashdata('error', $msg);
            return redirect()->to(site_url('/agent/mock-calls'));
        }
    }

    public function mockCallScore($callLogId)
    {
        if (!$this->request->isAJAX()) {
            return $this->response->setStatusCode(400)->setJSON(['ok' => false, 'error' => 'ajax-only']);
        }
        $userId = (int) session()->get('user_id');
        if ($userId <= 0) {
            return $this->response->setStatusCode(401)->setJSON(['ok' => false, 'error' => 'unauth']);
        }

        $callLogIdInt = (int)$callLogId;
        $call = $this->callLogModel->getWithScore($callLogIdInt);
        if (!$call) {
            return $this->response->setStatusCode(404)->setJSON(['ok' => false, 'error' => 'call log not found']);
        }

        $bindingId = (int)($call['binding_id'] ?? 0);
        $binding = $bindingId > 0 ? $this->bindingModel->find($bindingId) : null;
        $isAdmin = (string) (session()->get('role') ?? '') === 'admin';
        if ($binding) {
            $agentIds = !empty($binding['assigned_agent_ids']) ? (array)json_decode((string)$binding['assigned_agent_ids'], true) : [];
            if (!is_array($agentIds)) $agentIds = [];
            $isAssigned = in_array($userId, $agentIds, true);
        } else {
            $isAssigned = false;
        }
        $callAgentId = (int)($call['agent_id'] ?? 0);
        if (!$isAdmin && !$isAssigned && $callAgentId !== $userId) {
            return $this->response->setStatusCode(403)->setJSON(['ok' => false, 'error' => 'forbidden']);
        }

        if ($call['score_pct'] !== null) {
            $breakdown = !empty($call['breakdown_json']) ? (array)json_decode((string)$call['breakdown_json'], true) : [];
            if (!is_array($breakdown)) $breakdown = [];
            return $this->response->setJSON([
                'ok'           => true,
                'already_scored' => true,
                'score_pct'    => (float)$call['score_pct'],
                'ai_feedback'  => (string)($call['ai_feedback'] ?? ''),
                'breakdown'    => $breakdown,
                'msg'          => 'Already scored.',
            ]);
        }

        $docIds = [];
        $passThreshold = 70.0;
        if ($binding) {
            $passThreshold = (float)($binding['pass_threshold_pct'] ?? 70);
            $ids = !empty($binding['selected_doc_ids']) ? (array)json_decode((string)$binding['selected_doc_ids'], true) : [];
            if (is_array($ids)) {
                $docIds = array_values(array_filter(array_map('intval', $ids), static fn($x) => $x > 0));
            }
        }

        $basisText = '';
        foreach ($docIds as $did) {
            $doc = $this->documentModel->find($did);
            if ($doc) {
                $t = (string)($doc['content_text'] ?? '');
                if ($t !== '') {
                    $basisText .= ($basisText !== '' ? ' ' : '') . $t;
                }
            }
        }

        $scorecardCategories = [
            ['category' => 'Research relevance',            'points' => 15, 'fullCreditBehavior' => 'Agent researches and references relevant account/context details from the training docs before/during the call.'],
            ['category' => 'Identity and opener',           'points' => 10, 'fullCreditBehavior' => 'Agent clearly introduces self, company, and reason for the call in a confident, professional opener.'],
            ['category' => 'Observation accuracy',          'points' => 15, 'fullCreditBehavior' => 'Agent correctly observes and acknowledges the customer\'s situation, needs, and stated concerns.'],
            ['category' => 'Homeowner-to-business bridge',  'points' => 10, 'fullCreditBehavior' => 'Agent effectively connects the customer\'s personal situation/needs to the business value or offering.'],
            ['category' => 'Diagnostic question',           'points' => 10, 'fullCreditBehavior' => 'Agent asks open-ended diagnostic questions to uncover pain points, priorities, and decision criteria.'],
            ['category' => 'Listening and control',         'points' => 10, 'fullCreditBehavior' => 'Agent demonstrates active listening, controls the conversation appropriately, and stays on agenda.'],
            ['category' => 'Positioning',                   'points' => 10, 'fullCreditBehavior' => 'Agent positions the solution/product clearly using language aligned with training docs and customer needs.'],
            ['category' => 'Objection handling',            'points' => 10, 'fullCreditBehavior' => 'Agent acknowledges and handles objections with empathy, evidence, and training-doc aligned rebuttals.'],
            ['category' => 'Booking',                       'points' => 10, 'fullCreditBehavior' => 'Agent confidently attempts to book/appointment/next step and confirms details when possible.'],
            ['category' => 'Compliance and CRM',            'points' => 5,  'fullCreditBehavior' => 'Agent follows compliance rules, uses required disclaimers, and confirms CRM/data-capture expectations.'],
        ];

        $transcript = (string)($call['transcript'] ?? '');
        if (is_array(json_decode($transcript, true))) {
            $arr = json_decode($transcript, true);
            $parts = [];
            if (is_array($arr)) {
                foreach ($arr as $m) {
                    if (is_array($m)) {
                        $role = (string)($m['role'] ?? ($m['speaker'] ?? ''));
                        $text = (string)($m['content'] ?? ($m['text'] ?? ($m['message'] ?? '')));
                        if ($text !== '') {
                            $parts[] = ($role !== '' ? strtoupper($role) . ': ' : '') . $text;
                        }
                    }
                }
            }
            $transcript = implode("\n", $parts);
        }
        $summary = (string)($call['call_summary'] ?? '');

        $scoreResp = $this->vapiService->scoreCallAgainstDocs($transcript, $summary, $scorecardCategories, $basisText, $docIds);

        if (empty($scoreResp['ok'])) {
            $msg = $scoreResp['msg'] ?? 'Scoring failed.';
            return $this->response->setStatusCode(500)->setJSON([
                'ok'          => false,
                'error'       => $msg,
                'score_pct'   => 0,
                'ai_feedback' => '',
                'breakdown'   => [],
                'msg'         => $msg,
            ]);
        }

        $scorePct = (int)($scoreResp['score_pct'] ?? 0);
        $breakdown = is_array($scoreResp['breakdown'] ?? null) ? $scoreResp['breakdown'] : [];
        $aiFeedback = (string)($scoreResp['ai_feedback'] ?? '');

        $breakdownJson = json_encode($breakdown, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

        $existingScore = $this->callScoreModel->findByCallLogId($callLogIdInt);
        if ($existingScore === null) {
            $this->callScoreModel->insert([
                'call_log_id'          => $callLogIdInt,
                'score_pct'            => $scorePct,
                'pass_threshold_pct'   => $passThreshold,
                'breakdown_json'       => $breakdownJson,
                'ai_feedback'          => $aiFeedback,
            ]);
        } else {
            $this->callScoreModel->update((int)$existingScore['id'], [
                'score_pct'            => $scorePct,
                'pass_threshold_pct'   => $passThreshold,
                'breakdown_json'       => $breakdownJson,
                'ai_feedback'          => $aiFeedback,
            ]);
        }

        return $this->response->setJSON([
            'ok'           => true,
            'score_pct'    => $scorePct,
            'ai_feedback'  => $aiFeedback,
            'breakdown'    => $breakdown,
            'msg'          => 'Scoring complete.',
        ]);
    }

    public function mockCallReview($id)
    {
        $userId = (int) session()->get('user_id');
        if ($userId <= 0) return redirect()->to('/login');

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

        $idInt = (int)$id;
        $call = $this->callLogModel->getWithScore($idInt);
        if (!$call) {
            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
        }

        if ($isAdmin) {
            $agentBindings = $this->bindingModel->where('is_active', 1)->findAll();
        } else {
            $agentBindings = $this->bindingModel->listForAgent($userId);
        }
        $agentBindingIds = [];
        foreach ($agentBindings as $b) {
            $agentBindingIds[(int)$b['id']] = true;
        }

        $bindingId = (int)($call['binding_id'] ?? 0);
        $callAgentId = (int)($call['agent_id'] ?? 0);
        if (!$isAdmin && !isset($agentBindingIds[$bindingId]) && $callAgentId !== $userId) {
            session()->setFlashdata('error', 'You do not have permission to view this call.');
            return redirect()->to(site_url('/agent/mock-calls'));
        }

        $binding = $bindingId > 0 ? $this->bindingModel->find($bindingId) : null;

        $docIds = [];
        $docNames = [];
        if ($binding) {
            $ids = !empty($binding['selected_doc_ids']) ? (array)json_decode((string)$binding['selected_doc_ids'], true) : [];
            if (is_array($ids)) {
                $docIds = array_values(array_filter(array_map('intval', $ids), static fn($x) => $x > 0));
            }
            foreach ($docIds as $did) {
                $doc = $this->documentModel->find($did);
                $docNames[$did] = $doc ? (string)($doc['title'] ?? "Doc #{$did}") : "Doc #{$did}";
            }
        }

        if ($call['score_pct'] === null) {
            $basisText = '';
            foreach ($docIds as $did) {
                $doc = $this->documentModel->find($did);
                if ($doc) {
                    $t = (string)($doc['content_text'] ?? '');
                    if ($t !== '') {
                        $basisText .= ($basisText !== '' ? ' ' : '') . $t;
                    }
                }
            }

            $scorecardCategories = [
                ['category' => 'Research relevance',            'points' => 15, 'fullCreditBehavior' => 'Agent researches and references relevant account/context details from the training docs before/during the call.'],
                ['category' => 'Identity and opener',           'points' => 10, 'fullCreditBehavior' => 'Agent clearly introduces self, company, and reason for the call in a confident, professional opener.'],
                ['category' => 'Observation accuracy',          'points' => 15, 'fullCreditBehavior' => 'Agent correctly observes and acknowledges the customer\'s situation, needs, and stated concerns.'],
                ['category' => 'Homeowner-to-business bridge',  'points' => 10, 'fullCreditBehavior' => 'Agent effectively connects the customer\'s personal situation/needs to the business value or offering.'],
                ['category' => 'Diagnostic question',           'points' => 10, 'fullCreditBehavior' => 'Agent asks open-ended diagnostic questions to uncover pain points, priorities, and decision criteria.'],
                ['category' => 'Listening and control',         'points' => 10, 'fullCreditBehavior' => 'Agent demonstrates active listening, controls the conversation appropriately, and stays on agenda.'],
                ['category' => 'Positioning',                   'points' => 10, 'fullCreditBehavior' => 'Agent positions the solution/product clearly using language aligned with training docs and customer needs.'],
                ['category' => 'Objection handling',            'points' => 10, 'fullCreditBehavior' => 'Agent acknowledges and handles objections with empathy, evidence, and training-doc aligned rebuttals.'],
                ['category' => 'Booking',                       'points' => 10, 'fullCreditBehavior' => 'Agent confidently attempts to book/appointment/next step and confirms details when possible.'],
                ['category' => 'Compliance and CRM',            'points' => 5,  'fullCreditBehavior' => 'Agent follows compliance rules, uses required disclaimers, and confirms CRM/data-capture expectations.'],
            ];

            $transcript = (string)($call['transcript'] ?? '');
            if (is_array(json_decode($transcript, true))) {
                $arr = json_decode($transcript, true);
                $parts = [];
                if (is_array($arr)) {
                    foreach ($arr as $m) {
                        if (is_array($m)) {
                            $role = (string)($m['role'] ?? ($m['speaker'] ?? ''));
                            $text = (string)($m['content'] ?? ($m['text'] ?? ($m['message'] ?? '')));
                            if ($text !== '') {
                                $parts[] = ($role !== '' ? strtoupper($role) . ': ' : '') . $text;
                            }
                        }
                    }
                }
                $transcript = implode("\n", $parts);
            }
            $summary = (string)($call['call_summary'] ?? '');
            $passThreshold = $binding ? (float)($binding['pass_threshold_pct'] ?? 70) : 70.0;

            try {
                $scoreResp = $this->vapiService->scoreCallAgainstDocs($transcript, $summary, $scorecardCategories, $basisText, $docIds);
                if (!empty($scoreResp['ok'])) {
                    $scorePct = (int)($scoreResp['score_pct'] ?? 0);
                    $breakdown = is_array($scoreResp['breakdown'] ?? null) ? $scoreResp['breakdown'] : [];
                    $aiFeedback = (string)($scoreResp['ai_feedback'] ?? '');
                    $breakdownJson = json_encode($breakdown, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

                    $existingScore = $this->callScoreModel->findByCallLogId($idInt);
                    if ($existingScore === null) {
                        $this->callScoreModel->insert([
                            'call_log_id'          => $idInt,
                            'score_pct'            => $scorePct,
                            'pass_threshold_pct'   => $passThreshold,
                            'breakdown_json'       => $breakdownJson,
                            'ai_feedback'          => $aiFeedback,
                        ]);
                    } else {
                        $this->callScoreModel->update((int)$existingScore['id'], [
                            'score_pct'            => $scorePct,
                            'pass_threshold_pct'   => $passThreshold,
                            'breakdown_json'       => $breakdownJson,
                            'ai_feedback'          => $aiFeedback,
                        ]);
                    }

                    $call['score_pct'] = $scorePct;
                    $call['pass_threshold_pct'] = $passThreshold;
                    $call['breakdown_json'] = $breakdownJson;
                    $call['ai_feedback'] = $aiFeedback;
                }
            } catch (\Throwable $e) {
            }
        }

        $breakdown = !empty($call['breakdown_json']) ? (array)json_decode((string)$call['breakdown_json'], true) : [];
        if (!is_array($breakdown)) $breakdown = [];

        return view('agent/mock_call_review', [
            'call'      => $call,
            'binding'   => $binding,
            'breakdown' => $breakdown,
            'docNames'  => $docNames,
            'docIds'    => $docIds,
        ]);
    }

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

        if ($scheduleId <= 0 || $dateStr === '') {
            return $this->response->setJSON(['ok' => false, 'message' => 'Schedule and date are required.']);
        }

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

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

        $assigned = (array) json_decode($schedule['assigned_agent_ids_json'] ?? '[]', true);
        $assigned = array_values(array_filter(array_map('intval', $assigned)));
        if (!in_array($agentId, $assigned, true) && !empty($assigned)) {
            return $this->response->setJSON(['ok' => false, 'message' => 'You are not assigned to this schedule.']);
        }

        try {
            $dateDt = new \DateTimeImmutable($dateStr . ' 00:00:00', new \DateTimeZone($schedule['timezone'] ?? 'America/Los_Angeles'));
        } catch (\Throwable $e) {
            return $this->response->setJSON(['ok' => false, 'message' => 'Invalid date.']);
        }

        $shifts = $shiftModel->where('schedule_id', $scheduleId)->orderBy('sort_order', 'ASC')->orderBy('shift_start', 'ASC')->findAll();
        $exceptions = $exModel->getForScheduleBetween($scheduleId, $dateDt->format('Y-m-d'), $dateDt->format('Y-m-d'));

        $tz = $schedule['timezone'] ?? 'America/Los_Angeles';
        $startRange = $dateDt->setTime(0, 0, 0)->setTimezone(new \DateTimeZone('UTC'));
        $endRange   = $dateDt->setTime(23, 59, 59)->setTimezone(new \DateTimeZone('UTC'));
        $existingBookings = $bookingModel->getBetweenRange($startRange->format('Y-m-d H:i:s'), $endRange->format('Y-m-d H:i:s'), $agentId, $scheduleId);
        $bookedStarts = [];
        foreach ($existingBookings as $b) {
            if (($b['status'] ?? '') !== 'cancelled') {
                $bookedStarts[] = [
                    'start' => (string)($b['starts_at'] ?? ''),
                    'end'   => (string)($b['ends_at']   ?? ''),
                ];
            }
        }

        try {
            $gen = new BookingSlotGenerator();
            $slots = $gen->generateSlotsForDate($schedule, $shifts, $exceptions, $dateDt, $bookedStarts);
        } catch (\Throwable $e) {
            return $this->response->setJSON(['ok' => false, 'message' => $e->getMessage() ?: 'Slot generation error.']);
        }

        $out = [];
        foreach ($slots as $s) {
            $startUt = (int)($s['start_ut'] ?? 0);
            $endUt   = (int)($s['end_ut']   ?? 0);
            $startIso = (string)($s['start_str'] ?? '');
            $endIso   = (string)($s['end_str']   ?? '');
            $hmStart = strlen($startIso) >= 16 ? substr($startIso, 11, 5) : '';
            $hmEnd   = strlen($endIso)   >= 16 ? substr($endIso,   11, 5) : '';
            if ($startUt > 0) {
                $dt = new \DateTimeImmutable('@' . $startUt);
                $dtUtc = $dt->setTimezone(new \DateTimeZone('UTC'));
                $startsAtUtc = $dtUtc->format('Y-m-d H:i:s');
            } else {
                $startsAtUtc = '';
            }
            if ($endUt > 0) {
                $dt = new \DateTimeImmutable('@' . $endUt);
                $dtUtc = $dt->setTimezone(new \DateTimeZone('UTC'));
                $endsAtUtc = $dtUtc->format('Y-m-d H:i:s');
            } else {
                $endsAtUtc = '';
            }
            $out[] = [
                'start'     => $hmStart,
                'end'       => $hmEnd,
                'startsAt'  => $startsAtUtc,
                'endsAt'    => $endsAtUtc,
                'start_str' => $hmStart,
                'end_str'   => $hmEnd,
                'start_iso' => $startIso,
                'end_iso'   => $endIso,
                'start_ut'  => $startUt,
                'end_ut'    => $endUt,
                'available' => !empty($s['available']),
                'reason'    => $s['reason'] ?? null,
            ];
        }
        return $this->response->setJSON(['ok' => true, 'timezone' => $tz, 'slots' => $out]);
    }

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

        $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'));

            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 === '') {
                $assigned = (array) json_decode($schedule['assigned_agent_ids_json'] ?? '[]', true);
                $assigned = array_values(array_filter(array_map('intval', $assigned)));
                if (!empty($assigned) && !in_array($agentId, $assigned, true)) {
                    $error = 'You are not assigned to this schedule.';
                }
            }

            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, $agentId, $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. Please select another.';
                        break;
                    }
                }
            }

            if ($error === '') {
                $assignedAgentPost = (int) $this->request->getPost('assigned_agent_id');
                if ($assignedAgentPost <= 0) $assignedAgentPost = $agentId;
                $db = db_connect();
                $db->transStart();

                $booked = [
                    'lead_id'            => $leadId,
                    'schedule_id'        => $scheduleId,
                    'booked_by_user_id'  => $agentId,
                    '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';
                }
                $agentName = '';
                $um = new UserModel();
                $agent = $um->find($agentId);
                if ($agent) {
                    $agentName = (string)($agent['full_name'] ?? $agent['username'] ?? '');
                }
                $noteModel->insert([
                    'lead_id'    => $leadId,
                    'user_id'    => $agentId,
                    'note_text'  => 'Booked Assessment: ' . $displayDate . ' with ' . $agentName . ($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 {
                        $smtpConfig = new BookingSmtpConfigModel();
                        if ((bool)$smtpConfig->getValue('notify_enabled', '1')) {
                            $mailer = new BookingMailer();
                            $fullBooking = $bookingModel->find($bookingId);
                            $notifyAgentIds = (array) json_decode((string)$smtpConfig->getValue('notify_agent_ids', '[]'), true);
                            $notifyAdminIds = (array) json_decode((string)$smtpConfig->getValue('notify_admin_ids', '[]'), true);
                            $leadEnabled = (bool)$smtpConfig->getValue('notify_lead_enabled', '0');
                            $mailer->notifyBookingCreated($fullBooking, $notifyAgentIds, $notifyAdminIds, $leadEnabled);
                        }
                    } catch (\Throwable $e) {
                        log_message('error', 'Booking notify created failed: ' . $e->getMessage());
                    }
                    session()->setFlashdata('success', 'Assessment booked successfully.');
                    return redirect()->to('/agent/leads/view/' . $leadId);
                }
            }
        }

        $schedules = $scheduleModel->findForAgent($agentId);
        $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('/agent/leads/book/' . $leadId),
                'leadViewUrl'       => site_url('/agent/leads/view/' . $leadId),
                'slotsEndpoint'     => site_url('/agent/bookings/slots/'),
                'bookingsViewUrl'   => site_url('/agent/bookings/view/'),
            ]),
        ]);
    }

    public function leadBookingsCancel($id)
    {
        if ($this->request->getMethod(true) !== 'POST' || !$this->request->isAJAX()) {
            return $this->response->setStatusCode(405)->setJSON(['ok' => false, 'message' => 'Method not allowed.']);
        }
        $bookingId = (int)$id;
        $agentId = (int) session()->get('user_id');
        if ($agentId <= 0) {
            return $this->response->setJSON(['ok' => false, 'message' => 'Not logged in.']);
        }
        $bookingModel = new BookingModel();
        $noteModel = new LeadNoteModel();
        $booking = $bookingModel->find($bookingId);
        if (!$booking) {
            return $this->response->setJSON(['ok' => false, 'message' => 'Booking not found.']);
        }
        if ((int)($booking['assigned_agent_id'] ?? 0) !== $agentId && (int)($booking['booked_by_user_id'] ?? 0) !== $agentId) {
            return $this->response->setJSON(['ok' => false, 'message' => 'Permission denied.']);
        }
        if (($booking['status'] ?? '') === 'cancelled') {
            return $this->response->setJSON(['ok' => false, 'message' => 'Booking already cancelled.']);
        }
        $reason = trim((string)$this->request->getPost('reason'));

        $bookingModel->update($bookingId, ['status' => 'cancelled']);
        $leadId = (int)($booking['lead_id'] ?? 0);
        if ($leadId > 0) {
            $displayDate = (string)($booking['starts_at'] ?? '');
            try {
                $tz = (string)($booking['timezone'] ?? 'America/Los_Angeles');
                $d = new \DateTimeImmutable((string)($booking['starts_at'] ?? 'now'), new \DateTimeZone('UTC'));
                $d = $d->setTimezone(new \DateTimeZone($tz));
                $displayDate = $d->format('l, F j, Y g:ia') . ' ' . $tz;
            } catch (\Throwable $e) {
                $displayDate = (string)($booking['starts_at'] ?? '');
            }
            $noteModel->insert([
                'lead_id'    => $leadId,
                'user_id'    => $agentId,
                'note_text'  => 'Cancelled Assessment: ' . $displayDate . ($reason ? ' | Reason: ' . $reason : ''),
                'created_at' => date('Y-m-d H:i:s'),
            ]);
        }
        try {
            $smtpConfig = new BookingSmtpConfigModel();
            if ((bool)$smtpConfig->getValue('notify_enabled', '1')) {
                $mailer = new BookingMailer();
                $notifyAgentIds = (array) json_decode((string)$smtpConfig->getValue('notify_agent_ids', '[]'), true);
                $notifyAdminIds = (array) json_decode((string)$smtpConfig->getValue('notify_admin_ids', '[]'), true);
                $leadEnabled    = (bool)$smtpConfig->getValue('notify_lead_enabled', '0');
                $mailer->notifyBookingCancelled($booking, $notifyAgentIds, $notifyAdminIds, $leadEnabled, $reason);
            }
        } catch (\Throwable $e) {
            log_message('error', 'Booking notify cancelled failed: ' . $e->getMessage());
        }
        return $this->response->setJSON(['ok' => true, 'message' => 'Booking cancelled.']);
    }

    public function myBookings($segment = null)
    {
        $agentId = (int) session()->get('user_id');
        if ($agentId <= 0) return redirect()->to('/login');

        $db = db_connect();
        $fromDate = trim((string)$this->request->getGet('from'));
        $toDate   = trim((string)$this->request->getGet('to'));

        $builder = $db->table('bookings b');
        $builder->select('b.*');
        $builder->select('b.starts_at as booking_date, b.starts_at as start_time');
        $builder->select('leads.name as lead_name, leads.email as lead_email, leads.phone as lead_phone');
        $builder->select('agent.full_name as agent_name, agent.email as agent_email');
        $builder->select('agent.full_name as assigned_name, agent.email as assigned_email');
        $builder->select('agent.full_name as assigned_agent_name, agent.email as assigned_agent_email');
        $builder->select('booker.full_name as booked_by_name, booker.email as booked_by_email');
        $builder->select('s.title as schedule_title, s.title as schedule_name, s.timezone as tz, s.timezone as timezone');
        $builder->join('leads',                 'leads.id = b.lead_id', 'left');
        $builder->join('users as agent',         'agent.id = b.assigned_agent_id', 'left');
        $builder->join('users as booker',        'booker.id = b.booked_by_user_id', 'left');
        $builder->join('booking_schedules s',    's.id = b.schedule_id', 'left');
        $builder->groupStart();
        $builder->where('b.assigned_agent_id', $agentId);
        $builder->orWhere('b.booked_by_user_id', $agentId);
        $builder->groupEnd();
        if ($fromDate !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $fromDate)) {
            $builder->where('DATE(b.starts_at) >=', $fromDate);
        }
        if ($toDate !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $toDate)) {
            $builder->where('DATE(b.starts_at) <=', $toDate);
        }
        $builder->orderBy('b.starts_at', 'DESC');
        $allRows = $builder->get()->getResultArray();

        $laTz = new \DateTimeZone('America/Los_Angeles');
        try {
            $nowLa = new \DateTimeImmutable('now', $laTz);
        } catch (\Throwable $e) {
            $nowLa = new \DateTimeImmutable('now');
        }
        $nowLaStr = $nowLa->format('Y-m-d H:i:s');

        $upcoming  = [];
        $past      = [];
        $cancelled = [];
        foreach ($allRows as $row) {
            $status = strtolower((string)($row['status'] ?? 'booked'));
            if ($status === 'cancelled') {
                $cancelled[] = $row;
                continue;
            }
            $startsAtStr = (string)($row['starts_at'] ?? '');
            $cmp = $startsAtStr !== '' ? $startsAtStr : '0000-00-00 00:00:00';
            if ($cmp >= $nowLaStr) {
                $upcoming[] = $row;
            } else {
                $past[] = $row;
            }
        }
        usort($upcoming, static function ($a, $b) { return strcmp((string)($a['starts_at'] ?? ''), (string)($b['starts_at'] ?? '')); });
        usort($past,     static function ($a, $b) { return strcmp((string)($b['starts_at'] ?? ''), (string)($a['starts_at'] ?? '')); });
        usort($cancelled,static function ($a, $b) { return strcmp((string)($b['starts_at'] ?? ''), (string)($a['starts_at'] ?? '')); });

        $filterSegment = strtolower((string)($segment ?? 'upcoming'));
        $list = match(true) {
            $filterSegment === 'past'      => $past,
            $filterSegment === 'cancelled' => $cancelled,
            default                         => $upcoming,
        };

        return view('layout/app', [
            'title'      => 'My Bookings',
            'pageTitle'  => 'My Assessments Bookings',
            'content'    => view('agent/my_bookings', [
                'list'         => $list,
                'segment'      => $filterSegment,
                'upcomingCt'   => count($upcoming),
                'pastCt'       => count($past),
                'cancelledCt'  => count($cancelled),
            ]),
        ]);
    }

    public function bookingView($id)
    {
        $agentId = (int) session()->get('user_id');
        if ($agentId <= 0) return redirect()->to('/login');
        $id = (int)$id;
        $db = db_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,
                      u.full_name as assigned_name, u.email as assigned_email,
                      u.full_name as assigned_agent_name, u.email as assigned_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) {
            throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
        }
        if ((int)($booking['assigned_agent_id'] ?? 0) !== $agentId && (int)($booking['booked_by_user_id'] ?? 0) !== $agentId) {
            return redirect()->to('/agent/my-bookings')->with('error', 'Permission denied.');
        }
        return view('layout/app', [
            'title'      => 'Booking #' . $id,
            'pageTitle'  => 'Booking Details',
            'content'    => view('agent/booking_view', [
                'booking' => $booking,
                'agentId' => $agentId,
            ]),
        ]);
    }

    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.']);
        }
        $agentId = (int) session()->get('user_id');
        if ($agentId <= 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.']);
        }
        $leadModel = new LeadModel();
        $statusModel = new LeadStatusModel();
        $noteModel = new LeadNoteModel();
        $lead = $leadModel->find($leadId);
        if (! $lead) {
            return $this->response->setStatusCode(404)->setJSON(['ok' => false, 'error' => 'Lead not found.']);
        }
        $role = (string) (session()->get('role') ?? 'agent');
        $assignedTo = (int) ($lead['assigned_to'] ?? 0);
        if ($role !== 'admin' && $assignedTo !== 0 && $assignedTo !== $agentId) {
            return $this->response->setStatusCode(403)->setJSON(['ok' => false, 'error' => 'You are not assigned to this lead.']);
        }
        $newStatus = $statusModel->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();
        $leadModel->update($leadId, ['status_id' => $newStatusId]);
        $oldName = $oldStatusId ? ((($statusModel->find($oldStatusId)['name'] ?? null)) ?: 'N/A') : 'N/A';
        $noteText = 'Status changed from <b>' . esc($oldName) . '</b> → <b>' . esc($newStatus['name']) . '</b>.';
        $noteModel->insert([
            'lead_id' => $leadId,
            'user_id' => $agentId,
            '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,
        ]);
    }
}
