<?php

namespace App\Controllers\View;

use App\Controllers\BaseController;
use App\Models\UserModel; // Ensure this is the correct namespace for UserModel
use App\Models\StudentModel; // Ensure this is the correct namespace for StudentModel
use App\Models\StudentClassModel;
use App\Models\ClassSectionModel;
use App\Models\EmergencyContactModel;
use App\Models\EnrollmentModel;
use App\Models\ConfigurationModel;
use App\Models\StudentSectionDistributionDraftModel;
use App\Models\StudentAllergyModel;
use App\Models\StudentMedicalConditionModel;
use CodeIgniter\Database\Exceptions\DataException;
use Config\Services;
use Throwable;

class StudentController extends BaseController
{
    protected $configModel;
    protected $semester;
    protected $schoolYear;
    protected $teacherClassModel;
    protected $studentClassModel;
    protected $studentModel;
    protected $teacherModel;
    protected $userModel;
    protected $db;
    protected $conditionModel;
    protected $allergyModel;
    protected $classSectionModel;
    protected $emergencyContact;
    protected $enrollmentModel;

    public function __construct()
    {
        // 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.');
        }
        $this->userModel = new UserModel();
        $this->configModel = new ConfigurationModel();
        $this->studentClassModel = new StudentClassModel();
        $this->studentModel = new StudentModel();
        $this->allergyModel   = new StudentAllergyModel();
        $this->conditionModel = new StudentMedicalConditionModel();
        $this->classSectionModel = new ClassSectionModel();
        $this->emergencyContact = new EmergencyContactModel();
        $this->enrollmentModel = new EnrollmentModel();
        $this->semester = $this->configModel->getConfig('semester');
        $this->schoolYear = $this->configModel->getConfig('school_year');
        helper(['url', 'form']);
    }

    public function assignClassStudent()
    {
        $isAjax = $this->request->isAJAX();

        $studentId      = (int) $this->request->getPost('student_id');
        $rawSections    = $this->request->getPost('class_section_id');
        $isEventOnly    = $this->request->getPost('is_event_only') ? 1 : 0;
        $classSectionIds = [];

        // Normalize any input into an array of unique positive IDs
        if (is_array($rawSections)) {
            $classSectionIds = array_map('intval', $rawSections);
        } elseif ($rawSections !== null && $rawSections !== '') {
            $parts = is_string($rawSections) ? preg_split('/[,\s]+/', $rawSections) : [$rawSections];
            $classSectionIds = array_map('intval', $parts);
        }
        $classSectionIds = array_values(array_unique(array_filter($classSectionIds, static fn($v) => $v > 0)));

        $userId         = (int) (session()->get('user_id') ?? 0);
        $now            = utc_now();

        $jsonOut = function (array $payload, int $code = 200) {
            // Always return a fresh CSRF for next request (works with csrfRegenerate = true)
            $payload['csrfTokenName'] = csrf_token();
            $payload['csrfHash']      = csrf_hash();
            $payload[csrf_token()]    = csrf_hash();
            return $this->response->setStatusCode($code)->setJSON($payload);
        };

        // Validate input
        if (!$studentId || empty($classSectionIds)) {
            $msg = 'Missing required data (student/class section).';
            return $isAjax ? $jsonOut(['ok' => false, 'message' => $msg], 400)
                : redirect()->back()->with('error', $msg);
        }

        // Check student
        $student = $this->studentModel->find($studentId);
        if (!$student) {
            $msg = 'Student not found.';
            return $isAjax ? $jsonOut(['ok' => false, 'message' => $msg], 404)
                : redirect()->back()->with('error', $msg);
        }

        // Robust section lookup: allow id OR class_section_id
        $sections = $this->classSectionModel
            ->groupStart()
            ->whereIn('class_section_id', $classSectionIds)
            ->orWhereIn('id', $classSectionIds)
            ->groupEnd()
            ->findAll();

        if (empty($sections)) {
            $msg = 'Class/Section not found.';
            return $isAjax ? $jsonOut(['ok' => false, 'message' => $msg], 404)
                : redirect()->back()->with('error', $msg);
        }

        // Map for quick lookup; preserve requested order
        $sectionMap = [];
        foreach ($sections as $sec) {
            $sectionMap[(int)($sec['class_section_id'] ?? 0)] = $sec;
        }

        $existing = $this->studentClassModel
            ->where('student_id', $studentId)
            ->where('school_year', (string)$this->schoolYear)
            ->findAll();

        $existingBySection = [];
        foreach ($existing as $row) {
            $existingBySection[(int)($row['class_section_id'] ?? 0)] = $row;
        }

        $displayNames = [];
        foreach ($classSectionIds as $cid) {
            if (!isset($sectionMap[$cid])) continue;
            $eventFlag = $isEventOnly ? 1 : 0;
            if (isset($existingBySection[$cid])) {
                $eventFlag = (int)($existingBySection[$cid]['is_event_only'] ?? 0);
            }
            $name = $this->formatClassSectionDisplayName($sectionMap[$cid], $cid);
            if ($eventFlag) {
                $name .= ' (Event)';
            }
            $displayNames[] = $name;
        }

        if (empty($displayNames)) {
            $msg = 'Class/Section not found.';
            return $isAjax ? $jsonOut(['ok' => false, 'message' => $msg], 404)
                : redirect()->back()->with('error', $msg);
        }
        if (count($displayNames) !== count($classSectionIds)) {
            $msg = 'One or more selected classes do not exist.';
            return $isAjax ? $jsonOut(['ok' => false, 'message' => $msg], 404)
                : redirect()->back()->with('error', $msg);
        }

        $primarySectionId = $classSectionIds[0];
        $primarySection   = $sectionMap[$primarySectionId] ?? reset($sectionMap);

        // 🔎 Derive parent class_id from the section row (fallback to raw query if needed)
        $parentClassId = (int)($primarySection['class_id'] ?? 0);
        if (!$parentClassId) {
            // Adjust table name if your schema uses snake_case like 'class_section'
            $row = $this->db->table('classSection')
                ->select('class_id')
                ->groupStart()
                ->where('id', $primarySection['id'] ?? $primarySectionId)
                ->orWhere('class_section_id', $primarySectionId)
                ->groupEnd()
                ->get()->getRowArray();
            if ($row && isset($row['class_id'])) {
                $parentClassId = (int)$row['class_id'];
            }
        }

        $this->db->transBegin();

        try {
            // Upsert student_class entries per selection
            $scPk = $this->studentClassModel->primaryKey ?? 'id';

            foreach ($classSectionIds as $cid) {
                $eventFlag = $isEventOnly ? 1 : 0;
                if (isset($existingBySection[$cid])) {
                    $eventFlag = (int)($existingBySection[$cid]['is_event_only'] ?? 0);
                }
                $payload = [
                    'student_id'       => $studentId,
                    'class_section_id' => $cid,
                    'school_year'      => (string)$this->schoolYear,
                    'is_event_only'    => $eventFlag,
                    'updated_by'       => $userId ?: null,
                    'updated_at'       => $now,
                ];

                if (isset($existingBySection[$cid])) {
                    if (!$this->studentClassModel->update($existingBySection[$cid][$scPk], $payload)) {
                        throw new \RuntimeException('Failed to update assignment: ' . json_encode($this->studentClassModel->errors()));
                    }
                } else {
                    $payload['created_at'] = $now;
                    if (!$this->studentClassModel->insert($payload)) {
                        throw new \RuntimeException('Failed to insert assignment: ' . json_encode($this->studentClassModel->errors()));
                    }
                }
            }

            $attnStats = [];
            $scoreStats = [];
            if (!$isEventOnly) {
                // Update enrollment for current term (if exists)
                $enroll = $this->enrollmentModel
                    ->where('student_id', $studentId)
                    ->where('school_year', (string)$this->schoolYear)
                    ->where('semester', (string)$this->semester)
                    ->first();

                if ($enroll) {
                    $enPk = $this->enrollmentModel->primaryKey ?? 'id';
                    if (!$this->enrollmentModel->update($enroll[$enPk], [
                        'class_section_id'  => $primarySectionId,
                        'enrollment_status' => 'payment pending',
                        // Ensure admission is marked accepted once moved out of review
                        'admission_status'  => 'accepted',
                        'updated_at'        => $now,
                    ])) {
                        throw new \RuntimeException('Failed to update enrollment: ' . json_encode($this->enrollmentModel->errors()));
                    }
                }

                // 🔄 Re-tag attendance rows for this student in the current term (section + class)
                $attnStats = $this->updateStudentAttendanceSection(
                    $studentId,
                    $primarySectionId,
                    $parentClassId ?: 0,                  // safe default if missing
                    (string)$this->semester,
                    (string)$this->schoolYear,
                    $userId ?: null
                );

                // 🔄 Re-tag score rows (quiz, project, homework, participation, exams, aggregates) for current term
                $scoreStats = $this->updateStudentScoresSection(
                    $studentId,
                    $primarySectionId,
                    (string)$this->semester,
                    (string)$this->schoolYear,
                    $userId ?: null
                );
            }

            if ($this->db->transStatus() === false) {
                throw new \RuntimeException('Transaction failed.');
            }

            $this->db->transCommit();

            $resp = [
                'ok'                 => true,
                'student_id'         => $studentId,
                'class_section_id'   => $primarySectionId,
                'class_section_ids'  => $classSectionIds,
                'class_section_name' => implode(', ', $displayNames),
                'class_section_names'=> $displayNames,
                'attendance_updates' => $attnStats, // {attendance_data_updated, attendance_record_updated}
                'score_updates'      => $scoreStats, // per-table updated counts
                'message'            => 'Assignment saved.',
            ];

            return $isAjax ? $jsonOut($resp, 200)
                : redirect()->to(base_url('administrator/student_class_assignment'))->with('success', $resp['message']);
        } catch (\Throwable $e) {
            $this->db->transRollback();
            $msg = 'Unable to assign class: ' . $e->getMessage();
            return $isAjax ? $jsonOut(['ok' => false, 'message' => $msg], 500)
                : redirect()->back()->with('error', $msg);
        }
    }

    /**
     * Remove a student-class assignment for the current term.
     */
    public function removeClassStudent()
    {
        $isAjax = $this->request->isAJAX();

        $studentId      = (int) $this->request->getPost('student_id');
        $classSectionId = (int) $this->request->getPost('class_section_id');
        $userId         = (int) (session()->get('user_id') ?? 0);
        $now            = utc_now();

        $jsonOut = function (array $payload, int $code = 200) {
            $payload['csrfTokenName'] = csrf_token();
            $payload['csrfHash']      = csrf_hash();
            $payload[csrf_token()]    = csrf_hash();
            return $this->response->setStatusCode($code)->setJSON($payload);
        };

        if (!$studentId || !$classSectionId) {
            $msg = 'Missing required data (student/class section).';
            return $isAjax ? $jsonOut(['ok' => false, 'message' => $msg], 400)
                : redirect()->back()->with('error', $msg);
        }

        // Verify assignment exists for this term
        $row = $this->studentClassModel
            ->where('student_id', $studentId)
            ->where('class_section_id', $classSectionId)
            ->where('school_year', (string)$this->schoolYear)
            ->first();

        if (!$row) {
            $msg = 'Assignment not found for this student/class.';
            return $isAjax ? $jsonOut(['ok' => false, 'message' => $msg], 404)
                : redirect()->back()->with('error', $msg);
        }

        // Remaining assignments BEFORE delete (for enrollment swap)
        $beforeIds = $this->studentClassModel->getClassSectionIdsByStudentId($studentId, (string)$this->schoolYear);

        $this->db->transBegin();
        try {
            // Delete the assignment
            if (!$this->studentClassModel
                ->where('student_id', $studentId)
                ->where('class_section_id', $classSectionId)
                ->where('school_year', (string)$this->schoolYear)
                ->delete()) {
                throw new \RuntimeException('Failed to remove assignment.');
            }

            // Determine remaining assignments AFTER delete
            $remainingIds = $this->studentClassModel->getClassSectionIdsByStudentId($studentId, (string)$this->schoolYear);
            $remainingNames = $this->studentClassModel->getClassSectionsByStudentId($studentId, (string)$this->schoolYear, true);
            $remainingDisplay = !empty($remainingNames) ? implode(', ', $remainingNames) : '';

            // If enrollment pointed to removed class, move to another remaining class or null
            $enroll = $this->enrollmentModel
                ->where('student_id', $studentId)
                ->where('school_year', (string)$this->schoolYear)
                ->where('semester', (string)$this->semester)
                ->first();
            if ($enroll) {
                $enPk = $this->enrollmentModel->primaryKey ?? 'id';
                $newEnrollmentClass = null;
                if (!empty($remainingIds)) {
                    $newEnrollmentClass = $remainingIds[0];
                }
                if ((int)($enroll['class_section_id'] ?? 0) === $classSectionId || $newEnrollmentClass !== null) {
                    $this->enrollmentModel->update($enroll[$enPk], [
                        'class_section_id' => $newEnrollmentClass,
                        'updated_at'       => $now,
                        'updated_by'       => $userId ?: null,
                    ]);
                }
            }

            if ($this->db->transStatus() === false) {
                throw new \RuntimeException('Transaction failed.');
            }
            $this->db->transCommit();

            $resp = [
                'ok'                  => true,
                'student_id'          => $studentId,
                'removed_class_id'    => $classSectionId,
                'remaining_ids'       => $remainingIds,
                'remaining_names'     => $remainingNames,
                'remaining_display'   => $remainingDisplay !== '' ? $remainingDisplay : 'No class assigned',
                'message'             => 'Class removed.',
            ];

            return $isAjax ? $jsonOut($resp, 200)
                : redirect()->to(base_url('administrator/student_class_assignment'))->with('success', $resp['message']);
        } catch (\Throwable $e) {
            $this->db->transRollback();
            $msg = 'Unable to remove class: ' . $e->getMessage();
            return $isAjax ? $jsonOut(['ok' => false, 'message' => $msg], 500)
                : redirect()->back()->with('error', $msg);
        }
    }

    public function removedStudents()
    {
        $schoolYear = (string)($this->schoolYear ?? '');

        $classMap = [];
        $activeYearStudentIds = [];
        $classQuery = $this->db->table('student_class sc')
            ->select('sc.student_id, cs.class_section_name')
            ->join('classSection cs', 'cs.class_section_id = sc.class_section_id', 'left');
        if ($schoolYear !== '') {
            $classQuery->where('sc.school_year', $schoolYear);
        }
        $classRows = $classQuery->get()->getResultArray();

        foreach ($classRows as $row) {
            $sid = (int)($row['student_id'] ?? 0);
            if ($sid > 0) {
                $activeYearStudentIds[$sid] = true;
            }

            $name = trim((string)($row['class_section_name'] ?? ''));
            if ($sid <= 0 || $name === '') continue;
            $classMap[$sid][] = $name;
        }

        $students = $this->studentModel
            ->select('id, school_id, firstname, lastname, gender, age, is_active')
            ->orderBy('lastname', 'ASC')
            ->orderBy('firstname', 'ASC')
            ->findAll();

        $activeStudents = [];
        $removedStudents = [];
        foreach ($students as $student) {
            $studentId = (int)($student['id'] ?? 0);
            $isGloballyActive = (int)($student['is_active'] ?? 0) === 1;
            $hasSelectedYearClass = $studentId > 0 && isset($activeYearStudentIds[$studentId]);

            if ($isGloballyActive && $hasSelectedYearClass) {
                $activeStudents[] = $student;
            } else {
                $removedStudents[] = $student;
            }
        }

        $attachClassNames = static function (array $students) use ($classMap): array {
            foreach ($students as &$student) {
                $sid = (int)($student['id'] ?? 0);
                $names = $classMap[$sid] ?? [];
                $names = array_values(array_unique(array_filter($names)));
                $student['class_sections'] = $names;
                $student['class_section_name'] = !empty($names) ? implode(', ', $names) : 'No class assigned';
            }
            unset($student);
            return $students;
        };

        return view('administrator/removed_students', [
            'active_students'  => $attachClassNames($activeStudents),
            'removed_students' => $attachClassNames($removedStudents),
            'school_year'      => $schoolYear,
            'active_school_year' => (string)($this->schoolYear ?? ''),
        ]);
    }

    public function setStudentActive()
    {
        $studentId = (int) $this->request->getPost('student_id');
        $isActiveRaw = (string) $this->request->getPost('is_active');
        $isActive = $isActiveRaw === '1' ? 1 : 0;
        $now = utc_now();
        $userId = (int) (session()->get('user_id') ?? 0);

        if ($studentId <= 0) {
            return redirect()->back()->with('error', 'Invalid student ID.');
        }

        $student = $this->studentModel->find($studentId);
        if (!$student) {
            return redirect()->back()->with('error', 'Student not found.');
        }

        if (!$this->studentModel->update($studentId, ['is_active' => $isActive])) {
            return redirect()->back()->with('error', 'Unable to update student status.');
        }

        $message = $isActive ? 'Student restored successfully.' : 'Student removed successfully.';

        if ($isActive === 1) {
            $hasCurrentClass = $this->studentClassModel
                ->where('student_id', $studentId)
                ->where('school_year', (string)$this->schoolYear)
                ->where('semester', (string)$this->semester)
                ->first();

            if (!$hasCurrentClass) {
                $lastClass = $this->studentClassModel
                    ->where('student_id', $studentId)
                    ->orderBy('updated_at', 'DESC')
                    ->orderBy('created_at', 'DESC')
                    ->orderBy('id', 'DESC')
                    ->first();

                $restoreClassId = (int)($lastClass['class_section_id'] ?? 0);
                if ($restoreClassId > 0) {
                    $inserted = $this->studentClassModel->insert([
                        'student_id'       => $studentId,
                        'class_section_id' => $restoreClassId,
                        'semester'         => (string)$this->semester,
                        'school_year'      => (string)$this->schoolYear,
                        'description'      => $lastClass['description'] ?? null,
                        'updated_by'       => $userId ?: null,
                        'updated_at'       => $now,
                        'created_at'       => $now,
                    ]);

                    if ($inserted) {
                        $classLabel = (string)($this->classSectionModel->getClassSectionNameBySectionId($restoreClassId) ?? '');
                        if ($classLabel !== '') {
                            $message .= ' Class assignment restored to ' . $classLabel . '.';
                        }
                    } else {
                        $message .= ' No class assignment found for the current term.';
                        return redirect()->to(base_url('administrator/removed_students'))->with('warning', $message);
                    }
                } else {
                    $message .= ' No class assignment found for the current term.';
                    return redirect()->to(base_url('administrator/removed_students'))->with('warning', $message);
                }
            }
        }

        if ($isActive === 0) {
            if (!$this->notifyParentOfStudentRemoval($studentId)) {
                $message .= ' Parent notification email could not be sent.';
            }
        }

        return redirect()->to(base_url('administrator/removed_students'))->with('success', $message);
    }

    private function notifyParentOfStudentRemoval(int $studentId): bool
    {
        try {
            $studentRow = $this->studentModel
                ->select([
                    'students.firstname AS student_firstname',
                    'students.lastname  AS student_lastname',
                    'students.school_id',
                    'users.id        AS parent_id',
                    'users.firstname AS parent_firstname',
                    'users.lastname  AS parent_lastname',
                    'users.email     AS parent_email',
                ])
                ->join('users', 'users.id = students.parent_id', 'left')
                ->where('students.id', $studentId)
                ->first();

            if (empty($studentRow)) {
                log_message('warning', "Student removal email skipped: student {$studentId} not found.");
                return false;
            }

            $parentEmail = trim((string)($studentRow['parent_email'] ?? ''));
            if ($parentEmail === '') {
                log_message('warning', "Student removal email skipped: missing parent email for student {$studentId}.");
                return false;
            }

            $studentName = trim(trim((string)($studentRow['student_firstname'] ?? '')) . ' ' . trim((string)($studentRow['student_lastname'] ?? '')));
            $parentName  = trim(trim((string)($studentRow['parent_firstname'] ?? '')) . ' ' . trim((string)($studentRow['parent_lastname'] ?? '')));
            if ($parentName === '') {
                $parentName = 'Parent/Guardian';
            }

            $subject = 'Withdrawal Notice for ' . ($studentName !== '' ? $studentName : 'Your Student');

            $html = view('emails/student_removed', [
                'student_name'    => $studentName,
                'school_id'       => $studentRow['school_id'] ?? '',
                'parent_name'     => $parentName,
                'signature'       => 'AlRahma School Administration',
            ], ['saveData' => true]);

            $sent = $this->sendHtmlEmail($parentEmail, $subject, $html);
            $logLevel = $sent ? 'info' : 'warning';
            log_message($logLevel, "Student removal email " . ($sent ? '' : 'not ') . "sent (studentId: {$studentId}, parent: {$parentEmail}).");

            return $sent;
        } catch (\Throwable $e) {
            log_message('error', 'Student removal notification failed: ' . $e->getMessage());
            return false;
        }
    }

    private function sendHtmlEmail(string $to, string $subject, string $html): bool
    {
        try {
            $service = function_exists('service') ? service('emailService') : null;
            if (!$service && method_exists(Services::class, 'emailService')) {
                $service = Services::emailService();
            }

            if ($service && method_exists($service, 'send')) {
                $result = $service->send($to, $subject, $html, 'student-removal');
                if ($result) {
                    return true;
                }
                log_message('debug', 'Custom emailService failed to send student removal notice.');
            }

            $email = Services::email();
            $cfg   = config('Email');
            $fromEmail = $cfg->fromEmail ?? $cfg->SMTPUser ?? 'no-reply@example.com';
            $fromName  = $cfg->fromName  ?? 'Al Rahma Sunday School';

            $email->setTo($to);
            $email->setFrom($fromEmail, $fromName);
            $email->setSubject($subject);
            $email->setMessage($html);
            $email->setMailType('html');

            $ok = $email->send();
            if (!$ok) {
                $debug = method_exists($email, 'printDebugger') ? $email->printDebugger(['headers', 'subject']) : 'no debugger';
                log_message('debug', 'CI Email send failed for student removal: ' . print_r($debug, true));
            }

            return $ok;
        } catch (\Throwable $e) {
            log_message('debug', 'sendHtmlEmail exception for student removal: ' . $e->getMessage());
            return false;
        }
    }


    private function updateStudentAttendanceSection(
        int $studentId,
        int $newClassSectionId,
        int $newClassId,
        string $semester,
        string $schoolYear,
        ?int $modifiedBy = null
    ): array {
        $now = utc_now();

        // ---- attendance_data: set class_section_id + class_id ----
        $builderData = $this->db->table('attendance_data');
        $builderData
            ->where('student_id', $studentId)
            ->where('semester', $semester)
            ->where('school_year', $schoolYear)
            ->set([
                'class_section_id' => $newClassSectionId,
                'class_id'         => $newClassId,
                'updated_at'       => $now,
            ]);

        if ($modifiedBy) {
            $builderData->set('modified_by', $modifiedBy);
        }

        if ($builderData->update() === false) {
            $err = $this->db->error();
            throw new \RuntimeException('Failed to update attendance_data: ' . ($err['message'] ?? 'unknown DB error'));
        }
        $dataUpdated = $this->db->affectedRows();

        // ---- attendance_record: set class_section_id (table has no class_id) ----
        $builderRec = $this->db->table('attendance_record');
        $builderRec
            ->where('student_id', $studentId)
            ->where('semester', $semester)
            ->where('school_year', $schoolYear)
            ->set([
                'class_section_id' => $newClassSectionId,
                'updated_at'       => $now,
            ]);

        if ($modifiedBy) {
            $builderRec->set('modified_by', $modifiedBy);
        }

        if ($builderRec->update() === false) {
            $err = $this->db->error();
            throw new \RuntimeException('Failed to update attendance_record: ' . ($err['message'] ?? 'unknown DB error'));
        }
        $recUpdated = $this->db->affectedRows();

        return [
            'attendance_data_updated'   => $dataUpdated,
            'attendance_record_updated' => $recUpdated,
        ];
    }

    /**
     * Update all score-related tables for a student to the new class_section_id
     * within the provided semester and school year.
     * Affected tables: homework, quiz, project, participation, midterm_exam,
     * final_exam, final_score, semester_scores.
     */
    private function updateStudentScoresSection(
        int $studentId,
        int $newClassSectionId,
        string $semester,
        string $schoolYear,
        ?int $modifiedBy = null
    ): array {
        $now = utc_now();

        $tables = [
            'homework',
            'quiz',
            'project',
            'participation',
            'midterm_exam',
            'final_exam',
            'final_score',
            'semester_scores',
        ];

        $results = [];
        foreach ($tables as $tbl) {
            $builder = $this->db->table($tbl);
            $builder
                ->where('student_id', $studentId)
                ->where('semester', $semester)
                ->where('school_year', $schoolYear)
                ->set([
                    'class_section_id' => $newClassSectionId,
                    'updated_at'       => $now,
                ]);

            // Only set updated_by if the column exists in table schema. For most it does.
            if ($modifiedBy !== null) {
                $builder->set('updated_by', $modifiedBy);
            }

            if ($builder->update() === false) {
                $err = $this->db->error();
                throw new \RuntimeException('Failed to update ' . $tbl . ': ' . ($err['message'] ?? 'unknown DB error'));
            }

            $results[$tbl . '_updated'] = $this->db->affectedRows();
        }

        return $results;
    }


    /**
     * Build a display name from whatever fields the section row has.
     * Supports: class_section_name, section_name, name, title, grade/letter combos.
     */
    private function formatClassSectionDisplayName(array $row, int $fallbackId): string
    {
        if (!empty($row['class_section_name'])) return (string) $row['class_section_name'];
        if (!empty($row['section_name']))       return (string) $row['section_name'];
        if (!empty($row['name']))               return (string) $row['name'];
        if (!empty($row['title']))              return (string) $row['title'];

        $grade  = $row['grade_name'] ?? $row['grade'] ?? $row['class_name'] ?? null;
        $letter = $row['section'] ?? $row['section_letter'] ?? $row['letter'] ?? null;

        if ($grade && $letter) return "{$grade} - {$letter}";
        if ($grade)            return (string) $grade;

        return 'Section #' . $fallbackId;
    }

    public function studentClassAssignment()
    {
        // Resolve selected year and available years
        $selectedYear = trim((string)($this->request->getGet('schoolYear') ?? ''));
        if ($selectedYear === '') $selectedYear = (string)($this->schoolYear ?? '');

        $yearsRows = $this->db->table('enrollments')
            ->select('DISTINCT school_year', false)
            ->orderBy('school_year', 'DESC')
            ->get()->getResultArray();
        $schoolYears = array_values(array_filter(array_map(static function ($r) {
            return isset($r['school_year']) ? (string)$r['school_year'] : null;
        }, $yearsRows)));

        // Retrieve students with an enrollment in the selected year (any status)
        $students = $this->studentModel
            ->select('students.id, students.firstname, students.lastname, students.registration_date, students.is_new, students.age, students.parent_id, students.registration_grade')
            ->join('enrollments e', 'e.student_id = students.id', 'inner')
            ->where('e.school_year', $selectedYear)
            ->groupBy('students.id')
            ->orderBy('students.lastname', 'ASC')
            ->findAll();

        // Fallback: if none found (data inconsistency), include students even without an enrollment row
        if (empty($students)) {
            $students = $this->studentModel
                ->select('students.id, students.firstname, students.lastname, students.registration_date, students.is_new, students.age, students.parent_id, students.registration_grade')
                ->orderBy('students.lastname', 'ASC')
                ->findAll();
        }

        $studentData = [];
        foreach ($students as $student) {
            $sectionNames = $this->studentClassModel->getClassSectionsByStudentIdWithFlags((int)$student['id'], $selectedYear, true);
            $sectionIds   = $this->studentClassModel->getClassSectionIdsByStudentId((int)$student['id'], $selectedYear);
            $sectionDisplay = !empty($sectionNames) ? implode(', ', $sectionNames) : '';

            // Use primary parent or fallback to second
            $pid = (int)($student['parent_id'] ?? 0);
            if ($pid <= 0) $pid = (int)($student['secondparent_user_id'] ?? 0);
            $emergencyInfo = $pid > 0 ? ($this->emergencyContact->getEmergencyContactByParentId($pid) ?? []) : [];
            $emergencyContactName  = $emergencyInfo['emergency_contact_name'] ?? '';
            $emergencyContactPhone = $emergencyInfo['cellphone'] ?? '';

            $studentData[] = [
                'student_id'          => (int)$student['id'],
                'name'                => trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')),
                'age'                 => $student['age'] ?? 'N/A',
                'email'               => $emergencyContactName,
                'phone'               => $emergencyContactPhone,
                'registration_grade'  => $student['registration_grade'] ?? 'N/A',
                'class_section_name'  => $sectionDisplay !== '' ? $sectionDisplay : 'No class assigned',
                'class_section_names' => $sectionNames,
                'class_section_ids'   => $sectionIds,
                'new_student'         => ((int)($student['is_new'] ?? 0) === 1) ? 'Yes' : 'No',
                'registration_date'   => $student['registration_date'] ?? '',
                'school_year'         => $selectedYear,
                'semester'            => (string)($this->semester ?? ''),
            ];
        }

        // Classes for selected year (fallback to all)
        $classes = $this->classSectionModel
            ->select('id, class_section_id, class_section_name, school_year')
            ->where('school_year', $selectedYear)
            ->orderBy('class_section_name','ASC')
            ->findAll();
        if (empty($classes)) $classes = $this->classSectionModel->select('id, class_section_id, class_section_name')->orderBy('class_section_name','ASC')->findAll();

        // Prepare data for view
        return view('administrator/student_class_assignment', [
            'students'     => $studentData,
            'classes'      => $classes,
            'schoolYears'  => $schoolYears,
            'selectedYear' => $selectedYear,
            'currentYear'  => (string)($this->schoolYear ?? ''),
            'isCurrentYear'=> ((string)$selectedYear === (string)($this->schoolYear ?? '')),
        ]);
    }

    /**
     * GET admin view: page to trigger auto-distribution for a class/year.
     */
    public function autoDistributePage()
    {
        // Resolve selected year and available years
        $selectedYear = trim((string)($this->request->getGet('schoolYear') ?? ''));
        if ($selectedYear === '') $selectedYear = (string)($this->schoolYear ?? '');

        $yearsRows = $this->db->table('enrollments')
            ->select('DISTINCT school_year', false)
            ->orderBy('school_year', 'DESC')
            ->get()->getResultArray();
        $schoolYears = array_values(array_filter(array_map(static function ($r) {
            return isset($r['school_year']) ? (string)$r['school_year'] : null;
        }, $yearsRows)));

        // Classes for selected year (fallback to all)
        $classes = $this->classSectionModel
            ->select('id, class_id, class_section_id, class_section_name, school_year')
            ->where('school_year', $selectedYear)
            ->orderBy('class_section_name','ASC')
            ->findAll();
        if (empty($classes)) $classes = $this->classSectionModel->select('id, class_id, class_section_id, class_section_name')->orderBy('class_section_name','ASC')->findAll();

        return view('administrator/sections_auto_distribute', [
            'classes'      => $classes,
            'schoolYears'  => $schoolYears,
            'selectedYear' => $selectedYear,
            'currentYear'  => (string)($this->schoolYear ?? ''),
        ]);
    }

    /**
     * POST admin endpoint: create draft balanced distribution rows for a class.
     *
     * Input: class_id/class_section_id, section_count, min_students_per_section,
     * max_students_per_section, school_year.
     */
    public function autoDistributeSections()
    {
        $isAjax = $this->request->isAJAX();
        $json = function (array $p, int $code = 200) {
            $p['csrfTokenName'] = csrf_token();
            $p['csrfHash']      = csrf_hash();
            $p[csrf_token()]    = csrf_hash();
            return $this->response->setStatusCode($code)->setJSON($p);
        };

        try {
            $classId   = (int) $this->request->getPost('class_id');
            $classSectionId = (int) $this->request->getPost('class_section_id');
            $sectionCount = (int) $this->request->getPost('section_count');
            $minPerSection = (int) $this->request->getPost('min_students_per_section');
            $maxRaw = trim((string) ($this->request->getPost('max_students_per_section') ?? ''));
            $maxPerSection = $maxRaw === '' ? null : (int) $maxRaw;
            $year      = trim((string) ($this->request->getPost('school_year') ?? $this->schoolYear));

            if ($classId <= 0 && $classSectionId > 0) {
                $cid = $this->classSectionModel->getClassId($classSectionId);
                $classId = (int) ($cid ?? 0);
            }

            if ($classId <= 0 || $sectionCount <= 0 || $minPerSection <= 0 || ($maxPerSection !== null && $maxPerSection <= 0)) {
                $msg = 'Enter a valid class, section count, minimum size, and optional maximum size.';
                return $isAjax ? $json(['ok' => false, 'message' => $msg], 400) : redirect()->back()->with('error', $msg);
            }

            $cands = $this->distributionCandidates($classId, $year);

            if (empty($cands)) {
                $msg = 'No promoted students found to distribute for selected class/year.';
                return $isAjax ? $json(['ok' => false, 'message' => $msg], 200) : redirect()->back()->with('info', $msg);
            }

            $total = count($cands);
            if ($sectionCount * $minPerSection > $total) {
                $msg = 'Insufficient students: ' . $sectionCount . ' sections require at least ' . ($sectionCount * $minPerSection) . ' students, but only ' . $total . ' are available.';
                return $isAjax ? $json(['ok' => false, 'message' => $msg], 400) : redirect()->back()->with('error', $msg);
            }
            if ($maxPerSection !== null && $total > $sectionCount * $maxPerSection) {
                $msg = 'Capacity exceeded: ' . $sectionCount . ' sections can hold at most ' . ($sectionCount * $maxPerSection) . ' students, but ' . $total . ' must be assigned.';
                return $isAjax ? $json(['ok' => false, 'message' => $msg], 400) : redirect()->back()->with('error', $msg);
            }

            // Fetch lettered sections for this class
            $letters = $this->classSectionModel->getLetterSectionsByClassId($classId);
            if (empty($letters)) {
                $msg = 'No lettered sections found for the selected class.';
                return $isAjax ? $json(['ok' => false, 'message' => $msg], 400) : redirect()->back()->with('error', $msg);
            }

            if (count($letters) < $sectionCount) {
                $msg = 'Not enough sections available. Needed: ' . $sectionCount . ', available: ' . count($letters);
                return $isAjax ? $json(['ok' => false, 'message' => $msg], 400) : redirect()->back()->with('error', $msg);
            }

            $letters = array_slice($letters, 0, $sectionCount);
            $buckets = $this->buildBalancedDistribution($cands, $letters, $minPerSection, $maxPerSection);

            $draftModel = new StudentSectionDistributionDraftModel();
            $promo = new \App\Models\PromotionQueueModel();
            $updatedBy = (int)(session()->get('user_id') ?? 0) ?: null;
            $now = utc_now();
            $batchKey = sha1($year . ':' . $classId . ':' . microtime(true));

            $this->db->transStart();
            $studentIdsToReplace = array_values(array_unique(array_map(
                static fn(array $student): int => (int)($student['student_id'] ?? 0),
                $cands
            )));
            if (!empty($studentIdsToReplace)) {
                $draftModel->where('school_year', $year)
                    ->whereIn('student_id', $studentIdsToReplace)
                    ->where('status', 'pending')
                    ->delete();
            }
            foreach ($buckets as $b) {
                $secId = (int)$b['class_section_id'];
                foreach ($b['assigned'] as $student) {
                    $sid = (int)$student['student_id'];
                    $draftModel->insert([
                        'student_id'            => $sid,
                        'class_id'              => $classId,
                        'class_section_id'      => $secId,
                        'school_year'           => $year,
                        'previous_school_year'  => (string)($student['school_year_from'] ?? ''),
                        'previous_final_score'  => $student['previous_final_score'],
                        'score_group'           => $student['score_group'],
                        'status'                => 'pending',
                        'batch_key'             => $batchKey,
                        'created_by'            => $updatedBy,
                        'created_at'            => $now,
                        'updated_at'            => $now,
                    ]);
                    if ((int)($student['promotion_queue_id'] ?? 0) > 0) {
                        $promo->update((int)$student['promotion_queue_id'], [
                            'to_class_section_id' => $secId,
                            'status'              => 'assigned',
                            'updated_by'          => $updatedBy,
                            'updated_at'          => $now,
                        ]);
                    }
                }
            }
            $this->db->transComplete();

            if (!$this->db->transStatus()) {
                $msg = 'Distribution could not be saved. No official student class rows were changed.';
                return $isAjax ? $json(['ok' => false, 'message' => $msg], 500) : redirect()->back()->with('error', $msg);
            }

            $nameById = [];
            foreach ($letters as $secRow) {
                $nameById[(int)$secRow['class_section_id']] = (string)($secRow['class_section_name'] ?? '');
            }

            $summary = [];
            foreach ($buckets as $b) {
                $secId = (int)$b['class_section_id'];
                $scores = array_map(static fn($s): float => (float)$s['previous_final_score'], $b['assigned']);
                $groups = ['90_100' => 0, '80_89' => 0, '70_79' => 0, '69_below' => 0];
                $male = 0;
                $female = 0;
                $studentNames = [];
                foreach ($b['assigned'] as $student) {
                    $groups[$student['score_group']] = ($groups[$student['score_group']] ?? 0) + 1;
                    $gender = strtolower((string)($student['gender'] ?? ''));
                    if ($gender === 'female') $female++; else $male++;
                    $studentName = trim((string)($student['student_name'] ?? ''));
                    if ($studentName === '') {
                        $studentName = 'Student #' . (int)($student['student_id'] ?? 0);
                    }
                    $studentNames[] = $studentName;
                }
                $summary[] = [
                    'class_section_id'   => $secId,
                    'class_section_name' => $nameById[$secId] ?? (string)$secId,
                    'total'              => count($b['assigned']),
                    'male'               => $male,
                    'female'             => $female,
                    'score_groups'       => $groups,
                    'average_score'      => count($scores) > 0 ? round(array_sum($scores) / count($scores), 2) : null,
                    'student_names'      => $studentNames,
                ];
            }

            return $isAjax
                ? $json(['ok' => true, 'message' => 'Draft distribution saved. Students will move to student_class when they enroll.', 'sections' => $summary])
                : redirect()->back()->with('success', 'Draft distribution saved.');
        } catch (\Throwable $e) {
            $msg = 'Auto distribution failed: ' . $e->getMessage();
            return $isAjax ? $json(['ok' => false, 'message' => $msg], 500) : redirect()->back()->with('error', $msg);
        }
    }

    private function distributionCandidates(int $classId, string $year): array
    {
        $rows = $this->db->table('promotion_queue pq')
            ->select('pq.id AS promotion_queue_id, pq.student_id, pq.school_year_from, pq.to_class_id, students.firstname, students.lastname, students.gender, sd.year_score AS decision_score')
            ->join('students', 'students.id = pq.student_id', 'left')
            ->join('student_decisions sd', 'sd.student_id = pq.student_id AND sd.school_year = pq.school_year_from', 'left')
            ->where('pq.to_class_id', $classId)
            ->where('pq.school_year_to', $year)
            ->whereIn('pq.status', ['queued', 'assigned'])
            ->groupBy('pq.id')
            ->get()
            ->getResultArray();

        if (empty($rows)) {
            return $this->decisionDistributionCandidates($classId, $year);
        }

        $out = [];
        foreach ($rows as $row) {
            $score = is_numeric($row['decision_score'] ?? null)
                ? (float)$row['decision_score']
                : $this->previousAverageScore((int)$row['student_id'], (string)($row['school_year_from'] ?? ''));
            $score = $score === null ? 0.0 : max(0.0, min(100.0, $score));
            $row['previous_final_score'] = $score;
            $row['score_group'] = $this->scoreGroup($score);
            $row['student_name'] = $this->formatStudentName($row);
            $out[] = $row;
        }

        return $out;
    }

    private function decisionDistributionCandidates(int $classId, string $targetSchoolYear): array
    {
        $previousSchoolYear = $this->previousSchoolYearName($targetSchoolYear);
        if ($previousSchoolYear === null || ! $this->db->tableExists('student_decisions')) {
            return [];
        }

        $rows = $this->db->table('student_decisions sd')
            ->select('sd.id AS decision_id, sd.student_id, sd.class_section_name, sd.year_score, sd.decision, students.firstname, students.lastname, students.gender')
            ->join('students', 'students.id = sd.student_id', 'left')
            ->where('sd.school_year', $previousSchoolYear)
            ->where('students.is_active', 1)
            ->orderBy('sd.updated_at', 'DESC')
            ->orderBy('sd.id', 'DESC')
            ->get()
            ->getResultArray();

        $seen = [];
        $out = [];
        foreach ($rows as $row) {
            $studentId = (int)($row['student_id'] ?? 0);
            if ($studentId <= 0 || isset($seen[$studentId])) {
                continue;
            }
            $seen[$studentId] = true;

            $targetClassId = $this->targetClassIdFromDecision(
                (string)($row['class_section_name'] ?? ''),
                (string)($row['decision'] ?? '')
            );
            if ($targetClassId !== $classId) {
                continue;
            }

            $score = is_numeric($row['year_score'] ?? null) ? (float)$row['year_score'] : 0.0;
            $score = max(0.0, min(100.0, $score));
            $out[] = [
                'promotion_queue_id' => 0,
                'student_id' => $studentId,
                'school_year_from' => $previousSchoolYear,
                'to_class_id' => $classId,
                'student_name' => $this->formatStudentName($row),
                'gender' => (string)($row['gender'] ?? ''),
                'previous_final_score' => $score,
                'score_group' => $this->scoreGroup($score),
            ];
        }

        return $out;
    }

    private function targetClassIdFromDecision(string $classSectionName, string $decision): ?int
    {
        $decision = strtolower(trim($decision));
        $baseName = strtoupper(trim(preg_replace('/-.+$/', '', $classSectionName) ?? ''));
        if ($baseName === '') {
            return null;
        }

        $targetBaseName = $baseName;
        if ($decision === 'pass') {
            if ($baseName === 'KG') {
                $targetBaseName = '1';
            } elseif (ctype_digit($baseName)) {
                $level = (int)$baseName;
                $targetBaseName = $level >= 9 ? 'YOUTH' : (string)($level + 1);
            } elseif ($baseName === 'YOUTH') {
                $targetBaseName = 'YOUTH';
            }
        }

        $row = $this->classSectionModel
            ->select('class_id')
            ->where('UPPER(class_section_name)', $targetBaseName)
            ->where("class_section_name NOT LIKE '%-%'", null, false)
            ->first();

        return $row ? (int)$row['class_id'] : null;
    }

    private function formatStudentName(array $row): string
    {
        $name = trim(
            trim((string)($row['firstname'] ?? '')) . ' ' .
            trim((string)($row['lastname'] ?? ''))
        );

        return $name !== '' ? $name : 'Student #' . (int)($row['student_id'] ?? 0);
    }

    private function previousSchoolYearName(string $schoolYear): ?string
    {
        if (preg_match('/^(\d{4})-(\d{4})$/', trim($schoolYear), $matches) !== 1) {
            return null;
        }

        return ((int)$matches[1] - 1) . '-' . ((int)$matches[2] - 1);
    }

    private function previousAverageScore(int $studentId, string $schoolYear): ?float
    {
        if ($studentId <= 0 || $schoolYear === '') {
            return null;
        }

        $row = $this->db->table('semester_scores')
            ->select('AVG(semester_score) AS avg_score')
            ->where('student_id', $studentId)
            ->where('school_year', $schoolYear)
            ->where('semester_score IS NOT NULL', null, false)
            ->get()
            ->getRowArray();

        return is_numeric($row['avg_score'] ?? null) ? (float)$row['avg_score'] : null;
    }

    private function scoreGroup(float $score): string
    {
        if ($score >= 90) return '90_100';
        if ($score >= 80) return '80_89';
        if ($score >= 70) return '70_79';
        return '69_below';
    }

    private function buildBalancedDistribution(array $students, array $sections, int $minPerSection, ?int $maxPerSection): array
    {
        $sectionCount = count($sections);
        $total = count($students);
        $baseSize = intdiv($total, $sectionCount);
        $remainder = $total % $sectionCount;
        $targetSizes = [];

        foreach ($sections as $idx => $section) {
            $targetSizes[$idx] = $baseSize + ($idx < $remainder ? 1 : 0);
        }

        $buckets = [];
        foreach ($sections as $idx => $section) {
            $buckets[$idx] = [
                'class_section_id' => (int)$section['class_section_id'],
                'assigned' => [],
            ];
        }

        $groups = ['90_100' => [], '80_89' => [], '70_79' => [], '69_below' => []];
        foreach ($students as $student) {
            $groups[$student['score_group']][] = $student;
        }

        $currentCounts = array_fill(0, $sectionCount, 0);
        $allocations = [];
        $groupIndex = 0;
        foreach ($groups as $groupName => $groupStudents) {
            usort($groupStudents, static fn($a, $b): int => ((float)$b['previous_final_score']) <=> ((float)$a['previous_final_score']));
            $groupTotal = count($groupStudents);
            $base = intdiv($groupTotal, $sectionCount);
            $extra = $groupTotal % $sectionCount;
            $allocations[$groupName] = array_fill(0, $sectionCount, $base);
            foreach ($currentCounts as $idx => $cnt) {
                $currentCounts[$idx] += $base;
            }

            $order = range(0, $sectionCount - 1);
            $offset = $groupIndex % $sectionCount;
            $order = array_merge(array_slice($order, $offset), array_slice($order, 0, $offset));
            usort($order, static function ($a, $b) use ($targetSizes, $currentCounts) {
                $remainingA = $targetSizes[$a] - $currentCounts[$a];
                $remainingB = $targetSizes[$b] - $currentCounts[$b];
                return $remainingB <=> $remainingA;
            });

            foreach ($order as $sectionIdx) {
                if ($extra <= 0) break;
                if ($currentCounts[$sectionIdx] >= $targetSizes[$sectionIdx]) continue;
                $allocations[$groupName][$sectionIdx]++;
                $currentCounts[$sectionIdx]++;
                $extra--;
            }
            $groups[$groupName] = $groupStudents;
            $groupIndex++;
        }

        foreach ($groups as $groupName => $groupStudents) {
            $quotas = $allocations[$groupName];
            foreach ($groupStudents as $idx => $student) {
                $round = intdiv($idx, max(1, $sectionCount));
                $order = range(0, $sectionCount - 1);
                if ($round % 2 === 1) {
                    $order = array_reverse($order);
                }
                foreach ($order as $sectionIdx) {
                    if (($quotas[$sectionIdx] ?? 0) <= 0) continue;
                    $buckets[$sectionIdx]['assigned'][] = $student;
                    $quotas[$sectionIdx]--;
                    break;
                }
            }
        }

        return $this->balanceDistributionAverages($buckets, $minPerSection, $maxPerSection);
    }

    private function balanceDistributionAverages(array $buckets, int $minPerSection, ?int $maxPerSection): array
    {
        for ($i = 0; $i < 50; $i++) {
            $averages = array_map(function ($bucket): float {
                if (empty($bucket['assigned'])) return 0.0;
                $scores = array_map(static fn($student): float => (float)$student['previous_final_score'], $bucket['assigned']);
                return array_sum($scores) / count($scores);
            }, $buckets);
            $highIdx = array_keys($averages, max($averages), true)[0];
            $lowIdx = array_keys($averages, min($averages), true)[0];
            if (($averages[$highIdx] - $averages[$lowIdx]) <= 1.0) {
                break;
            }

            $best = null;
            foreach ($buckets[$highIdx]['assigned'] as $hiPos => $hiStudent) {
                foreach ($buckets[$lowIdx]['assigned'] as $loPos => $loStudent) {
                    if ($hiStudent['score_group'] !== $loStudent['score_group']) continue;
                    $trial = $buckets;
                    $trial[$highIdx]['assigned'][$hiPos] = $loStudent;
                    $trial[$lowIdx]['assigned'][$loPos] = $hiStudent;
                    $trialAvg = array_map(function ($bucket): float {
                        if (empty($bucket['assigned'])) return 0.0;
                        $scores = array_map(static fn($student): float => (float)$student['previous_final_score'], $bucket['assigned']);
                        return array_sum($scores) / count($scores);
                    }, $trial);
                    $newSpread = max($trialAvg) - min($trialAvg);
                    if ($newSpread < ($averages[$highIdx] - $averages[$lowIdx])) {
                        $best = [$hiPos, $loPos, $newSpread];
                    }
                }
            }

            if ($best === null) {
                break;
            }
            [$hiPos, $loPos] = $best;
            $tmp = $buckets[$highIdx]['assigned'][$hiPos];
            $buckets[$highIdx]['assigned'][$hiPos] = $buckets[$lowIdx]['assigned'][$loPos];
            $buckets[$lowIdx]['assigned'][$loPos] = $tmp;
        }

        return $buckets;
    }

    /**
     * API: Return promoted-student totals per base class for the selected year.
     */
    public function promotionTotalsApi()
    {
        try {
            $year = trim((string)($this->request->getGet('school_year') ?? $this->schoolYear));

            // Fetch base sections (no dash) and filter to KG, 1..9, youth
            $bases = $this->classSectionModel
                ->where("class_section_name NOT LIKE '%-%'", null, false)
                ->orderBy('class_id', 'ASC')
                ->findAll();

            $wanted = [];
            foreach ($bases as $r) {
                $nameRaw = (string)($r['class_section_name'] ?? '');
                $name = strtolower($nameRaw);

                // Only KG, 1..9, Youth per request
                if ($name === 'kg' || $name === 'youth') {
                    $wanted[] = $r;
                    continue;
                }

                if (ctype_digit($name)) {
                    $num = (int)$name;
                    if ($num >= 1 && $num <= 9) {
                        $wanted[] = $r;
                    }
                }
            }

            $out = [];
            foreach ($wanted as $r) {
                $classId = (int)$r['class_id'];
                $cands = $this->db->table('promotion_queue pq')
                    ->select('pq.student_id')
                    ->where('pq.to_class_id', $classId)
                    ->where('pq.school_year_to', $year)
                    ->whereIn('pq.status', ['queued','assigned'])
                    ->groupBy('pq.student_id')
                    ->get()->getResultArray();
                $total = count($cands);
                if ($total === 0) {
                    $total = count($this->decisionDistributionCandidates($classId, $year));
                }

                $out[] = [
                    'class_id'          => $classId,
                    'class_section_id'  => (int)($r['class_section_id'] ?? 0),
                    'class_section_name'=> (string)($r['class_section_name'] ?? ''),
                    'total'             => $total,
                    'sections'          => $this->savedDistributionSections($classId, $year),
                ];
            }

            return $this->response->setJSON(['ok' => true, 'year' => $year, 'rows' => $out]);
        } catch (\Throwable $e) {
            return $this->response->setStatusCode(500)->setJSON(['ok' => false, 'message' => $e->getMessage()]);
        }
    }

    private function savedDistributionSections(int $classId, string $year): array
    {
        if (! $this->db->tableExists('student_section_distribution_drafts')) {
            return [];
        }

        $rows = $this->db->table('student_section_distribution_drafts d')
            ->select('d.class_section_id, cs.class_section_name, students.firstname, students.lastname, d.student_id')
            ->join('classSection cs', 'cs.class_section_id = d.class_section_id', 'left')
            ->join('students', 'students.id = d.student_id', 'left')
            ->where('d.class_id', $classId)
            ->where('d.school_year', $year)
            ->where('d.status', 'pending')
            ->orderBy('cs.class_section_name', 'ASC')
            ->orderBy('students.lastname', 'ASC')
            ->orderBy('students.firstname', 'ASC')
            ->get()
            ->getResultArray();

        $sections = [];
        foreach ($rows as $row) {
            $sectionId = (int)($row['class_section_id'] ?? 0);
            if ($sectionId <= 0) {
                continue;
            }
            if (!isset($sections[$sectionId])) {
                $sections[$sectionId] = [
                    'class_section_id' => $sectionId,
                    'class_section_name' => (string)($row['class_section_name'] ?? $sectionId),
                    'total' => 0,
                    'student_names' => [],
                ];
            }

            $name = trim(trim((string)($row['firstname'] ?? '')) . ' ' . trim((string)($row['lastname'] ?? '')));
            if ($name === '') {
                $name = 'Student #' . (int)($row['student_id'] ?? 0);
            }
            $sections[$sectionId]['student_names'][] = $name;
            $sections[$sectionId]['total']++;
        }

        return array_values($sections);
    }

    /**
     * POST /students/update/{id}
     */
    public function editStudentData(?int $id = null)
    {
        $request = $this->request;

        if ($id === null) {
            $id = (int) $request->getPost('id');
        }
        if (!$id) {
            return redirect()->back()->with('error', 'Invalid student ID.');
        }

        // Keep original post to detect presence of keys
        $rawPost = $request->getPost();

        // Validate all columns we edit (except id)
        $rules = [
            'school_id'           => "permit_empty|alpha_numeric_punct|max_length[100]|is_unique[students.school_id,id,{$id}]",
            'firstname'           => 'required|regex_match[/^[A-Za-z\s\-]{2,30}$/]',
            'lastname'            => 'required|regex_match[/^[A-Za-z\s\-]{2,30}$/]',
            'dob'                 => 'required|valid_date[Y-m-d]',
            'age'                 => 'permit_empty|integer', // we compute it anyway
            'gender'              => 'required|in_list[Male,Female,Other]',
            'registration_grade'  => 'required|max_length[50]',
            'photo_consent'       => 'permit_empty|in_list[0,1]',

            'parent_id'           => 'required|integer|greater_than[0]',
            'registration_date'   => 'permit_empty', // accept Y-m-d or Y-m-d\TH:i; parse manually
            'tuition_paid'        => 'permit_empty|in_list[0,1]',
            'year_of_registration' => 'permit_empty|regex_match[/^\d{4}(-\d{4})?$/]',
            'school_year'         => 'permit_empty|regex_match[/^\d{4}-\d{4}$/]',
            'rfid_tag'            => 'permit_empty|max_length[100]',
            'semester'            => 'permit_empty|in_list[Fall,Spring,Summer]',
            'is_new'              => 'required|in_list[0,1]',
            'is_active'           => 'permit_empty|in_list[0,1]',

            // Free-text lists; parsed later
            'medical_conditions'  => 'permit_empty',
            'allergies'           => 'permit_empty',
            // Touch flags from UI; used to decide whether to sync health lists
            'medical_touched'     => 'permit_empty|in_list[0,1]',
            'allergies_touched'   => 'permit_empty|in_list[0,1]',
        ];

        if (!$this->validate($rules)) {
            return redirect()->back()
                ->withInput()
                ->with('error', 'Please correct the highlighted errors.')
                ->with('errors', $this->validator->getErrors());
        }

        // Gather & sanitize inputs
        $in = [
            'school_id'            => trim((string) $request->getPost('school_id')),
            'firstname'            => $this->titleCase((string) $request->getPost('firstname')),
            'lastname'             => $this->titleCase((string) $request->getPost('lastname')),
            'dob'                  => trim((string) $request->getPost('dob')),           // Y-m-d
            'gender'               => trim((string) $request->getPost('gender')),
            'registration_grade'   => trim((string) $request->getPost('registration_grade')),
            'photo_consent'        => (string) $request->getPost('photo_consent', FILTER_SANITIZE_NUMBER_INT),

            'parent_id'            => (string) $request->getPost('parent_id', FILTER_SANITIZE_NUMBER_INT),
            'registration_date'    => trim((string) $request->getPost('registration_date')), // '' or 'Y-m-d' or 'Y-m-d\TH:i'
            'tuition_paid'         => (string) $request->getPost('tuition_paid', FILTER_SANITIZE_NUMBER_INT),
            'year_of_registration' => trim((string) $request->getPost('year_of_registration')),
            'school_year'          => trim((string) $request->getPost('school_year')),
            'rfid_tag'             => trim((string) $request->getPost('rfid_tag')),
            'semester'             => trim((string) $request->getPost('semester')),
            'is_new'               => (string) $request->getPost('is_new', FILTER_SANITIZE_NUMBER_INT),
            'is_active'            => (string) ($rawPost['is_active'] ?? ''),

            // raw lists (may be missing from POST entirely)
            'medical_conditions'   => (string) ($rawPost['medical_conditions'] ?? ''),
            'allergies'            => (string) ($rawPost['allergies'] ?? ''),
            // touch flags ("1" when user interacted)
            'medical_touched'      => (string) $request->getPost('medical_touched', FILTER_SANITIZE_NUMBER_INT),
            'allergies_touched'    => (string) $request->getPost('allergies_touched', FILTER_SANITIZE_NUMBER_INT),
        ];

        $tzName = (string) (config('School')->attendance['timezone'] ?? user_timezone());
        $tzLocal = new \DateTimeZone($tzName);
        $tzUtc   = new \DateTimeZone('UTC');

        try {
            // DOB -> Y-m-d and compute age
            $dob = \DateTimeImmutable::createFromFormat('Y-m-d', $in['dob'], $tzLocal);
            if ($dob === false) {
                throw new \RuntimeException('Invalid date of birth.');
            }
            $dobStr = $dob->format('Y-m-d');

            $today = new \DateTimeImmutable('today', $tzLocal);
            $age   = (int)$today->format('Y') - (int)$dob->format('Y');
            if ((int)$today->format('md') < (int)$dob->format('md')) $age--;
            if ($age < 0) throw new \RuntimeException('DOB results in negative age.');

            // registration_date: accept date or datetime-local; store as UTC DATETIME
            $regDateStr = null;
            if ($in['registration_date'] !== '') {
                $rd = null;
                foreach (['Y-m-d\TH:i', 'Y-m-d'] as $fmt) {
                    $tmp = \DateTimeImmutable::createFromFormat($fmt, $in['registration_date'], $tzLocal);
                    if ($tmp !== false) {
                        $rd = $tmp;
                        break;
                    }
                }
                if ($rd === null) {
                    throw new \RuntimeException('Invalid registration date.');
                }
                // normalize to full minute precision in UTC
                $regDateStr = $rd->setTimezone($tzUtc)->format('Y-m-d H:i:00');
            }

            // Build payload strictly to StudentModel::$allowedFields
            $studentData = [
                'school_id'            => $in['school_id'] ?: null,
                'firstname'            => $in['firstname'],
                'lastname'             => $in['lastname'],
                'dob'                  => $dobStr,
                'age'                  => $age,
                'gender'               => $in['gender'],
                'registration_grade'   => $in['registration_grade'],
                'photo_consent'        => (int) ($in['photo_consent'] === '1'),

                'parent_id'            => (int) $in['parent_id'],
                'registration_date'    => $regDateStr, // nullable
                'tuition_paid'         => (int) ($in['tuition_paid'] === '1'),
                'year_of_registration' => $in['year_of_registration'] ?: null,
                'school_year'          => $in['school_year'] ?: null,
                'rfid_tag'             => $in['rfid_tag'] ?: null,
                'semester'             => $in['semester'] ?: null,
            'is_new'               => (int) ($in['is_new'] === '1'),
        ];
        if (array_key_exists('is_active', $rawPost)) {
            $studentData['is_active'] = (int) ($in['is_active'] === '1');
        }

            // Normalize health lists (server-side safety)
            $normConditions = $this->normalizeHealthList($in['medical_conditions'], 100); // -> condition_name
            $normAllergies  = $this->normalizeHealthList($in['allergies'], 100);          // -> allergy

            $this->db->transStart();

            $row = $this->studentModel->find($id);
            if (!$row) {
                $this->db->transRollback();
                return redirect()->back()->with('error', 'Student not found.');
            }

            if (!$this->studentModel->update($id, $studentData)) {
                $this->db->transRollback();
                return redirect()->back()
                    ->withInput()
                    ->with('error', 'Could not update student.')
                    ->with('errors', $this->studentModel->errors() ?: []);
            }

            // Only sync health lists when user actually changed them (touched=1)
            // Fallback: if no touch flag but a non-empty value was explicitly posted, also sync.
            $medTouched = ($in['medical_touched'] === '1');
            $allTouched = ($in['allergies_touched'] === '1');

            if ($medTouched || (array_key_exists('medical_conditions', $rawPost) && trim($in['medical_conditions']) !== '')) {
                $this->syncHealthList($id, $this->conditionModel, 'condition_name', $normConditions);
            }
            if ($allTouched || (array_key_exists('allergies', $rawPost) && trim($in['allergies']) !== '')) {
                $this->syncHealthList($id, $this->allergyModel, 'allergy', $normAllergies);
            }

            $this->db->transComplete();
            if ($this->db->transStatus() === false) {
                return redirect()->back()->withInput()->with('error', 'Transaction failed while updating student.');
            }

            return redirect()->to('/administrator/student_profiles')->with('success', 'Student updated successfully.');
        } catch (\CodeIgniter\Database\Exceptions\DataException $e) {
            log_message('error', '[Students:update] DataException: {msg}', ['msg' => $e->getMessage()]);
            return redirect()->back()->withInput()->with('error', 'Database error while updating student.');
        } catch (\Throwable $e) {
            log_message('error', '[Students:update] Exception: {msg}', ['msg' => $e->getMessage()]);
            return redirect()->back()->withInput()->with('error', $e->getMessage());
        }
    }



    /**
     * Normalize a comma/semicolon/newline list:
     * - Trim & collapse spaces
     * - Drop placeholders like "none", "n/a", "na", "null", "nil"
     * - Deduplicate case-insensitively
     * - Truncate to $maxLen
     */
    private function normalizeHealthList(?string $s, int $maxLen = 100): array
    {
        $s = (string)$s;
        if ($s === '') return []; // empty means "no change" unless key missing (handled by caller)

        $parts = preg_split('/[,\n;]+/u', $s, -1, PREG_SPLIT_NO_EMPTY) ?: [];
        $outAssoc = [];
        foreach ($parts as $p) {
            $v = trim(preg_replace('/\s+/u', ' ', $p));
            if ($v === '') continue;
            if (preg_match('/^(none|n\/a|na|null|nil|no)$/i', $v)) continue;
            $v = mb_substr($v, 0, $maxLen, 'UTF-8');
            $outAssoc[mb_strtolower($v, 'UTF-8')] = $v; // dedupe case-insensitively
        }
        return array_values($outAssoc);
    }

    /**
     * Diff-sync helper:
     *   - Reads existing rows for student
     *   - Deletes removed values
     *   - Inserts only new values
     * Requires $model to have columns: id, student_id, and $field (e.g., allergy / condition_name)
     */
    private function syncHealthList(int $studentId, \CodeIgniter\Model $model, string $field, array $newValues): void
    {
        // Fetch current values
        $rows = $model->where('student_id', $studentId)->select("id, {$field}")->findAll();
        $current = [];
        $byId    = [];
        foreach ($rows as $r) {
            $val = (string)($r[$field] ?? '');
            $key = mb_strtolower(trim($val), 'UTF-8');
            if ($key === '') continue;
            $current[$key] = $val;
            $byId[$key]    = (int)$r['id'];
        }

        // Build new set
        $incoming = [];
        foreach ($newValues as $v) {
            $key = mb_strtolower(trim($v), 'UTF-8');
            if ($key === '') continue;
            $incoming[$key] = $v;
        }

        // Compute diffs
        $toDeleteKeys = array_diff(array_keys($current), array_keys($incoming));
        $toInsertKeys = array_diff(array_keys($incoming), array_keys($current));

        // Delete removed
        if (!empty($toDeleteKeys)) {
            $ids = array_map(fn($k) => $byId[$k], $toDeleteKeys);
            if (!empty($ids)) {
                $model->whereIn('id', $ids)->delete();
            }
        }

        // Insert new
        if (!empty($toInsertKeys)) {
            $batch = [];
            foreach ($toInsertKeys as $k) {
                $batch[] = [
                    'student_id' => $studentId,
                    $field       => $incoming[$k],
                ];
            }
            if (!empty($batch)) {
                // Use insertBatch; ignore duplicates if unique index exists
                $model->insertBatch($batch);
            }
        }
    }


    /**
     * Split a free-text list by commas/semicolons/newlines, trim items,
     * de-duplicate (case-insensitive), and drop empties. Truncates to $maxLen.
     */
    private function parseList(string $input, int $maxLen = 100): array
    {
        if ($input === '') return [];
        $parts = preg_split('/[,\n;]+/u', $input);
        if (!$parts) return [];

        $seen = [];
        $out  = [];
        foreach ($parts as $raw) {
            $item = trim(strip_tags($raw));
            if ($item === '') continue;

            // Case-insensitive de-dup
            $key = mb_strtolower($item, 'UTF-8');
            if (isset($seen[$key])) continue;
            $seen[$key] = true;

            // Enforce maxLen to align with model validation
            $out[] = mb_substr($item, 0, $maxLen, 'UTF-8');
        }
        return $out;
    }

    private function titleCase(string $s): string
    {
        $s = strip_tags($s);
        $s = mb_strtolower($s ?: '', 'UTF-8');
        return mb_convert_case($s, MB_CASE_TITLE_SIMPLE, 'UTF-8');
    }

    public function scoreCard()
    {
        $studentId = (int)($this->request->getPost('student_id') ?? 0);
        if ($studentId <= 0) {
            return redirect()->back()->with('error', 'Invalid student id.');
        }

        $student = $this->studentModel
            ->select('id, firstname, lastname, school_id')
            ->where('id', $studentId)
            ->first();
        if (!$student) {
            return $this->response->setStatusCode(404)->setBody('<div class="p-3 text-danger">Student not found.</div>');
        }

        $rows = $this->db->table('semester_scores ss')
            ->select([
                'ss.school_year',
                'ss.semester',
                'ss.homework_avg',
                'ss.project_avg',
                'ss.participation_score',
                'ss.quiz_avg',
                'ss.test_avg',
                'ss.attendance_score',
                'ss.ptap_score',
                'ss.midterm_exam_score',
                'ss.semester_score',
                'cs.class_section_name',
            ])
            ->join('classSection cs', 'cs.class_section_id = ss.class_section_id', 'left')
            ->where('ss.student_id', $studentId)
            ->get()
            ->getResultArray();

        $semesterOrder = static function (string $semester): int {
            $s = strtolower(trim($semester));
            if ($s === 'fall') return 1;
            if ($s === 'spring') return 2;
            return 3;
        };

        usort($rows, static function (array $a, array $b) use ($semesterOrder) {
            $ay = (string)($a['school_year'] ?? '');
            $by = (string)($b['school_year'] ?? '');
            if ($ay !== $by) return strcmp($by, $ay);
            $as = $semesterOrder((string)($a['semester'] ?? ''));
            $bs = $semesterOrder((string)($b['semester'] ?? ''));
            if ($as !== $bs) return $as <=> $bs;
            return 0;
        });

        $yearScoreMap = [];
        foreach ($rows as $r) {
            $year = (string)($r['school_year'] ?? '');
            if ($year === '') continue;
            $semKey = strtolower(trim((string)($r['semester'] ?? '')));
            $scoreVal = is_numeric($r['semester_score'] ?? null) ? (float)$r['semester_score'] : null;
            if ($scoreVal === null) continue;
            if (!isset($yearScoreMap[$year])) $yearScoreMap[$year] = [];
            $yearScoreMap[$year][$semKey] = $scoreVal;
        }

        foreach ($yearScoreMap as $year => $vals) {
            $scores = [];
            if (isset($vals['fall'])) $scores[] = $vals['fall'];
            if (isset($vals['spring'])) $scores[] = $vals['spring'];
            if (empty($scores)) {
                $yearScoreMap[$year]['avg'] = null;
            } else {
                $yearScoreMap[$year]['avg'] = round(array_sum($scores) / count($scores), 1);
            }
        }

        $commentRows = $this->db->table('score_comments')
            ->select('school_year, semester, score_type, comment, comment_review, created_at')
            ->where('student_id', $studentId)
            ->orderBy('created_at', 'DESC')
            ->get()
            ->getResultArray();

        $commentMap = [];
        foreach ($commentRows as $row) {
            $year = (string)($row['school_year'] ?? '');
            $semester = strtolower(trim((string)($row['semester'] ?? '')));
            $typeRaw = strtolower(trim((string)($row['score_type'] ?? '')));
            if ($year === '' || $typeRaw === '') continue;
            if ($typeRaw === 'attendance_comment') $typeRaw = 'attendance';
            $commentVal = trim((string)($row['comment'] ?? ''));
            $reviewVal  = trim((string)($row['comment_review'] ?? ''));
            if (in_array($typeRaw, ['midterm', 'final', 'ptap'], true)) {
                $commentVal = $reviewVal !== '' ? $reviewVal : $commentVal;
            } elseif ($typeRaw === 'attendance' && $commentVal === '' && $reviewVal !== '') {
                $commentVal = $reviewVal;
            }
            if ($commentVal === '') continue;

            $key = $year . '|' . $semester;
            if (!isset($commentMap[$key])) $commentMap[$key] = [];
            if (!isset($commentMap[$key][$typeRaw])) {
                $commentMap[$key][$typeRaw] = $commentVal;
            }
        }

        foreach ($rows as &$r) {
            $year = (string)($r['school_year'] ?? '');
            $semKey = strtolower(trim((string)($r['semester'] ?? '')));
            $key = $year . '|' . $semKey;
            $r['comments'] = $commentMap[$key] ?? [];
            $r['year_score'] = $yearScoreMap[$year]['avg'] ?? null;
        }
        unset($r);

        $rowsByYear = [];
        foreach ($rows as $r) {
            $year = (string)($r['school_year'] ?? '');
            if ($year === '') continue;
            $semKey = strtolower(trim((string)($r['semester'] ?? '')));
            if ($semKey === '') continue;
            if (!isset($rowsByYear[$year])) $rowsByYear[$year] = [];
            $rowsByYear[$year][$semKey] = $r;
        }

        $expandedRows = [];
        foreach ($rowsByYear as $year => $bySem) {
            $fallbackGrade = $bySem['fall']['class_section_name'] ?? ($bySem['spring']['class_section_name'] ?? null);
            foreach (['fall', 'spring'] as $semKey) {
                if (isset($bySem[$semKey])) {
                    $expandedRows[] = $bySem[$semKey];
                    continue;
                }
                $expandedRows[] = [
                    'school_year' => $year,
                    'semester' => ucfirst($semKey),
                    'homework_avg' => null,
                    'project_avg' => null,
                    'participation_score' => null,
                    'quiz_avg' => null,
                    'test_avg' => null,
                    'attendance_score' => null,
                    'ptap_score' => null,
                    'midterm_exam_score' => null,
                    'semester_score' => null,
                    'class_section_name' => $fallbackGrade,
                    'comments' => [],
                    'year_score' => $yearScoreMap[$year]['avg'] ?? null,
                ];
            }
        }

        if (!empty($expandedRows)) {
            $rows = $expandedRows;
        }

        return view('student/score_card', [
            'student' => $student,
            'rows' => $rows,
        ]);
    }

    public function scoreCardIndex()
    {
        return redirect()->to('/student/score-card/list');
    }

    public function scoreCardAdmin()
    {
        $query = trim((string)($this->request->getGet('q') ?? ''));
        $students = [];
        $suggestions = [];

        if ($query !== '') {
            $schoolIdPart = '';
            $namePart = $query;
            if (preg_match('/^\s*([^-\s]+)\s*-\s*(.+)\s*$/', $query, $m)) {
                $schoolIdPart = trim((string)$m[1]);
                $namePart = trim((string)$m[2]);
            }
            $students = $this->studentModel
                ->select('id, school_id, firstname, lastname, is_active')
                ->groupStart()
                ->like('firstname', $namePart)
                ->orLike('lastname', $namePart)
                ->orLike('school_id', $query)
                ->groupEnd()
                ->orderBy('lastname', 'ASC')
                ->orderBy('firstname', 'ASC')
                ->limit(50)
                ->findAll();
            if ($schoolIdPart !== '' && empty($students)) {
                $students = $this->studentModel
                    ->select('id, school_id, firstname, lastname, is_active')
                    ->groupStart()
                    ->like('school_id', $schoolIdPart)
                    ->orLike("CONCAT_WS(' ', firstname, lastname)", $namePart, 'both', null, false)
                    ->groupEnd()
                    ->orderBy('lastname', 'ASC')
                    ->orderBy('firstname', 'ASC')
                    ->limit(50)
                    ->findAll();
            }
        }

        try {
            $suggestions = $this->studentModel
                ->select('id, school_id, firstname, lastname')
                ->where('is_active', 1)
                ->orderBy('lastname', 'ASC')
                ->orderBy('firstname', 'ASC')
                ->limit(1000)
                ->findAll();
        } catch (\Throwable $e) {
            $suggestions = [];
        }

        return view('admin/student_score_card', [
            'query' => $query,
            'students' => $students,
            'suggestions' => $suggestions,
        ]);
    }

    public function scoreCardList()
    {
        $roleRaw = session()->get('role');
        if (is_array($roleRaw)) {
            $roleRaw = $roleRaw[0] ?? 'guest';
        }
        $role = strtolower((string)($roleRaw ?? 'guest'));

        $students = [];
        if (in_array($role, ['parent', 'parent_dashboard'], true)) {
            $schoolYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
            $parentId = (int)(session()->get('user_id') ?? 0);
            if ($parentId > 0 && $schoolYear !== '') {
                $students = $this->studentModel->getByParentAndYear($parentId, $schoolYear);
            }
        } elseif (in_array($role, ['teacher', 'teacher_assistant', 'teacher_dashboard'], true)) {
            $schoolYear = $this->currentSchoolYearName((string) ($this->schoolYear ?? ''));
            $classSectionId = (int)(session()->get('class_section_id') ?? 0);

            if ($classSectionId > 0 && !empty($schoolYear)) {
                $students = $this->studentModel->getByClassAndYear($classSectionId, $schoolYear);
            }

            if (empty($students)) {
                $userId = (int)(session()->get('user_id') ?? 0);
                if ($userId > 0 && !empty($schoolYear)) {
                    $db = \Config\Database::connect();
                    $sectionRows = $db->table('teacher_class')
                        ->select('class_section_id')
                        ->where('teacher_id', $userId)
                        ->where('school_year', (string)$schoolYear)
                        ->get()
                        ->getResultArray();
                    $sectionIds = array_values(array_filter(array_unique(array_map(
                        static fn($r) => (int)($r['class_section_id'] ?? 0),
                        $sectionRows
                    )), static fn($v) => $v > 0));
                    if (!empty($sectionIds)) {
                        $studentClassModel = new \App\Models\StudentClassModel();
                        $rows = $studentClassModel->getStudentsByClassSectionIds($sectionIds, $schoolYear);
                        $unique = [];
                        foreach ($rows as $r) {
                            $sid = (int)($r['student_id'] ?? 0);
                            if ($sid > 0) {
                                $unique[$sid] = [
                                    'id' => $sid,
                                    'school_id' => $r['school_id'] ?? '',
                                    'firstname' => $r['firstname'] ?? '',
                                    'lastname' => $r['lastname'] ?? '',
                                ];
                            }
                        }
                        $students = array_values($unique);
                    }
                }
            }
        } else {
            return redirect()->to('/login')->with('error', 'Access denied.');
        }

        return view('student/score_card_list', [
            'students' => $students,
            'role' => $role,
        ]);
    }
}
