<?php

namespace App\Controllers\View;
use App\Controllers\BaseController;
use App\Models\AttendanceDataModel;
use App\Models\StudentModel;
use App\Models\StudentClassModel;
use App\Models\ClassSectionModel;
use App\Models\EmergencyContactModel;
use App\Models\AttendanceRecordModel;
use CodeIgniter\Controller;
use App\Models\UserModel;
use App\Models\ConfigurationModel;
use App\Models\TeacherClassModel;
use App\Models\AttendanceDayModel;
use App\Models\TeacherAttendanceModel;
use App\Models\StaffAttendanceModel;
use App\Services\SemesterScoreService;
use App\Services\SemesterRangeService;
use App\Libraries\AttendanceAutoPublish;
use App\Models\UserRoleModel;
use RuntimeException;
use App\Models\CalendarModel;

class AttendanceController extends Controller
{
    protected $db;
    protected $configModel;
    protected $semesterScoreService;
    protected $semester;
    protected $schoolYear;
    protected $attendanceDataModel;
    protected $studentModel;
    protected $emergencyContactModel;
    protected $userModel;
    protected $teacherClassSection;
    protected $attendanceRecordModel;
    protected $classSectionModel;
    protected $studentClassModel;
    protected $enableAttendance;
    protected $staffAttendanceModel;
    protected $attendanceDayModel;
    protected $calendarModel;
    protected $userRoleModel;
    protected $adminRoleCache = [];
    protected $semesterRangeService;

    public function __construct()
    {
        // Initialize the database connection in the constructor
        $this->db = \Config\Database::connect();
        $this->configModel = new ConfigurationModel();
        $this->attendanceDataModel = new AttendanceDataModel();
        $this->studentModel = new StudentModel();
        $this->emergencyContactModel = new EmergencyContactModel();
        $this->userModel = new UserModel();
        $this->teacherClassSection = new TeacherClassModel();
        $this->attendanceRecordModel = new AttendanceRecordModel();
        $this->classSectionModel = new ClassSectionModel();
        $this->studentClassModel = new StudentClassModel();
        $this->attendanceDayModel = new AttendanceDayModel();
        $this->staffAttendanceModel = new StaffAttendanceModel();
        $this->calendarModel       = model(CalendarModel::class); 
        $this->userRoleModel       = new UserRoleModel();
        $this->semesterScoreService = service('semesterScoreService');
        $this->semester = $this->configModel->getConfig('semester');
        $this->schoolYear = $this->currentSchoolYearName();
        $this->enableAttendance = $this->configModel->getConfig('enable_attendance');
        $this->semesterRangeService = new SemesterRangeService($this->configModel);
    }

    private function currentSchoolYearName(): string
    {
        try {
            return service('schoolYearContext')->resolve(service('request'))->yearName();
        } catch (\Throwable $e) {
            return (string) ($this->configModel->getConfig('school_year') ?? '');
        }
    }


    /**
     * GET: filter + grid for a single date/section.
     */
    public function index()
    {
        $semester   = (string)($this->semester   ?? ($this->request->getGet('semester')    ?: 'Fall'));
        $schoolYear = (string)($this->schoolYear ?? ($this->request->getGet('school_year') ?: date('Y') . '-' . (date('Y') + 1)));
        $date       = $this->request->getGet('date') ?: local_date(utc_now(), 'Y-m-d');

        // IMPORTANT: this is the SECTION *CODE* (classSection.class_section_id)
        $sectionCode = (int)($this->request->getGet('class_section_id') ?? 0);

        // 1) Build the sections dropdown: code -> label (join by CODE, not PK)
        // 1) which sections have assignments this term?
        $bySection   = $this->teacherClassSection->assignedBySectionForTerm($semester, $schoolYear);
        $sectionCodes = array_keys($bySection);

        // 2) fetch labels (names) in one query
        $labels = [];
        if (!empty($sectionCodes)) {
            $labelRows = $this->classSectionModel
                ->select('class_section_id, class_section_name')
                ->whereIn('class_section_id', $sectionCodes)
                ->orderBy('class_section_id', 'ASC')
                ->findAll();

            foreach ($labelRows as $r) {
                $code = (int)$r['class_section_id'];
                $name = trim((string)$r['class_section_name']);
                $labels[$code] = ($name !== '') ? $name : ('Section #' . $code);
            }

            // ensure any missing codes still have a label
            foreach ($sectionCodes as $code) {
                if (!isset($labels[$code])) $labels[$code] = 'Section #' . (int)$code;
            }
        }


        // 2) Grid (for the chosen section code & date)
        $grid   = [];
        $locked = false;

        if ($sectionCode > 0) {
            // a) Assigned teachers/TAs for this section & term
            $assigned = $this->teacherClassSection
                ->assignedForSectionTerm($sectionCode /* or $class_section_id */, $semester, $schoolYear);


            // b) Pull statuses for those teachers from staff_attendance for THIS date
            $teacherIds = array_map(static fn($r) => (int)$r['teacher_id'], $assigned);
            $statusMap = []; // [user_id] => ['status'=>..., 'reason'=>...]
            if (!empty($teacherIds)) {
                $rows = $this->db->table('staff_attendance sa')
                    ->select('sa.user_id, sa.status, sa.reason')
                    ->where('sa.semester', $semester)
                    ->where('sa.school_year', $schoolYear)
                    ->where('sa.date', $date)
                    ->whereIn('sa.user_id', $teacherIds)
                    ->get()->getResultArray();

                foreach ($rows as $r) {
                    $statusMap[(int)$r['user_id']] = [
                        'status' => $r['status'] ?? null,
                        'reason' => $r['reason'] ?? null,
                    ];
                }
            }

            // c) Build grid rows expected by the view (one row per assigned teacher/TA)
            foreach ($assigned as $t) {
                $tid    = (int)$t['teacher_id'];
                $posRaw = strtolower((string)($t['position'] ?? 'main'));
                $pos    = in_array($posRaw, ['main', 'ta'], true) ? $posRaw : 'main';
                $name   = trim(($t['firstname'] ?? '') . ' ' . ($t['lastname'] ?? '')) ?: ('User#' . $tid);

                $cell   = $statusMap[$tid] ?? ['status' => null, 'reason' => null];

                $grid[] = [
                    'teacher_id' => $tid,
                    'name'       => $name,
                    'position'   => $pos,                // 'main' | 'ta'
                    'status'     => $cell['status'],     // 'present' | 'absent' | 'late' | null
                    'reason'     => $cell['reason'],     // nullable
                ];
            }

            // d) Lock check keyed by section CODE
            try {
                $locked = $this->attendanceDayModel->isFinalized($sectionCode, $date, $semester, $schoolYear);
            } catch (\Throwable $e) {
                $locked = false;
            }
        }

        return view('attendance/teacher_attendance_form', [
            'semester'         => $semester,
            'schoolYear'       => $schoolYear,
            'date'             => $date,
            // Keep the GET param name as-is; it represents the SECTION CODE
            'class_section_id' => $sectionCode,
            'sections'         => $labels,  // assoc: [sectionCode => label]
            'grid'             => $grid,
            'locked'           => $locked,
        ]);
    }

/**
 * Show Update Attendance (students + staff) with No-School integration
 */
public function showUpdateAttendanceForm()
{
    $user_id = (int)(session()->get('user_id') ?? 0);
    if ($user_id <= 0) {
        echo "User not logged in.";
        return;
    }

    $teacher = $this->userModel->find($user_id);
    $teacher_name = $teacher ? ($teacher['firstname'] . ' ' . $teacher['lastname']) : 'Unknown Teacher';

    // Resolve class_section_id for this user (respect active selection)
    $assignments = $this->teacherClassSection->getClassAssignmentsByUserId(
        $user_id,
        (string)$this->schoolYear,
        (string)$this->semester
    );

    if (empty($assignments)) {
        log_message('info', 'No classes found for teacher ID: ' . $user_id);
        return redirect()->to('no-classes')
            ->with('message', 'You do not have an assigned class yet. Please contact the administration.');
    }
    // Resolve active section (supports both section code and PK for robustness)
    $requestedIdRaw = $this->request->getGet('class_section_id') ?? '';
    $requestedId    = is_numeric($requestedIdRaw) ? (int)$requestedIdRaw : null;
    $sessionId      = (int)(session()->get('class_section_id') ?? 0);

    $activeSection = null;
    foreach ($assignments as $a) {
        $cid = (int)($a['class_section_id'] ?? 0);
        $pk  = (int)($a['class_section_pk'] ?? 0);
        if ($requestedId && ($requestedId === $cid || $requestedId === $pk)) {
            $activeSection = $a;
            break;
        }
        if ($sessionId && ($sessionId === $cid || $sessionId === $pk)) {
            $activeSection = $a;
        }
    }
    if ($activeSection === null) {
        $activeSection = $assignments[0];
    }

    $classSectionCode = (int)($activeSection['class_section_id'] ?? 0); // section code
    $classSectionPk   = (int)($activeSection['class_section_pk'] ?? 0); // DB PK

    // prefer the code for queries; fall back to PK only if missing
    $class_section_id = $classSectionCode > 0 ? $classSectionCode : $classSectionPk;

    if ($class_section_id <= 0) {
        return redirect()->to('no-classes')
            ->with('message', 'Unable to determine your class section. Please contact administration.');
    }

    // Persist selection for navbar-driven navigation
    session()->set('class_section_id', $class_section_id);

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

    $today = new \DateTimeImmutable(local_date(utc_now(), 'Y-m-d'));
    // "Last Sunday" definition:
    // - If today is Sunday => use today
    // - Otherwise => use the next Sunday
    $weekday = (int)$today->format('w'); // 0 (Sun) .. 6 (Sat)
    $anchorSunday = ($weekday === 0) ? $today : $today->modify('next sunday');

    // Build the last 3 Sundays in chronological order (oldest → newest)
    $sundayDates = [
        $anchorSunday->modify('-2 weeks')->format('Y-m-d'),
        $anchorSunday->modify('-1 weeks')->format('Y-m-d'),
        $anchorSunday->format('Y-m-d'),
    ];
    $currentSunday = $sundayDates[2];

    // Students roster for this section/year/semester (try section code, then PK fallback)
    $students = $this->studentModel->getByClassAndYear($class_section_id, $schoolYear, $semester);
    if (empty($students) && $classSectionPk > 0 && $classSectionPk !== $class_section_id) {
        $students = $this->studentModel->getByClassAndYear($classSectionPk, $schoolYear, $semester);
    }

    // Build student attendance rows
    $attendanceData = [];
    $lockedByStudent = [];
    foreach ($students as $student) {
        $sid = (int)$student['id'];
        // Pull attendance strictly for this section; fallback to PK only if none exists
        $rowsQuery = $this->attendanceDataModel
            ->select('date, status, is_reported, reason, modified_by')
            ->where('student_id', $sid)
            ->where('school_year', $schoolYear)
            ->where('semester', $semester)
            ->groupStart()
                ->where('class_section_id', $class_section_id)
                ->orWhere('class_section_id', (int)$class_section_id); // tolerate PK/code mismatches

        $rows = $rowsQuery
            ->groupEnd()
            ->whereIn('date', $sundayDates)
            ->findAll();

        // Normalize status/is_reported so comparisons like === 'late' always work
        $rows = $this->normalizeAttendanceEntries($rows);

        if (empty($rows) && $classSectionPk > 0 && $classSectionPk !== $class_section_id) {
            $rows = $this->attendanceDataModel
                ->select('date, status, is_reported, reason, modified_by')
                ->where('student_id', $sid)
                ->where('school_year', $schoolYear)
                ->where('semester', $semester)
                ->where('class_section_id', $classSectionPk)
                ->whereIn('date', $sundayDates)
                ->findAll();
            $rows = $this->normalizeAttendanceEntries($rows);
        }

        // Grab the latest 3 attendance days for quick context beside today's selector
        $recentRows = $this->attendanceDataModel->builder()
            ->select('date, status')
            ->where('student_id', $sid)
            ->where('school_year', $schoolYear)
            ->where('semester', $semester)
            ->groupStart()
                ->where('class_section_id', $class_section_id)
                ->orWhere('class_section_id', (int)$class_section_id) // tolerate PK/code mismatches
            ->groupEnd()
            ->orderBy('date', 'DESC')
            ->limit(3)
            ->get()
            ->getResultArray();

        if (empty($recentRows) && $classSectionPk > 0 && $classSectionPk !== $class_section_id) {
            $recentRows = $this->attendanceDataModel->builder()
                ->select('date, status')
                ->where('student_id', $sid)
                ->where('school_year', $schoolYear)
                ->where('semester', $semester)
                ->where('class_section_id', $classSectionPk)
                ->orderBy('date', 'DESC')
                ->limit(3)
                ->get()
                ->getResultArray();
        }
        $recentHistory = [];
        foreach ($recentRows as $rr) {
            $recentHistory[] = [
                'date'   => substr((string)($rr['date'] ?? ''), 0, 10),
                'status' => strtolower((string)($rr['status'] ?? '')),
            ];
        }

        $snapshot = [];
        $rowsByDate = [];
        foreach ($rows as $entry) {
            $d = (string)($entry['date'] ?? '');
            $snapshot[$d]  = $entry['status'] ?? null;
            $rowsByDate[$d] = $entry;
        }

        $statusHistory = [];
        foreach ($sundayDates as $date) {
            $statusHistory[$date] = array_key_exists($date, $snapshot)
                ? $snapshot[$date]
                : ($date === $currentSunday ? null : 'N/A');
        }

        $todayRow   = $rowsByDate[$currentSunday] ?? null;
        $lockInfo   = $this->detectExternalSubmission($todayRow);
        $todayReason = $todayRow['reason'] ?? null;

        $attendanceData[] = [
            'student' => $student,
            'sunday_statuses' => $statusHistory,
            'today' => $statusHistory[$currentSunday] ?? null,
            'today_reason' => $todayReason,
            'locked' => $lockInfo['locked'],
            'locked_source' => $lockInfo['source'],
            'recent_three' => $recentHistory,
        ];

        $lockedByStudent[$sid] = [
            'locked'   => $lockInfo['locked'],
            'source'   => $lockInfo['source'],
            'reason'   => $todayReason,
            'status'   => $statusHistory[$currentSunday] ?? null,
        ];
    }

    // 🔹 Build TEACHER / TA rows from unified staff_attendance
    $assigned = $this->teacherClassSection
        ->assignedForSectionTerm($class_section_id, $semester, $schoolYear);

    $teacherRows = [];
    if (!empty($assigned)) {
        $wantedDates = array_values(array_filter($sundayDates));
        $teacherIds = array_map(static fn($r) => (int)$r['teacher_id'], $assigned);

        $statusMap = []; // [user_id][date] => ['status'=>..., 'reason'=>...]
        if (!empty($teacherIds) && !empty($wantedDates)) {
            $rows = $this->db->table('staff_attendance sa')
                ->select('sa.user_id, sa.date, sa.status, sa.reason')
                ->where('sa.semester', $semester)
                ->where('sa.school_year', $schoolYear)
                ->whereIn('sa.user_id', $teacherIds)
                ->whereIn('sa.date', $wantedDates)
                ->get()->getResultArray();

            foreach ($rows as $r) {
                $tid = (int)$r['user_id'];
                $d   = (string)$r['date'];
                $statusMap[$tid][$d] = [
                    'status' => $r['status'] ?? null,
                    'reason' => $r['reason'] ?? null,
                ];
            }
        }

        // Fetch last 3 attendance entries per teacher for quick context
        $recentByTeacher = [];
        if (!empty($teacherIds)) {
            $recentRows = $this->db->table('staff_attendance sa')
                ->select('sa.user_id, sa.date, sa.status')
                ->where('sa.semester', $semester)
                ->where('sa.school_year', $schoolYear)
                ->whereIn('sa.user_id', $teacherIds)
                ->orderBy('sa.user_id', 'ASC')
                ->orderBy('sa.date', 'DESC')
                ->get()->getResultArray();

            $counts = [];
            foreach ($recentRows as $r) {
                $uid = (int)($r['user_id'] ?? 0);
                if ($uid <= 0) continue;
                $counts[$uid] = ($counts[$uid] ?? 0) + 1;
                if ($counts[$uid] > 3) continue;
                $recentByTeacher[$uid][] = [
                    'date'   => substr((string)($r['date'] ?? ''), 0, 10),
                    'status' => strtolower((string)($r['status'] ?? '')),
                ];
            }
        }

        foreach ($assigned as $t) {
            $tid  = (int)$t['teacher_id'];
            $pos  = strtolower((string)($t['position'] ?? 'main'));
            $pos  = in_array($pos, ['main', 'ta'], true) ? $pos : 'main';
            $full = trim(($t['firstname'] ?? '') . ' ' . ($t['lastname'] ?? ''));
            $name = $full !== '' ? $full : ('User#' . $tid);

            $history = [];
            foreach ($sundayDates as $date) {
                $history[$date] = isset($statusMap[$tid][$date]['status'])
                    ? $statusMap[$tid][$date]['status']
                    : ($date === $currentSunday ? null : 'N/A');
            }

            $teacherRows[] = [
                'teacher_id' => $tid,
                'position'   => $pos,
                'name'       => $name,
                'sunday_statuses' => $history,
                'today' => $history[$currentSunday] ?? null,
                'recent_three' => $recentByTeacher[$tid] ?? [],
            ];
        }
    }

    // 🔹 Fetch No-School events for the Sunday columns
    $wantedDates = array_values(array_filter($sundayDates));

    $noSchoolDays = [];
    if (!empty($wantedDates)) {
        $rows = $this->calendarModel
            ->where('no_school', 1)
            ->whereIn('date', $wantedDates)
            // If your calendar isn’t term-scoped, remove these 2 lines:
            ->where('semester', $semester)
            ->where('school_year', $schoolYear)
            ->findAll();

        foreach ($rows as $r) {
            $d = (string)$r['date'];
            $noSchoolDays[$d] = [
                'title'       => $r['title'] ?? 'No School',
                'description' => $r['description'] ?? '',
            ];
        }
    }

    $data = [
        'teacher_name'     => $teacher_name,
        'students'         => $attendanceData,
        'teachers'         => $teacherRows,
        'class_section_id' => $class_section_id,
        'sunday_dates'     => $sundayDates,
        'current_sunday'   => $currentSunday,
        'enableAttendance' => $this->enableAttendance,
        'canEdit'          => ((int)$this->enableAttendance === 1),
        'class_id'         => ((int)$this->classSectionModel->getClassId($class_section_id)),
        'no_school_days'   => $noSchoolDays,
        'today'            => $currentSunday,
        'locked_by_student' => $lockedByStudent,
    ];

    return view('/teacher/showupdate_attendance', $data);
}



    public function noClasses()
    {
        return view('/teacher/no_classes');
    }

    // This method renders the attendance page with the buttons and the student list
    public function listAttendance()
    {
        $data = $this->buildDailyAttendanceData();
        return view('administrator/daily_attendance', $data);
    }

    public function dailyAttendanceAnalysis()
    {
        $data = $this->buildDailyAttendanceData();
        return view('administrator/daily_attendance_analysis', $data);
    }

    private function buildDailyAttendanceData(): array
    {
        // Current term (allow override via GET; default to configured/current)
        $termSemester = (string)($this->request->getGet('semester')    ?? $this->semester   ?? '');
        $termYear     = (string)($this->request->getGet('school_year') ?? $this->schoolYear ?? '');
        if ($termSemester === '' && $this->semester) {
            $termSemester = (string)$this->semester;
        }
        if ($termYear === '' && $this->schoolYear) {
            $termYear = (string)$this->schoolYear;
        }

        // Fetch only sections that actually have students in the selected term.
        // If there are no assignments yet for the requested semester (e.g., Spring roster not populated),
        // fall back to the school year only so the roster still renders and can be used for the new term.
        $classSections = $this->classSectionModel
            ->select('classSection.id, classSection.class_id, classSection.class_section_id, classSection.class_section_name')
            ->join('student_class sc', 'sc.class_section_id = classSection.class_section_id', 'inner')
            ->where('sc.school_year', $termYear)
            ->groupBy(['classSection.id', 'classSection.class_id', 'classSection.class_section_id', 'classSection.class_section_name'])
            ->findAll();

        $useRosterFallback = false;
        if (empty($classSections) && $termYear !== '') {
            $classSections = $this->classSectionModel
                ->select('classSection.id, classSection.class_id, classSection.class_section_id, classSection.class_section_name')
                ->join('student_class sc', 'sc.class_section_id = classSection.class_section_id', 'inner')
                ->where('sc.school_year', $termYear)
                ->groupBy(['classSection.id', 'classSection.class_id', 'classSection.class_section_id', 'classSection.class_section_name'])
                ->findAll();
            $useRosterFallback = !empty($classSections);
        }

        $attendanceData     = [];
        $attendanceRecord   = [];
        $studentsBySection  = [];
        $grades             = [];
        $datesBySection     = [];
        $studentSectionMap  = [];
        $studentSchoolMap   = [];

        $sectionCounts = $this->studentClassModel->getStudentCountsBySection($termYear);

        foreach ($classSections as $classSection) {
            $secPk   = (int)($classSection['id'] ?? 0);
            $secCodeRaw = (string)($classSection['class_section_id'] ?? '');
            $secCode = $secCodeRaw !== '' ? $secCodeRaw : (string)$secPk; // preserve non-numeric codes (e.g., "Arabic-1")
            $classId = (int)$classSection['class_id'];

            $studentsBySection[$secCode] = [];
            $datesBySection[$secCode] = [];
            $attendanceData[$secCode]    = [];
            $attendanceRecord[$secCode]  = [];

            // Keep grade grouping only if section has students
            // Students may be stored against either section code or PK; try both
            $countForCode = (int)($sectionCounts[$secCode] ?? 0);
            $countForPk   = (int)($sectionCounts[$secPk]   ?? 0);
            $sectionHasStudents = ($countForCode + $countForPk) > 0;

            $students = $this->studentClassModel->getClassStudents($secCode, $termYear);
            if (!$students && $secPk && (string)$secPk !== (string)$secCode) {
                $students = $this->studentClassModel->getClassStudents($secPk, $termYear);
            }

            if (!$sectionHasStudents || !$students) {
                continue;
            }

            $hasRoster = false;

            foreach ($students as $sc) {
                $studentId = (int)$sc['student_id'];
                $student   = $this->studentModel
                    ->select('id, firstname, lastname, school_id')
                    ->where('id', $studentId)
                    ->where('is_active', 1)
                    ->first();
                if (!$student) continue;

                $studentsBySection[$secCode][] = $student;
                $hasRoster = true;
                $studentSectionMap[$studentId] = $secCode;
                $studentSchoolMap[$studentId] = (string)($student['school_id'] ?? '');

                // ---------- ATTENDANCE DATA (history) ----------
                $qb = $this->attendanceDataModel
                    ->where('student_id', $studentId)
                    ->groupStart()
                    ->where('class_section_id', $secCode);
                // Fallback: historical rows may have used the PK instead of the code
                if ($secPk && (string)$secPk !== (string)$secCode) {
                    $qb->orWhere('class_section_id', $secPk);
                }
                $qb->groupEnd();

                // Apply term filters only if present (otherwise show all history)
                if ($termSemester !== '') $qb->where('semester', $termSemester);
                if ($termYear     !== '') $qb->where('school_year', $termYear);

                $entries = $qb->orderBy('date', 'asc')->findAll();
                $entries = $this->normalizeAttendanceEntries($entries);
                $attendanceData[$secCode][$studentId] = $entries;

                // ---------- ATTENDANCE SUMMARY ----------
                $qbSum = $this->attendanceRecordModel
                    ->where('student_id', $studentId)
                    ->groupStart()
                    ->where('class_section_id', $secCode);
                if ($secPk && (string)$secPk !== (string)$secCode) {
                    $qbSum->orWhere('class_section_id', $secPk);
                }
                $qbSum->groupEnd();

                if ($termSemester !== '') $qbSum->where('semester', $termSemester);
                if ($termYear     !== '') $qbSum->where('school_year', $termYear);

                $summary = $qbSum->first();

                $attendanceRecord[$secCode][$studentId] = $summary ?? [
                    'total_presence'   => 0,
                    'total_late'       => 0,
                    'total_absence'    => 0,
                    'total_attendance' => 0,
                ];
            }
            if (!$hasRoster) {
                unset($studentsBySection[$secCode], $datesBySection[$secCode], $attendanceData[$secCode], $attendanceRecord[$secCode]);
                continue;
            }
            $grades[$classId][] = $classSection;
        }

        // ------ Merge any parent-submitted reports that are missing from attendance_data -------
        if (!empty($studentSectionMap)) {
            $builder = $this->db->table('parent_attendance_reports par')
                ->select([
                    'par.student_id',
                    'par.class_section_id',
                    'par.report_date',
                    'par.type',
                    'par.reason',
                    'par.arrival_time',
                    'par.dismiss_time',
                ])
                ->whereIn('par.student_id', array_keys($studentSectionMap))
                ->orderBy('par.report_date', 'ASC')
                // Only pull absence/late; early dismissals are managed on the dedicated page.
                ->whereIn('par.type', ['absent', 'late']);

            if ($termYear !== '') {
                $builder->where('par.school_year', $termYear);
            }
            if ($termSemester !== '') {
                $builder->where('par.semester', $termSemester);
            }

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

            foreach ($parentReports as $report) {
                $studentId  = (int) ($report['student_id'] ?? 0);
                $reportDate = substr((string) ($report['report_date'] ?? ''), 0, 10);
                if ($studentId <= 0 || $reportDate === '') {
                    continue;
                }
                $sectionIdRaw = $studentSectionMap[$studentId] ?? ($report['class_section_id'] ?? '');
                $sectionKey = null;
                if ($sectionIdRaw !== '' && isset($studentsBySection[$sectionIdRaw])) {
                    $sectionKey = $sectionIdRaw;
                } elseif (is_numeric($sectionIdRaw)) {
                    $secInt = (int)$sectionIdRaw;
                    if (isset($studentsBySection[$secInt])) {
                        $sectionKey = $secInt;
                    }
                }
                if ($sectionKey === null) {
                    continue;
                }

                $existingEntries = $attendanceData[$sectionKey][$studentId] ?? [];
                $alreadyHas = false;
                foreach ($existingEntries as $entry) {
                    if (($entry['date'] ?? '') === $reportDate) {
                        $alreadyHas = true;
                        break;
                    }
                }
                if ($alreadyHas) {
                    continue;
                }

                $typeRaw = trim((string) ($report['type'] ?? ''));
                $status = match ($typeRaw) {
                    'absent' => 'absent',
                    'late' => 'late',
                    // Early dismissals should not appear on the daily attendance grid.
                    'early_dismissal' => null,
                    default => null,
                };
                if ($status === null) {
                    continue;
                }

                $label = str_replace('_', ' ', ucfirst($typeRaw !== '' ? $typeRaw : 'report'));
                $reasonParts = ['Parent reported ' . $label];
                if ($typeRaw === 'late' && ($report['arrival_time'] ?? '') !== '') {
                    $reasonParts[] = 'ETA ' . substr((string) $report['arrival_time'], 0, 5);
                }
                if ($typeRaw === 'early_dismissal' && ($report['dismiss_time'] ?? '') !== '') {
                    $reasonParts[] = 'Dismiss ' . substr((string) $report['dismiss_time'], 0, 5);
                }
                $extraReason = trim((string) ($report['reason'] ?? ''));
                if ($extraReason !== '') {
                    $reasonParts[] = $extraReason;
                }
                $reasonText = implode(' — ', $reasonParts);

                $attendanceData[$sectionKey][$studentId][] = [
                    'date' => $reportDate,
                    'status' => $status,
                    'is_reported' => 'yes',
                    'reason' => $reasonText !== '' ? $reasonText : null,
                    'source' => 'parent_report',
                ];
                $datesBySection[$sectionKey][$reportDate] = true;
            }
        }

        foreach ($attendanceData as $secId => &$studentEntries) {
            foreach ($studentEntries as &$entries) {
                usort($entries, static fn($a, $b) => strcmp((string) ($a['date'] ?? ''), (string) ($b['date'] ?? '')));
            }
        }
        unset($studentEntries, $entries);

        foreach ($datesBySection as $sec => $set) {
            $arr = array_keys($set);
            sort($arr, SORT_STRING);
            $datesBySection[$sec] = $arr;
        }

        // Prune grade sections that ended up with no roster (extra safety for UI)
        foreach ($grades as $cid => $sections) {
            $filtered = [];
            foreach ($sections as $section) {
                $secKey = (string)($section['class_section_id'] ?? ($section['id'] ?? ''));
                if ($secKey !== '' && !empty($studentsBySection[$secKey])) {
                    $filtered[] = $section;
                }
            }
            if (empty($filtered)) {
                unset($grades[$cid]);
            } else {
                $grades[$cid] = $filtered;
            }
        }

        // Sort grades (class_id) numerically
        ksort($grades, SORT_NUMERIC);

        $schoolYearForRange = $termYear !== '' ? $termYear : (string)($this->schoolYear ?? '');
        [$rangeStart, $rangeEnd] = $this->semesterRangeService->getSchoolYearRange($schoolYearForRange);
        $semesterNorm = $this->semesterRangeService->normalizeSemester($termSemester);
        if ($semesterNorm !== '' && $schoolYearForRange !== '') {
            $semRange = $this->semesterRangeService->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 {
            $events = $this->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;
        }

        // Total passed attendance days up to this Sunday (inclusive), excluding no-school days.
        $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');

        $passedDatesSet = [];
        if (!empty($dateList) && $anchorSundayYmd !== '') {
            foreach ($dateList as $d) {
                if ($d <= $anchorSundayYmd && empty($noSchoolDays[$d])) {
                    $passedDatesSet[$d] = true;
                }
            }
        }
        $totalPassedDays = count($passedDatesSet);

        if ($totalPassedDays > 0 && $termYear !== '' && $termSemester !== '') {
            foreach ($attendanceData as $secCode => $studentEntries) {
                foreach ($studentEntries as $studentId => $entries) {
                    $statusByDate = [];
                    foreach ($entries as $entry) {
                        $d = substr((string)($entry['date'] ?? ''), 0, 10);
                        if ($d === '' || empty($passedDatesSet[$d])) {
                            continue;
                        }
                        $st = strtolower(trim((string)($entry['status'] ?? '')));
                        if (!in_array($st, ['present', 'absent', 'late'], true)) {
                            continue;
                        }
                        $statusByDate[$d] = $st;
                    }

                    $p = 0;
                    $l = 0;
                    $a = 0;
                    foreach ($statusByDate as $st) {
                        if ($st === 'present') {
                            $p++;
                        } elseif ($st === 'late') {
                            $l++;
                        } elseif ($st === 'absent') {
                            $a++;
                        }
                    }
                    $sum = $p + $l + $a;

                    $record = $attendanceRecord[$secCode][$studentId] ?? [];
                    $storedSum = (int)($record['total_presence'] ?? 0)
                        + (int)($record['total_late'] ?? 0)
                        + (int)($record['total_absence'] ?? 0);

                    if ($storedSum !== $totalPassedDays && $storedSum !== $sum) {
                        $schoolId = $studentSchoolMap[$studentId] ?? '';
                        $updated = $this->upsertAttendanceRecordTotals(
                            $studentId,
                            (string)$secCode,
                            $schoolId,
                            $termSemester,
                            $termYear,
                            $p,
                            $l,
                            $a,
                            $sum
                        );
                        if ($updated) {
                            $attendanceRecord[$secCode][$studentId]['total_presence'] = $p;
                            $attendanceRecord[$secCode][$studentId]['total_late'] = $l;
                            $attendanceRecord[$secCode][$studentId]['total_absence'] = $a;
                            $attendanceRecord[$secCode][$studentId]['total_attendance'] = $sum;
                        }
                    }
                }
            }
        }

        // Resolve current admin full name from users table via session user_id
        $adminName = '';
        try {
            $uid = (int) (session()->get('user_id') ?? 0);
            if ($uid > 0) {
                $u = $this->userModel->select('firstname, lastname')->find($uid);
                if ($u) {
                    $adminName = trim((string)($u['firstname'] ?? '') . ' ' . (string)($u['lastname'] ?? ''));
                }
            }
        } catch (\Throwable $e) { /* ignore */ }

        return [
            'attendanceData'    => $attendanceData,
            'attendanceRecord'  => $attendanceRecord,
            'studentsBySection' => $studentsBySection,
            'grades'            => $grades,
            'datesBySection'    => $datesBySection,
            'dateList'          => $dateList,
            'noSchoolDays'      => $noSchoolDays,
            'totalPassedDays'   => $totalPassedDays ?? 0,
            // Expose current term info for slip printing and UI
            'semester'          => (string)$termSemester,
            'schoolYear'        => (string)$termYear,
            'currentAdminName'  => $adminName,
        ];
    }

    // API: daily attendance data payload (used by admin management view)
    public function dailyData()
    {
        $attendanceData     = [];
        $attendanceRecord   = [];
        $studentsBySection  = [];
        $grades             = [];
        $datesBySection     = [];

        $termSemester = (string)($this->request->getGet('semester')    ?? $this->semester   ?? '');
        $termYear     = (string)($this->request->getGet('school_year') ?? $this->schoolYear ?? '');
        if ($termSemester === '' && $this->semester) {
            $termSemester = (string)$this->semester;
        }
        if ($termYear === '' && $this->schoolYear) {
            $termYear = (string)$this->schoolYear;
        }

        // Fetch only sections that actually have students in the selected term.
        // If no roster exists yet for the requested semester, fall back to school-year-only so the grid still shows students.
        $classSections = $this->classSectionModel
            ->select('classSection.id, classSection.class_id, classSection.class_section_id, classSection.class_section_name')
            ->join('student_class sc', 'sc.class_section_id = classSection.class_section_id', 'inner')
            ->where('sc.school_year', $termYear)
            ->groupBy(['classSection.id', 'classSection.class_id', 'classSection.class_section_id', 'classSection.class_section_name'])
            ->findAll();

        $useRosterFallback = false;
        if (empty($classSections) && $termYear !== '') {
            $classSections = $this->classSectionModel
                ->select('classSection.id, classSection.class_id, classSection.class_section_id, classSection.class_section_name')
                ->join('student_class sc', 'sc.class_section_id = classSection.class_section_id', 'inner')
                ->where('sc.school_year', $termYear)
                ->groupBy(['classSection.id', 'classSection.class_id', 'classSection.class_section_id', 'classSection.class_section_name'])
                ->findAll();
            $useRosterFallback = !empty($classSections);
        }

        $sectionCounts = $this->studentClassModel->getStudentCountsBySection(
            $termYear,
            $useRosterFallback ? null : $termSemester
        );

        foreach ($classSections as $classSection) {
            $secPk   = (int)($classSection['id'] ?? 0);
            $secCodeRaw = (string)($classSection['class_section_id'] ?? '');
            $secCode = $secCodeRaw !== '' ? $secCodeRaw : (string)$secPk;
            $classId = (int)$classSection['class_id'];

            $studentsBySection[$secCode] = [];
            $attendanceData[$secCode]    = [];
            $attendanceRecord[$secCode]  = [];
            $datesBySection[$secCode]    = [];

            $countForCode = (int)($sectionCounts[$secCode] ?? 0);
            $countForPk   = (int)($sectionCounts[$secPk]   ?? 0);
            $sectionHasStudents = ($countForCode + $countForPk) > 0;

            $students = $this->studentClassModel->getClassStudents($secCode, $termYear);
            if (!$students && $secPk && (string)$secPk !== (string)$secCode) {
                $students = $this->studentClassModel->getClassStudents($secPk, $termYear);
            }
            if (!$sectionHasStudents || !$students) {
                continue;
            }

            $hasRoster = false;
            $seenStudents = [];

            foreach ($students as $sc) {
                $studentId = (int)$sc['student_id'];
                if ($studentId <= 0 || isset($seenStudents[$studentId])) {
                    continue;
                }
                $student   = $this->studentModel
                    ->select('id, firstname, lastname, school_id')
                    ->find($studentId);
                if (!$student) continue;

                $studentsBySection[$secCode][] = $student;
                $hasRoster = true;
                $seenStudents[$studentId] = true;

                // Attendance history
                $qb = $this->attendanceDataModel
                    ->where('student_id', $studentId)
                    ->groupStart()
                    ->where('class_section_id', $secCode);
                if ($secPk && (string)$secPk !== (string)$secCode) {
                    $qb->orWhere('class_section_id', $secPk);
                }
                $qb->groupEnd();

                if ($termSemester !== '') $qb->where('semester', $termSemester);
                if ($termYear     !== '') $qb->where('school_year', $termYear);

                $entries = $qb->orderBy('date', 'asc')->findAll();
                $entries = $this->normalizeAttendanceEntries($entries);
                $attendanceData[$secCode][$studentId] = $entries;

                foreach ($entries as $e) {
                    $d = (string)($e['date'] ?? '');
                    if ($d !== '') $datesBySection[$secCode][$d] = true;
                }

                // Attendance summary
                $qbSum = $this->attendanceRecordModel
                    ->where('student_id', $studentId)
                    ->groupStart()
                    ->where('class_section_id', $secCode);
                if ($secPk && (string)$secPk !== (string)$secCode) {
                    $qbSum->orWhere('class_section_id', $secPk);
                }
                $qbSum->groupEnd();

                if ($termSemester !== '') $qbSum->where('semester', $termSemester);
                if ($termYear     !== '') $qbSum->where('school_year', $termYear);

                $summary = $qbSum->first();
                $attendanceRecord[$secCode][$studentId] = $summary ?? [
                    'total_presence'   => 0,
                    'total_late'       => 0,
                    'total_absence'    => 0,
                    'total_attendance' => 0,
                ];
            }
            if (!$hasRoster) {
                unset($studentsBySection[$secCode], $attendanceData[$secCode], $attendanceRecord[$secCode], $datesBySection[$secCode]);
                continue;
            }
            $grades[$classId][] = $classSection;
        }

        // Normalize dates arrays
        foreach ($datesBySection as $sec => $set) {
            $arr = array_keys($set);
            sort($arr, SORT_STRING);
            $datesBySection[$sec] = $arr;
        }

        ksort($grades, SORT_NUMERIC);

        // Prune grade sections that ended up with no roster (extra safety for UI)
        foreach ($grades as $cid => $sections) {
            $filtered = [];
            foreach ($sections as $section) {
                $secKey = (string)($section['class_section_id'] ?? ($section['id'] ?? ''));
                if ($secKey !== '' && !empty($studentsBySection[$secKey])) {
                    $filtered[] = $section;
                }
            }
            if (empty($filtered)) {
                unset($grades[$cid]);
            } else {
                $grades[$cid] = $filtered;
            }
        }

        $payload = [
            'attendanceData'    => $attendanceData,
            'attendanceRecord'  => $attendanceRecord,
            'studentsBySection' => $studentsBySection,
            'grades'            => $grades,
            'datesBySection'    => $datesBySection,
            'semester'          => $termSemester,
            'school_year'       => $termYear,
            'csrfHash'          => csrf_hash(),
        ];

        return $this->response->setJSON($payload);
    }

    /**
     * Normalize attendance rows so status/flags are consistent for comparisons.
     */
    private function normalizeAttendanceEntries(array $entries): array
    {
        foreach ($entries as &$row) {
            $row['status'] = $this->normalizeStatus($row['status'] ?? '');
            $reportedRaw = $row['is_reported'] ?? ($row['reported'] ?? '');
            $row['is_reported'] = $this->normalizeReportedFlag($reportedRaw);
        }
        return $entries;
    }

    private function normalizeStatus(?string $status): string
    {
        $s = strtolower(trim((string)$status));
        return in_array($s, ['present', 'absent', 'late'], true) ? $s : $s;
    }

    private function normalizeReportedFlag($flag): string
    {
        $v = strtolower(trim((string)$flag));
        return in_array($v, ['yes', '1', 'true', 'y', 'on'], true) ? 'yes' : 'no';
    }


    private function updateAttendanceRecord($classSectionId, $studentId, $schoolId, $newStatus, $semester, $schoolYear, $oldStatus)
    {
        $newStatus = strtolower($newStatus);
        $oldStatus = strtolower($oldStatus ?? '');
        $record = $this->attendanceRecordModel->where([
            'student_id' => $studentId,
            'semester'   => $semester,
            'school_year' => $schoolYear
        ])->first();

        if (!$record) return;

        // Initialize counters from record, defaulting to 0 if null
        $total_presence = (int) ($record['total_presence'] ?? 0);
        $total_absence = (int) ($record['total_absence'] ?? 0);
        $total_late = (int) ($record['total_late'] ?? 0);

        // Decrease old status count
        if ($oldStatus === 'present') {
            $total_presence--;
        } elseif ($oldStatus === 'absent') {
            $total_absence--;
        } elseif ($oldStatus === 'late') {
            $total_late--;
        }

        // Increase new status count
        if ($newStatus === 'present') {
            $total_presence++;
        } elseif ($newStatus === 'absent') {
            $total_absence++;
        } elseif ($newStatus === 'late') {
            $total_late++;
        }

        $updates = [];
        $updates['total_presence'] = max(0, $total_presence);
        $updates['total_absence'] = max(0, $total_absence);
        $updates['total_late'] = max(0, $total_late);

        $updates['updated_at'] = utc_now();
        $updates['modified_by'] = session()->get('user_id');

        $this->attendanceRecordModel->update($record['id'], $updates);
    }

    private function upsertAttendanceRecordTotals(
        int $studentId,
        string $classSectionId,
        string $schoolId,
        string $semester,
        string $schoolYear,
        int $totalPresence,
        int $totalLate,
        int $totalAbsence,
        int $totalAttendance
    ): bool {
        if ($semester === '' || $schoolYear === '') {
            return false;
        }

        $record = $this->attendanceRecordModel->where([
            'student_id' => $studentId,
            'semester'   => $semester,
            'school_year' => $schoolYear,
        ])->first();

        $now = utc_now();
        $payload = [
            'total_presence'   => max(0, $totalPresence),
            'total_late'       => max(0, $totalLate),
            'total_absence'    => max(0, $totalAbsence),
            'total_attendance' => max(0, $totalAttendance),
            'updated_at'       => $now,
            'modified_by'      => session()->get('user_id'),
        ];

        if ($record) {
            return (bool) $this->attendanceRecordModel->update((int) $record['id'], $payload);
        }

        $payload['class_section_id'] = (int) $classSectionId;
        $payload['student_id'] = $studentId;
        $payload['school_id'] = $schoolId;
        $payload['semester'] = $semester;
        $payload['school_year'] = $schoolYear;
        $payload['created_at'] = $now;
        return (bool) $this->attendanceRecordModel->insert($payload);
    }

    private function updateAttendanceData($attendanceId, $newStatus, $newReason = null, $reported = null)
    {
        $status   = strtolower((string) $newStatus);

        $data = [
            'status'       => $status,
            'reason'       => ($newReason !== null && $newReason !== '') ? $newReason : null,
            'modified_by'  => (int) (session()->get('user_id') ?? 0),
            'updated_at'   => utc_now(),
        ];

        $reportedLog = 'unchanged';
        if ($reported !== null) {
            $reported = in_array(strtolower((string) $reported), ['yes', 'no'], true) ? strtolower($reported) : 'no';
            $data['is_reported'] = $reported; // ← correct column name
            $reportedLog = $reported;
        }

        try {
            $ok = $this->attendanceDataModel->update($attendanceId, $data);
            if (!$ok) {
                log_message('error', 'Failed to UPDATE attendance_data ID {id}: {errs}', [
                    'id'   => $attendanceId,
                    'errs' => json_encode($this->attendanceDataModel->errors(), JSON_UNESCAPED_SLASHES),
                ]);
                return false;
            }
            log_message('info', "Updated attendance_data ID {$attendanceId} with status={$status}, reported={$reportedLog}.");
            return true;
        } catch (\Throwable $e) {
            log_message('error', "Failed to update attendance_data ID {$attendanceId}: " . $e->getMessage());
            return false;
        }
    }

    protected function insertAttendanceData(
        $classSectionId,
        $studentId,
        $studentSchoolId,
        $status,
        $reason,
        $reported,
        $semester,
        $schoolYear,
        $date,
        $classId
    ) {
        $status   = strtolower((string) $status);
        $reported = in_array(strtolower((string) $reported), ['yes', 'no'], true) ? strtolower($reported) : 'no';

        $data = [
            'class_id'         => (int) $classId,          // ← required by schema
            'class_section_id' => (int) $classSectionId,
            'student_id'       => (int) $studentId,
            'school_id'        => (string) $studentSchoolId,
            'status'           => $status,
            'reason'           => ($reason !== null && $reason !== '') ? $reason : null,
            'is_reported'      => $reported,               // ← correct column name
            'is_notified'      => 'no',                    // default: not notified
            'semester'         => (string) $semester,
            'school_year'      => (string) $schoolYear,
            'date'             => (string) $date,
            'modified_by'      => (int) (session()->get('user_id') ?? 0),
            'created_at'       => utc_now(),
            'updated_at'       => utc_now(),
        ];

        try {
            // pass false to get a boolean result (not insertId)
            $ok = $this->attendanceDataModel->insert($data, false);
            if (!$ok) {
                log_message('error', 'Failed to INSERT attendance_data for student {sid} on {date}: {errs}', [
                    'sid'  => $studentId,
                    'date' => $date,
                    'errs' => json_encode($this->attendanceDataModel->errors(), JSON_UNESCAPED_SLASHES),
                ]);
                return false;
            }
            return true;
        } catch (\Throwable $e) {
            log_message('error', "Insert into attendance_data failed for student_id {$studentId}: " . $e->getMessage());
            return false;
        }
    }

    /**
     * Determine if an attendance row was submitted by a parent or an admin-like user.
     */
    private function detectExternalSubmission(?array $row): array
    {
        if (empty($row) || !is_array($row)) {
            return ['locked' => false, 'source' => null];
        }

        $reportedRaw = strtolower((string)($row['is_reported'] ?? ''));
        $reason      = strtolower((string)($row['reason'] ?? ''));

        $isParent = in_array($reportedRaw, ['yes', '1', 'true', 'y'], true)
            || (strpos($reason, 'parent-reported') !== false)
            || (strpos($reason, 'parent reported') !== false);

        if ($isParent) {
            return ['locked' => true, 'source' => 'parent'];
        }

        $modifierId = (int)($row['modified_by'] ?? 0);
        if ($modifierId > 0 && $this->isAdminLikeUserId($modifierId)) {
            return ['locked' => true, 'source' => 'admin'];
        }

        return ['locked' => false, 'source' => null];
    }

    /**
     * True if the given user ID carries an admin-like role (anything other than teacher/parent/guest/assistant).
     */
    private function isAdminLikeUserId(int $userId): bool
    {
        if ($userId <= 0) {
            return false;
        }
        if (array_key_exists($userId, $this->adminRoleCache)) {
            return (bool)$this->adminRoleCache[$userId];
        }

        try {
            $roles = $this->userRoleModel->getRolesByUserId($userId);
        } catch (\Throwable $e) {
            return $this->adminRoleCache[$userId] = false;
        }

        if (empty($roles)) {
            return $this->adminRoleCache[$userId] = false;
        }

        $normalize = static function ($v) {
            $v = strtolower((string)$v);
            return str_replace([' ', '-'], '_', $v);
        };
        $excluded = ['parent', 'guest', 'teacher', 'teacher_assistant', 'assistant_teacher', 'ta'];

        foreach ($roles as $r) {
            $name = $normalize($r['role_name'] ?? '');
            if ($name === '') {
                continue;
            }
            if (!in_array($name, $excluded, true)) {
                return $this->adminRoleCache[$userId] = true;
            }
        }

        return $this->adminRoleCache[$userId] = false;
    }

    //update attendance Mgnt
    public function updateAttendanceManagement()
    {
        $isAjax = $this->request->isAJAX() || $this->request->getPost('ajax');

        // --- collect input ---
        $classSectionId   = (int)$this->request->getPost('class_section_id');
        $studentId        = (int)$this->request->getPost('student_id');
        $studentSchoolId  = $this->request->getPost('school_id');
        $status           = strtolower((string)$this->request->getPost('status'));
        $date             = $this->request->getPost('date');
        $reason           = $this->request->getPost('reason');
        $classId          = $this->request->getPost('class_id'); // optional

        // reported yes/no (present always no)
        $reportedRaw  = strtolower((string)($this->request->getPost('is_reported') ?? ''));
        $is_reported  = ($status === 'present')
            ? 'no'
            : (in_array($reportedRaw, ['1', 'yes', 'y', 'true', 'on'], true) ? 'yes' : 'no');

        // required fields (DO NOT require class_id)
        if (empty($studentId) || empty($status) || empty($date) || empty($classSectionId)) {
            $msg = 'Required data missing.';
            return $isAjax
                ? $this->response->setStatusCode(400)->setJSON(['ok' => false, 'message' => $msg, 'csrfHash' => csrf_hash()])
                : redirect()->back()->with('status', 'error')->with('message', $msg);
        }

        // ---------- NEW: Policy gate for admins/teachers on the management page ----------
        $semesterIn = (string)($this->request->getPost('semester') ?? '');
        $schoolYearIn = (string)($this->request->getPost('school_year') ?? '');
        $semester   = $semesterIn !== '' ? $semesterIn : (string)($this->semester   ?? '');
        $schoolYear = $schoolYearIn !== '' ? $schoolYearIn : (string)($this->schoolYear ?? '');
        $userId       = (int) (session()->get('user_id') ?? 0);
        $userRoles    = (array) (session()->get('roles') ?? []);
        $permissions  = (array) (session()->get('permissions') ?? []);
        $currentUser  = ['id' => $userId, 'roles' => $userRoles, 'permissions' => $permissions];

        // Load day row (create a synthetic 'draft' if missing so admins can start correcting)
        if (method_exists($this->attendanceDayModel, 'findBySectionDate')) {
            $dayRow = $this->attendanceDayModel->findBySectionDate($classSectionId, $date, $semester, $schoolYear);
        } else {
            $dayRow = $this->db->table('attendance_day')
                ->where('class_section_id', $classSectionId)
                ->where('date', $date)
                ->where('semester', $semester)
                ->where('school_year', $schoolYear)
                ->get()->getFirstRow('array');
        }
        $dayRowForPolicy = $dayRow ?: [
            'status'          => 'draft',
            'class_section_id' => $classSectionId,
            'date'            => $date,
            'semester'        => $semester,
            'school_year'     => $schoolYear,
        ];

        if (!\App\Libraries\AttendancePolicy::canEditDay($dayRowForPolicy, $currentUser)) {
            $msg = 'Attendance is locked for your role.';
            return $isAjax
                ? $this->response->setStatusCode(403)->setJSON(['ok' => false, 'message' => $msg, 'csrfHash' => csrf_hash()])
                : redirect()->back()->with('status', 'error')->with('message', $msg);
        }
        // ---------------------------------------------------------------------------------

        // ensure school_id
        if (empty($studentSchoolId)) {
            $studentRow = $this->studentModel->select('school_id')->find($studentId);
            if (!$studentRow || empty($studentRow['school_id'])) {
                $msg = 'Student school ID not found.';
                return $isAjax
                    ? $this->response->setStatusCode(400)->setJSON(['ok' => false, 'message' => $msg, 'csrfHash' => csrf_hash()])
                    : redirect()->back()->with('status', 'error')->with('message', $msg);
            }
            $studentSchoolId = $studentRow['school_id'];
        }

        // existing row?
        $existing = $this->attendanceDataModel->where([
            'student_id'  => $studentId,
            'date'        => $date,
            'semester'    => $semester,
            'school_year' => $schoolYear,
        ])->first();
        if (!$existing && $date !== '') {
            $existing = $this->attendanceDataModel->builder()
                ->where('student_id', $studentId)
                ->where('semester', $semester)
                ->where('school_year', $schoolYear)
                ->where("DATE(`date`)", $date, false)
                ->get()->getFirstRow('array');
        }
        if (!$existing && $studentSchoolId && $date !== '') {
            $existing = $this->attendanceDataModel->builder()
                ->where('school_id', $studentSchoolId)
                ->where('semester', $semester)
                ->where('school_year', $schoolYear)
                ->where("DATE(`date`)", $date, false)
                ->get()->getFirstRow('array');
        }
        if (!$existing && $date !== '') {
            $existing = $this->attendanceDataModel->builder()
                ->where('student_id', $studentId)
                ->where("DATE(`date`)", $date, false)
                ->get()->getFirstRow('array');
        }
        if (!$existing && $studentSchoolId && $date !== '') {
            $existing = $this->attendanceDataModel->builder()
                ->where('school_id', $studentSchoolId)
                ->where("DATE(`date`)", $date, false)
                ->get()->getFirstRow('array');
        }

        $now = utc_now();

        $row = [
            'class_section_id' => $classSectionId,
            'student_id'       => $studentId,
            'school_id'        => $studentSchoolId,
            'status'           => $status,
            'reason'           => $reason ?: null,
            'semester'         => $semester,
            'school_year'      => $schoolYear,
            'date'             => $date,
            'modified_by'      => $userId,
            'updated_at'       => $now,
            'class_id'         => $classId,
        ];
        $hasIsReported = $this->db->fieldExists('is_reported', 'attendance_data');
        $hasLegacyReported = $this->db->fieldExists('reported', 'attendance_data');
        $hasIsNotified = $this->db->fieldExists('is_notified', 'attendance_data');
        if ($hasIsReported) {
            $row['is_reported'] = $is_reported;
        }
        if ($hasLegacyReported) {
            $row['reported'] = ($is_reported === 'yes') ? 1 : 0;
        }
        if ($hasIsNotified) {
            $row['is_notified'] = 'no';
        }

        try {
            if ($existing) {
                $row['created_at'] = $existing['created_at'];
                if (!$this->attendanceDataModel->update($existing['id'], $row)) {
                    $errors = $this->attendanceDataModel->errors();
                    $msg    = 'Failed to update attendance: ' . json_encode($errors);
                    log_message('error', $msg);
                    return $isAjax
                        ? $this->response->setStatusCode(422)->setJSON(['ok' => false, 'message' => $msg, 'csrfHash' => csrf_hash()])
                        : redirect()->back()->with('status', 'error')->with('message', $msg);
                }

                if ($status !== $existing['status']) {
                    try {
                        $this->updateAttendanceRecord(
                            $classSectionId,
                            $studentId,
                            $studentSchoolId,
                            $status,
                            $semester,
                            $schoolYear,
                            $existing['status']
                        );
                    } catch (\Throwable $e) {
                        log_message('error', 'updateAttendanceRecord error: ' . $e->getMessage());
                    }
                }
            } else {
                $row['created_at'] = $now;
                if (!$this->attendanceDataModel->insert($row)) {
                    $errors = $this->attendanceDataModel->errors();
                    $msg    = 'Failed to save attendance: ' . json_encode($errors);
                    log_message('error', $msg);
                    return $isAjax
                        ? $this->response->setStatusCode(422)->setJSON(['ok' => false, 'message' => $msg, 'csrfHash' => csrf_hash()])
                        : redirect()->back()->with('status', 'error')->with('message', $msg);
                }

                try {
                    $this->addNewAttendanceRecord(
                        $classSectionId,
                        $studentId,
                        $studentSchoolId,
                        $status,
                        $semester,
                        $schoolYear
                    );
                } catch (\Throwable $e) {
                    log_message('error', 'addNewAttendanceRecord error: ' . $e->getMessage());
                }
            }

            // (Optional) ensure there is an anchor day row for this edit, so later publish works
            if (!$dayRow) {
                // create draft anchor if missing
                $this->db->table('attendance_day')->insert([
                    'class_section_id' => $classSectionId,
                    'date'             => $date,
                    'status'           => 'draft',
                    'semester'         => $semester,
                    'school_year'      => $schoolYear,
                    'created_at'       => $now,
                    'updated_at'       => $now,
                ]);
            }

            // optional score updates (never bubble)
            try {
                $studentUserInfo = $this->studentModel->getStudentInfoByClassSectionId(
                    $classSectionId,
                    $semester,
                    $schoolYear
                );
                $this->semesterScoreService->updateScoresForStudents($studentUserInfo);
            } catch (\Throwable $e) {
                log_message('error', 'semesterScoreService->updateScoresForStudents error: ' . $e->getMessage());
            }

            if ($isAjax) {
                return $this->response->setJSON(['ok' => true, 'csrfHash' => csrf_hash()]);
            }

            $params = ['class_section_id' => $classSectionId];
            if (!empty($classId)) $params['class_id'] = $classId;

            $query = http_build_query($params);
            return redirect()->to("/administrator/daily_attendance?" . $query)
                ->with('status', 'success')
                ->with('message', 'Attendance updated successfully.');
        } catch (\Throwable $e) {
            log_message('error', 'updateAttendanceManagement fatal: ' . $e->getMessage());
            $msg = 'Internal error while saving attendance.';
            return $isAjax
                ? $this->response->setStatusCode(500)->setJSON(['ok' => false, 'message' => $msg, 'csrfHash' => csrf_hash()])
                : redirect()->back()->with('status', 'error')->with('message', $msg);
        }
    }

    //attendance update teacher
    public function updateAttendance()
    {
        // 0) Read posted section as STRING (avoid losing leading zeros)
        $rawSection = trim((string) $this->request->getPost('class_section_id'));

        if ($rawSection === '') {
            return redirect()->to('/teacher/showupdate_attendance')
                ->with('status', 'error')
                ->with('message', 'Missing or invalid class section.');
        }

        // Optional: the view also sends class_id; we’ll only use it for logging
        $postedClassId = (int) ($this->request->getPost('class_id') ?? 0);

        // 0.1) Normalize to canonical CODE (classSection.class_section_id) with NO ambiguity:
        // Try by CODE first; ONLY if not found, try by PK id.
        $sectionTbl = $this->db->table('classSection')->select('id, class_id, class_section_id');

        // Try literal code match
        $sectionRow = $sectionTbl->where('class_section_id', $rawSection)->get()->getFirstRow('array');

        // If not found and looks numeric, try numeric code (in case column is INT)
        if (!$sectionRow && ctype_digit($rawSection)) {
            $sectionRow = $this->db->table('classSection')
                ->select('id, class_id, class_section_id')
                ->where('class_section_id', (int) $rawSection)
                ->get()->getFirstRow('array');
        }

        // If still not found and looks numeric, fall back to PRIMARY KEY id
        if (!$sectionRow && ctype_digit($rawSection)) {
            $sectionRow = $this->db->table('classSection')
                ->select('id, class_id, class_section_id')
                ->where('id', (int) $rawSection)
                ->get()->getFirstRow('array');
        }

        if (!$sectionRow) {
            return redirect()->to('/teacher/showupdate_attendance')
                ->with('status', 'error')
                ->with('message', 'Invalid class section. Please refresh and try again.');
        }

        // Canonical identifiers
        $classSectionId = (int) ($sectionRow['class_section_id'] ?? 0); // SECTION CODE
        $classId        = (int) ($sectionRow['class_id'] ?? 0);

        // If the view posted class_id and it disagrees with DB, log it (use DB value)
        if ($postedClassId > 0 && $postedClassId !== $classId) {
            log_message(
                'warning',
                sprintf(
                    'Posted class_id (%d) != DB class_id (%d) for class_section_id=%s; using DB value.',
                    $postedClassId,
                    $classId,
                    (string) $sectionRow['class_section_id']
                )
            );
        }

        $attendanceData = $this->request->getPost('attendance'); // students
        $teachersData   = $this->request->getPost('teachers');   // teachers

        if (empty($attendanceData) || !is_array($attendanceData)) {
            return redirect()->to('/teacher/showupdate_attendance?class_section_id=' . $classSectionId)
                ->with('status', 'error')
                ->with('message', 'No student attendance data provided.');
        }
        if (empty($teachersData) || !is_array($teachersData)) {
            return redirect()->to('/teacher/showupdate_attendance?class_section_id=' . $classSectionId)
                ->with('status', 'error')
                ->with('message', 'No teacher attendance data provided.');
        }

        // Date (safe parse)
        $firstRow   = reset($attendanceData);
        $rawDate    = $this->request->getPost('date') ?: ($firstRow['date'] ?? null);
        try {
            $dt = new \DateTimeImmutable($rawDate ?: 'today');
        } catch (\Throwable $e) {
            $dt = new \DateTimeImmutable('today');
        }
        $attendDate = $dt->format('Y-m-d');

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

        // Existing attendance rows for today (used to lock parent/admin submissions)
        $existingRows = $this->attendanceDataModel
            ->where('class_section_id', $classSectionId)
            ->where('date', $attendDate)
            ->where('semester', $semester)
            ->where('school_year', $schoolYear)
            ->findAll();
        $existingByStudent = [];
        foreach ($existingRows as $r) {
            $existingByStudent[(int)($r['student_id'] ?? 0)] = $r;
        }

        // Current user & roles
        $userId       = (int) (session()->get('user_id') ?? 0);
        $userRoles    = (array) (session()->get('roles') ?? []);
        $permissions  = (array) (session()->get('permissions') ?? []);
        $isTeacher    = in_array('teacher', $userRoles, true);

        // Policy gate
        $dayRow = method_exists($this->attendanceDayModel, 'findBySectionDate')
            ? $this->attendanceDayModel->findBySectionDate($classSectionId, $attendDate, $semester, $schoolYear)
            : $this->db->table('attendance_day')
            ->where('class_section_id', $classSectionId)
            ->where('date', $attendDate)
            ->where('semester', $semester)
            ->where('school_year', $schoolYear)
            ->get()->getFirstRow('array');

        $dayRowForPolicy = $dayRow ?: [
            'status'           => 'draft',
            'class_section_id' => $classSectionId,
            'date'             => $attendDate,
            'semester'         => $semester,
            'school_year'      => $schoolYear,
        ];
        $currentUser = ['id' => $userId, 'roles' => $userRoles, 'permissions' => $permissions];

        if (!\App\Libraries\AttendancePolicy::canEditDay($dayRowForPolicy, $currentUser)) {
            return redirect()->to('/teacher/showupdate_attendance?class_section_id=' . $classSectionId)
                ->with('status', 'error')
                ->with('message', 'Attendance is locked for your role.');
        }

        if ($classId <= 0) {
            return redirect()->to('/teacher/showupdate_attendance?class_section_id=' . $classSectionId)
                ->with('status', 'error')
                ->with('message', 'Cannot resolve class_id for this section. Please contact admin.');
        }

        $allowedStatuses = ['present', 'absent', 'late'];

        // === TEACHERS (assignments from teacher_class; statuses saved to staff_attendance) ===
        $assignedTeachers = $this->teacherClassSection
            ->assignedForSectionTerm($classSectionId, $semester, $schoolYear);

        // ensure teacher statuses present in POST
        $missingTeachers = [];
        foreach ($assignedTeachers as $t) {
            $tid = (int)$t['teacher_id'];
            $sel = $teachersData[$tid]['status'] ?? null;
            if (!$sel || !in_array(strtolower($sel), $allowedStatuses, true)) {
                $missingTeachers[] = trim(($t['firstname'] ?? '') . ' ' . ($t['lastname'] ?? ''));
            }
        }
        if (!empty($missingTeachers)) {
            return redirect()->to('/teacher/showupdate_attendance?class_section_id=' . $classSectionId)
                ->with('status', 'error')
                ->with('message', 'Please fill teacher attendance for: ' . implode(', ', $missingTeachers));
        }

        // Ensure student rows
        $missingStudents = [];
        foreach ($attendanceData as $row) {
            $sid    = (int)($row['student_id'] ?? 0);
            $status = isset($row['status']) ? strtolower(trim((string)$row['status'])) : null;
            if ($sid <= 0 || !$status || !in_array($status, $allowedStatuses, true)) {
                $missingStudents[] = (string)($row['school_id'] ?? ("SID#" . $sid));
            }
        }
        if (!empty($missingStudents)) {
            return redirect()->to('/teacher/showupdate_attendance?class_section_id=' . $classSectionId)
                ->with('status', 'error')
                ->with('message', 'Please fill student attendance for School IDs: ' . implode(', ', $missingStudents));
        }

        // Prevent teachers from changing parent/admin-submitted attendance
        if ($isTeacher) {
            $lockedConflicts = [];
            foreach ($attendanceData as $key => $row) {
                $sid = (int)($row['student_id'] ?? $key);
                if ($sid <= 0) {
                    continue;
                }
                $existing = $existingByStudent[$sid] ?? null;
                $lockInfo = $this->detectExternalSubmission($existing);
                if (!$lockInfo['locked']) {
                    continue;
                }

                $postedStatus   = strtolower(trim((string)($row['status'] ?? '')));
                $existingStatus = strtolower((string)($existing['status'] ?? ''));
                $postedReason   = trim((string)($row['reason'] ?? ''));
                $existingReason = trim((string)($existing['reason'] ?? ''));

                $changedStatus = ($postedStatus !== '' && $existingStatus !== '' && $postedStatus !== $existingStatus);
                $changedReason = ($existingReason !== '' && $postedReason !== '' && $postedReason !== $existingReason);

                if ($changedStatus || $changedReason) {
                    $lockedConflicts[] = (string)($row['school_id'] ?? ("SID#" . $sid));
                }

                // Freeze values to avoid downstream updates even if equal
                $attendanceData[$key]['status'] = $existingStatus !== '' ? $existingStatus : $postedStatus;
                $attendanceData[$key]['reason'] = $existingReason !== '' ? $existingReason : $postedReason;
            }

            if (!empty($lockedConflicts)) {
                return redirect()->to('/teacher/showupdate_attendance?class_section_id=' . $classSectionId)
                    ->with('status', 'error')
                    ->with('message', 'Attendance was submitted by parent/admin for: ' . implode(', ', array_unique($lockedConflicts)) . '. Please contact admin for changes.');
            }
        }

        // Save
        $this->db->transException(true);
        $this->db->transStart();

        try {
            // Ensure draft anchor
            $dayRow = method_exists($this->attendanceDayModel, 'getOrCreateDraft')
                ? $this->attendanceDayModel->getOrCreateDraft($classSectionId, $attendDate, $semester, $schoolYear, $userId)
                : (function () use ($classSectionId, $attendDate, $semester, $schoolYear) {
                    $r = $this->db->table('attendance_day')
                        ->where('class_section_id', $classSectionId)
                        ->where('date', $attendDate)
                        ->where('semester', $semester)
                        ->where('school_year', $schoolYear)
                        ->get()->getFirstRow('array');
                    if ($r) return $r;
                    $now = utc_now();
                    $this->db->table('attendance_day')->insert([
                        'class_section_id' => $classSectionId,
                        'date'             => $attendDate,
                        'status'           => 'draft',
                        'semester'         => $semester,
                        'school_year'      => $schoolYear,
                        'created_at'       => $now,
                        'updated_at'       => $now,
                    ]);
                    return $this->db->table('attendance_day')
                        ->where('class_section_id', $classSectionId)
                        ->where('date', $attendDate)
                        ->where('semester', $semester)
                        ->where('school_year', $schoolYear)
                        ->get()->getFirstRow('array');
                })();

            // 1) TEACHERS: write to staff_attendance
            /** @var \App\Models\StaffAttendanceModel $staffModel */
            $staffModel = $this->staffAttendanceModel;

            foreach ($assignedTeachers as $t) {
                $tid      = (int)$t['teacher_id'];
                $position = strtolower((string)($t['position'] ?? 'main'));
                $position = ($position === 'ta') ? 'ta' : 'main';
                $status   = strtolower(trim((string)($teachersData[$tid]['status'] ?? '')));
                $reason   = trim((string)($teachersData[$tid]['reason'] ?? ''));

                // unified upsert: one row per (user_id, date)
                $ok = $staffModel->upsertCell(
                    userId: $tid,
                    date: $attendDate,
                    status: $status,
                    semester: $semester,
                    schoolYear: $schoolYear,
                    roleName: 'Teacher',            // label; optional
                    classSectionId: $classSectionId,      // scope to this section
                    position: $position,            // main|ta
                    reason: ($reason !== '' ? $reason : null)
                );

                if (!$ok) {
                    throw new \RuntimeException("Failed to save teacher attendance for user #{$tid}");
                }
            }

            // 2) STUDENTS (unchanged logic)
            foreach ($attendanceData as $row) {
                $studentId       = (int) ($row['student_id'] ?? 0);
                $studentSchoolId = isset($row['school_id']) ? trim((string) $row['school_id']) : '';
                $status          = strtolower(trim((string) ($row['status'] ?? '')));
                $reason          = isset($row['reason']) ? trim((string) $row['reason']) : null;

                if ($studentSchoolId === '') {
                    $stuRow = $this->studentModel->select('school_id')->find($studentId);
                    if ($stuRow && !empty($stuRow['school_id'])) $studentSchoolId = $stuRow['school_id'];
                }

                $existing = $existingByStudent[$studentId] ?? null;

                // Teachers never update is_reported; new rows default to 'no'.
                $reported = null;
                if ($isTeacher) {
                    if (!$existing) {
                        $reported = 'no';
                    }
                } else {
                    $reportedRaw = strtolower((string)($row['is_reported'] ?? $row['reported'] ?? ''));
                    $reported = ($status === 'absent' && in_array($reportedRaw, ['1', 'yes', 'y', 'true', 'on'], true)) ? 'yes' : 'no';
                }

                $lockInfo = $this->detectExternalSubmission($existing);
                if ($isTeacher && $lockInfo['locked']) {
                    // Teacher cannot overwrite parent/admin submissions
                    continue;
                }

                if ($existing && ($existing['status'] ?? null) === 'late' && $isTeacher) {
                    // Allow override if the existing LATE was created from a Parent report
                    $exReason = strtolower((string)($existing['reason'] ?? ''));
                    $fromParent = (strpos($exReason, 'parent-reported') !== false);
                    if (!$fromParent) {
                        log_message('debug', "Teacher blocked from modifying LATE entry: student {$studentId} on {$attendDate}");
                        continue;
                    }
                }

                if ($existing) {
                    $this->updateAttendanceData((int)$existing['id'], $status, $reason, $reported);
                    $this->updateAttendanceRecord(
                        $classSectionId,
                        $studentId,
                        $studentSchoolId,
                        $status,
                        $semester,
                        $schoolYear,
                        $existing['status'] ?? null
                    );
                } else {
                    $this->insertAttendanceData(
                        $classSectionId,
                        $studentId,
                        $studentSchoolId,
                        $status,
                        $reason,
                        $reported ?? 'no',
                        $semester,
                        $schoolYear,
                        $attendDate,
                        $classId
                    );
                    $this->addNewAttendanceRecord(
                        $classSectionId,
                        $studentId,
                        $studentSchoolId,
                        $status,
                        $semester,
                        $schoolYear
                    );
                }
            }

            // Final completeness check for teachers (no DB call; we already validated input)
            // If you prefer verifying against DB, uncomment the block below.
            /*
        $countAssigned = count($assignedTeachers);
        $countSaved = $this->db->table('staff_attendance')
            ->where('class_section_id', $classSectionId)
            ->where('date', $attendDate)
            ->where('semester', $semester)
            ->where('school_year', $schoolYear)
            ->whereIn('user_id', array_map(fn($r)=> (int)$r['teacher_id'], $assignedTeachers))
            ->countAllResults();
        if ($countSaved < $countAssigned) {
            throw new \RuntimeException('Teacher attendance incomplete—cannot submit.');
        }
        */

            // Auto-publish at second Sunday after the day (end of day)
            $autoPublishAt = AttendanceAutoPublish::secondSundayAfterEndOfDay($attendDate);

            if (method_exists($this->attendanceDayModel, 'submit')) {
                $this->attendanceDayModel->submit((int)$dayRow['id'], $userId, $autoPublishAt);
            } else {
                $this->db->table('attendance_day')
                    ->where('id', (int)$dayRow['id'])
                    ->update([
                        'status'           => 'submitted',
                        'submitted_by'     => $userId,
                        'submitted_at'     => utc_now(),
                        'auto_publish_at'  => $autoPublishAt,
                        'updated_at'       => utc_now(),
                    ]);
            }

            $this->db->transComplete();

            if ($this->db->transStatus() === false) {
                log_message('error', 'Attendance transaction failed (transStatus=false).');
                return redirect()->to('/teacher/showupdate_attendance?class_section_id=' . $classSectionId)
                    ->with('status', 'error')
                    ->with('message', 'Could not save attendance due to a database error.');
            }

            try {
                $studentUserInfo = $this->studentModel->getStudentInfoByClassSectionId($classSectionId, $this->semester, $this->schoolYear);
                $this->semesterScoreService->updateScoresForStudents($studentUserInfo);
            } catch (\Throwable $e) {
                // ignore
            }

            return redirect()->to('/teacher/showupdate_attendance?class_section_id=' . $classSectionId)
                ->with('status', 'success')
                ->with('message', 'Attendance submitted for ' . date('m-d-Y', strtotime($attendDate)) . '. Admins can still review before publish.');
        } catch (\Throwable $e) {
            $this->db->transRollback();
            log_message('error', 'Attendance save failed: ' . $e->getMessage());
            return redirect()->to('/teacher/showupdate_attendance?class_section_id=' . $classSectionId)
                ->with('status', 'error')
                ->with('message', 'Could not save attendance: ' . $e->getMessage());
        }
    }


    private function addNewAttendanceRecord($classSectionId, $studentId, $schoolId, $status, $semester, $schoolYear)
    {
        $status = strtolower($status);

        $existing = $this->attendanceRecordModel->where([
            'student_id' => $studentId,
            'semester'   => $semester,
            'school_year' => $schoolYear
        ])->first();

        if ($existing) {
            $updates = [
                'total_presence'   => (int)($existing['total_presence'] ?? 0),
                'total_absence'    => (int)($existing['total_absence'] ?? 0),
                'total_late'       => (int)($existing['total_late'] ?? 0),
                'total_attendance' => (int)($existing['total_attendance'] ?? 0) + 1,
                'updated_at'       => utc_now(),
                'modified_by'      => session()->get('user_id'),
            ];
            if ($status === 'present') {
                $updates['total_presence']++;
            } elseif ($status === 'absent') {
                $updates['total_absence']++;
            } elseif ($status === 'late') {
                $updates['total_late']++;
            }
            $this->attendanceRecordModel->update($existing['id'], $updates);
        } else {
            $updates = [
                'class_section_id' => $classSectionId,
                'student_id'       => $studentId,
                'school_id'        => $schoolId,
                'semester'         => $semester,
                'school_year'      => $schoolYear,
                'created_at'       => utc_now(),
                'updated_at'       => utc_now(),
                'modified_by'      => session()->get('user_id'),
                'total_presence'   => ($status === 'present') ? 1 : 0,
                'total_absence'    => ($status === 'absent')  ? 1 : 0,
                'total_late'       => ($status === 'late')    ? 1 : 0,
                'total_attendance' => 1,
            ];
            $this->attendanceRecordModel->insert($updates);
        }
    }

    public function adminAddEntry()
    {
        // POST data
        $classSectionId  = (int) $this->request->getPost('class_section_id');
        $studentId       = (int) $this->request->getPost('student_id');
        $studentSchoolId = trim((string) $this->request->getPost('school_id'));
        $status          = strtolower((string) $this->request->getPost('status')); // present|late|absent
        $date            = $this->request->getPost('date') ?: local_date(utc_now(), 'Y-m-d');
        $reason          = $this->request->getPost('reason');
        $classId         = (int) $this->request->getPost('class_id'); // optional incoming
        $now             = utc_now();
        $userId          = (int) (session()->get('user_id') ?? 0);

        // 🔘 NEW: normalize is_reported (default 'no' if not posted / invalid)
        $isReportedIn = strtolower((string) $this->request->getPost('is_reported'));
        $isReported   = in_array($isReportedIn, ['yes', 'no'], true) ? $isReportedIn : 'no';

        // Validate required
        if ($classSectionId <= 0 || $studentId <= 0 || empty($status) || empty($date)) {
            return redirect()->back()->with('status', 'error')->with('message', 'Required data missing.');
        }
        if (!in_array($status, ['present', 'late', 'absent'], true)) {
            return redirect()->back()->with('status', 'error')->with('message', 'Invalid status.');
        }

        // If school_id not sent, fetch from DB
        if ($studentSchoolId === '') {
            $studentRow = $this->studentModel->select('school_id')->find($studentId);
            if (!$studentRow || empty($studentRow['school_id'])) {
                return redirect()->back()->with('status', 'error')->with('message', 'Student school ID not found.');
            }
            $studentSchoolId = $studentRow['school_id'];
        }

        // 🔎 Resolve class_id for this section
        $resolvedClassId = 0;
        $row = $this->classSectionModel
            ->select('class_id, id, class_section_id')
            ->groupStart()
            ->where('id', $classSectionId)
            ->orWhere('class_section_id', $classSectionId)
            ->groupEnd()
            ->first();

        if ($row && isset($row['class_id'])) {
            $resolvedClassId = (int) $row['class_id'];
        }
        if ($resolvedClassId <= 0) {
            $fallback = $this->db->table('class_section') // change if your table is named differently
                ->select('class_id')
                ->groupStart()
                ->where('id', $classSectionId)
                ->orWhere('class_section_id', $classSectionId)
                ->groupEnd()
                ->get()->getRowArray();
            if ($fallback && isset($fallback['class_id'])) {
                $resolvedClassId = (int) $fallback['class_id'];
            }
        }
        if ($resolvedClassId <= 0) {
            return redirect()->back()->with('status', 'error')->with('message', 'Cannot resolve class_id for this section.');
        }
        if ($classId <= 0) {
            $classId = $resolvedClassId; // for redirect URL
        }

        // 🔐 Day lock
        if ($this->attendanceDayModel->isFinalized($classSectionId, $date)) {
            return redirect()->back()->with('status', 'error')->with('message', 'Day is locked for this class and date.');
        }

        $this->db->transBegin();
        try {
            // Lock/draft row
            $dayRow = $this->attendanceDayModel->getOrCreateDraft(
                $classSectionId,
                $date,
                (string) $this->semester,
                (string) $this->schoolYear,
                $userId
            );

            // Existing row for THIS student/date/term
            $existing = $this->attendanceDataModel->where([
                'student_id'  => $studentId,
                'date'        => $date,
                'semester'    => (string) $this->semester,
                'school_year' => (string) $this->schoolYear,
            ])->first();

            $attendanceData = [
                'class_id'         => $resolvedClassId,
                'class_section_id' => $classSectionId,
                'student_id'       => $studentId,
                'school_id'        => $studentSchoolId,
                'status'           => $status,
                'reason'           => strlen((string)$reason) ? $reason : null,
                'semester'         => (string) $this->semester,
                'school_year'      => (string) $this->schoolYear,
                'date'             => $date,
                'is_reported'      => $isReported,       // 🔘 NEW: always set it
                'is_notified'      => 'no',              // default state for notifications
                'modified_by'      => $userId ?: null,
                'updated_at'       => $now,
            ];

            if ($existing) {
                $oldStatus = $existing['status'] ?? null;
                $attendanceData['created_at'] = $existing['created_at'] ?? null;

                if (!$this->attendanceDataModel->update((int)$existing['id'], $attendanceData)) {
                    $errs = $this->attendanceDataModel->errors();
                    throw new \RuntimeException('Failed to update attendance_data: ' . json_encode($errs, JSON_UNESCAPED_SLASHES));
                }

                if ($oldStatus !== null && $status !== $oldStatus) {
                    $this->updateAttendanceRecord(
                        $classSectionId,
                        $studentId,
                        $studentSchoolId,
                        $status,                    // new
                        (string)$this->semester,
                        (string)$this->schoolYear,
                        $oldStatus                  // old
                    );
                }
            } else {
                $attendanceData['created_at'] = $now;

                if (!$this->attendanceDataModel->insert($attendanceData)) {
                    $errs = $this->attendanceDataModel->errors();
                    throw new \RuntimeException('Failed to insert attendance_data: ' . json_encode($errs, JSON_UNESCAPED_SLASHES));
                }

                $this->addNewAttendanceRecord(
                    $classSectionId,
                    $studentId,
                    $studentSchoolId,
                    $status,
                    (string)$this->semester,
                    (string)$this->schoolYear
                );
            }

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

            return redirect()->to("/administrator/daily_attendance?class_id={$classId}&class_section_id={$classSectionId}")
                ->with('status', 'success')
                ->with('message', 'Entry saved' . ($status === 'late' ? ' and locked for teachers.' : '.'));
        } catch (\Throwable $e) {
            $this->db->transRollback();
            log_message('error', 'adminAddEntry failed: ' . $e->getMessage());
            return redirect()->back()->with('status', 'error')->with('message', 'Failed to add entry: ' . $e->getMessage());
        }
    }

    // app/Controllers/AdminTeacherAttendanceController.php (append inside class)
    // ---------------------------------------------------------------------
    // Monthly view (teachers/TAs + admins)   — uses staff_attendance only
    // ---------------------------------------------------------------------
    public function month()
    {
        // Switch to school-year range (remove month filter)
        $semester   = (string)($this->request->getGet('semester')    ?? ($this->semester   ?? 'Fall'));
        $schoolYear = (string)($this->request->getGet('school_year') ?? ($this->schoolYear ?? (date('Y') . '-' . (date('Y') + 1))));

        // Build school year options from staff_attendance (fallback to configured year)
        try {
            $yearsRows = $this->db->table('staff_attendance')
                ->distinct()
                ->select('school_year')
                ->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)));
            if (empty($schoolYears)) {
                $schoolYears = [(string)$this->schoolYear];
            }
        } catch (\Throwable $e) {
            $schoolYears = [(string)$this->schoolYear];
        }

        // Compute configured school-year date range, then narrow by semester if requested
        [$rangeStart, $rangeEnd] = $this->semesterRangeService->getSchoolYearRange($schoolYear);
        $semesterNorm = $this->semesterRangeService->normalizeSemester($semester);
        $rangeLabel = 'School Year ' . (string)$schoolYear;
        if (in_array($semesterNorm, ['Fall','Spring'], true)) {
            $rangeLabel = ($semesterNorm === 'Fall') ? 'Fall 21/09–18/01' : 'Spring 25/01–31/05';
        }
        if (in_array($semesterNorm, ['Fall', 'Spring'], true)) {
            $semRange = $this->semesterRangeService->getSemesterRange($schoolYear, $semesterNorm);
            if ($semRange) {
                [$rangeStart, $rangeEnd] = $semRange;
                $rangeLabel = ($semesterNorm === 'Fall') ? 'Fall 21/09–18/01' : 'Spring 25/01–31/05';
            }
        }

        $query = http_build_query([
            'semester'    => $semester,
            'school_year' => $schoolYear,
        ]);

        $isCurrentYear = ((string)$schoolYear === (string)$this->schoolYear);
        $missingYear   = empty($this->schoolYear);

        return view('attendance/teacher_attendance_month', [
            'semester'      => $semester,
            'schoolYear'    => $schoolYear,
            'schoolYears'   => $schoolYears,
            'currentYear'   => (string)$this->schoolYear,
            'isCurrentYear' => $isCurrentYear,
            'missingYear'   => $missingYear,
            'rangeStart'    => $rangeStart,
            'rangeEnd'      => $rangeEnd,
            'rangeLabel'    => $rangeLabel,
            // Maintain endpoint without month
            'endpoint'      => site_url('api/admin/teacher-attendance/month' . ($query ? ('?' . $query) : '')),
            'saveEndpoint'  => site_url('admin/teacher-attendance/save-cell'),
        ]);
    }

    public function monthData()
    {
        // Year-range version with optional semester narrowing
        $semester   = (string)($this->request->getGet('semester')    ?? ($this->semester   ?? 'Fall'));
        $schoolYear = (string)($this->request->getGet('school_year') ?? ($this->schoolYear ?? (date('Y') . '-' . (date('Y') + 1))));
        [$rangeStart, $rangeEnd] = $this->semesterRangeService->getSchoolYearRange($schoolYear);
        $semesterNorm = $this->semesterRangeService->normalizeSemester($semester);
        if (in_array($semesterNorm, ['Fall', 'Spring'], true)) {
            $semRange = $this->semesterRangeService->getSemesterRange($schoolYear, $semesterNorm);
            if ($semRange) {
                [$rangeStart, $rangeEnd] = $semRange;
            }
        }
        $payload = $this->buildYearPayload($semesterNorm ?: $semester, $schoolYear, $rangeStart, $rangeEnd);
        return $this->response->setJSON($payload);
    }

    private function resolveMonthFilters(): array
    {
        $semesterParam = trim((string)($this->request->getGet('semester') ?? ''));
        $schoolYearParam = trim((string)($this->request->getGet('school_year') ?? ''));
        $monthParam = trim((string)($this->request->getGet('month') ?? ''));

        $semester = $semesterParam !== '' ? $semesterParam : (string)($this->semester ?? 'Fall');
        $schoolYear = $schoolYearParam !== '' ? $schoolYearParam : (string)($this->schoolYear ?? (date('Y') . '-' . (date('Y') + 1)));

        $month = $monthParam !== '' ? $monthParam : date('Y-m');
        try {
            $dt = new \DateTimeImmutable($month . '-01');
            $month = $dt->format('Y-m');
        } catch (\Throwable $e) {
            $month = date('Y-m');
        }

        return [$semester, $schoolYear, $month];
    }

    private function formatMonthLabel(string $month): string
    {
        try {
            $dt = new \DateTimeImmutable($month . '-01');
            return $dt->format('F Y');
        } catch (\Throwable $e) {
            return date('F Y');
        }
    }

    private function buildYearPayload(string $semester, string $schoolYear, string $rangeStart, string $rangeEnd): array
    {
        try {
            $start = new \DateTimeImmutable($rangeStart);
        } catch (\Throwable $e) {
            $start = new \DateTimeImmutable(date('Y-09-01'));
        }
        try {
            $end = new \DateTimeImmutable($rangeEnd);
        } catch (\Throwable $e) {
            $end = (new \DateTimeImmutable($start->format('Y-m-d')))->modify('+9 months');
        }

        // Build all Sundays within the configured range
        $sundays = [];
        // Find first Sunday on/after start
        $cursor = $start;
        $w = (int)$cursor->format('w');
        if ($w !== 0) {
            $cursor = $cursor->modify('next sunday');
        }
        for (; $cursor <= $end; $cursor = $cursor->modify('+7 days')) {
            $sundays[] = $cursor->format('Y-m-d');
        }

        // Pull No School days from calendar_events for the range
        $noSchoolDays = [];
        try {
            $rows = $this->db->table('calendar_events')
                ->select('date')
                ->where('school_year', $schoolYear)
                ->where('no_school', 1)
                ->where('date >=', $start->format('Y-m-d'))
                ->where('date <=', $end->format('Y-m-d'))
                ->get()->getResultArray();
            foreach ($rows as $r) {
                $d = substr((string)($r['date'] ?? ''), 0, 10);
                if ($d !== '') $noSchoolDays[$d] = true;
            }
        } catch (\Throwable $e) { /* ignore */ }

        // Fallback if DB filters fail or column missing: use CalendarModel heuristics
        if (empty($noSchoolDays)) {
            try {
                /** @var \App\Models\CalendarModel $cal */
                $cal = model(\App\Models\CalendarModel::class);
                $events = (array) ($cal->getEventsBySchoolYear($schoolYear) ?? []);
                foreach ($events as $ev) {
                    $d = substr((string)($ev['date'] ?? ''), 0, 10);
                    $no = (int) ($ev['no_school'] ?? 0);
                    if ($d !== '' && $no === 1 && $d >= $start->format('Y-m-d') && $d <= $end->format('Y-m-d')) {
                        $noSchoolDays[$d] = true;
                    }
                }
            } catch (\Throwable $e) { /* ignore */ }
        }

        $rangeLabel = 'School Year ' . (string)$schoolYear;

        // Prepare base payload
        $payload = [
            'filters' => [
                'semester'     => $semester,
                'school_year'  => $schoolYear,
                'range_label'  => $rangeLabel,
                'range_start'  => $start->format('Y-m-d'),
                'range_end'    => $end->format('Y-m-d'),
                // Backward-compat for existing JS which may read month_label
                'month_label'  => $rangeLabel,
            ],
            'sundays'  => [],
            'noSchoolDays' => [],
            'sections' => [],
            'admins'   => [],
        ];

        // =========================
        // Teacher / TA sections
        // =========================
        $assignedBySection = [];
        try {
            $semesterNorm = $this->semesterRangeService->normalizeSemester($semester);
            if (in_array($semesterNorm, ['Fall','Spring'], true)) {
                $assignedBySection = (array)$this->teacherClassSection->assignedBySectionForTerm($semesterNorm, $schoolYear);
            } else {
                // Whole-year: merge both terms
                $fall  = (array)$this->teacherClassSection->assignedBySectionForTerm('Fall', $schoolYear);
                $spr   = (array)$this->teacherClassSection->assignedBySectionForTerm('Spring', $schoolYear);
                $codes = array_unique(array_merge(array_keys($fall), array_keys($spr)));
                foreach ($codes as $code) {
                    $assignedBySection[$code] = array_merge($fall[$code] ?? [], $spr[$code] ?? []);
                }
            }
        } catch (\Throwable $e) {
            log_message('error', 'buildMonthPayload: failed to load assigned sections - ' . $e->getMessage());
        }

        $sectionCodes = array_keys($assignedBySection);

        $sectionLabels = [];
        if (!empty($sectionCodes)) {
            $labelRows = $this->classSectionModel
                ->select('class_section_id, class_section_name')
                ->whereIn('class_section_id', $sectionCodes)
                ->get()->getResultArray();

            foreach ($labelRows as $row) {
                $code = (int)($row['class_section_id'] ?? 0);
                if ($code <= 0) {
                    continue;
                }
                $name = trim((string)($row['class_section_name'] ?? ''));
                $sectionLabels[$code] = $name !== '' ? $name : ('Section #' . $code);
            }
        }

        $teacherIds = [];
        foreach ($assignedBySection as $sectionRows) {
            foreach ($sectionRows as $row) {
                $tid = (int)($row['teacher_id'] ?? 0);
                if ($tid > 0) {
                    $teacherIds[$tid] = true;
                }
            }
        }

        $teacherIds = array_keys($teacherIds);

        // Use Sundays only for the specified school-year range
        $days = $sundays;

        // Build teacher statuses for the chosen day set
        $statusByTeacher = [];
        if (!empty($teacherIds) && !empty($days)) {
            $qb = $this->db->table('staff_attendance sa')
                ->select('sa.user_id, sa.date, sa.status, sa.reason')
                ->where('sa.school_year', $schoolYear)
                ->whereIn('sa.user_id', $teacherIds)
                ->whereIn('sa.date', $days);
            $semesterNorm = $this->semesterRangeService->normalizeSemester($semester);
            if (in_array($semesterNorm, ['Fall','Spring'], true)) {
                $qb->where('sa.semester', $semesterNorm);
            }
            $statusRows = $qb->get()->getResultArray();

            foreach ($statusRows as $row) {
                $uid  = (int)($row['user_id'] ?? 0);
                $date = (string)($row['date'] ?? '');
                if ($uid <= 0 || $date === '') {
                    continue;
                }
                $statusByTeacher[$uid][$date] = [
                    'status' => strtolower((string)($row['status'] ?? '')),
                    'reason' => $row['reason'] ?? null,
                ];
            }
        }

        // Finalize days and no-school list in payload
        $payload['sundays'] = $days;
        $noSchoolDaysList = [];
        foreach ($days as $d) {
            if (isset($noSchoolDays[$d])) $noSchoolDaysList[] = $d;
        }
        $payload['noSchoolDays'] = $noSchoolDaysList;

        foreach ($sectionCodes as $code) {
            $teacherRows = $assignedBySection[$code] ?? [];
            $teachersPayload = [];

            foreach ($teacherRows as $row) {
                $tid = (int)($row['teacher_id'] ?? 0);
                if ($tid <= 0) {
                    continue;
                }

                $first = trim((string)($row['firstname'] ?? ''));
                $last  = trim((string)($row['lastname'] ?? ''));
                $name  = trim($first . ' ' . $last);
                if ($name === '') {
                    $name = 'User#' . $tid;
                }

                $posRaw   = strtolower((string)($row['position'] ?? 'main'));
                $position = in_array($posRaw, ['main', 'ta'], true) ? $posRaw : 'main';

                $cells = [];
                $totals = ['p' => 0, 'a' => 0, 'l' => 0];
                foreach ($days as $date) {
                    $cell = $statusByTeacher[$tid][$date] ?? null;
                    if (!$cell || empty($cell['status'])) {
                        continue;
                    }
                    $status = (string)$cell['status'];
                    if (!in_array($status, ['present', 'absent', 'late'], true)) {
                        continue;
                    }

                    $cells[$date] = [
                        'status' => $status,
                        'reason' => $cell['reason'] ?? null,
                    ];

                    if ($status === 'present') {
                        $totals['p']++;
                    } elseif ($status === 'absent') {
                        $totals['a']++;
                    } elseif ($status === 'late') {
                        $totals['l']++;
                    }
                }

                $teachersPayload[] = [
                    'teacher_id' => $tid,
                    'user_id'    => $tid,
                    'name'       => $name,
                    'position'   => $position,
                    'cells'      => $cells,
                    'totals'     => $totals,
                ];
            }

            $payload['sections'][] = [
                'section_code' => (int)$code,
                'label'        => $sectionLabels[$code] ?? ('Section #' . (int)$code),
                'teachers'     => $teachersPayload,
            ];
        }

        // =========================
        // Admin roster
        // =========================
        $excludedSlugs = ['parent', 'guest', 'teacher', 'teacher_assistant', 'ta', 'assistant_teacher'];

        $adminCandidates = $this->db->table('users u')
            ->select('u.id AS user_id, u.firstname, u.lastname, r.id AS role_id, r.name AS role_name, r.slug, r.priority')
            ->join('user_roles ur', 'ur.user_id = u.id', 'left')
            ->join('roles r', 'r.id = ur.role_id', 'left')
            ->where('r.is_active', 1)
            ->get()->getResultArray();

        $admins = [];
        foreach ($adminCandidates as $row) {
            $uid  = (int)($row['user_id'] ?? 0);
            $rid  = (int)($row['role_id'] ?? 0);
            $slug = strtolower((string)($row['slug'] ?? $row['role_name'] ?? ''));
            if ($uid <= 0 || $rid <= 0) {
                continue;
            }
            if (in_array($slug, $excludedSlugs, true)) {
                continue;
            }

            $priority = (int)($row['priority'] ?? 100);
            if (!isset($admins[$uid]) || $priority < (int)$admins[$uid]['priority']) {
                $admins[$uid] = [
                    'user_id'   => $uid,
                    'firstname' => $row['firstname'] ?? '',
                    'lastname'  => $row['lastname'] ?? '',
                    'role_name' => $row['role_name'] ?? 'Admin',
                    'priority'  => $priority,
                ];
            }
        }

        $adminIds = array_keys($admins);

        $statusByAdmin = [];
        if (!empty($adminIds) && !empty($sundays)) {
            $qb = $this->db->table('staff_attendance')
                ->select('user_id, date, status, reason')
                ->where('school_year', $schoolYear)
                ->whereIn('user_id', $adminIds)
                ->whereIn('date', $sundays);
            $semesterNorm = $this->semesterRangeService->normalizeSemester($semester);
            if (in_array($semesterNorm, ['Fall','Spring'], true)) {
                $qb->where('semester', $semesterNorm);
            }
            $statusRows = $qb->get()->getResultArray();

            foreach ($statusRows as $row) {
                $uid  = (int)($row['user_id'] ?? 0);
                $date = (string)($row['date'] ?? '');
                if ($uid <= 0 || $date === '') {
                    continue;
                }
                $statusByAdmin[$uid][$date] = [
                    'status' => strtolower((string)($row['status'] ?? '')),
                    'reason' => $row['reason'] ?? null,
                ];
            }
        }

        foreach ($admins as $uid => $admin) {
            $name = trim(($admin['firstname'] ?? '') . ' ' . ($admin['lastname'] ?? ''));
            if ($name === '') {
                $name = 'User#' . $uid;
            }

            $cells = [];
            $totals = ['p' => 0, 'a' => 0, 'l' => 0];
            foreach ($sundays as $date) {
                $cell = $statusByAdmin[$uid][$date] ?? null;
                if (!$cell || empty($cell['status'])) {
                    continue;
                }

                $status = (string)$cell['status'];
                if (!in_array($status, ['present', 'absent', 'late'], true)) {
                    continue;
                }

                $cells[$date] = [
                    'status' => $status,
                    'reason' => $cell['reason'] ?? null,
                ];

                if ($status === 'present') {
                    $totals['p']++;
                } elseif ($status === 'absent') {
                    $totals['a']++;
                } elseif ($status === 'late') {
                    $totals['l']++;
                }
            }

            $payload['admins'][] = [
                'user_id' => $uid,
                'name'    => $name,
                'role'    => $admin['role_name'] ?? 'Admin',
                'cells'   => $cells,
                'totals'  => $totals,
            ];
        }

        return $payload;
    }

    public function monthCsv()
    {
        // Export CSV across the configured school-year Sundays
        $semester   = (string)($this->request->getGet('semester')    ?: ($this->semester   ?? 'Fall'));
        $schoolYear = (string)($this->request->getGet('school_year') ?: ($this->schoolYear ?? (date('Y') . '-' . (date('Y') + 1))));
        $scope      = strtolower((string)$this->request->getGet('scope') ?? ''); // 'admins' to export admins CSV

        [$rangeStart, $rangeEnd] = $this->semesterRangeService->getSchoolYearRange($schoolYear);
        $semesterNorm = $this->semesterRangeService->normalizeSemester($semester);
        if (in_array($semesterNorm, ['Fall','Spring'], true)) {
            $semRange = $this->semesterRangeService->getSemesterRange($schoolYear, $semesterNorm);
            if ($semRange) { [$rangeStart, $rangeEnd] = $semRange; }
        }
        try { $start = new \DateTimeImmutable($rangeStart); } catch (\Throwable $e) { $start = new \DateTimeImmutable(date('Y-09-01')); }
        try { $end   = new \DateTimeImmutable($rangeEnd);   } catch (\Throwable $e) { $end   = (new \DateTimeImmutable($start->format('Y-m-d')))->modify('+9 months'); }

        // Sundays list
        $daysList = [];
        $cursor = $start;
        $w = (int)$cursor->format('w');
        if ($w !== 0) $cursor = $cursor->modify('next sunday');
        for (; $cursor <= $end; $cursor = $cursor->modify('+7 days')) {
            $daysList[] = $cursor->format('Y-m-d');
        }

        // =========================
        // Admins CSV
        // =========================
        if ($scope === 'admins') {
            $excludedSlugs = ['parent', 'guest', 'teacher', 'teacher_assistant', 'ta', 'assistant_teacher'];

            // Build roster of admins with best role by priority
            $cand = $this->db->table('users u')
                ->select('u.id AS user_id, u.firstname, u.lastname, r.id AS role_id, r.name AS role_name, r.slug, r.priority')
                ->join('user_roles ur', 'ur.user_id = u.id', 'left')
                ->join('roles r', 'r.id = ur.role_id', 'left')
                ->where('r.is_active', 1)
                ->get()->getResultArray();

            $admins = [];
            foreach ($cand as $row) {
                $rid  = (int)($row['role_id'] ?? 0);
                $slug = strtolower((string)($row['slug'] ?? $row['role_name'] ?? ''));
                if ($rid <= 0 || in_array($slug, $excludedSlugs, true)) continue;
                $uid  = (int)$row['user_id'];
                $prio = (int)($row['priority'] ?? 100);
                if (!isset($admins[$uid]) || $prio < (int)$admins[$uid]['priority']) {
                    $admins[$uid] = [
                        'user_id'   => $uid,
                        'firstname' => $row['firstname'] ?? '',
                        'lastname'  => $row['lastname'] ?? '',
                        'role_name' => $row['role_name'] ?? 'Admin',
                        'priority'  => $prio,
                    ];
                }
            }
            $adminUserIds = array_keys($admins);

            // Statuses (user/date only)
            $adminStatusMap = [];
            if (!empty($adminUserIds)) {
                $qb = $this->db->table('staff_attendance')
                    ->select('user_id, date, status')
                    ->where('school_year', $schoolYear)
                    ->where('date >=', $start->format('Y-m-d'))
                    ->where('date <=', $end->format('Y-m-d'))
                    ->whereIn('user_id', $adminUserIds);
                if (in_array($semesterNorm, ['Fall','Spring'], true)) {
                    $qb->where('semester', $semesterNorm);
                }
                $rows = $qb->get()->getResultArray();
                foreach ($rows as $r) {
                    $uid = (int)$r['user_id'];
                    $d   = (string)$r['date'];
                    $adminStatusMap[$uid][$d] = strtolower((string)($r['status'] ?? ''));
                }
            }

            // Stream CSV
            $filename = 'admin_attendance_' . $semester . '_' . str_replace('/', '-', $schoolYear) . '_SY.csv';
            header('Content-Type: text/csv; charset=utf-8');
            header('Content-Disposition: attachment; filename="' . $filename . '"');
            $out = fopen('php://output', 'w');

            $hdr = ['User ID', 'Name', 'Role'];
            foreach ($daysList as $d) $hdr[] = $d;
            $hdr[] = 'Total P';
            $hdr[] = 'Total A';
            $hdr[] = 'Total L';
            fputcsv($out, $hdr);

            foreach ($admins as $uid => $u) {
                $name = trim(($u['firstname'] ?? '') . ' ' . ($u['lastname'] ?? '')) ?: ('User#' . $uid);
                $role = $u['role_name'] ?? 'Admin';

                $totP = $totA = $totL = 0;
                $row = [$uid, $name, $role];
                foreach ($daysList as $d) {
                    $s = $adminStatusMap[$uid][$d] ?? '';
                    $v = '';
                    if ($s === 'present') {
                        $v = 'P';
                        $totP++;
                    } elseif ($s === 'absent') {
                        $v = 'A';
                        $totA++;
                    } elseif ($s === 'late') {
                        $v = 'L';
                        $totL++;
                    }
                    $row[] = $v;
                }
                $row[] = $totP;
                $row[] = $totA;
                $row[] = $totL;
                fputcsv($out, $row);
            }
            fclose($out);
            exit;
        }

        // =========================
        // Teachers/TAs CSV (replicate status to all assigned sections)
        // =========================

        // 1) Assignments for this term (keyed by SECTION CODE)
        // Build roster for selected term or whole year
        if (in_array($semesterNorm, ['Fall','Spring'], true)) {
            $bySection = $this->teacherClassSection->assignedBySectionForTerm($semesterNorm, $schoolYear);
        } else {
            $fall = $this->teacherClassSection->assignedBySectionForTerm('Fall', $schoolYear);
            $spr  = $this->teacherClassSection->assignedBySectionForTerm('Spring', $schoolYear);
            $bySection = [];
            foreach (array_unique(array_merge(array_keys($fall), array_keys($spr))) as $code) {
                $bySection[$code] = array_merge($fall[$code] ?? [], $spr[$code] ?? []);
            }
        }
        // $bySection: [sectionCode] => [rows...]

        // If you actually need a flat list like $tcRows:
        $tcRows = [];
        foreach ($bySection as $rows) {
            foreach ($rows as $r) $tcRows[] = $r;
        }

        $assignedBySection = []; // [sectionCode][] = teacher rows
        $teacherIdsGlobal  = []; // [teacherId=>true]
        foreach ($tcRows as $r) {
            $sid = (int)$r['class_section_id']; // SECTION CODE
            $tid = (int)$r['teacher_id'];
            $pos = strtolower((string)($r['position'] ?? 'main'));
            $pos = ($pos === 'ta') ? 'ta' : 'main';
            $assignedBySection[$sid][] = [
                'teacher_id' => $tid,
                'firstname'  => $r['firstname'] ?? '',
                'lastname'   => $r['lastname'] ?? '',
                'position'   => $pos,
            ];
            $teacherIdsGlobal[$tid] = true;
        }
        $sectionCodes = array_keys($assignedBySection);
        $teacherIds   = array_keys($teacherIdsGlobal);

        // 2) Statuses from staff_attendance for those teachers within month range & term (user/date only)
        $statusByUser = []; // [teacherId][Y-m-d] => status
        if (!empty($teacherIds)) {
            $qb = $this->db->table('staff_attendance sa')
                ->select('sa.user_id, sa.date, sa.status')
                ->where('sa.school_year', $schoolYear)
                ->where('sa.date >=', $start->format('Y-m-d'))
                ->where('sa.date <=', $end->format('Y-m-d'))
                ->whereIn('sa.user_id', $teacherIds);
            if (in_array($semesterNorm, ['Fall','Spring'], true)) {
                $qb->where('sa.semester', $semesterNorm);
            }
            $rows = $qb->get()->getResultArray();

            foreach ($rows as $r) {
                $uid = (int)$r['user_id'];
                $d   = (string)$r['date'];
                $statusByUser[$uid][$d] = strtolower((string)($r['status'] ?? ''));
            }
        }

        // 3) Stream CSV
        $filename = 'teacher_attendance_' . $semester . '_' . str_replace('/', '-', $schoolYear) . '_SY.csv';
        header('Content-Type: text/csv; charset=utf-8');
        header('Content-Disposition: attachment; filename="' . $filename . '"');

        $out = fopen('php://output', 'w');
        $hdr = ['Class Section ID', 'Teacher ID', 'Teacher Name', 'Role'];
        foreach ($daysList as $d) $hdr[] = $d;
        $hdr[] = 'Total P';
        $hdr[] = 'Total A';
        $hdr[] = 'Total L';
        fputcsv($out, $hdr);

        foreach ($sectionCodes as $sid) {
            foreach ($assignedBySection[$sid] as $t) {
                $tid  = (int)$t['teacher_id'];
                $name = trim(($t['firstname'] ?? '') . ' ' . ($t['lastname'] ?? '')) ?: ('User#' . $tid);
                $pos  = (string)$t['position'];

                $totP = $totA = $totL = 0;
                $row = [$sid, $tid, $name, $pos];

                foreach ($daysList as $d) {
                    $s = $statusByUser[$tid][$d] ?? ''; // replicate teacher’s status to all sections
                    $v = '';
                    if ($s === 'present') {
                        $v = 'P';
                        $totP++;
                    } elseif ($s === 'absent') {
                        $v = 'A';
                        $totA++;

                    } elseif ($s === 'late') {
                        $v = 'L';
                        $totL++;
                    }
                    $row[] = $v;
                }

                $row[] = $totP;
                $row[] = $totA;
                $row[] = $totL;
                fputcsv($out, $row);
            }
        }
        fclose($out);
        exit;
    }

    // Semester helpers are now provided by SemesterRangeService.

    //admins attendance
    public function admins()
    {
        // Context
        $semester   = (string)($this->request->getGet('semester')    ?: ($this->semester   ?? 'Fall'));
        $schoolYear = (string)($this->request->getGet('school_year') ?: ($this->schoolYear ?? (date('Y') . '-' . (date('Y') + 1))));
        $date       = (string)($this->request->getGet('date')        ?: local_date(utc_now(), 'Y-m-d'));

        // 1) Get all staff rows for the day/term
        $allStaff = (array)$this->staffAttendanceModel->staffWithStatusForDay($date, $semester, $schoolYear, []);

        if (empty($allStaff)) {
            return view('attendance/admins_attendance_form', [
                'semester'   => $semester,
                'schoolYear' => $schoolYear,
                'date'       => $date,
                'adminsGrid' => [],
            ]);
        }

        // Collect unique user_ids
        $userIds = [];
        foreach ($allStaff as $row) {
            $uid = (int)($row['user_id'] ?? 0);
            if ($uid > 0) $userIds[$uid] = true;
        }
        $userIds = array_keys($userIds);

        // 2) Fetch ALL roles for those users
        $rolesRows = $this->db->table('user_roles ur')
            ->select('ur.user_id, r.id AS role_id, r.name AS role_name, r.slug AS role_slug, r.priority')
            ->join('roles r', 'r.id = ur.role_id', 'inner')
            ->where('r.is_active', 1)
            ->whereIn('ur.user_id', $userIds)
            ->get()->getResultArray();

        // Group roles per user, sort by priority ASC
        $rolesByUser = [];
        foreach ($rolesRows as $r) {
            $uid = (int)$r['user_id'];
            if ($uid <= 0) continue;
            $rolesByUser[$uid][] = $r;
        }
        foreach ($rolesByUser as &$list) {
            usort($list, static function ($a, $b) {
                return (int)($a['priority'] ?? 100) <=> (int)($b['priority'] ?? 100);
            });
        }
        unset($list);

        // 3) Exclude parent, guest, teacher, teacher_assistant
        $excludeSlugs = ['parent', 'guest', 'teacher', 'teacher_assistant', 'ta', 'assistant_teacher'];

        // Map attendance rows by user_id
        $staffByUser = [];
        foreach ($allStaff as $row) {
            $uid = (int)($row['user_id'] ?? 0);
            if ($uid <= 0) continue;
            if (!isset($staffByUser[$uid])) {
                $staffByUser[$uid] = $row;
            }
        }

        // 4) Pick valid role for Admins list
        $adminsGrid = [];
        foreach ($userIds as $uid) {
            $roles = $rolesByUser[$uid] ?? [];

            $picked = null;
            foreach ($roles as $r) {
                $slug = strtolower((string)($r['role_slug'] ?? $r['role_name'] ?? ''));
                if (!in_array($slug, $excludeSlugs, true)) {
                    $picked = $r;
                    break;
                }
            }

            if ($picked === null) {
                continue; // skip users with only excluded roles
            }

            $base = $staffByUser[$uid] ?? ['user_id' => $uid];
            $base['role_name'] = $picked['role_name'] ?? 'Admin';
            $base['role_slug'] = $picked['role_slug'] ?? null;

            $adminsGrid[] = $base;
        }

        // 5) Sort nicely by name
        usort($adminsGrid, static function ($a, $b) {
            $an = trim(($a['firstname'] ?? '') . ' ' . ($a['lastname'] ?? ''));
            $bn = trim(($b['firstname'] ?? '') . ' ' . ($b['lastname'] ?? ''));
            return strcasecmp($an, $bn);
        });

        return view('attendance/admins_attendance_form', [
            'semester'   => $semester,
            'schoolYear' => $schoolYear,
            'date'       => $date,
            'adminsGrid' => $adminsGrid,
        ]);
    }

    public function saveAdmins()
    {
        $date       = (string)$this->request->getPost('date');
        $semester   = (string)$this->request->getPost('semester');
        $schoolYear = (string)$this->request->getPost('school_year');
        // Prefer 'admins' (your admins page); fallback to 'staff' if form used that name
        $admins     = (array)($this->request->getPost('admins') ?: $this->request->getPost('staff'));

        if (empty($date) || empty($semester) || empty($schoolYear)) {
            return redirect()->to('/admin/admins-attendance?date=' . $date . '&semester=' . urlencode($semester) . '&school_year=' . urlencode($schoolYear))
                ->with('error', 'Missing required fields (date/term).');
        }

        $allowedStatuses = ['present', 'absent', 'late'];
        // roles we explicitly do NOT want to record in the admins page
        $excludedRoleSlugs = ['parent', 'guest', 'teacher', 'teacher_assistant'];

        $normalize = static function (?string $s): string {
            $s = strtolower(trim((string)$s));
            // turn "Teacher Assistant", "teacher-assistant", "teacher_assistant" → "teacher_assistant"
            $s = str_replace(['-', ' '], '_', $s);
            return $s;
        };

        $this->db->transException(true);
        $this->db->transStart();

        try {
            $errors = [];
            $saved  = 0;

            foreach ($admins as $uid => $row) {
                $uid    = (int)$uid;
                $status = $normalize($row['status'] ?? '');
                $reason = trim((string)($row['reason'] ?? ''));
                // hidden inputs from the view; if you also have role_slug, prefer that
                $roleName = (string)($row['role_name'] ?? '');
                $roleSlug = $normalize($row['role_slug'] ?? $roleName);

                // Skip excluded roles (parent/guest/teacher/teacher_assistant)
                if (in_array($roleSlug, $excludedRoleSlugs, true)) {
                    continue;
                }

                // Empty status means clear any existing record for this admin for the day
                if ($status === '') {
                    $existing = $this->staffAttendanceModel->where([
                        'user_id'     => $uid,
                        'date'        => $date,
                        'semester'    => $semester,
                        'school_year' => $schoolYear,
                    ])->first();
                    if (!empty($existing)) {
                        $this->staffAttendanceModel->delete((int)$existing['id']);
                    }
                    continue;
                }

                if (!in_array($status, $allowedStatuses, true)) {
                    $errors[] = "Invalid status for admin user #{$uid}";
                    continue;
                }

                // Upsert (role stored as a label; if blank, store a generic 'Admin')
                $ok = $this->staffAttendanceModel->upsertOne(
                    $uid,
                    $roleName !== '' ? $roleName : 'Admin',
                    $date,
                    $semester,
                    $schoolYear,
                    $status,
                    $reason !== '' ? $reason : null
                );

                if ($ok === false) {
                    $errors[] = "DB error while saving user #{$uid}";
                } else {
                    $saved++;
                }
            }

            $this->db->transComplete();
            if ($this->db->transStatus() === false) {
                return redirect()->to('/admin/admins-attendance?date=' . $date . '&semester=' . urlencode($semester) . '&school_year=' . urlencode($schoolYear))
                    ->with('error', 'Database transaction failed.');
            }

            $msg = "Admins attendance saved ({$saved} row(s)).";
            if (!empty($errors)) {
                $msg .= ' Some rows skipped: ' . implode('; ', $errors);
            }

            return redirect()->to('/admin/admins-attendance?date=' . $date . '&semester=' . urlencode($semester) . '&school_year=' . urlencode($schoolYear))
                ->with('status', $msg);
        } catch (\Throwable $e) {
            $this->db->transRollback();
            return redirect()->to('/admin/admins-attendance?date=' . $date . '&semester=' . urlencode($semester) . '&school_year=' . urlencode($schoolYear))
                ->with('error', 'Save failed: ' . $e->getMessage());
        }
    }

    // app/Controllers/AttendanceController.php

    public function saveCell()
    {
        if (!$this->request->is('post')) {
            return $this->response->setStatusCode(405)
                ->setJSON(['ok' => false, 'error' => 'Method not allowed']);
        }

        $date       = (string) $this->request->getPost('date');            // YYYY-MM-DD
        $status     = (string) $this->request->getPost('status');          // '', present, absent, late
        $targetId   = (int)    $this->request->getPost('user_id');         // SUBJECT of the cell
        $roleName   = trim((string) $this->request->getPost('role_name')); // optional label
        $semester   = (string) $this->request->getPost('semester');
        $schoolYear = (string) $this->request->getPost('school_year');
        $reason     = trim((string) $this->request->getPost('reason'));

        // Validate
        if ($date === '' || $targetId <= 0 || $semester === '' || $schoolYear === '') {
            return $this->jsonErr('Missing required fields.');
        }
        if (!in_array($status, ['', 'present', 'absent', 'late'], true)) {
            return $this->jsonErr('Invalid status.');
        }

        $model = $this->staffAttendanceModel;

        // Find existing for upsert key (user_id + date + term)
        $existing = $model->where([
            'user_id'     => $targetId,
            'date'        => $date,
            'semester'    => $semester,
            'school_year' => $schoolYear,
        ])->first();

        // If cleared -> delete row when present
        if ($status === '') {
            if ($existing) {
                $model->delete((int)$existing['id']);
            }
            return $this->jsonOk();
        }

        // UPSERT using the model helper (this sets created_by/updated_by)
        $ok = $model->upsertOne(
            $targetId,
            $roleName !== '' ? $roleName : ($existing['role_name'] ?? null),
            $date,
            $semester,
            $schoolYear,
            $status,
            $reason !== '' ? $reason : null
        );

        if (!$ok) {
            log_message('error', 'saveCell upsert failed for uid={uid}, date={date}, term={sem}/{yr}', [
                'uid'  => $targetId,
                'date' => $date,
                'sem'  => $semester,
                'yr'   => $schoolYear,
            ]);
            return $this->jsonErr('Database error.');
        }

        return $this->jsonOk();
    }


    private function jsonOk(array $extra = [])
    {
        return $this->response->setJSON(array_merge([
            'ok' => true,
            'csrf_token' => csrf_token(),
            'csrf_hash' => csrf_hash(),
        ], $extra));
    }

    private function jsonErr(string $msg, int $code = 422)
    {
        return $this->response->setStatusCode($code)
            ->setJSON([
                'ok' => false,
                'error' => $msg,
                'csrf_token' => csrf_token(),
                'csrf_hash' => csrf_hash(),
            ]);
    }
}
