<?php

namespace App\Controllers\View;
use App\Controllers\BaseController;
use App\Models\UserModel;
use App\Models\RoleModel;
use App\Models\PermissionModel;
use App\Models\RolePermissionModel;
use App\Models\StudentModel;
use App\Models\LoginActivityModel;
use CodeIgniter\Controller;
use App\Models\InvoiceModel;
use App\Models\ClassSectionModel;
use App\Models\UserRoleModel;
use App\Models\ConfigurationModel;
use App\Models\RefundModel;
use App\Models\EnrollmentModel;
use App\Models\AdminNotificationSubjectModel;
use Doctrine\DBAL\Configuration;
use App\Services\FeeCalculationService;
use App\Models\StudentClassModel;
use App\Models\StudentSectionDistributionDraftModel;
use App\Controllers\View\EmailController;
use App\Controllers\View\InvoiceController;
use App\Models\StaffAttendanceModel;
use App\Libraries\StaffTimeOffLinkService;
use App\Models\AttendanceDayModel;
use App\Models\ScoreCommentModel;
use App\Models\SemesterScoreModel;
use App\Models\TeacherClassModel;
use App\Models\TeacherSubmissionNotificationHistoryModel;
use App\Models\ExamDraftModel;
use App\Models\HomeworkModel;
use App\Services\SemesterRangeService;

use CodeIgniter\Events\Events;

class AdministratorController extends BaseController
{
    protected $permissionModel;
    protected $rolePermissionModel;
    protected $roleModel;
    protected $userModel;
    protected $userRoleModel;
    protected $db;
    protected $configModel;
    protected $semester;
    protected $schoolYear;
    protected $studentModel;
    protected $loginActivityModel;
    protected $invoiceModel;
    protected $refundModel;
    protected $enrollmentModel;
    protected $classSectionModel;
    protected $studentClassModel;
    protected $staffAttendanceModel;
    protected $adminNotificationSubjectModel;

    public function __construct()
    {
        helper('auth');
        // Load models
        $this->rolePermissionModel = new RolePermissionModel();
        $this->permissionModel = new PermissionModel();
        $this->roleModel = new RoleModel();
        $this->userModel = new UserModel();
        $this->configModel = new ConfigurationModel();
        $this->studentModel = new StudentModel();
        $this->loginActivityModel = new LoginActivityModel();
        $this->userRoleModel = new UserRoleModel();
        $this->invoiceModel = new InvoiceModel();
        $this->refundModel = new RefundModel();
        $this->enrollmentModel = new EnrollmentModel();
        $this->classSectionModel = new ClassSectionModel();
        $this->adminNotificationSubjectModel = new AdminNotificationSubjectModel();

        $this->semester = $this->configModel->getConfig('semester');
        $this->schoolYear = $this->configModel->getConfig('school_year');
        $this->studentClassModel = new StudentClassModel();
        $this->staffAttendanceModel = new StaffAttendanceModel();
        // Load the database service
        $this->db = \Config\Database::connect();
        // Check if the database connection is established
        if (!$this->db->connect()) {
            log_message('error', 'Database connection failed.');
            throw new \Exception('Database connection failed.');
        } else {
            log_message('info', 'Database connection successful.');
        }
    }

    /**
     * Compute all allowed absence dates (future Sundays within Sep..May for the configured school year)
     */
    private function allowedAbsenceDates(): array
    {
        $todayStr = local_date(utc_now(), 'Y-m-d');
        try { $today = new \DateTimeImmutable($todayStr); } catch (\Throwable $e) { $today = new \DateTimeImmutable(); }

        $sy = (string)($this->schoolYear ?? '');
        $startYear = null; $endYear = null;
        if (preg_match('/^(\d{4})\D+(\d{4})$/', $sy, $m)) {
            $startYear = (int)$m[1];
            $endYear   = (int)$m[2];
        } else {
            $cy = (int)date('Y');
            $cm = (int)date('n');
            if ($cm >= 9) { // Sep-Dec
                $startYear = $cy;
                $endYear   = $cy + 1;
            } else {
                $startYear = $cy - 1;
                $endYear   = $cy;
            }
        }

        try {
            $start = new \DateTimeImmutable(sprintf('%04d-09-01', $startYear));
            $end   = new \DateTimeImmutable(sprintf('%04d-05-31', $endYear));
        } catch (\Throwable $e) {
            return [];
        }

        if ($start < $today) {
            $start = $today;
        }

        $dates = [];
        for ($cursor = $start; $cursor <= $end; $cursor = $cursor->modify('+1 day')) {
            if ((int)$cursor->format('w') === 0) { // Sunday
                $dates[] = $cursor->format('Y-m-d');
            }
        }
        return $dates;
    }

    private function getPreviousSchoolYear(string $schoolYear): string
    {
        $schoolYear = trim($schoolYear);
        if ($schoolYear === '') {
            return '';
        }

        if (preg_match('/^(\d{4})\s*-\s*(\d{4})$/', $schoolYear, $m)) {
            return ((int)$m[1] - 1) . '-' . ((int)$m[2] - 1);
        }

        if (preg_match('/^(\d{4})\s*-\s*(\d{2})$/', $schoolYear, $m)) {
            $start = (int)$m[1] - 1;
            $end = (int)$m[2] - 1;
            if ($end < 0) {
                $end += 100;
            }
            return sprintf('%04d-%02d', $start, $end);
        }

        if (preg_match('/^\d{4}$/', $schoolYear)) {
            return (string)((int)$schoolYear - 1);
        }

        return '';
    }

    private function getSchoolYearStartYear(string $schoolYear): ?int
    {
        $schoolYear = trim($schoolYear);
        if ($schoolYear === '') {
            return null;
        }

        if (preg_match('/^(\d{4})\s*-\s*(\d{4})$/', $schoolYear, $m)) {
            return (int)$m[1];
        }

        if (preg_match('/^(\d{4})\s*-\s*(\d{2})$/', $schoolYear, $m)) {
            return (int)$m[1];
        }

        if (preg_match('/^\d{4}$/', $schoolYear)) {
            return (int)$schoolYear;
        }

        return null;
    }

    /**
     * Admin self-service absence/vacation page (same features as teacher page)
     */
    public function absenceFormAdmin()
    {
        $userId = (int)(session()->get('user_id') ?? 0);
        if ($userId <= 0) {
            return redirect()->to('login')->with('error', 'Please log in first.');
        }

        $admin = $this->userModel->find($userId);
        $displayName = $admin ? trim(($admin['firstname'] ?? '') . ' ' . ($admin['lastname'] ?? '')) : 'Administrator';

        $semester   = (string)($this->semester   ?? '');
        $schoolYear = (string)($this->schoolYear ?? '');

        $existing = $this->staffAttendanceModel
            ->where('user_id', $userId)
            ->where('semester', $semester)
            ->where('school_year', $schoolYear)
            ->orderBy('date', 'DESC')
            ->findAll();

        return view('administrator/absence_vacation', [
            'admin_name'     => $displayName,
            'semester'       => $semester,
            'schoolYear'     => $schoolYear,
            'existing'       => $existing,
            'availableDates' => $this->allowedAbsenceDates(),
        ]);
    }

    /**
     * Handle admin absence submission (upserts staff_attendance like the teacher flow)
     */
    public function submitAbsenceAdmin()
    {
        $userId = (int)(session()->get('user_id') ?? 0);
        if ($userId <= 0) {
            return redirect()->to('login')->with('error', 'Please log in first.');
        }

        $semester   = (string)($this->semester   ?? '');
        $schoolYear = (string)($this->schoolYear ?? '');
        if ($schoolYear === '') {
            return redirect()->to('/administrator/absence')->with('status', 'error')->with('message', 'Semester or school year not configured.');
        }
        $semesterResolver = new SemesterRangeService($this->configModel);

        $dates      = (array)($this->request->getPost('dates') ?? []);
        $reasonType = trim((string)($this->request->getPost('reason_type') ?? ''));
        $reasonText = trim((string)($this->request->getPost('reason') ?? ''));

        if ($reasonText === '') {
            return redirect()->to('/administrator/absence')
                ->with('status', 'error')
                ->with('message', 'Reason is required.');
        }

        $reasonBase = ($reasonType !== '') ? strtolower($reasonType) : '';
        $reason     = $reasonBase !== '' ? ($reasonBase . ': ' . $reasonText) : $reasonText;

        $allowedDates = $this->allowedAbsenceDates();
        $allowedSet = array_fill_keys($allowedDates, true);

        $roleName = $this->userModel->getUserRole($userId) ?: null;

        $saved = 0; $invalid = [];
        $savedDates = [];
        $dates = array_values(array_unique(array_map('strval', $dates)));
        foreach ($dates as $d) {
            $d = trim((string)$d);
            if ($d === '') continue;
            $dt = date_create_from_format('Y-m-d', $d);
            if (!$dt || $dt->format('Y-m-d') !== $d || empty($allowedSet[$d])) {
                $invalid[] = $d;
                continue;
            }

            $semesterForDate = $semesterResolver->getSemesterForDate($d);
            if ($semesterForDate === '') {
                $semesterForDate = $semester;
            }

            $ok = $this->staffAttendanceModel->upsertOne(
                userId: $userId,
                roleName: $roleName,
                date: $d,
                semester: $semesterForDate,
                schoolYear: $schoolYear,
                status: StaffAttendanceModel::STATUS_ABSENT,
                reason: $reason,
                editorId: $userId
            );
            if ($ok) { $saved++; $savedDates[] = $d; }
        }

        if (!empty($invalid)) {
            return redirect()->to('/administrator/absence')
                ->with('status', 'error')
                ->with('message', 'Invalid dates: ' . implode(', ', $invalid));
        }

        // Send notification email to principal with request details
        try {
            $user = $this->userModel->find($userId) ?: [];
            $fullName = trim(($user['firstname'] ?? '') . ' ' . ($user['lastname'] ?? '')) ?: 'Administrator';
            $userEmail = $user['email'] ?? '';
            $role      = $roleName ?: 'admin';
            $dateList  = !empty($savedDates) ? implode(', ', $savedDates) : implode(', ', $dates);

            $subject = sprintf('TimeOff Request: %s (%s) — %s', $fullName, ucfirst((string)$role), $dateList ?: 'No dates');
            $submittedAt = utc_now();

            $body = '<div style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:1.5;">'
                  . '<h3 style="margin:0 0 8px;">New TimeOff Request</h3>'
                  . '<p style="margin:0 0 12px;">A staff time-off request was submitted from the administrator portal.</p>'
                  . '<table cellpadding="6" cellspacing="0" style="border-collapse:collapse;">'
                  . '<tr><td><strong>Name</strong></td><td>' . esc($fullName) . '</td></tr>'
                  . '<tr><td><strong>Email</strong></td><td>' . esc($userEmail) . '</td></tr>'
                  . '<tr><td><strong>Role</strong></td><td>' . esc((string)$role) . '</td></tr>'
                  . '<tr><td><strong>Semester</strong></td><td>' . esc($semester) . '</td></tr>'
                  . '<tr><td><strong>School Year</strong></td><td>' . esc($schoolYear) . '</td></tr>'
                  . '<tr><td><strong>Reason Type</strong></td><td>' . esc($reasonType ?: '-') . '</td></tr>'
                  . '<tr><td><strong>Reason</strong></td><td>' . esc($reasonText) . '</td></tr>'
                  . '<tr><td><strong>Dates</strong></td><td>' . esc($dateList ?: '-') . '</td></tr>'
                  . '<tr><td><strong>Submitted At</strong></td><td>' . esc($submittedAt) . '</td></tr>'
                  . '</table>'
                  . '</div>';

            $linkService = new StaffTimeOffLinkService();
            $token = $linkService->createToken([
                'uid'          => $userId,
                'email'        => $userEmail,
                'name'         => $fullName,
                'role'         => $role,
                'dates'        => $dateList ?: '-',
                'reason'       => $reasonText,
                'reason_type'  => $reasonType,
                'submitted_at' => $submittedAt,
                'origin'       => 'administrator portal',
            ]);
            $notifyUrl = base_url('timeoff/notify/' . rawurlencode($token));
            $body .= '<p style="margin-top:16px;">Click <a href="' . esc($notifyUrl) . '">Send confirmation email to '
                  . esc($fullName) . '</a> so the staff member is notified automatically. '
                  . 'This link expires in 14 days.</p>';

            $mailer = \Config\Services::emailService();
            $principalEmail = env('PRINCIPAL_EMAIL') ?: 'principal@alrahmaisgl.org';
            $mailer->send($principalEmail, $subject, $body, 'notifications');
        } catch (\Throwable $e) {
            log_message('error', 'Failed to send TimeOff email (admin): ' . $e->getMessage());
        }

        return redirect()->to('/administrator/absence')
            ->with('status', 'success')
            ->with('message', $saved . ' day(s) saved as absent.');
    }

    public function index()
    {
        $data = [
            'users' => $this->userModel->findAll(),
            'roles' => $this->roleModel->findAll(),
        ];

        return view('administrator/dashboard', $data);
    }

    public function isadministrator()
    {
        $session = session();
        $allowedRoles = [
            'administrator',
            'admin',
            'principal',
            //'vice_principal',
            //'vice principal',
        ];

        $role = strtolower((string) $session->get('role'));
        if ($role !== '' && in_array($role, $allowedRoles, true)) {
            return true;
        }

        $roles = $session->get('roles');
        if (is_array($roles)) {
            foreach ($roles as $candidate) {
                $candidate = strtolower((string) $candidate);
                if ($candidate !== '' && in_array($candidate, $allowedRoles, true)) {
                    return true;
                }
            }
        }

        return false;
    }

    public function administratorDashboard()
    {
        helper('url');

        $searchData = $this->buildUserSearchData((string) $this->request->getGet('query'));

        return view('administrator/administratordashboard', array_merge($searchData, [
            'dashboardEndpoint' => site_url('api/administrator/dashboard'),
        ]));
    }

    public function dashboardMetrics()
    {
        return $this->response->setJSON($this->buildDashboardMetrics());
    }

    private function buildDashboardMetrics(): array
    {
        $recentActivities = $this->loginActivityModel->getLastActivities(4);
        if (!is_array($recentActivities)) {
            $recentActivities = [];
        }

        $totalAdmins = (int) ($this->userModel->countAdminsBySchoolYear($this->schoolYear) ?? 0);

        $teachers = $this->userModel->getUsersByRoleAndSchoolYear('teacher', $this->schoolYear);
        $totalTeachers = $this->countUniqueEntities($teachers);

        $teacherAssistants = $this->userModel->getUsersByRoleAndSchoolYear('teacher_assistant', $this->schoolYear);
        $totalTeacherAssistants = $this->countUniqueEntities($teacherAssistants);

        $parents = $this->userModel->getUsersByRoleAndSchoolYear('parent', $this->schoolYear);
        $totalParents = $this->countUniqueEntities($parents);

        // Count only students that have a class assigned and exist in student_class for the current school year
        $totalStudents = (int) (
            $this->db->table('student_class')
                ->select('COUNT(DISTINCT student_class.student_id) AS cnt')
                ->join('students', 'students.id = student_class.student_id', 'inner')
                ->where('student_class.school_year', $this->schoolYear)
                ->where('student_class.class_section_id IS NOT NULL', null, false)
                ->where('students.is_active', 1)
                ->get()
                ->getRow('cnt')
            ?? 0
        );

        return [
            'counts' => [
                'students'          => $totalStudents,
                'teachers'          => $totalTeachers,
                'teacherAssistants' => $totalTeacherAssistants,
                'admins'            => $totalAdmins,
                'parents'           => $totalParents,
            ],
            'recentActivities' => array_map(static function ($activity) {
                if (!is_array($activity)) {
                    return [];
                }
                return [
                    'login_time' => $activity['login_time'] ?? null,
                    'email'      => $activity['email'] ?? null,
                ];
            }, $recentActivities),
            'meta' => [
                'schoolYear' => $this->schoolYear,
                'semester'   => $this->semester,
            ],
        ];
    }

    private function countUniqueEntities($rows): int
    {
        if (!is_array($rows) || $rows === []) {
            return 0;
        }

        $ids = [];
        foreach ($rows as $row) {
            if (!is_array($row)) {
                continue;
            }
            if (isset($row['id'])) {
                $ids[] = (int) $row['id'];
                continue;
            }
            if (isset($row['user_id'])) {
                $ids[] = (int) $row['user_id'];
            }
        }

        return count(array_unique($ids));
    }

    public function userSearch()
    {
        $data = $this->buildUserSearchData((string) $this->request->getGet('query'));

        return view('administrator/search_results', $data);
    }

    private function buildUserSearchData(string $query): array
    {
        $q = trim($query);

        if ($q === '') {
            return [
                'query'       => '',
                'results'     => [],
                'scope_used'  => 'unscoped-raw',
                'scope_label' => 'all years/semesters (raw)',
                'total_found' => 0,
            ];
        }

        $db = $this->db;

        // 1) Tokenize input: split by whitespace and punctuation, keep meaningful pieces
        $rawTokens = preg_split('/[,\s]+/u', $q, -1, PREG_SPLIT_NO_EMPTY) ?: [];
        $tokens = array_values(array_filter(array_map('trim', $rawTokens)));

        // 2) Build phone variants for any token that looks numeric-ish
        $phoneMap = []; // token => variants[]
        foreach ($tokens as $t) {
            $digits = preg_replace('/\D+/', '', $t);
            if ($digits === '') {
                continue;
            }

            $v = [];
            if (strlen($digits) >= 7) {
                // base forms
                $v[] = $digits;
                if (strlen($digits) === 10) {
                    $v[] = sprintf('(%s)-%s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6));
                    $v[] = sprintf('%s-%s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6));
                    $v[] = sprintf('%s %s %s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6));
                    // country-code forms
                    $v[] = '1' . $digits;
                    $v[] = '+1' . $digits;
                    $v[] = '+1 ' . sprintf('(%s) %s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6));
                    $v[] = '+1-' . sprintf('%s-%s-%s', substr($digits, 0, 3), substr($digits, 3, 3), substr($digits, 6));
                } elseif (strlen($digits) === 11 && str_starts_with($digits, '1')) {
                    $ten = substr($digits, 1);
                    $v[] = $ten;
                    $v[] = sprintf('(%s)-%s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6));
                    $v[] = sprintf('%s-%s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6));
                    $v[] = sprintf('%s %s %s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6));
                    $v[] = '+1' . $ten;
                    $v[] = '+1 ' . sprintf('(%s) %s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6));
                    $v[] = '+1-' . sprintf('%s-%s-%s', substr($ten, 0, 3), substr($ten, 3, 3), substr($ten, 6));
                } else {
                    // 7-9 digits: keep as-is (partial phone fragment)
                    $v[] = $digits;
                }
            }
            if (!empty($v)) {
                $phoneMap[$t] = array_values(array_unique($v));
            }
        }

        // Helper: ( token1 AND token2 AND ... ), each token may match ANY of $columns,
        // and phone variants are only applied to the provided $phoneCols for THIS table.
        $applyMultiTokenLike = function ($qb, array $columns, array $tokens, array $phoneCols = []) use ($phoneMap) {
            foreach ($tokens as $t) {
                $qb->groupStart(); // OR across columns for this token
                foreach ($columns as $i => $col) {
                    if ($i === 0) {
                        $qb->like($col, $t);
                    } else {
                        $qb->orLike($col, $t);
                    }
                }
                if (!empty($phoneMap[$t]) && !empty($phoneCols)) {
                    foreach ($phoneMap[$t] as $pv) {
                        foreach ($phoneCols as $pcol) {
                            $qb->orLike($pcol, $pv);
                        }
                    }
                }
                $qb->groupEnd();
            }
            return $qb;
        };

        // ===== RAW UNscoped searches (flat arrays) =====

        // USERS (phone col: cellphone)
        $uCols = ['firstname', 'lastname', 'email', 'cellphone', 'school_id', 'city', 'state'];
        $uQB = $db->table('users')
            ->select('id, firstname, lastname, email, cellphone, school_id, city, state, semester');
        $applyMultiTokenLike($uQB, $uCols, $tokens, ['cellphone']);
        $users = $uQB->limit(150)->get()->getResultArray();

        // STUDENTS (no phone column to search)
        $sCols = ['firstname', 'lastname', 'school_id', 'rfid_tag', 'dob', 'gender'];
        $sQB = $db->table('students')
            ->select('id, parent_id, school_id, firstname, lastname, dob, gender, semester, rfid_tag');
        $applyMultiTokenLike($sQB, $sCols, $tokens, []);
        $students = $sQB->limit(150)->get()->getResultArray();

        // PARENTS (phone col: secondparent_phone)
        $pCols = ['secondparent_firstname', 'secondparent_lastname', 'secondparent_email', 'secondparent_phone'];
        $pQB = $db->table('parents')
            ->select('id, firstparent_id, secondparent_firstname, secondparent_lastname, secondparent_email, secondparent_phone');
        $applyMultiTokenLike($pQB, $pCols, $tokens, ['secondparent_phone']);
        foreach ($tokens as $t) {
            if (ctype_digit($t)) {
                $pQB->orWhere('firstparent_id', (int) $t)->orWhere('id', (int) $t);
            }
        }
        $parents = $pQB->limit(150)->get()->getResultArray();

        // STAFF (phone col: phone)
        $stCols = ['firstname', 'lastname', 'email', 'role_name', 'phone'];
        $stQB = $db->table('staff')
            ->select('id, user_id, firstname, lastname, email, phone, role_name, active_role');
        $applyMultiTokenLike($stQB, $stCols, $tokens, ['phone']);
        $staff = $stQB->limit(150)->get()->getResultArray();

        // EMERGENCY CONTACTS (phone col: cellphone)
        $ecCols = ['emergency_contact_name', 'relation', 'email', 'cellphone'];
        $ecQB = $db->table('emergency_contacts')
            ->select('id, parent_id, emergency_contact_name, relation, cellphone, email');
        $applyMultiTokenLike($ecQB, $ecCols, $tokens, ['cellphone']);
        $emergency = $ecQB->limit(150)->get()->getResultArray();

        $raw = [
            'users'              => $users,
            'students'           => $students,
            'parents'            => $parents,
            'staff'              => $staff,
            'emergency_contacts' => $emergency,
        ];

        $total = count($users) + count($students) + count($parents) + count($staff) + count($emergency);

        return [
            'query'       => $q,
            'results'     => $raw,
            'scope_used'  => 'unscoped-raw',
            'scope_label' => 'all years/semesters (raw, tokenized)',
            'total_found' => $total,
        ];
    }

    public function teachers()
    {
        return view('administrator/teachers');  // This is the correct view path
    }

    public function classes()
    {
        return view('administrator/classes');
    }

    public function subjects()
    {
        return view('administrator/subjects');
    }

    public function reports()
    {
        return view('administrator/reports');
    }

    public function teacherDashboard()
    {
        return view('teacher/dashboard');
    }

    public function parentDashboard()
    {
        return view('parent/dashboard');
    }

    public function CourseMaterials()
    {
        return view('administrator/course_materials');
    }

    public function administratorProfiles()
    {
        return view('administrator/administrator_profiles');
    }

    public function payrollManagement()
    {
        return view('administrator/payroll_management');
    }

    public function administratorAttendanceRecords()
    {
        return view('administrator/administrator_attendance_records');
    }

    public function performanceReviews()
    {
        return view('administrator/performance_reviews');
    }

    public function communicationLogs()
    {
        return view('administrator/communication_logs');
    }

    public function feePaymentRecords()
    {
        return view('administrator/fee_payment_records');
    }

    public function feedbackComplaints()
    {
        return view('administrator/feedback_complaints');
    }

    public function teacherSubmissionsReport()
    {
        $semester = (string)($this->configModel->getConfig('semester') ?? $this->semester ?? '');
        $schoolYear = trim((string) ($this->request->getGet('school_year') ?? ''));
        if ($schoolYear === '') {
            $schoolYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
        }
        $semesterResolver = new SemesterRangeService($this->configModel);
        $semesterNorm = $semesterResolver->normalizeSemester($semester);
        $semesterFilter = $semesterNorm !== '' ? $semesterNorm : $semester;
        $semesterCandidates = $this->buildSemesterCandidates($semesterFilter);
        $lowProgressRaw = (string) $this->request->getGet('low_progress_sections');
        $lowProgressSectionIds = array_values(array_unique(array_filter(array_map(
            'intval',
            preg_split('/\s*,\s*/', $lowProgressRaw, -1, PREG_SPLIT_NO_EMPTY)
        ))));

        $scoreComments = new ScoreCommentModel();
        $semesterScores = new SemesterScoreModel();
        $attendanceDays = new AttendanceDayModel();
        $examDrafts = new ExamDraftModel();
        $homeworkModel = new HomeworkModel();
        $historyModel = new TeacherSubmissionNotificationHistoryModel();

        $assignmentQuery = $this->db->table('teacher_class tc')
            ->select([
                'tc.class_section_id',
                'cs.class_section_name',
                'tc.teacher_id',
                'u.firstname',
                'u.lastname',
                'tc.position',
            ])
            ->join('classSection cs', 'cs.class_section_id = tc.class_section_id', 'left')
            ->join('users u', 'u.id = tc.teacher_id', 'left')
            ->orderBy('cs.class_section_name', 'ASC');

        // teacher_class assignments are scoped by school year only.
        // The table has no semester column; semester filtering belongs on
        // semester-specific records such as scores, comments, attendance,
        // homework, and exam drafts.
        if ($schoolYear !== '') {
            $assignmentQuery->where('tc.school_year', $schoolYear);
        }

        $assignmentRows = $assignmentQuery->get()->getResultArray();

        $studentCounts = $this->studentClassModel->getStudentCountsBySection($schoolYear !== '' ? $schoolYear : null);
        $sectionRows = $this->classSectionModel
            ->select('class_section_id, class_section_name')
            ->orderBy('class_section_name', 'ASC')
            ->findAll();
        $sectionMap = [];
        foreach ($sectionRows as $sectionRow) {
            $sectionId = (int) ($sectionRow['class_section_id'] ?? 0);
            if ($sectionId <= 0) {
                continue;
            }
            if (empty($studentCounts[$sectionId])) {
                continue;
            }
            $sectionMap[$sectionId] = $sectionRow['class_section_name'] ?? "Section {$sectionId}";
        }
        $sectionIds = array_keys($sectionMap);

        [$progressExpectedWeeks, $progressSubmittedBySection] = $this->buildClassProgressStats($sectionIds);
        $examDraftCounts = [];
        $examDraftDeadline = $this->resolveTeacherDashboardExamDraftDeadline($semester, $schoolYear);
        $examDraftDeadlineConfig = trim((string) ($this->configModel->getConfig('exam_draft_deadline') ?? ''));
        $examDraftDeadlineFormatted = '';
        if ($examDraftDeadlineConfig !== '') {
            $parsedUi = $this->parseExamDraftDeadlineConfigValue();
            $examDraftDeadlineFormatted = $parsedUi !== null ? $parsedUi->format('M j, Y') : '';
        }
        $homeworkCounts = [];
        if (! empty($sectionIds)) {
            $draftBuilder = $examDrafts
                ->select('class_section_id')
                ->whereIn('class_section_id', $sectionIds);
            if ($schoolYear !== '') {
                $draftBuilder->where('school_year', $schoolYear);
            }
            if (!empty($semesterCandidates)) {
                $draftBuilder->whereIn('semester', $semesterCandidates);
            }
            if ($this->db->fieldExists('is_legacy', 'exam_drafts')) {
                $draftBuilder->where('is_legacy', 0);
            }
            $draftRows = $draftBuilder->findAll();
            foreach ($draftRows as $draft) {
                $sectionId = (int) ($draft['class_section_id'] ?? 0);
                if ($sectionId <= 0) {
                    continue;
                }
                $examDraftCounts[$sectionId] = ($examDraftCounts[$sectionId] ?? 0) + 1;
            }

            $homeworkBuilder = $homeworkModel
                ->select('class_section_id, homework_index')
                ->whereIn('class_section_id', $sectionIds);
            if ($schoolYear !== '') {
                $homeworkBuilder->where('school_year', $schoolYear);
            }
            if (!empty($semesterCandidates)) {
                $homeworkBuilder->whereIn('semester', $semesterCandidates);
            }
            $homeworkRows = $homeworkBuilder
                ->where('score IS NOT NULL', null, false)
                ->where('score !=', '')
                ->groupBy('class_section_id, homework_index')
                ->findAll();
            foreach ($homeworkRows as $row) {
                $sectionId = (int) ($row['class_section_id'] ?? 0);
                if ($sectionId <= 0) {
                    continue;
                }
                $homeworkCounts[$sectionId] = ($homeworkCounts[$sectionId] ?? 0) + 1;
            }
        }

        if (empty($lowProgressSectionIds)) {
            $lowProgressSectionIds = $this->resolveLowProgressSectionIds($sectionIds);
        }

        $teachersBySection = [];
        foreach ($assignmentRows as $assignment) {
            $sectionId = (int)($assignment['class_section_id'] ?? 0);
            if ($sectionId <= 0) {
                continue;
            }

            $positionKey = strtolower(trim((string)($assignment['position'] ?? '')));
            $roleKey = $positionKey !== '' ? $positionKey : 'teacher';
            $positionLabel = match ($roleKey) {
                'ta' => 'TA',
                'main' => 'Main',
                default => $roleKey !== '' ? ucfirst($roleKey) : 'Teacher',
            };

            $teacherFullName = trim(($assignment['firstname'] ?? '') . ' ' . ($assignment['lastname'] ?? ''));
            $teacherId = (int)($assignment['teacher_id'] ?? 0);
            if ($teacherFullName === '' || $teacherId <= 0) {
                continue;
            }

            $entry = &$teachersBySection[$sectionId];
            if (!isset($entry)) {
                $entry = [
                    'class_section' => $assignment['class_section_name'] ?? ($sectionMap[$sectionId] ?? "Section {$sectionId}"),
                    'teachers' => [],
                ];
            }

            $entry['teachers'][] = [
                'id' => $teacherId,
                'label' => "{$positionLabel}: {$teacherFullName}",
                'role_key' => $roleKey,
            ];
            unset($entry);
        }

        $today = (new \DateTimeImmutable('now', new \DateTimeZone(date_default_timezone_get() ?: 'UTC')))->format('Y-m-d');

        $rows = [];
        $totalStatuses = 0;
        $missingItemCount = 0;
        $allTeacherIds = [];
        $allClassSectionIds = [];
        $examTerm = $this->resolveExamTermLabel($semester);
        $examScoreField = $examTerm === 'final' ? 'final_exam_score' : 'midterm_exam_score';

        foreach ($sectionMap as $classSectionId => $sectionName) {
            $classSectionId = (int)$classSectionId;
            if ($classSectionId <= 0) {
                continue;
            }

            $studentEntries = $this->db->table('student_class')
                ->select('student_id')
                ->where('class_section_id', $classSectionId)
                ->where('school_year', $schoolYear)
                ->get()
                ->getResultArray();
            if (empty($studentEntries)) {
                $studentEntries = $this->studentClassModel
                    ->select('student_id')
                    ->where('class_section_id', $classSectionId)
                    ->where('school_year', $schoolYear)
                    ->findAll();
            }
            $studentIds = array_filter(array_map(static fn($entry) => (int)($entry['student_id'] ?? 0), $studentEntries));
            $expected = count($studentIds);

            $midtermStudents = [];
            $participationStudents = [];
            if ($classSectionId > 0) {
                $scoreQuery = $semesterScores
                    ->where('class_section_id', $classSectionId)
                    ->where('school_year', $schoolYear);
                if (!empty($semesterCandidates)) {
                    $scoreQuery->whereIn('semester', $semesterCandidates);
                }
                $scoreRecords = $scoreQuery->findAll();
                foreach ($scoreRecords as $score) {
                    $sid = (int)($score['student_id'] ?? 0);
                    if ($sid <= 0 || ($expected > 0 && !in_array($sid, $studentIds, true))) {
                        continue;
                    }
                    $midtermValue = trim((string)($score[$examScoreField] ?? ''));
                    if ($midtermValue !== '') {
                        $midtermStudents[$sid] = true;
                    }
                    $participationValue = trim((string)($score['participation_score'] ?? ''));
                    if ($participationValue !== '') {
                        $participationStudents[$sid] = true;
                    }
                }
            }

            $midtermCommentStudents = [];
            $ptapCommentStudents = [];
            if (!empty($studentIds)) {
                $commentQuery = $scoreComments
                    ->select('student_id, score_type, comment')
                    ->whereIn('student_id', $studentIds)
                    ->where('school_year', $schoolYear)
                    ->whereIn('score_type', [$examTerm, 'ptap']);
                if (!empty($semesterCandidates)) {
                    $commentQuery->whereIn('semester', $semesterCandidates);
                }
                $comments = $commentQuery->findAll();
                foreach ($comments as $comment) {
                    $sid = (int)($comment['student_id'] ?? 0);
                    if ($sid <= 0) {
                        continue;
                    }
                    $text = trim((string)($comment['comment'] ?? ''));
                    if ($text === '') {
                        continue;
                    }
                    $type = strtolower(trim((string)($comment['score_type'] ?? '')));
                    if ($type === $examTerm) {
                        $midtermCommentStudents[$sid] = true;
                    }
                    if ($type === 'ptap') {
                        $ptapCommentStudents[$sid] = true;
                    }
                }
            }

            $attendanceQuery = $attendanceDays
                ->where('class_section_id', $classSectionId)
                ->where('school_year', $schoolYear)
                ->where('date', $today);
            if (!empty($semesterCandidates)) {
                $attendanceQuery->whereIn('semester', $semesterCandidates);
            }
            $attendanceRow = $attendanceQuery->first();
            $attendanceSubmitted = $attendanceRow && in_array(strtolower((string)($attendanceRow['status'] ?? '')), ['submitted', 'published', 'finalized'], true);

            $section = $teachersBySection[$classSectionId] ?? ['teachers' => []];
            $teacherList = $section['teachers'] ?? [];
            if (!empty($teacherList)) {
                usort($teacherList, function ($a, $b) {
                    return $this->teacherRolePriority($a['role_key'] ?? 'teacher') <=> $this->teacherRolePriority($b['role_key'] ?? 'teacher');
                });
                $teacherList = array_values($teacherList);
            }

            foreach ($teacherList as $teacherEntry) {
                if (!empty($teacherEntry['id'])) {
                    $allTeacherIds[] = $teacherEntry['id'];
                }
            }
            $allClassSectionIds[] = $classSectionId;

            $midtermScoreStatus = $this->submissionStatus(count($midtermStudents), $expected);
            $midtermCommentStatus = $this->submissionStatus(count($midtermCommentStudents), $expected);
            $participationStatus = $this->submissionStatus(count($participationStudents), $expected);
            $ptapCommentStatus = $this->submissionStatus(count($ptapCommentStudents), $expected);
            $attendanceStatus = $this->attendanceStatus($attendanceSubmitted);
            $progressSubmitted = (int) ($progressSubmittedBySection[$classSectionId] ?? 0);
            $classProgressStatus = $this->progressStatus($progressSubmitted, $progressExpectedWeeks);
            $draftSubmitted = (int) ($examDraftCounts[$classSectionId] ?? 0);
            $examDraftStatus = $this->draftStatus($draftSubmitted, $examDraftDeadline);
            $homeworkSubmitted = (int) ($homeworkCounts[$classSectionId] ?? 0);
            $homeworkStatus = $this->homeworkStatus($homeworkSubmitted);
            $statusDetails = [
                'midterm_score_status' => $midtermScoreStatus,
                'midterm_comment_status' => $midtermCommentStatus,
                'participation_status' => $participationStatus,
                'ptap_comment_status' => $ptapCommentStatus,
                'class_progress_status' => $classProgressStatus,
                'exam_draft_status' => $examDraftStatus,
                'homework_status' => $homeworkStatus,
            ];
            $missingItemsForSection = $this->buildMissingItems($statusDetails, $semester);
            $missingItemCount += count($missingItemsForSection);
            $totalStatuses += count($statusDetails);

            $rows[] = [
                'class_section' => $sectionMap[$classSectionId] ?? ($section['class_section'] ?? "Section {$classSectionId}"),
                'class_section_id' => $classSectionId,
                'teachers' => $teacherList,
                'midterm_score_status' => $midtermScoreStatus,
                'midterm_comment_status' => $midtermCommentStatus,
                'participation_status' => $participationStatus,
                'ptap_comment_status' => $ptapCommentStatus,
                'attendance_status' => $attendanceStatus,
                'class_progress_status' => $classProgressStatus,
                'exam_draft_status' => $examDraftStatus,
                'homework_status' => $homeworkStatus,
                'missing_items' => $missingItemsForSection,
                'student_count' => $expected,
            ];
        }

        $historyMap = [];
        $teacherIds = array_values(array_unique($allTeacherIds));
        $classSectionIds = array_values(array_unique($allClassSectionIds));
        if (!empty($teacherIds) && !empty($classSectionIds)) {
            $historyRecords = $historyModel
                ->select('teacher_submission_notification_history.*, u.firstname, u.lastname')
                ->join('users u', 'u.id = teacher_submission_notification_history.admin_id', 'left')
                ->where('notification_category', 'teacher_submissions')
                ->whereIn('teacher_submission_notification_history.teacher_id', $teacherIds)
                ->whereIn('teacher_submission_notification_history.class_section_id', $classSectionIds)
                ->orderBy('sent_at', 'DESC')
                ->findAll();

            foreach ($historyRecords as $record) {
                $sectionId = (int)($record['class_section_id'] ?? 0);
                $teacherId = (int)($record['teacher_id'] ?? 0);
                if ($sectionId <= 0 || $teacherId <= 0) {
                    continue;
                }
                $sentAt = $record['sent_at'] ?? null;
                $sentAtText = $sentAt ? local_datetime($sentAt, 'M j, Y g:i A') : '';
                $adminName = trim(($record['firstname'] ?? '') . ' ' . ($record['lastname'] ?? ''));
                if ($adminName === '') {
                    $adminName = 'Administrator';
                }
                $historyMap[$sectionId][$teacherId][] = [
                    'sent_at_text' => $sentAtText,
                    'admin_name' => $adminName,
                    'status' => strtolower((string)($record['status'] ?? 'sent')),
                ];
            }

            foreach ($historyMap as &$teachersHistory) {
                foreach ($teachersHistory as &$entries) {
                    $entries = array_slice($entries, 0, 3);
                }
                unset($entries);
            }
            unset($teachersHistory);
        }

        $summary = [
            'total_items' => $totalStatuses,
            'missing_items' => $missingItemCount,
            'submitted_items' => max(0, $totalStatuses - $missingItemCount),
            'submission_percentage' => $totalStatuses > 0
                ? (int)round((($totalStatuses - $missingItemCount) / $totalStatuses) * 100)
                : 100,
        ];

        return view('administrator/teacher_submissions', [
            'rows' => $rows,
            'semester' => $semester,
            'schoolYear' => $schoolYear,
            'notificationHistory' => $historyMap,
            'summary' => $summary,
            'lowProgressSectionIds' => $lowProgressSectionIds,
            'examDraftDeadlineConfig' => $examDraftDeadlineConfig,
            'examDraftDeadlineFormatted' => $examDraftDeadlineFormatted,
        ]);
    }

    private function resolveLowProgressSectionIds(array $sectionIds): array
    {
        [$expectedWeeks, $submittedBySection] = $this->buildClassProgressStats($sectionIds);
        if ($expectedWeeks <= 0) {
            return [];
        }

        $lowProgressSectionIds = [];
        foreach ($sectionIds as $sectionId) {
            $submitted = (int) ($submittedBySection[$sectionId] ?? 0);
            $percent = ($submitted / $expectedWeeks) * 100;
            if ($percent < 50) {
                $lowProgressSectionIds[] = $sectionId;
            }
        }

        return $lowProgressSectionIds;
    }

    private function buildClassProgressStats(array $sectionIds): array
    {
        $sectionIds = array_values(array_unique(array_filter(array_map('intval', $sectionIds))));
        if (empty($sectionIds)) {
            return [0, []];
        }

        $semesterResolver = new SemesterRangeService($this->configModel);
        $schoolYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
        $semester = (string)($this->configModel->getConfig('semester') ?? '');
        $schoolYearForRange = $schoolYear !== '' ? $schoolYear : $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
        [$rangeStart, $rangeEnd] = $semesterResolver->getSchoolYearRange($schoolYearForRange);
        $semesterNorm = $semesterResolver->normalizeSemester($semester);
        if ($semesterNorm !== '' && $schoolYearForRange !== '') {
            $semRange = $semesterResolver->getSemesterRange($schoolYearForRange, $semesterNorm);
            if ($semRange) {
                [$rangeStart, $rangeEnd] = $semRange;
            }
        }

        $dateList = [];
        try {
            $start = new \DateTimeImmutable($rangeStart);
            $end = new \DateTimeImmutable($rangeEnd);
            $cursor = $start;
            $w = (int) $cursor->format('w');
            if ($w !== 0) {
                $cursor = $cursor->modify('next sunday');
            }
            while ($cursor <= $end) {
                $dateList[] = $cursor->format('Y-m-d');
                $cursor = $cursor->modify('+7 days');
            }
        } catch (\Throwable $e) {
            $dateList = [];
        }

        $noSchoolDays = [];
        $events = [];
        try {
            $calendarModel = new \App\Models\CalendarModel();
            $events = $calendarModel->getEvents();
        } catch (\Throwable $e) {
            $events = [];
        }
        foreach ($events as $event) {
            $d = substr((string) ($event['date'] ?? ''), 0, 10);
            if ($d === '' || empty($event['no_school'])) {
                continue;
            }
            if ($d < $rangeStart || $d > $rangeEnd) {
                continue;
            }
            $eventYear = trim((string) ($event['school_year'] ?? ''));
            if ($schoolYearForRange !== '' && $eventYear !== '' && $eventYear !== $schoolYearForRange) {
                continue;
            }
            $noSchoolDays[$d] = true;
        }

        $anchorSundayYmd = '';
        try {
            $tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
            $tzObj = new \DateTimeZone($tzName ?: 'UTC');
        } catch (\Throwable $e) {
            try {
                $tzObj = new \DateTimeZone(user_timezone() ?: 'UTC');
            } catch (\Throwable $e2) {
                $tzObj = new \DateTimeZone('UTC');
            }
        }
        try {
            $nowDate = new \DateTime('now', $tzObj);
        } catch (\Throwable $e) {
            $nowDate = new \DateTime('now');
        }
        $weekday = (int) $nowDate->format('w');
        $anchorSundayYmd = $weekday === 0
            ? $nowDate->format('Y-m-d')
            : $nowDate->modify('next sunday')->format('Y-m-d');

        $activeDatesSet = [];
        if (! empty($dateList) && $anchorSundayYmd !== '') {
            foreach ($dateList as $d) {
                if ($d <= $anchorSundayYmd && empty($noSchoolDays[$d])) {
                    $activeDatesSet[$d] = true;
                }
            }
        }
        $expectedWeeks = count($activeDatesSet);
        if ($expectedWeeks === 0) {
            return [0, []];
        }

        $builder = $this->db->table('class_progress_reports')
            ->select('class_section_id, week_start')
            ->whereIn('class_section_id', $sectionIds);
        if (! empty($activeDatesSet)) {
            $builder->whereIn('week_start', array_keys($activeDatesSet));
        }
        $rows = $builder->get()->getResultArray();

        $submittedBySection = [];
        foreach ($rows as $row) {
            $sectionId = (int) ($row['class_section_id'] ?? 0);
            $weekStart = (string) ($row['week_start'] ?? '');
            if ($sectionId === 0 || $weekStart === '' || empty($activeDatesSet[$weekStart])) {
                continue;
            }
            $submittedBySection[$sectionId][$weekStart] = true;
        }

        $counts = [];
        foreach ($sectionIds as $sectionId) {
            $counts[$sectionId] = isset($submittedBySection[$sectionId])
                ? count($submittedBySection[$sectionId])
                : 0;
        }

        return [$expectedWeeks, $counts];
    }

    public function sendTeacherSubmissionNotifications()
    {$notify = $this->request->getPost('notify');
        if (!is_array($notify)) {
            return redirect()->back()->with('info', 'Select at least one teacher to notify.');
        }
        $semester = (string)($this->configModel->getConfig('semester') ?? $this->semester ?? '');
        $missingItemsPayload = $this->request->getPost('missing_items') ?? [];
        $homeworkNotifyAll = (bool) $this->request->getPost('homework_notify_all');
        $examTerm = $this->resolveExamTermLabel($semester);
        $examScoreLabel = $examTerm === 'final' ? 'final scores' : 'midterm scores';
        $examCommentLabel = $examTerm === 'final' ? 'final comments' : 'midterm comments';
        $forcedItems = [];
        if ($this->request->getPost('notify_midterm_score')) {
            $forcedItems[] = $examScoreLabel;
        }
        if ($this->request->getPost('notify_midterm_comment')) {
            $forcedItems[] = $examCommentLabel;
        }
        if ($this->request->getPost('notify_participation')) {
            $forcedItems[] = 'participation';
        }
        if ($this->request->getPost('notify_ptap_comment')) {
            $forcedItems[] = 'PTAP comments';
        }
        if ($this->request->getPost('notify_class_progress')) {
            $forcedItems[] = 'class progress';
        }
        if ($this->request->getPost('notify_exam_draft')) {
            $forcedItems[] = 'exam draft';
        }

        $targets = [];
        foreach ($notify as $sectionIdRaw => $teachers) {
            $sectionId = (int)$sectionIdRaw;
            if ($sectionId <= 0 || !is_array($teachers)) {
                continue;
            }
            foreach ($teachers as $teacherIdRaw => $value) {
                $teacherId = (int)$teacherIdRaw;
                if ($teacherId <= 0 || $value === null || $value === '') {
                    continue;
                }
                $key = "{$sectionId}_{$teacherId}";
                $targets[$key] = [
                    'class_section_id' => $sectionId,
                    'teacher_id' => $teacherId,
                ];
            }
        }

        if (empty($targets)) {
            return redirect()->back()->with('info', 'Select at least one teacher to notify.');
        }

        $targets = array_values($targets);
        $teacherIds = array_values(array_unique(array_column($targets, 'teacher_id')));
        $classSectionIds = array_values(array_unique(array_column($targets, 'class_section_id')));

        $classSections = $this->classSectionModel
            ->select('class_section_id, class_section_name')
            ->whereIn('class_section_id', $classSectionIds)
            ->findAll();
        $classSectionMap = [];
        foreach ($classSections as $section) {
            $classSectionMap[(int)($section['class_section_id'] ?? 0)] = $section['class_section_name'] ?? '';
        }

        $teachers = $this->userModel
            ->select('id, firstname, lastname, email')
            ->whereIn('id', $teacherIds)
            ->findAll();
        $teacherLookup = [];
        foreach ($teachers as $teacher) {
            $teacherLookup[(int)$teacher['id']] = $teacher;
        }

        $mailer = new EmailController();
        $adminId = (int)(session()->get('user_id') ?? 0);
        if ($adminId <= 0) {
            return redirect()->to('/login');
        }
        $adminUser = $this->userModel->find($adminId);
        $adminName = trim(($adminUser['firstname'] ?? '') . ' ' . ($adminUser['lastname'] ?? '')) ?: 'Administrator';

        $historyModel = new TeacherSubmissionNotificationHistoryModel();
        $scoreUrl = site_url('/');
        $progressUrl = site_url('teacher/progress/history');
        $examDraftUrl = site_url('teacher/exam-drafts');
        $homeworkUrl = site_url('teacher/addHomework');
        $examDraftDeadlineEmailHtml = $this->buildExamDraftDeadlineEmailHtml();
        $sentCount = 0;
        $failCount = 0;

        foreach ($targets as $target) {
            $classSectionId = (int)$target['class_section_id'];
            $teacherId = (int)$target['teacher_id'];
            $teacher = $teacherLookup[$teacherId] ?? null;
            $sectionName = $classSectionMap[$classSectionId] ?? "Section {$classSectionId}";
            $teacherName = $teacher ? trim(($teacher['firstname'] ?? '') . ' ' . ($teacher['lastname'] ?? '')) : '';
            if ($teacherName === '') {
                $teacherName = 'Teacher';
            }
            $subject = "Reminder: Complete submissions for {$sectionName}";
            $missingPayload = $missingItemsPayload[$classSectionId][$teacherId] ?? '';
            $missingItems = $this->parseMissingItemsPayload((string)$missingPayload);
            $selectedItems = $forcedItems;
            if ($homeworkNotifyAll && !in_array('homework', $selectedItems, true)) {
                $selectedItems[] = 'homework';
            }
            if (!empty($selectedItems)) {
                $missingItems = array_values(array_unique($selectedItems));
            }
            if (!empty($missingItems)) {
                $missingText = htmlspecialchars(
                    $this->formatMissingItemsText($missingItems),
                    ENT_QUOTES,
                    'UTF-8'
                );
                $missingNote = "<p>Outstanding items: {$missingText}.</p>";
            } else {
                $missingNote = "<p>Our records show no outstanding submissions for this section, but please verify if anything still needs attention.</p>";
            }

            $subject = "Reminder: Complete submissions for {$sectionName}";
            $progressNote = '';
            if (in_array('class progress', $missingItems, true)) {
                $progressNote = "<p>Class progress submissions can be updated at <a href=\"{$progressUrl}\">Teacher Progress History</a>.</p>";
            }
            $examDraftNote = '';
            if (in_array('exam draft', $missingItems, true)) {
                $semesterLabel = strtolower(trim((string) $semester));
                if ($semesterLabel === 'fall') {
                    $draftLabel = 'midterm exam draft';
                } elseif ($semesterLabel === 'spring') {
                    $draftLabel = 'final exam draft';
                } else {
                    $draftLabel = 'exam draft';
                }
                $examDraftNote = "<p>" . ucfirst($draftLabel) . " submissions can be updated at <a href=\"{$examDraftUrl}\">Teacher Exam Drafts</a>.</p>"
                    . $examDraftDeadlineEmailHtml;
            }
            $homeworkNote = '';
            if (in_array('homework', $missingItems, true)) {
                $homeworkNote = "<p>Homework scores can be submitted at <a href=\"{$homeworkUrl}\">Teacher Homework</a>.</p>";
            }
            $hasScoreItems = (bool) array_intersect($missingItems, [
                'midterm scores',
                'midterm comments',
                'final scores',
                'final comments',
                'participation',
                'PTAP comments',
                'homework',
            ]);
            $nonScoreOnly = ! empty($missingItems) && ! $hasScoreItems;
            $body = "<p>Dear {$teacherName},</p>"
                . "<p>Administration is gently reminding you to wrap up any remaining "
                . ($nonScoreOnly ? "submissions for {$sectionName}." : "score submissions, comments, and related items for {$sectionName}.")
                . "</p>"
                . $missingNote
                . $progressNote
                . $examDraftNote
                . $homeworkNote
                . ($nonScoreOnly ? '' : "<p>Visit <a href=\"{$scoreUrl}\">Teacher Score Submission</a> to address any remaining items.</p>")
                . "<p>Thank you,<br>Al Rahma Administration</p>";

            $email = $teacher['email'] ?? '';
            $status = 'failed';
            if (!empty($email) && filter_var($email, FILTER_VALIDATE_EMAIL)) {
                $ok = $mailer->sendEmail($email, $subject, $body, 'notifications');
                $status = $ok ? 'sent' : 'failed';
            }

            if ($status === 'sent') {
                $sentCount++;
            } else {
                $failCount++;
            }

            $historyModel->insert([
                'teacher_id' => $teacherId,
                'class_section_id' => $classSectionId,
                'admin_id' => $adminId,
                'notification_category' => 'teacher_submissions',
                'message' => $this->truncateNotificationMessage($body),
                'status' => $status,
                'school_year' => $this->schoolYear,
                'semester' => $this->semester,
                'sent_at' => utc_now(),
            ]);
        }

        $statusParts = [];
        if ($sentCount > 0) {
            $statusParts[] = $sentCount . ' reminder' . ($sentCount === 1 ? '' : 's') . ' sent';
        }
        if ($failCount > 0) {
            $statusParts[] = $failCount . ' reminder' . ($failCount === 1 ? '' : 's') . ' failed';
        }

        $message = !empty($statusParts) ? implode(' and ', $statusParts) : 'No notifications were sent.';
        $flashType = $failCount === 0 ? 'success' : 'warning';

        return redirect()->back()->with($flashType, $message);
    }

    private function submissionStatus(int $filled, int $expected): array
    {
        if ($expected <= 0) {
            return [
                'label' => 'No students',
                'badge' => 'bg-secondary',
                'detail' => '',
                'completed' => true,
            ];
        }
        $completed = $filled >= $expected;
        return [
            'label' => $completed ? 'Submitted' : 'Missing',
            'badge' => $completed ? 'bg-success' : 'bg-danger',
            'detail' => "{$filled}/{$expected}",
            'completed' => $completed,
        ];
    }

    private function progressStatus(int $submitted, int $expected): array
    {
        if ($expected <= 0) {
            return [
                'label' => 'N/A',
                'badge' => 'bg-secondary',
                'detail' => '',
                'completed' => true,
            ];
        }
        $completed = $submitted >= $expected;
        return [
            'label' => $completed ? 'Submitted' : 'Missing',
            'badge' => $completed ? 'bg-success' : 'bg-danger',
            'detail' => "{$submitted}/{$expected}",
            'completed' => $completed,
        ];
    }

    private function homeworkStatus(int $submitted): array
    {
        $completed = $submitted > 0;
        return [
            'label' => $completed ? 'Submitted' : 'Missing',
            'badge' => $completed ? 'bg-success' : 'bg-danger',
            'detail' => $completed ? (string) $submitted : '0',
            'completed' => $completed,
        ];
    }

    private function draftStatus(int $submitted, ?\DateTimeImmutable $deadline): array
    {
        if ($deadline !== null) {
            $today = new \DateTimeImmutable('today');
            if ($today < $deadline) {
                return [
                    'label' => 'Pending',
                    'badge' => 'bg-secondary',
                    'detail' => 'Not due',
                    'completed' => true,
                ];
            }
        }
        $completed = $submitted > 0;
        return [
            'label' => $completed ? 'Submitted' : 'Missing',
            'badge' => $completed ? 'bg-success' : 'bg-danger',
            'detail' => $completed ? (string) $submitted : '0',
            'completed' => $completed,
        ];
    }

    /**
     * Exam draft due date for the teacher submissions dashboard: prefers the configuration key
     * `exam_draft_deadline` (same as automated reminders); otherwise fall/spring exam deadlines.
     */
    private function resolveTeacherDashboardExamDraftDeadline(string $semester, string $schoolYear): ?\DateTimeImmutable
    {
        $fromExamDraftKey = $this->parseExamDraftDeadlineConfigValue();
        if ($fromExamDraftKey !== null) {
            return $fromExamDraftKey;
        }

        return $this->resolveExamDraftDeadline($semester, $schoolYear);
    }

    /**
     * Parses the `exam_draft_deadline` configuration value using the application timezone (midnight that calendar day).
     */
    private function parseExamDraftDeadlineConfigValue(): ?\DateTimeImmutable
    {
        $raw = trim((string) ($this->configModel->getConfig('exam_draft_deadline') ?? ''));
        if ($raw === '') {
            return null;
        }
        $tz = new \DateTimeZone(config('App')->appTimezone ?? 'UTC');
        try {
            $deadline = new \DateTimeImmutable($raw, $tz);
        } catch (\Throwable $e) {
            return null;
        }

        return $deadline->setTime(0, 0, 0);
    }

    /**
     * HTML snippet for reminder emails when exam draft is included (deadline from exam_draft_deadline config).
     */
    private function buildExamDraftDeadlineEmailHtml(): string
    {
        $raw = trim((string) ($this->configModel->getConfig('exam_draft_deadline') ?? ''));
        if ($raw === '') {
            return '';
        }
        $parsed = $this->parseExamDraftDeadlineConfigValue();
        $display = $parsed !== null
            ? htmlspecialchars($parsed->format('l, F j, Y'), ENT_QUOTES, 'UTF-8')
            : htmlspecialchars($raw, ENT_QUOTES, 'UTF-8');
        $rawEsc = htmlspecialchars($raw, ENT_QUOTES, 'UTF-8');

        return '<p><strong>Exam draft submission deadline</strong> (<code>exam_draft_deadline</code>): '
            . "<strong>{$display}</strong>"
            . ($parsed !== null && $rawEsc !== $display ? " <span style=\"color:#555;\">(configured value: {$rawEsc})</span>" : '')
            . '.</p>';
    }

    private function resolveExamDraftDeadline(string $semester, string $schoolYear): ?\DateTimeImmutable
    {
        $semesterKey = strtolower(trim($semester));
        if ($semesterKey === 'fall') {
            $deadlineValue = (string)($this->configModel->getConfig('fall_exam_deadline') ?? '');
        } elseif ($semesterKey === 'spring') {
            $deadlineValue = (string)($this->configModel->getConfig('spring_exam_deadline') ?? '');
        } else {
            return null;
        }
        $deadlineValue = trim($deadlineValue);
        if ($deadlineValue === '') {
            return null;
        }
        try {
            $deadline = new \DateTimeImmutable($deadlineValue);
        } catch (\Throwable $e) {
            return null;
        }
        if ($schoolYear !== '' && preg_match('/^\d{4}-\d{4}$/', $schoolYear)) {
            $deadlineYear = $deadline->format('Y');
            if ($deadlineYear === '1970') {
                return null;
            }
        }
        return $deadline->setTime(0, 0, 0);
    }

    private function resolveExamTermLabel(string $semester): string
    {
        $semesterKey = strtolower(trim($semester));
        if ($semesterKey === '') {
            return 'midterm';
        }
        if (str_contains($semesterKey, 'spring')) {
            return 'final';
        }
        if (str_contains($semesterKey, 'fall')) {
            return 'midterm';
        }
        return 'midterm';
    }

    private function buildSemesterCandidates(string $semester): array
    {
        $semester = trim((string) $semester);
        if ($semester === '') {
            return [];
        }
        $candidates = [
            $semester,
            strtolower($semester),
            strtoupper($semester),
            ucfirst(strtolower($semester)),
        ];
        $candidates = array_values(array_unique(array_filter($candidates, static fn ($v) => $v !== '')));
        return $candidates;
    }
    private function attendanceStatus(bool $submitted): array
    {
        return [
            'label' => $submitted ? 'Submitted' : 'Missing',
            'badge' => $submitted ? 'bg-success' : 'bg-danger',
            'completed' => $submitted,
        ];
    }

    private function buildMissingItems(array $statusMap, string $semester): array
    {
        $examTerm = $this->resolveExamTermLabel($semester);
        $examScoreLabel = $examTerm === 'final' ? 'final scores' : 'midterm scores';
        $examCommentLabel = $examTerm === 'final' ? 'final comments' : 'midterm comments';
        $labels = [
            'midterm_score_status' => $examScoreLabel,
            'midterm_comment_status' => $examCommentLabel,
            'participation_status' => 'participation',
            'ptap_comment_status' => 'PTAP comments',
            'attendance_status' => 'attendance',
            'class_progress_status' => 'class progress',
            'exam_draft_status' => 'exam draft',
            'homework_status' => 'homework',
        ];

        $items = [];
        foreach ($statusMap as $key => $status) {
            $completed = $status['completed'] ?? true;
            if (!$completed && isset($labels[$key])) {
                $items[] = $labels[$key];
            }
        }

        return array_values($items);
    }

    private function formatMissingItemsText(array $items): string
    {
        $items = array_values(array_filter(array_map('trim', $items), static fn($v) => $v !== ''));
        $count = count($items);
        if ($count === 0) {
            return '';
        }
        if ($count === 1) {
            return $items[0];
        }
        if ($count === 2) {
            return $items[0] . ' and ' . $items[1];
        }
        $last = array_pop($items);
        return implode(', ', $items) . ' and ' . $last;
    }

    private function teacherRolePriority(string $roleKey): int
    {
        switch (strtolower($roleKey)) {
            case 'main':
                return 1;
            case 'ta':
                return 2;
            default:
                return 3;
        }
    }

    private function truncateNotificationMessage(string $html, int $limit = 1000): string
    {
        $text = trim(strip_tags($html));
        if ($text === '') {
            return '';
        }
        if (mb_strlen($text) <= $limit) {
            return $text;
        }
        return mb_substr($text, 0, $limit) . '…';
    }

    private function parseMissingItemsPayload(string $payload): array
    {
        if ($payload === '') {
            return [];
        }
        $decoded = @json_decode(base64_decode($payload, true) ?: '', true);
        if (!is_array($decoded)) {
            return [];
        }

        $items = [];
        foreach ($decoded as $item) {
            $item = trim((string)$item);
            if ($item === '') {
                continue;
            }
            $items[] = $item;
        }

        return array_values(array_unique($items));
    }

    public function notificationsAlerts()
    {
        if (!$this->canManageAdminNotifications()) {
            return redirect()->to('/login');
        }

        $admins = $this->fetchAdminNotificationUsers();
        $subjects = $this->notificationSubjectOptions();
        $assignedSubjects = [];
        $tableReady = $this->db->tableExists('admin_notification_subjects');

        if ($tableReady && !empty($admins)) {
            $adminIds = array_map('intval', array_column($admins, 'id'));
            $rows = $this->adminNotificationSubjectModel
                ->select('id, admin_id, subject')
                ->whereIn('admin_id', $adminIds)
                ->findAll();

            foreach ($rows as $row) {
                $adminId = (int) ($row['admin_id'] ?? 0);
                $subject = (string) ($row['subject'] ?? '');
                if ($adminId <= 0 || $subject === '') {
                    continue;
                }
                if (!isset($assignedSubjects[$adminId])) {
                    $assignedSubjects[$adminId] = [];
                }
                $assignedSubjects[$adminId][$subject] = true;
            }
        }

        return view('administrator/notifications_alerts', [
            'admins' => $admins,
            'subjects' => $subjects,
            'assignedSubjects' => $assignedSubjects,
            'tableReady' => $tableReady,
        ]);
    }

    public function saveNotificationSubjects()
    {
        if (!$this->canManageAdminNotifications()) {
            return redirect()->to('/login');
        }

        if (!$this->db->tableExists('admin_notification_subjects')) {
            return redirect()->to('/administrator/notifications_alerts')
                ->with('error', 'Notification subject storage is missing. Run migrations first.');
        }

        $posted = $this->request->getPost('subjects');
        if (!is_array($posted)) {
            return redirect()->to('/administrator/notifications_alerts')
                ->with('error', 'No selections were submitted.');
        }

        $subjects = $this->notificationSubjectOptions();
        $allowed = array_keys($subjects);

        $admins = $this->fetchAdminNotificationUsers();
        $adminIds = array_map('intval', array_column($admins, 'id'));
        if (empty($adminIds)) {
            return redirect()->to('/administrator/notifications_alerts')
                ->with('info', 'No admins found to update.');
        }

        $existing = $this->adminNotificationSubjectModel
            ->select('id, admin_id, subject')
            ->whereIn('admin_id', $adminIds)
            ->findAll();

        $existingMap = [];
        foreach ($existing as $row) {
            $adminId = (int) ($row['admin_id'] ?? 0);
            $subject = (string) ($row['subject'] ?? '');
            if ($adminId <= 0 || $subject === '') {
                continue;
            }
            if (!isset($existingMap[$adminId])) {
                $existingMap[$adminId] = [];
            }
            $existingMap[$adminId][$subject] = (int) ($row['id'] ?? 0);
        }

        $updates = 0;

        foreach ($adminIds as $adminId) {
            $subjectRaw = $posted[$adminId] ?? [];

            $selected = [];
            if (is_array($subjectRaw)) {
                foreach ($subjectRaw as $key => $value) {
                    $candidate = is_string($key) ? $key : $value;
                    if (!is_string($candidate)) {
                        continue;
                    }
                    $candidate = trim($candidate);
                    if ($candidate !== '') {
                        $selected[] = $candidate;
                    }
                }
            }

            $selected = array_values(array_unique(array_filter($selected, function ($value) use ($allowed) {
                return in_array($value, $allowed, true);
            })));

            $current = array_keys($existingMap[$adminId] ?? []);
            $toDelete = array_values(array_diff($current, $selected));
            $toInsert = array_values(array_diff($selected, $current));

            if (!empty($toDelete)) {
                $this->adminNotificationSubjectModel
                    ->where('admin_id', $adminId)
                    ->whereIn('subject', $toDelete)
                    ->delete();
                $updates += count($toDelete);
            }

            if (!empty($toInsert)) {
                $batch = [];
                foreach ($toInsert as $subject) {
                    $batch[] = [
                        'admin_id' => $adminId,
                        'subject' => $subject,
                    ];
                }
                $this->adminNotificationSubjectModel->insertBatch($batch);
                $updates += count($toInsert);
            }
        }

        return redirect()->to('/administrator/notifications_alerts')
            ->with('success', $updates > 0 ? 'Notification subjects updated.' : 'No changes were made.');
    }

    public function printNotificationRecipients()
    {
        if (!$this->canManageAdminNotifications()) {
            return redirect()->to('/login');
        }

        $admins = $this->fetchAdminNotificationUsers();
        $tableReady = $this->db->tableExists('admin_notification_subjects');
        $assigned = [];

        if ($tableReady && !empty($admins)) {
            $adminIds = array_map('intval', array_column($admins, 'id'));
            $rows = $this->adminNotificationSubjectModel
                ->select('admin_id')
                ->where('subject', 'print_requests')
                ->whereIn('admin_id', $adminIds)
                ->findAll();

            foreach ($rows as $row) {
                $adminId = (int) ($row['admin_id'] ?? 0);
                if ($adminId <= 0) {
                    continue;
                }
                $assigned[$adminId] = true;
            }
        }

        return view('administrator/print_notification_admins', [
            'admins' => $admins,
            'assigned' => $assigned,
            'tableReady' => $tableReady,
        ]);
    }

    public function savePrintNotificationRecipients()
    {
        if (!$this->canManageAdminNotifications()) {
            return redirect()->to('/login');
        }

        if (!$this->db->tableExists('admin_notification_subjects')) {
            return redirect()->to('/administrator/print-notifications')
                ->with('error', 'Notification subject storage is missing. Run migrations first.');
        }

        $admins = $this->fetchAdminNotificationUsers();
        $adminIds = array_map('intval', array_column($admins, 'id'));
        if (empty($adminIds)) {
            return redirect()->to('/administrator/print-notifications')
                ->with('info', 'No admins found to update.');
        }

        $posted = (array) $this->request->getPost('notify');
        $selected = [];
        foreach ($posted as $key => $value) {
            $adminId = (int) $key;
            if ($adminId <= 0) {
                continue;
            }
            if (!in_array($adminId, $adminIds, true)) {
                continue;
            }
            $selected[] = $adminId;
        }
        $selected = array_values(array_unique($selected));

        $existingRows = $this->adminNotificationSubjectModel
            ->select('admin_id')
            ->where('subject', 'print_requests')
            ->whereIn('admin_id', $adminIds)
            ->findAll();

        $current = array_values(array_unique(array_map(
            fn($row) => (int) ($row['admin_id'] ?? 0),
            $existingRows
        )));

        $toDelete = array_values(array_diff($current, $selected));
        $toInsert = array_values(array_diff($selected, $current));

        if (!empty($toDelete)) {
            $this->adminNotificationSubjectModel
                ->where('subject', 'print_requests')
                ->whereIn('admin_id', $toDelete)
                ->delete();
        }

        if (!empty($toInsert)) {
            $batch = [];
            foreach ($toInsert as $adminId) {
                $batch[] = [
                    'admin_id' => $adminId,
                    'subject' => 'print_requests',
                ];
            }
            $this->adminNotificationSubjectModel->insertBatch($batch);
        }

        $changes = count($toDelete) + count($toInsert);
        return redirect()->to('/administrator/print-notifications')
            ->with('success', $changes > 0 ? 'Print notification recipients updated.' : 'No changes were made.');
    }

    private function notificationSubjectOptions(): array
    {
        return [
             'academics' => 'Academics',
             'attendance' => 'Attendance',
             'events' => 'Events',
             'finance' => 'Finance',
             'general' => 'General',
             'print_requests' => 'Print Requests',
        ];
    }

    private function getAdminNotificationExcludedRoles(): array
    {
        return [
            'parent',
            'student',
            'guest',
            'teacher',
            'assistant teacher',
            'teacher assistant',
            'teacher_assistant',
            'assistant_teacher',
            'ta',
            'authorized_user',
        ];
    }

    private function canManageAdminNotifications(): bool
    {
        $session = session();
        if (! $session->get('is_logged_in')) {
            return false;
        }

        $role = trim((string) ($session->get('role') ?? ''));
        if ($role === '') {
            return false;
        }

        $excluded = array_map(
            fn ($value) => strtolower(trim((string) $value)),
            $this->getAdminNotificationExcludedRoles()
        );

        return !in_array(strtolower($role), $excluded, true);
    }

    private function fetchAdminNotificationUsers(): array
    {
        $excluded = $this->getAdminNotificationExcludedRoles();
        $excludedList = "'" . implode("','", $excluded) . "'";

        return $this->db->table('users u')
            ->select('u.id, u.firstname, u.lastname, u.email')
            ->join('user_roles ur', 'ur.user_id = u.id', 'inner')
            ->join('roles r', 'r.id = ur.role_id', 'inner')
            ->where('r.name IS NOT NULL', null, false)
            ->where('ur.deleted_at', null)
            ->where("LOWER(r.name) NOT IN ({$excludedList})", null, false)
            ->groupBy('u.id, u.firstname, u.lastname, u.email')
            ->orderBy('u.lastname', 'ASC')
            ->orderBy('u.firstname', 'ASC')
            ->get()
            ->getResultArray();
    }

    public function feeCollection()
    {
        return view('administrator/fee_collection');
    }

    public function expenseManagement()
    {
        return view('administrator/expense_management');
    }

    public function budgetReports()
    {
        return view('administrator/budget_reports');
    }

    public function scholarshipInformation()
    {
        return view('administrator/scholarship_information');
    }

    public function attendanceRecords()
    {
        return view('administrator/attendance_records');
    }

    public function academicPerformance()
    {
        return view('administrator/academic_performance');
    }

    public function behaviorReports()
    {
        return view('administrator/behavior_reports');
    }

    public function healthRecords()
    {
        return view('administrator/health_records');
    }

    public function contactInformation()
    {
        return view('administrator/contact_information');
    }

    public function studentProfiles()
    {
        $db   = db_connect();
        $isPg = ($db->getPlatform() === 'Postgre'); // 'MySQLi', 'Postgre', 'SQLSRV', ...

        // In MySQL, avoid truncation of long lists
        if (!$isPg) {
            $db->query('SET SESSION group_concat_max_len = 8192');
        }

        $b = $db->table('students');

        if ($isPg) {
            // Postgres: use subqueries for aggregates to avoid GROUP BY on students.*
            $students = $b->select([
                'students.*',
                'users.firstname AS parent_firstname',
                'users.lastname  AS parent_lastname',
                'users.email     AS parent_email',
                'users.cellphone AS parent_phone',

                // Pick one emergency contact (first by id)
                "(SELECT ec.emergency_contact_name FROM emergency_contacts ec
              WHERE ec.parent_id = students.parent_id
              ORDER BY ec.id ASC LIMIT 1) AS emergency_name",
                "(SELECT ec.relation FROM emergency_contacts ec
              WHERE ec.parent_id = students.parent_id
              ORDER BY ec.id ASC LIMIT 1) AS emergency_relationship",
                "(SELECT ec.cellphone FROM emergency_contacts ec
              WHERE ec.parent_id = students.parent_id
              ORDER BY ec.id ASC LIMIT 1) AS emergency_phone",
                "(SELECT ec.email FROM emergency_contacts ec
              WHERE ec.parent_id = students.parent_id
              ORDER BY ec.id ASC LIMIT 1) AS emergency_email",

                // Aggregated lists
                "(SELECT STRING_AGG(DISTINCT sa.allergy, ', ' ORDER BY sa.allergy)
              FROM student_allergies sa WHERE sa.student_id = students.id) AS allergies",
                "(SELECT STRING_AGG(DISTINCT smc.condition_name, ', ' ORDER BY smc.condition_name)
              FROM student_medical_conditions smc WHERE smc.student_id = students.id) AS medical_conditions",
            ])
                ->join('users', 'users.id = students.parent_id', 'left')
                ->orderBy('students.lastname', 'ASC')
                ->orderBy('students.firstname', 'ASC')
                ->get()
                ->getResultArray();
        } else {
            // MySQL: GROUP_CONCAT + MIN() for emergency_* to satisfy ONLY_FULL_GROUP_BY
            $students = $b->select([
                'students.*',
                'users.firstname AS parent_firstname',
                'users.lastname  AS parent_lastname',
                'users.email     AS parent_email',
                'users.cellphone AS parent_phone',

                'MIN(emergency_contacts.emergency_contact_name) AS emergency_name',
                'MIN(emergency_contacts.relation)               AS emergency_relationship',
                'MIN(emergency_contacts.cellphone)              AS emergency_phone',
                'MIN(emergency_contacts.email)                  AS emergency_email',

                "GROUP_CONCAT(DISTINCT student_allergies.allergy
              ORDER BY student_allergies.allergy SEPARATOR ', ') AS allergies",
                "GROUP_CONCAT(DISTINCT student_medical_conditions.condition_name
              ORDER BY student_medical_conditions.condition_name SEPARATOR ', ') AS medical_conditions",
            ])
                ->join('users', 'users.id = students.parent_id', 'left')
                ->join('emergency_contacts', 'emergency_contacts.parent_id = students.parent_id', 'left')
                ->join('student_allergies', 'student_allergies.student_id = students.id', 'left')
                ->join('student_medical_conditions', 'student_medical_conditions.student_id = students.id', 'left')
                ->groupBy('students.id')
                ->orderBy('students.lastname', 'ASC')
                ->orderBy('students.firstname', 'ASC')
                ->get()
                ->getResultArray();
        }

        // === Inject class_section_name from student_class via your method and replace grade ===
        foreach ($students as $i => $row) {
            $sid = (int) ($row['id'] ?? 0);
            if ($sid > 0) {
                // Fetch class_section_name for this student
                // Assumes your method signature: getClassSectionNameByStudentId(int $studentId): ?string
                $classSectionName = (string) ($this->studentClassModel->getClassSectionNameByStudentId($sid) ?? '');

                // Expose it explicitly and replace original grade if present
                $students[$i]['class_section_name'] = $classSectionName;
            } else {
                // Keep keys consistent even if id missing
                $students[$i]['class_section_name'] = '';
            }
        }
        // === end injection ===

        return view('administrator/student_profiles', [
            'students'      => $students,
            'gradeOptions'  => ['K', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'Youth'],
            'genderOptions' => ['Male', 'Female', 'Other'],
        ]);
    }



    public function parentProfiles()
    {
        if ($redirect = $this->redirectWithoutLegacyTermFilters('administrator/parent_profiles')) {
            return $redirect;
        }

        // Fetch all users with their roles in one go
        $allUsers = $this->userModel->findAll();
        $parents = [];

        // Fetch roles for users in one query to avoid looping over all users
        foreach ($allUsers as $user) {
            // Get the roles of the user directly
            $roles = $this->userRoleModel->getRolesByUserId($user['id']);
            $isParent = false;

            // Check if $roles is iterable and contains the 'parent' role
            if (is_array($roles)) {
                foreach ($roles as $role) {
                    if (isset($role['role_name']) && $role['role_name'] === 'parent') {
                        $isParent = true;
                        break;
                    }
                }
            }

            if ($isParent) {
                // Retrieve the latest paid amount and balance for this parent
                $paidAmount = $this->invoiceModel->getLatestInvoicePaidAmount($user['id']) ?? 0;
                $balance = $this->invoiceModel->getLatestInvoiceBalance($user['id']) ?? 0;

                // Get students for this parent
                $studentsData = $this->studentModel->where('parent_id', $user['id'])->findAll();
                $students = [];
                foreach ($studentsData as $student) {
                    $classSectionName = $this->studentClassModel->getClassSectionNameByStudentId($student['id']) ?? 'N/A';
                    $students[] = [
                        'name' => $student['firstname'] . ' ' . $student['lastname'],
                        'class_section' => $classSectionName
                    ];
                }

                // Add parent data to the array
                $parents[] = [
                    'id' => $user['id'],
                    'school_id' => $user['school_id'],
                    'firstname' => $user['firstname'],
                    'lastname' => $user['lastname'],
                    'email' => $user['email'],
                    'cellphone' => $user['cellphone'],
                    'gender' => $user['gender'],
                    'created_at' => $user['created_at'],
                    'paid_amount' => $paidAmount,
                    'balance' => $balance,
                    'students' => $students,
                ];
            }
        }

        return view('administrator/parent_profile', ['parents' => $parents]);
    }

    private function redirectWithoutLegacyTermFilters(string $route): ?\CodeIgniter\HTTP\RedirectResponse
    {
        $legacyTermKeys = ['school_year', 'schoolYear', 'year', 'semester'];
        $query = $this->request->getGet();
        $hasLegacyTermFilter = false;

        foreach ($legacyTermKeys as $key) {
            if (array_key_exists($key, $query)) {
                unset($query[$key]);
                $hasLegacyTermFilter = true;
            }
        }

        if (!$hasLegacyTermFilter) {
            return null;
        }

        $target = site_url($route) . (!empty($query) ? '?' . http_build_query($query) : '');
        return redirect()->to($target);
    }

    public function manageUsers()
    {// Fetch all users
        $users = $this->userModel->findAll();

        // Fetch roles for each user
        foreach ($users as &$user) {
            $userRoles = ($this->userRoleModel)->where('user_id', $user['id'])->findAll();
            $user['roles'] = [];
            foreach ($userRoles as $userRole) {
                $role = $this->roleModel->find($userRole['role_id']);
                if ($role) {
                    $user['roles'][] = $role['name'];
                }
            }
        }

        $data['users'] = $users;

        return view('administrator/manage_users', $data);
    }

    public function editUser($userId)
    {$user = $this->userModel->find($userId);

        // Fetch roles assigned to the user
        $userRoles = $this->userRoleModel->where('user_id', $userId)->findAll();
        $assignedRoles = [];
        foreach ($userRoles as $userRole) {
            $role = $this->roleModel->find($userRole['role_id']);
            if ($role) {
                $assignedRoles[] = $role['id'];
            }
        }

        $roles = $this->roleModel->findAll();

        $data = [
            'user' => $user,
            'roles' => $roles,
            'assignedRoles' => $assignedRoles,
        ];

        return view('administrator/edit_user', $data);
    }

    public function updateUser()
    {$userId = $this->request->getPost('id');
        $roleIds = $this->request->getPost('role_ids');

        // Update user details
        $data = [
            'id' => $userId,
            'firstname' => $this->request->getPost('firstname'),
            'lastname' => $this->request->getPost('lastname'),
            'email' => $this->request->getPost('email'),
        ];
        $this->userModel->save($data);

        // Update user roles
        $this->userRoleModel->where('user_id', $userId)->delete();
        foreach ($roleIds as $roleId) {
            $this->userRoleModel->save([
                'user_id' => $userId,
                'role_id' => $roleId,
            ]);
        }

        return redirect()->to('/administrator/manage-users');
    }

    public function deleteUser($userId)
    {// Delete user roles first
        $this->userRoleModel->where('user_id', $userId)->delete();

        // Delete the user
        $this->userModel->delete($userId);

        return redirect()->to('/administrator/manage-users');
    }

    public function showEnrollmentWithdrawalPage()
    {
        try {
            $schoolYearContext = $this->resolveSchoolYearContext();
            $selectedYear = $schoolYearContext->yearName();

            $students = $this->studentModel->getStudentsWithClassAndEnrollment($selectedYear);

            $selectedStartYear = $this->getSchoolYearStartYear((string)$selectedYear);
            $removedPriorIds = [];
            if ($selectedStartYear !== null) {
                $removedRows = $this->db->table('enrollments')
                    ->select('student_id, school_year')
                    ->where('is_withdrawn', 1)
                    ->get()->getResultArray();
                foreach ($removedRows as $row) {
                    $rowYear = $this->getSchoolYearStartYear((string)($row['school_year'] ?? ''));
                    if ($rowYear !== null && $rowYear < $selectedStartYear) {
                        $removedPriorIds[(int)($row['student_id'] ?? 0)] = true;
                    }
                }
            }
            $returningStudentIds = $this->priorYearStudentIds($selectedYear);

            foreach ($students as &$s) {
                // ===== Ensure IDs needed by the modal =====
                $s['student_id'] = (int)($s['id'] ?? 0);
                $s['removed_previous_year'] = isset($removedPriorIds[$s['student_id']]) ? 'Yes' : 'No';

                // Prefer parent_id; fallback to secondparent_user_id if present
                if (empty($s['parent_id']) && !empty($s['secondparent_user_id'])) {
                    $s['parent_id'] = (int)$s['secondparent_user_id'];
                } else {
                    $s['parent_id'] = (int)($s['parent_id'] ?? 0);
                }

                // ===== Parent display + sort keys (keep existing behavior) =====
                $pf = trim((string)($s['parent_firstname'] ?? ''));
                $pl = trim((string)($s['parent_lastname']  ?? ''));

                // Fallback: if only a single full name exists
                if ($pf === '' && $pl === '' && !empty($s['parent_fullname'])) {
                    $parts = preg_split('/\s+/', trim((string)$s['parent_fullname']), 2);
                    $pf = $parts[0] ?? '';
                    $pl = $parts[1] ?? '';
                }

                $s['parent_label'] = trim($pf . ' ' . $pl);
                $s['parent_sort']  = trim(($pl !== '' ? $pl : $pf) . ' ' . $pf);

                if ($s['parent_label'] === '') {
                    $s['parent_label'] = 'Unknown Parent';
                    $s['parent_sort']  = 'ZZZ Unknown Parent';
                }

                // ===== New-student flags =====
                $s['is_new']      = (int) ($s['is_new'] ?? 0);
                if (isset($returningStudentIds[$s['student_id']])) {
                    $s['is_new'] = 0;
                }
                $s['new_student'] = $s['is_new'] === 1 ? 'Yes' : 'No';

                // ===== Admission override =====
                // Enrollment status for selected year
                $statusForYear = $this->enrollmentModel->getEnrollmentStatus((int)$s['student_id'], $selectedYear);
                if (!empty($statusForYear)) {
                    $s['enrollment_status'] = $statusForYear;
                } elseif (($s['admission_status'] ?? null) === 'denied') {
                    $s['enrollment_status'] = 'denied';
                } else {
                    $s['enrollment_status'] = 'admission under review';
                    $s['admission_status'] = 'pending';
                }

                // ===== Class section name for the selected year =====
                $name = $this->studentClassModel->getClassSectionsByStudentId((int)$s['student_id'], $selectedYear);
                $s['class_section'] = $name ?: 'Class not Assigned';

                // ===== Sortable registration date (for data-order in view) =====
                $s['registration_date_order'] = !empty($s['registration_date'])
                    ? date('Y-m-d', strtotime($s['registration_date']))
                    : '';
            }
            unset($s); // break reference

            // ===== Sort by parent, then student (lastname, firstname) =====
            usort($students, function (array $a, array $b) {
                $pa = $a['parent_sort'] ?? '';
                $pb = $b['parent_sort'] ?? '';
                if (strcasecmp($pa, $pb) === 0) {
                    $la = $a['lastname']  ?? '';
                    $lb = $b['lastname']  ?? '';
                    $cmp = strcasecmp($la, $lb);
                    if ($cmp !== 0) return $cmp;
                    return strcasecmp($a['firstname'] ?? '', $b['firstname'] ?? '');
                }
                return strcasecmp($pa, $pb);
            });

            $classes = $this->enrollmentClassOptions((string)$selectedYear);

            return view('enroll_withdraw/enrollment_withdrawal', [
                'students'     => $students,
                'classes'      => $classes,   // <-- used by the modal <select>
                'selectedYear' => $selectedYear,
                'currentYear'  => $selectedYear,
                'isCurrentYear' => ! $schoolYearContext->isReadonly(),
                'missingYear'  => $selectedYear === '',
            ]);
        } catch (\Throwable $e) {
            log_message('error', 'Enrollment/Withdrawal page error: {msg}', ['msg' => $e->getMessage()]);
            return view('errors/html/error_500');
        }
    }

    // API: Enrollment/Withdrawal data for admin page
    public function enrollmentWithdrawalData()
    {
        try {
            $selectedYear = $this->currentSchoolYearName((string)($this->schoolYear ?? ''));

            $students = $this->studentModel->getStudentsWithClassAndEnrollment($selectedYear);

            $selectedStartYear = $this->getSchoolYearStartYear((string)$selectedYear);
            $removedPriorIds = [];
            if ($selectedStartYear !== null) {
                $removedRows = $this->db->table('enrollments')
                    ->select('student_id, school_year')
                    ->where('is_withdrawn', 1)
                    ->get()->getResultArray();
                foreach ($removedRows as $row) {
                    $rowYear = $this->getSchoolYearStartYear((string)($row['school_year'] ?? ''));
                    if ($rowYear !== null && $rowYear < $selectedStartYear) {
                        $removedPriorIds[(int)($row['student_id'] ?? 0)] = true;
                    }
                }
            }
            $returningStudentIds = $this->priorYearStudentIds($selectedYear);

            foreach ($students as &$s) {
                $s['student_id'] = (int)($s['id'] ?? 0);
                $s['removed_previous_year'] = isset($removedPriorIds[$s['student_id']]) ? 'Yes' : 'No';

                if (empty($s['parent_id']) && !empty($s['secondparent_user_id'])) {
                    $s['parent_id'] = (int)$s['secondparent_user_id'];
                } else {
                    $s['parent_id'] = (int)($s['parent_id'] ?? 0);
                }

                $pf = trim((string)($s['parent_firstname'] ?? ''));
                $pl = trim((string)($s['parent_lastname']  ?? ''));
                if ($pf === '' && $pl === '' && !empty($s['parent_fullname'])) {
                    $parts = preg_split('/\s+/', trim((string)$s['parent_fullname']), 2);
                    $pf = $parts[0] ?? '';
                    $pl = $parts[1] ?? '';
                }
                $s['parent_label'] = trim($pf . ' ' . $pl) ?: 'Unknown Parent';
                $s['parent_sort']  = trim(($pl !== '' ? $pl : $pf) . ' ' . $pf) ?: 'ZZZ Unknown Parent';

                $s['is_new']      = (int) ($s['is_new'] ?? 0);
                if (isset($returningStudentIds[$s['student_id']])) {
                    $s['is_new'] = 0;
                }
                $s['new_student'] = $s['is_new'] === 1 ? 'Yes' : 'No';

                $statusForYear = $this->enrollmentModel->getEnrollmentStatus((int)$s['student_id'], $selectedYear);
                if (!empty($statusForYear)) {
                    $s['enrollment_status'] = $statusForYear;
                } elseif (($s['admission_status'] ?? null) === 'denied') {
                    $s['enrollment_status'] = 'denied';
                } else {
                    $s['enrollment_status'] = 'admission under review';
                    $s['admission_status'] = 'pending';
                }

                $className = $this->studentClassModel->getClassSectionsByStudentId((int)$s['student_id'], $selectedYear);
                $s['class_section'] = $className ?: 'Class not Assigned';

                $s['registration_date_order'] = !empty($s['registration_date'])
                    ? date('Y-m-d', strtotime($s['registration_date']))
                    : '';
            }
            unset($s);

            usort($students, function (array $a, array $b) {
                $pa = $a['parent_sort'] ?? '';
                $pb = $b['parent_sort'] ?? '';
                if (strcasecmp($pa, $pb) === 0) {
                    $la = $a['lastname']  ?? '';
                    $lb = $b['lastname']  ?? '';
                    $cmp = strcasecmp($la, $lb);
                    if ($cmp !== 0) return $cmp;
                    return strcasecmp($a['firstname'] ?? '', $b['firstname'] ?? '');
                }
                return strcasecmp($pa, $pb);
            });

            $classes = $this->enrollmentClassOptions((string)$selectedYear);

            return $this->response->setJSON([
                'students'    => $students,
                'classes'     => $classes,
                'csrfHash'    => csrf_hash(),
                'semester'    => (string)$this->semester,
                'school_year' => (string)$selectedYear,
            ]);
        } catch (\Throwable $e) {
            log_message('error', 'enrollmentWithdrawalData error: {msg}', ['msg' => $e->getMessage()]);
            return $this->response->setStatusCode(500)->setJSON(['error' => 'Server error']);
        }
    }

    private function enrollmentClassOptions(string $selectedYear): array
    {
        $select = ['id', 'class_section_id', 'class_section_name'];
        $hasSchoolYear = $this->db->fieldExists('school_year', 'classSection');
        $hasSemester = $this->db->fieldExists('semester', 'classSection');

        if ($hasSchoolYear) {
            $select[] = 'school_year';
        }
        if ($hasSemester) {
            $select[] = 'semester';
        }

        $query = $this->classSectionModel
            ->select(implode(', ', $select))
            ->orderBy('class_section_name', 'ASC');

        if ($hasSchoolYear && $selectedYear !== '') {
            $query->where('school_year', $selectedYear);
        }
        if ($hasSemester) {
            $query->where('semester', (string)$this->semester);
        }

        $classes = $query->findAll();

        if (! empty($classes) || (! $hasSchoolYear && ! $hasSemester)) {
            return $classes;
        }

        return $this->classSectionModel
            ->select('id, class_section_id, class_section_name')
            ->orderBy('class_section_name', 'ASC')
            ->findAll();
    }

    private function priorYearStudentIds(string $selectedYear): array
    {
        $selectedStartYear = $this->getSchoolYearStartYear($selectedYear);
        if ($selectedStartYear === null) {
            return [];
        }

        $studentIds = [];
        foreach (['enrollments', 'student_class'] as $table) {
            if (! $this->db->tableExists($table) || ! $this->db->fieldExists('school_year', $table)) {
                continue;
            }

            $rows = $this->db->table($table)
                ->select('student_id, school_year')
                ->where('student_id IS NOT NULL', null, false)
                ->where('school_year IS NOT NULL', null, false)
                ->get()
                ->getResultArray();

            foreach ($rows as $row) {
                $rowStartYear = $this->getSchoolYearStartYear((string) ($row['school_year'] ?? ''));
                $studentId = (int) ($row['student_id'] ?? 0);
                if ($studentId > 0 && $rowStartYear !== null && $rowStartYear < $selectedStartYear) {
                    $studentIds[$studentId] = true;
                }
            }
        }

        return $studentIds;
    }

    /**
     * Show only newly registered students, with contact modal support.
     *
     * Route example:
     *   $routes->get('admin/enrollment/new-students', 'EnrollmentController::showNewStudents', ['filter' => 'auth:view_new_students']);
     */
    public function showNewStudents(): string
    {
        $rows = $this->studentModel->getStudentsWithParentsAndEmergency($this->schoolYear);

        $newStudents = [];
        foreach ($rows as $r) {
            $r['new_student']   = 'Yes';
            $classSection = $this->studentClassModel->getClassSectionsByStudentId($r['id'], $this->schoolYear);
            $enrollmentstatus = $this->enrollmentModel->getEnrollmentStatus($r['id'], $this->schoolYear);
            // robust default
            $r['class_section'] = (isset($classSection) && trim((string)$classSection) !== '')
                ? $classSection
                : 'Class not Assigned';

            $r['is_new'] = (int) $r['is_new'];
            $r['new_student'] = $r['is_new'] === 1 ? "Yes" : "No";
            $r['modalIdContact'] = 'contact_' . (int)($r['id'] ?? 0);
            $r['enrollment_status'] = $enrollmentstatus;

            // ✅ Format registration_date for display
            if (!empty($r['registration_date'])) {
                try {
                    $r['registration_date'] = (new \DateTime($r['registration_date']))->format('Y-m-d');
                } catch (\Throwable $e) {
                    $r['registration_date'] = '';
                }
            }

            // Age comes directly from DB (already stored in students.age)
            $newStudents[] = $r;
        }

        return view('enroll_withdraw/new-students', [
            'new_students' => $newStudents,
            'total_new'    => count($newStudents),
        ]);
    }




    //update enrollment status
    public function adminEnrollmentWithdrawalHandler()
    {
        $refundService = new FeeCalculationService();
        $this->db->transStart();

        try {
            $enrollmentStatuses = $this->request->getPost('enrollment_status');
            if (empty($enrollmentStatuses)) {
                return redirect()->to(base_url('enroll_withdraw/enrollment_withdrawal'))
                    ->with('error', 'No enrollment statuses were submitted.');
            }

            $errors = [];

            // For batching emails: parent -> status -> [students...]
            $groupsByParentStatus = []; // [parent_id][status][] = ['student_id'=>, 'student_name'=>]
            $parentInfo           = []; // [parent_id] = ['user_id','email','firstname','lastname']
            $refundParents        = []; // parent_id => true (for refund calc)
            $refundAmountByParent = []; // parent_id => amount

            $validStatuses = [
                'admission under review',
                'payment pending',
                'enrolled',
                'withdraw under review',
                'refund pending',
                'withdrawn',
                'denied',
                'waitlist',
            ];

            foreach ($enrollmentStatuses as $studentId => $newEnrollmentStatus) {
                if (!in_array($newEnrollmentStatus, $validStatuses, true)) {
                    $errors[] = "Invalid enrollment status '$newEnrollmentStatus' for student ID $studentId.";
                    continue;
                }

                // Map admission_status based on the desired new status (needed for create-or-update)
                if ($newEnrollmentStatus === 'denied') {
                    $admissionStatus = 'denied';
                } elseif (in_array($newEnrollmentStatus, ['enrolled', 'payment pending'], true)) {
                    $admissionStatus = 'accepted';
                } else {
                    $admissionStatus = 'pending';
                }

                // Current enrollment row
                $enrollmentRow = $this->db->table('enrollments')
                    ->where('student_id', $studentId)
                    ->where('school_year', $this->schoolYear)
                    ->get()
                    ->getRowArray();

                // If no enrollment found for this student/year, create one so invoice generation has data
                if (!$enrollmentRow) {
                    $stu = $this->studentModel->find((int)$studentId) ?? [];
                    $parentId = (int)($stu['parent_id'] ?? ($stu['secondparent_user_id'] ?? 0));
                    if (!$parentId) {
                        $errors[] = "No parent ID found for student ID $studentId.";
                        continue;
                    }

                    $isWithdrawn = in_array($newEnrollmentStatus, ['withdrawn', 'refund pending', 'withdraw under review'], true) ? 1 : 0;

                    $ok = $this->db->table('enrollments')->insert([
                        'student_id'        => (int)$studentId,
                        'parent_id'         => $parentId,
                        'school_year'       => (string)$this->schoolYear,
                        'semester'          => (string)$this->semester,
                        'enrollment_date'   => local_date(utc_now(), 'Y-m-d'),
                        'is_withdrawn'      => $isWithdrawn,
                        'enrollment_status' => $newEnrollmentStatus,
                        'admission_status'  => $admissionStatus,
                        'created_at'        => utc_now(),
                        'updated_at'        => utc_now(),
                    ]);

                    if (!$ok) {
                        $errors[] = "Failed to create enrollment for student ID $studentId.";
                        continue;
                    }

                    // Group this newly-created change for notifications
                    $studentRow  = $this->studentModel->find($studentId) ?? [];
                    $studentName = trim(($studentRow['firstname'] ?? '') . ' ' . ($studentRow['lastname'] ?? '')) ?: "Student #{$studentId}";

                    if (!isset($parentInfo[$parentId])) {
                        $p = $this->userModel->find($parentId) ?? [];
                        $parentInfo[$parentId] = [
                            'user_id'   => $p['id']        ?? $parentId,
                            'email'     => $p['email']     ?? null,
                            'firstname' => $p['firstname'] ?? '',
                            'lastname'  => $p['lastname']  ?? '',
                        ];
                    }
                    $groupsByParentStatus[$parentId][$newEnrollmentStatus][] = [
                        'student_id'   => (int) $studentId,
                        'student_name' => $studentName,
                    ];

                    if ($newEnrollmentStatus === 'refund pending') {
                        $refundParents[$parentId] = true;
                    }

                    log_message('info', "Created enrollment for student ID {$studentId} with status {$newEnrollmentStatus} and admission {$admissionStatus}.");
                    if (in_array($newEnrollmentStatus, ['payment pending', 'enrolled'], true)) {
                        $this->applyDistributionDraftToStudentClass((int)$studentId, (string)$this->schoolYear);
                    }
                    continue; // go to next student
                }

                $oldStatus = $enrollmentRow['enrollment_status'] ?? null;
                $parentId  = $enrollmentRow['parent_id'] ?? null;

                if (!$parentId) {
                    $errors[] = "No parent ID found for student ID $studentId.";
                    continue;
                }

                // admissionStatus computed above

                // Skip if no actual change
                if ($oldStatus === $newEnrollmentStatus) {
                    log_message('debug', "No status change for student {$studentId} ({$oldStatus}) — skipping.");
                    if (in_array($newEnrollmentStatus, ['payment pending', 'enrolled'], true)) {
                        $this->applyDistributionDraftToStudentClass((int)$studentId, (string)$this->schoolYear);
                    }
                    continue;
                }

                // Update enrollment
                $updated = $this->db->table('enrollments')
                    ->where('student_id', $studentId)
                    ->where('school_year', $this->schoolYear)
                    ->update([
                        'enrollment_status' => $newEnrollmentStatus,
                        'admission_status'  => $admissionStatus,
                        'updated_at'        => utc_now(),
                    ]);

                if (!$updated) {
                    $errors[] = "Failed to update enrollment for student ID $studentId.";
                    continue;
                }

                log_message('info', "Updated enrollment for student ID $studentId: {$oldStatus} → {$newEnrollmentStatus} (admission: {$admissionStatus})");
                if (in_array($newEnrollmentStatus, ['payment pending', 'enrolled'], true)) {
                    $this->applyDistributionDraftToStudentClass((int)$studentId, (string)$this->schoolYear);
                }

                // Student name
                $studentRow  = $this->studentModel->find($studentId);
                $studentName = trim(($studentRow['firstname'] ?? '') . ' ' . ($studentRow['lastname'] ?? '')) ?: "Student #{$studentId}";

                // Cache parent info once
                if (!isset($parentInfo[$parentId])) {
                    $p = $this->userModel->find($parentId) ?? [];
                    $parentInfo[$parentId] = [
                        'user_id'   => $p['id']        ?? $parentId, // assuming parent_id == user_id
                        'email'     => $p['email']     ?? null,
                        'firstname' => $p['firstname'] ?? '',
                        'lastname'  => $p['lastname']  ?? '',
                    ];
                }

                // Group by parent & status for minimal emails
                $groupsByParentStatus[$parentId][$newEnrollmentStatus][] = [
                    'student_id'   => (int) $studentId,
                    'student_name' => $studentName,
                ];

                // Mark for refund calc
                if ($newEnrollmentStatus === 'refund pending') {
                    $refundParents[$parentId] = true;
                }
            }

            // Compute refunds ONCE per parent needing it
            foreach (array_keys($refundParents) as $pid) {
                $students = $this->enrollmentModel
                    ->where('parent_id', $pid)
                    ->where('school_year', $this->schoolYear)
                    ->findAll();

                if (empty($students)) {
                    // If a parent is marked for refund but has no enrollments, just log and continue.
                    log_message('info', "No enrollments found for parent ID {$pid} (for refund calc); skipping refund.");
                    continue;
                }

                $invoice = $this->invoiceModel->where('parent_id', $pid)
                    ->where('school_year', $this->schoolYear)
                    ->orderBy('created_at', 'DESC')
                    ->first();

                if (!$invoice) {
                    $errors[] = "No invoice found for parent ID $pid (for refund calc).";
                    continue;
                }

                $refundAmount = $refundService->calculateRefund($students, $pid);
                $refundAmountByParent[$pid] = $refundAmount;

                $existingRefund = $this->refundModel->where('invoice_id', $invoice['id'])->first();

                if ($existingRefund) {
                    $this->refundModel->update($existingRefund['id'], [
                        'refund_amount' => $refundAmount,
                        'status'        => 'Pending',
                        'updated_by'    => session()->get('user_id') ?? null,
                    ]);
                } else {
                    $this->refundModel->insert([
                        'parent_id'          => $pid,
                        'school_year'        => $invoice['school_year'],
                        'invoice_id'         => $invoice['id'],
                        'refund_amount'      => $refundAmount,
                        'refund_paid_amount' => 0.0,
                        'status'             => 'Pending',
                        'requested_at'       => utc_now(),
                        'updated_by'         => session()->get('user_id') ?? null,
                    ]);
                }

                log_message('info', "Refund of $refundAmount created/updated for invoice ID {$invoice['id']} (parent {$pid}).");
            }

            $this->db->transComplete();

            if (!$this->db->transStatus()) {
                return redirect()->to(base_url('enroll_withdraw/enrollment_withdrawal'))
                    ->with('error', 'A database error occurred. Changes were rolled back.');
            }

            // === AFTER COMMIT: fire specific events, batched per parent/status ===
            $eventMap = [
                'admission under review' => 'admissionUnderReview',
                'payment pending'        => 'paymentPending',
                'enrolled'               => 'studentEnrolled',
                'withdraw under review'  => 'withdrawUnderReview',
                'refund pending'         => 'refundPending',
                'withdrawn'              => 'withdrawn',
                'denied'                 => 'denied',
                'waitlist'               => 'waitlist',
            ];

            foreach ($groupsByParentStatus as $pid => $byStatus) {
                // Common parent data
                $p = $parentInfo[$pid] ?? ['user_id' => $pid, 'email' => null, 'firstname' => '', 'lastname' => ''];

                // Fetch invoice once for this parent (for payment pending email data if needed)
                $invoice = $this->invoiceModel->where('parent_id', $pid)
                    ->where('school_year', $this->schoolYear)
                    ->orderBy('created_at', 'DESC')
                    ->first();

                foreach ($byStatus as $status => $studentsArr) {
                    if (empty($eventMap[$status])) {
                        continue; // unknown mapping
                    }

                    // Build second arg: student list
                    $studentData = [];
                    foreach ($studentsArr as $s) {
                        $studentData[] = ['name' => $s['student_name'], 'student_id' => $s['student_id']];
                    }

                    // Parent payload (first arg)
                    $parentData = [
                        'user_id'     => $p['user_id'],
                        'email'       => $p['email'],
                        'firstname'   => $p['firstname'],
                        'lastname'    => $p['lastname'],
                        'school_year' => $this->schoolYear,
                        'portalLink'  => base_url('/login'),
                    ];

                    // Enrich with status-specific fields
                    if ($status === 'payment pending') {
                        if ($invoice) {
                            $parentData['amount']   = (float) ($invoice['balance'] ?? $invoice['amount_due'] ?? $invoice['total_amount'] ?? 0);
                            $parentData['due_date'] = $invoice['due_date'] ?? null;
                        }
                    } elseif ($status === 'refund pending') {
                        $parentData['amount'] = $refundAmountByParent[$pid] ?? null;
                    }

                    $eventName = $eventMap[$status];

                    log_message('info', "Triggering event '{$eventName}' for parent {$pid} with " . count($studentData) . " student(s).");
                    Events::trigger($eventName, $parentData, $studentData);
                }
            }

            // === Server-side safety net: generate/update invoices for parents whose statuses require it ===
            try {
                $needsInvoiceFor = ['payment pending', 'enrolled', 'withdrawn', 'refund pending'];
                $invCtl = new InvoiceController();
                foreach ($groupsByParentStatus as $pid => $byStatus) {
                    $statuses = array_keys($byStatus);
                    $requires = array_intersect($statuses, $needsInvoiceFor);
                    if (!empty($requires)) {
                        // Best-effort; ignore response object
                        try {
                            $invCtl->generateInvoice((string)$pid);
                        } catch (\Throwable $e) {
                            log_message('error', 'Invoice fallback generation failed for parent {pid}: {err}', ['pid' => $pid, 'err' => $e->getMessage()]);
                        }
                    }
                }
            } catch (\Throwable $e) {
                log_message('error', 'Invoice fallback block error: ' . $e->getMessage());
            }

            if (!empty($errors)) {
                return redirect()->to(base_url('enroll_withdraw/enrollment_withdrawal'))
                    ->with('error', implode(' ', $errors));
            }

            return redirect()->to(base_url('enroll_withdraw/enrollment_withdrawal'))
                ->with('success', 'Enrollment statuses updated and notifications sent.');
        } catch (\Throwable $e) {
            $this->db->transRollback();
            log_message('error', 'Enrollment withdrawal error: ' . $e->getMessage());
            return redirect()->to(base_url('enroll_withdraw/enrollment_withdrawal'))
                ->with('error', 'An unexpected error occurred while processing enrollments.');
        }
    }

    private function applyDistributionDraftToStudentClass(int $studentId, string $year): void
    {
        try {
            $draftModel = new StudentSectionDistributionDraftModel();
            $draft = $draftModel->where('student_id', $studentId)
                ->where('school_year', $year)
                ->where('status', 'pending')
                ->first();

            if (!$draft) {
                return;
            }

            $targetSectionId = (int)($draft['class_section_id'] ?? 0);
            if ($targetSectionId <= 0) {
                return;
            }

            $studentClass = new StudentClassModel();
            $exists = $studentClass->where('student_id', $studentId)
                ->where('school_year', $year)
                ->first();

            $payload = [
                'student_id'       => $studentId,
                'class_section_id' => $targetSectionId,
                'school_year'      => $year,
                'updated_by'       => (int)(session()->get('user_id') ?? 0) ?: null,
                'updated_at'       => utc_now(),
            ];

            if ($exists) {
                $studentClass->update((int)$exists['id'], $payload);
            } else {
                $payload['created_at'] = utc_now();
                $studentClass->insert($payload);
            }

            $this->db->table('enrollments')
                ->where('student_id', $studentId)
                ->where('school_year', $year)
                ->whereIn('enrollment_status', ['payment pending', 'enrolled'])
                ->update([
                    'class_section_id' => $targetSectionId,
                    'updated_at'       => utc_now(),
                ]);

            $this->db->table('promotion_queue')
                ->where('student_id', $studentId)
                ->where('school_year_to', $year)
                ->update([
                    'to_class_section_id' => $targetSectionId,
                    'status'              => 'applied',
                    'updated_by'          => (int)(session()->get('user_id') ?? 0) ?: null,
                    'updated_at'          => utc_now(),
                ]);

            $draftModel->update((int)$draft['id'], [
                'status'     => 'applied',
                'applied_at' => utc_now(),
                'updated_at' => utc_now(),
            ]);
        } catch (\Throwable $e) {
            log_message('error', 'applyDistributionDraftToStudentClass failed: ' . $e->getMessage());
        }
    }
}
