<?php

namespace App\Controllers\View;
use App\Controllers\BaseController;
use App\Models\AttendanceTrackingModel;
use App\Models\StudentClassModel;
use App\Models\StudentModel;
use App\Models\ConfigurationModel;
use App\Models\AttendanceDataModel;
use CodeIgniter\Events\Events;
use App\Models\ParentNotificationModel;
use App\Models\AttendanceEmailTemplateModel;


class AttendanceTrackingController extends BaseController
{
    protected $attendanceEmailTemplateModel;
    protected $attendanceDataModel;
    protected $notificationModel;

    protected $studentModel;
    protected $semester;
    protected $schoolYear;
    protected $studentClassModel;
    protected $attendanceTrackingModel;
    protected $configModel;
    protected $email;
    protected $db;
    protected array $debugCompute = [];
    protected ?array $attendanceReportedColumns = null;

    public function __construct()
    {
        $this->studentModel            = new StudentModel();
        $this->studentClassModel       = new StudentClassModel();
        $this->attendanceDataModel     = new AttendanceDataModel();
        $this->attendanceTrackingModel = new AttendanceTrackingModel();
        $this->configModel             = new ConfigurationModel();
        $this->email                   = \Config\Services::email();
        $this->db                      = \Config\Database::connect();
        $this->notificationModel       = new ParentNotificationModel();
        $this->attendanceEmailTemplateModel = new AttendanceEmailTemplateModel();
        $this->semester   = (string) $this->configModel->getConfig('semester');
        $this->schoolYear = (string) $this->configModel->getConfig('school_year');
    }

    public function pendingViolationsView()
    {
        $syParam = $this->request->getGet('school_year');
        $semParam = $this->request->getGet('semester');
        $schoolYear = (is_string($syParam) && $syParam !== '') ? (string)$syParam : (string)$this->schoolYear;
        $semester   = (is_string($semParam) && $semParam !== '') ? (string)$semParam : null; // semester filter disabled

        $debugInfo = [
            'school_year_param' => $schoolYear,
            'semester_param'    => $semester,
            'class_students'    => 0,
            'student_ids'       => 0,
            'attendance_rows'   => 0,
            'filtered_rows'     => 0,
            'violations'        => 0,
            'active_weeks'      => 0,
            'abs_last5'         => 0,
            'late_last5'        => 0,
        ];

        $classStudents = $this->studentClassModel
            ->active()
            ->where('student_class.school_year', $schoolYear)
            ->findAll();
        $debugInfo['class_students'] = count($classStudents);

        // Gather candidate student identifiers (numeric IDs and code-like IDs)
        $studentIds = [];
        $studentCodes = [];
        foreach ($classStudents as $row) {
            $raw = $row['student_id'] ?? null;
            if (is_numeric($raw)) {
                $studentIds[] = (int)$raw;
            } elseif (is_string($raw) && $raw !== '') {
                $studentCodes[] = trim($raw);
            }
        }

        // Fallback #2: if still empty, derive student IDs directly from attendance_data (school year optional)
        if (empty($studentIds) && empty($studentCodes)) {
            $builder = $this->db->table('attendance_data')
                ->select('DISTINCT student_id', false)
                ->orderBy('student_id', 'ASC');
            if (!empty($schoolYear)) {
                $builder->where('school_year', $schoolYear);
            }
            $attendanceStudents = $builder->get()->getResultArray();
            foreach ($attendanceStudents as $r) {
                $raw = $r['student_id'] ?? null;
                if (is_numeric($raw)) {
                    $studentIds[] = (int)$raw;
                } elseif (is_string($raw) && $raw !== '') {
                    $studentCodes[] = trim($raw);
                }
            }
            if (!empty($studentIds) || !empty($studentCodes)) {
                $classStudents = array_map(fn($sid) => ['student_id' => $sid], array_merge($studentIds, $studentCodes));
            }
        }

        $studentIds   = array_values(array_unique(array_filter($studentIds, fn($v) => $v > 0)));
        $studentCodes = array_values(array_unique(array_filter($studentCodes, fn($v) => $v !== '')));

        // Remove inactive students from the candidate list (avoid listing them in tracking views)
        $inactiveIdSet = [];
        if (!empty($studentIds)) {
            $inactiveRows = $this->studentModel
                ->select('id')
                ->whereIn('id', $studentIds)
                ->where('is_active', 0)
                ->findAll();
            $inactiveIdSet = array_flip(array_map(
                static fn($r) => (int)($r['id'] ?? 0),
                $inactiveRows
            ));
            $studentIds = array_values(array_filter(
                $studentIds,
                static fn($id) => !isset($inactiveIdSet[$id])
            ));
        }

        $inactiveCodeSet = [];
        if (!empty($studentCodes)) {
            $inactiveCodeRows = $this->studentModel
                ->select('school_id')
                ->whereIn('school_id', $studentCodes)
                ->where('is_active', 0)
                ->findAll();
            $inactiveCodeSet = array_flip(array_map(
                static fn($r) => (string)($r['school_id'] ?? ''),
                $inactiveCodeRows
            ));
            $studentCodes = array_values(array_filter(
                $studentCodes,
                static fn($code) => $code !== '' && !isset($inactiveCodeSet[$code])
            ));
        }

        $debugInfo['student_ids'] = count($studentIds) + count($studentCodes);

        if (empty($studentIds) && empty($studentCodes)) {
            return view('attendance/violations_pending', [
                'title'        => 'Pending Attendance Violations',
                'students'     => [],
                'school_year'  => $schoolYear,
                'semester'     => $semester,
                'error'        => 'No student identifiers found for the current school year.',
            ]);
        }

        $students = [];
        if (!empty($studentIds)) {
            $students = $this->studentModel
                ->whereIn('id', $studentIds)
                ->where('is_active', 1)
                ->findAll();
        }
        if (!empty($studentCodes)) {
            $byCode = $this->studentModel
                ->whereIn('school_id', $studentCodes)
                ->where('is_active', 1)
                ->findAll();
            // merge unique by id
            $seen = array_flip(array_map(fn($s) => (int)($s['id'] ?? 0), $students));
            foreach ($byCode as $s) {
                $sid = (int)($s['id'] ?? 0);
                if ($sid > 0 && !isset($seen[$sid])) {
                    $students[] = $s;
                    $seen[$sid] = true;
                }
            }
        }

        // If some codes were not found in students table, create lightweight placeholder entries
        $knownCodes = [];
        foreach ($students as $s) {
            $code = trim((string)($s['school_id'] ?? ''));
            if ($code !== '') {
                $knownCodes[$code] = true;
            }
        }
        foreach ($studentCodes as $code) {
            if (!isset($knownCodes[$code])) {
                $virtualId = abs(crc32($code)) ?: mt_rand(1000000, 1999999);
                $students[] = [
                    'id'         => $virtualId,
                    'school_id'  => $code,
                    'firstname'  => $code,
                    'lastname'   => '',
                    'parent_id'  => 0,
                    'class_name' => '',
                ];
            }
        }

        if (empty($students)) {
            return view('attendance/violations_pending', [
                'title'        => 'Pending Attendance Violations',
                'students'     => [],
                'school_year'  => $schoolYear,
                'semester'     => $semester,
                'error'        => 'No students found matching attendance records.',
                'debug'        => $debugInfo,
            ]);
        }

        // Ensure every attendance student (numeric or code) has a student entry
        $studentCodeToId = [];
        foreach ($students as $stu) {
            $code = trim((string)($stu['student_code'] ?? $stu['school_id'] ?? ''));
            if ($code !== '') {
                $studentCodeToId[$code] = (int)($stu['id'] ?? 0);
            }
        }
        $existingIds = array_flip(array_map(fn($s) => (int)($s['id'] ?? 0), $students));
        foreach (array_merge($studentIds, $studentCodes) as $sidOrCode) {
            $sid = null;
            $code = null;
            if (is_numeric($sidOrCode)) {
                $sid = (int)$sidOrCode;
            } else {
                $code = (string)$sidOrCode;
            }
            if ($sid !== null && $sid > 0) {
                if (!isset($existingIds[$sid])) {
                    $students[] = [
                        'id'         => $sid,
                        'school_id'  => $code ?? (string)$sid,
                        'firstname'  => (string)$sid,
                        'lastname'   => '',
                        'parent_id'  => 0,
                        'class_name' => '',
                    ];
                    $existingIds[$sid] = true;
                }
            } elseif ($code !== null && $code !== '') {
                if (!isset($studentCodeToId[$code])) {
                    $virtualId = abs(crc32($code)) ?: mt_rand(2000000, 2999999);
                    $students[] = [
                        'id'         => $virtualId,
                        'school_id'  => $code,
                        'firstname'  => $code,
                        'lastname'   => '',
                        'parent_id'  => 0,
                        'class_name' => '',
                    ];
                    $studentCodeToId[$code] = $virtualId;
                    $existingIds[$virtualId] = true;
                }
            }
        }

        // Map code-like identifiers to numeric IDs (attendance_data may store codes in student_id)
        $studentCodeToId = [];
        foreach ($students as $stu) {
            $code = trim((string)($stu['student_code'] ?? $stu['school_id'] ?? ''));
            if ($code !== '') {
                $studentCodeToId[$code] = (int)($stu['id'] ?? 0);
            }
        }
        $existingIds = array_flip(array_map(fn($s) => (int)($s['id'] ?? 0), $students));

        $today           = new \DateTime('today');
        [$syStart, $syEnd] = $this->deriveSchoolYearBounds($schoolYear);

        // Use the whole school year window (capped at today) so older absences are considered
        $start = new \DateTime($syStart . ' 00:00:00');
        $rangeEnd = new \DateTime(min($syEnd, $today->format('Y-m-d')) . ' 00:00:00');
        $endEx = (clone $rangeEnd)->modify('+1 day');

        // not notified yet (tolerant: != 'yes', 0, '0', null)
        $qb = $this->attendanceDataModel->builder();
        $qb->select('id, student_id, class_id, class_section_id, date, status, reason, school_year, semester, is_reported, is_notified');
        if (!empty($schoolYear)) {
            $qb->where('school_year', $schoolYear);
        }
        if (!empty($semester)) {
            $qb->where('semester', $semester);
        }
        // Support attendance_data that stores student codes instead of numeric IDs
        $qb->groupStart();
        $studentCodes = array_keys($studentCodeToId);
        $hasClause = false;
        if (!empty($studentIds)) {
            $qb->whereIn('student_id', $studentIds);
            $hasClause = true;
        }
        if (!empty($studentCodes)) {
            if ($hasClause) {
                $qb->orWhereIn('student_id', $studentCodes);
            } else {
                $qb->whereIn('student_id', $studentCodes);
            }
            $hasClause = true;
        }
        $qb->groupEnd();

        // ✅ unreported records only (honor legacy columns)
        $this->applyUnreportedAttendanceFilter($qb);

        // ✅ not yet notified records (treat both string 'yes' and numeric 1 as notified)
        $qb->groupStart()
            ->where("(LOWER(TRIM(COALESCE(is_notified, ''))) NOT IN ('yes','1'))", null, false)
        ->groupEnd();

        // ✅ only absents/lates
        // Normalize status with TRIM/LOWER to catch trailing spaces/case differences
        $qb->where("LOWER(TRIM(status)) IN ('absent','late')", null, false);

        // School-year date window (inclusive start, exclusive end)
        $qb->where('date >=', $start->format('Y-m-d'))
            ->where('date <',  $endEx->format('Y-m-d'));

        $termRows = $qb->orderBy('date', 'DESC')->get()->getResultArray();

        if (empty($termRows)) {
            log_message('debug', 'pendingViolationsView: no attendance rows found', [
                'school_year' => $schoolYear,
                'semester'    => $semester,
                'student_ids' => $studentIds,
            ]);
            // Second chance: detect the latest attendance_data term for these students
            $latestAttendanceTerm = $this->db->table('attendance_data')
                ->select('school_year, semester, date')
                ->whereIn('student_id', $studentIds)
                ->orderBy('date', 'DESC')
                ->limit(1)
                ->get()
                ->getRowArray();

            if ($latestAttendanceTerm) {
                $schoolYear = !empty($latestAttendanceTerm['school_year'])
                    ? (string)$latestAttendanceTerm['school_year']
                    : $schoolYear;
                $semester = !empty($latestAttendanceTerm['semester'])
                    ? (string)$latestAttendanceTerm['semester']
                    : $semester;

                [$syStart, $syEnd] = $this->deriveSchoolYearBounds($schoolYear ?: $this->schoolYear);
                $start = new \DateTime($syStart . ' 00:00:00');
                $rangeEnd = new \DateTime(min($syEnd, $today->format('Y-m-d')) . ' 00:00:00');
                $endEx = (clone $rangeEnd)->modify('+1 day');

                $qb = $this->attendanceDataModel->builder();
                $qb->select('id, student_id, class_id, class_section_id, date, status, reason, school_year, semester, is_reported, is_notified');
                if (!empty($schoolYear)) {
                    $qb->where('school_year', $schoolYear);
                }
                if (!empty($semester)) {
                    $qb->where('semester', $semester);
                }

                $qb->groupStart();
                $hasClause = false;
                if (!empty($studentIds)) {
                    $qb->whereIn('student_id', $studentIds);
                    $hasClause = true;
                }
                if (!empty($studentCodes)) {
                    if ($hasClause) {
                        $qb->orWhereIn('student_id', $studentCodes);
                    } else {
                        $qb->whereIn('student_id', $studentCodes);
                    }
                }
                $qb->groupEnd();
                $this->applyUnreportedAttendanceFilter($qb);
                $qb->groupStart()
                    ->where("(LOWER(TRIM(COALESCE(is_notified, ''))) NOT IN ('yes','1'))", null, false)
                ->groupEnd();
                $qb->where("LOWER(TRIM(status)) IN ('absent','late')", null, false);
                $qb->where('date >=', $start->format('Y-m-d'))
                    ->where('date <',  $endEx->format('Y-m-d'));

                $termRows = $qb->orderBy('date', 'DESC')->get()->getResultArray();
            }

            if (empty($termRows)) {
                log_message('debug', 'pendingViolationsView: still no rows after fallback', [
                    'school_year' => $schoolYear,
                    'semester'    => $semester,
                    'student_ids' => $studentIds,
                ]);
                return view('attendance/violations_pending', [
                    'title'        => 'Pending Attendance Violations',
                    'students'     => [],
                    'school_year'  => $schoolYear,
                    'semester'     => $semester,
                ]);
            }
        }

        $attendanceRows = [];
        foreach ($termRows as $row) {
            $dStr = $row['date'] ?? null;
            if (!$dStr) continue;

            try {
                $d = new \DateTime($dStr);
            } catch (\Throwable $e) {
                continue;
            }
            if ($d < $start || $d >= $endEx) continue;

            // Normalize student_id: some rows store student_code instead of numeric id
            $rawSid = $row['student_id'] ?? null;
            $sid = null;
            if (is_numeric($rawSid)) {
                $sid = (int)$rawSid;
            } elseif (is_string($rawSid)) {
                $code = trim($rawSid);
                $sid = $studentCodeToId[$code] ?? null;
                if ($sid === null) {
                    // Create a placeholder student for unseen code
                    $sid = abs(crc32($code)) ?: mt_rand(3000000, 3999999);
                    $studentCodeToId[$code] = $sid;
                    if (!isset($existingIds[$sid])) {
                        $students[] = [
                            'id'        => $sid,
                            'school_id' => $code,
                            'firstname' => $code,
                            'lastname'  => '',
                            'parent_id' => 0,
                        ];
                        $existingIds[$sid] = true;
                    }
                }
            }
            if ($sid === null || $sid <= 0) continue;
            $row['student_id'] = $sid;

            $row['status'] = strtolower(trim((string)($row['status'] ?? '')));
            $row['date']   = substr($d->format('Y-m-d H:i:s'), 0, 10);

            $attendanceRows[] = $row;
        }

        // Ensure every attendance student ID has a student entry
        foreach ($attendanceRows as $arow) {
            $sid = (int)($arow['student_id'] ?? 0);
            if ($sid > 0 && !isset($existingIds[$sid])) {
                $students[] = [
                    'id'        => $sid,
                    'school_id' => (string)($arow['student_code'] ?? $arow['student_id']),
                    'firstname' => (string)($arow['student_code'] ?? $arow['student_id']),
                    'lastname'  => '',
                    'parent_id' => 0,
                ];
                $existingIds[$sid] = true;
            }
        }

        if (empty($attendanceRows)) {
            log_message('debug', 'pendingViolationsView: filtered attendance rows empty', [
                'raw_rows' => count($termRows),
                'school_year' => $schoolYear,
                'semester'    => $semester,
            ]);
            return view('attendance/violations_pending', [
                'title'        => 'Pending Attendance Violations',
                'students'     => [],
                'school_year'  => $schoolYear,
                'semester'     => $semester,
                'debug'        => $debugInfo,
            ]);
        }

        $debugInfo['attendance_rows'] = count($termRows);
        $debugInfo['filtered_rows']   = count($attendanceRows);

        log_message('debug', 'pendingViolationsView: attendance rows ready', [
            'school_year' => $schoolYear,
            'semester'    => $semester,
            'rows'        => count($attendanceRows),
            'students'    => count($students),
        ]);

        $violations = $this->computeViolations($students, $attendanceRows, $schoolYear, null, $attendanceRows);
        $debugInfo['violations'] = count($violations);
        $debugInfo['active_weeks'] = $this->debugCompute['active_weeks'] ?? 0;
        $debugInfo['by_student']   = $this->debugCompute['by_student']   ?? 0;
        $debugInfo['abs_last5']    = $this->debugCompute['abs_last5']    ?? 0;
        $debugInfo['late_last5']   = $this->debugCompute['late_last5']   ?? 0;

        return view('attendance/violations_pending', [
            'title'        => 'Pending Attendance Violations',
            'students'     => $violations,
            'school_year'  => $schoolYear,
            'semester'     => $semester,
            'debug'        => $debugInfo,
        ]);
    }



    private function notificationAlreadySent(int $studentId, string $code, string $ymd, string $channel = 'email', ?string $to = null): bool
    {
        try {
            return $this->notificationModel->hasSent($studentId, $code, $ymd, $channel, $to);
        } catch (\Throwable $e) {
            log_message('debug', 'notificationAlreadySent(): ' . $e->getMessage());
            return false;
        }
    }

    private function logNotification(int $studentId, string $code, string $ymd, string $channel, ?string $to, string $subject, string $status, string $response = ''): void
    {
        try {
            // Upsert by unique key (student_id, code, incident_date, channel, to_address)
            $where = [
                'student_id'    => $studentId,
                'code'          => $code,
                'incident_date' => $ymd,
                'channel'       => $channel,
            ];
            if (!empty($to)) {
                $where['to_address'] = $to;
            }

            $existing = $this->notificationModel->where($where)->orderBy('id', 'DESC')->first();

            if ($existing && !empty($existing['id'])) {
                // Update status/response/subject to reflect latest attempt
                $this->notificationModel->update((int) $existing['id'], [
                    'subject'      => $subject,
                    'status'       => $status,
                    'response'     => $response,
                    'semester'     => $this->semester,
                    'school_year'  => $this->schoolYear,
                    'updated_at'   => date('Y-m-d H:i:s'),
                ]);
            } else {
                // Insert a new log row
                $this->notificationModel->insert([
                    'student_id'    => $studentId,
                    'code'          => $code,
                    'incident_date' => $ymd,
                    'channel'       => $channel,
                    'to_address'    => $to,
                    'subject'       => $subject,
                    'status'        => $status,
                    'response'      => $response,
                    'semester'      => $this->semester,
                    'school_year'   => $this->schoolYear,
                ]);
            }
        } catch (\Throwable $e) {
            log_message('debug', 'logNotification(): ' . $e->getMessage());
        }
    }

    public function record()
    {
        if (strtolower($this->request->getMethod()) !== 'post') {
            return redirect()->to(site_url('attendance/violations'));
        }

        $post = $this->request->getPost();

        $rules = [
            'student_id'   => 'required|is_natural_no_zero',
            'date'         => 'required|valid_date[Y-m-d]',
            'parent_email' => 'permit_empty|valid_email',
            'parent_name'  => 'permit_empty|string',
            'subject_type' => 'permit_empty|string',
            'semester'     => 'permit_empty|string',
            'school_year'  => 'permit_empty|string',
            'return_url'   => 'permit_empty|string',
        ];
        if (! $this->validate($rules)) {
            return redirect()->back()->withInput()->with('error', implode(' ', $this->validator->getErrors()));
        }

        $sid        = (int) $post['student_id'];
        $ymd        = substr((string) $post['date'], 0, 10);
        $semester   = $post['semester']    ?? $this->semester;
        $schoolYear = $post['school_year'] ?? $this->schoolYear;
        $returnUrl  = $post['return_url']  ?? site_url('attendance/violations');
        $subject    = $post['subject_type'] ?? 'Absent';

        $student      = $this->studentModel->find($sid) ?? [];
        $parentEmail  = $post['parent_email'] ?? '';
        $parentName   = $post['parent_name']  ?? '';

        if (!$parentEmail) {
            $parent      = $this->getPrimaryParentForStudent($sid) ?? [];
            $parentEmail = $parent['email'] ?? '';
            $parentName  = $parent['parent_name'] ?? '';
        }
        if (!$parentEmail) {
            return redirect()->to($returnUrl)->with('error', 'No parent email found for this student.');
        }

        $tracking = $this->attendanceTrackingModel;
        [$start, $end] = $this->dayBounds($ymd);
        $row = $tracking->builder()
            ->where('student_id', $sid)
            ->where('semester', $semester)
            ->where('school_year', $schoolYear)
            ->where('date >=', $start)
            ->where('date <',  $end)
            ->orderBy('date', 'DESC')
            ->get()->getFirstRow('array');

        $consequence = null;
        $reasonTxt   = (string) ($row['reason'] ?? '');
        if (preg_match('/\bABS_4\b|dismissal/i', $reasonTxt))      $consequence = 'dismissal';
        elseif (preg_match('/\bABS_3\b|final_warning/i', $reasonTxt)) $consequence = 'final_warning';
        elseif (preg_match('/\bABS_2\b|follow_up/i', $reasonTxt))  $consequence = 'follow_up';

        $class   = $this->studentClassModel->getClassSectionsByStudentId($sid, $this->schoolYear);

        $payload = [
            'student_id'  => $sid,
            'student'     => $student,
            'parent'      => ['email' => $parentEmail, 'name' => $parentName],
            'semester'    => $semester,
            'school_year' => $schoolYear,
            'class'       => $class,
            'now'         => date('Y-m-d H:i:s'),
            'date'        => $ymd,
            'subject'     => $subject,
        ];

        switch ($consequence) {
            case 'dismissal':
                Events::trigger('attendance.dismissal', $payload);
                break;
            case 'final_warning':
                Events::trigger('attendance.final_warning', $payload);
                break;
            case 'follow_up':
            default:
                Events::trigger('attendance.follow_up', $payload);
                break;
        }

        try {
            if ($row && !empty($row['id'])) {
                $tracking->markAsNotified((int) $row['id']);
            }
        } catch (\Throwable $e) {
            log_message('debug', 'Could not update notif_counter: ' . $e->getMessage());
        }

        return redirect()->to($returnUrl)->with('message', 'Notification queued.');
    }

    public function viewStudentCase($studentId)
    {
        $student = $this->studentModel->find($studentId);
        if (!$student) {
            return redirect()->to('/attendance')->with('error', 'Student not found');
        }
        $student['name'] = trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? ''));

        // Optional filters passed from violations list
        $codeParam    = strtoupper((string)($this->request->getGet('code') ?? ''));
        $incidentDate = (string)($this->request->getGet('incident_date') ?? '');
        $incidentDate = preg_match('/^\d{4}-\d{2}-\d{2}$/', $incidentDate) ? $incidentDate : '';
        $includeNotified = filter_var($this->request->getGet('include_notified'), FILTER_VALIDATE_BOOL) === true;
        $pendingOnly = filter_var($this->request->getGet('pending_only'), FILTER_VALIDATE_BOOL) === true;

        // Map code -> statuses
        $statusFilter = ['absent', 'late'];
        if (str_starts_with($codeParam, 'ABS')) {
            $statusFilter = ['absent'];
        } elseif (str_starts_with($codeParam, 'LATE')) {
            $statusFilter = ['late'];
        } elseif (str_starts_with($codeParam, 'MIX')) {
            $statusFilter = ['absent', 'late'];
        }

        $syParam = $this->request->getGet('school_year');
        $semParam = $this->request->getGet('semester');
        $schoolYearFilter = (is_string($syParam) && $syParam !== '') ? (string)$syParam : null;
        $semesterFilter   = (is_string($semParam) && $semParam !== '') ? (string)$semParam : null;

        $schoolYear = $schoolYearFilter
            ?? ($incidentDate !== '' ? $this->schoolYearForDate($incidentDate) : (string)$this->schoolYear);
        $semester = $semesterFilter;
        // For non-violation detail pages, default to current semester when no filter provided.
        if ($semester === null && $codeParam === '' && $incidentDate === '') {
            $semester = (string)$this->semester;
        }
        [$syStart, $syEnd] = $this->deriveSchoolYearBounds($schoolYear ?: date('Y'));
        $today     = new \DateTime('today');
        $start     = new \DateTime($syStart . ' 00:00:00');
        $rangeEnd  = new \DateTime(min($syEnd, $today->format('Y-m-d')) . ' 00:00:00');
        $endEx     = (clone $rangeEnd)->modify('+1 day');

        // Date window: use the last N active weeks ending at incidentDate (or today)
        $windowWeeks = $this->windowWeeksForViolationCode($codeParam);
        $endYmd   = $incidentDate !== '' ? $incidentDate : date('Y-m-d');
        $startYmd = $this->activeWeekWindowStartYmd($endYmd, $windowWeeks, $schoolYear, $semester);

        $studentIdValues = [(int)$studentId];
        $studentCodeRaw = trim((string)($student['student_code'] ?? $student['school_id'] ?? ''));
        $studentCodeValues = [];
        if ($studentCodeRaw !== '') {
            if (is_numeric($studentCodeRaw)) {
                $studentCodeNum = (int)$studentCodeRaw;
                if ($studentCodeNum > 0 && $studentCodeNum !== (int)$studentId) {
                    $studentIdValues[] = $studentCodeNum;
                }
            } else {
                $studentCodeValues[] = $studentCodeRaw;
            }
        }
        $studentIdValues = array_values(array_unique(array_filter($studentIdValues, fn($v) => $v > 0)));
        $studentCodeValues = array_values(array_unique(array_filter($studentCodeValues, fn($v) => $v !== '')));

        $qb = $this->attendanceDataModel->builder();
        $qb->select('id, student_id, class_id, class_section_id, date, status, reason, school_year, semester, is_reported, is_notified')
            ->groupStart()
            ->whereIn('student_id', $studentIdValues);
        if (!empty($studentCodeValues)) {
            $qb->orWhereIn('student_id', $studentCodeValues);
        }
        $qb->groupEnd()
            ->where('date >=', max($start->format('Y-m-d'), $startYmd))
            ->where('date <',  $this->dayBounds($endYmd)[1]);

        // Status filter (tolerate legacy codes like ABS/A/LATE/L)
        if ($statusFilter === ['absent']) {
            $qb->where("(LOWER(TRIM(status)) LIKE 'abs%' OR LOWER(TRIM(status)) = 'a')", null, false);
        } elseif ($statusFilter === ['late']) {
            $qb->where("(LOWER(TRIM(status)) LIKE 'late%' OR LOWER(TRIM(status)) = 'l')", null, false);
        } else {
            $qb->where("(LOWER(TRIM(status)) LIKE 'abs%' OR LOWER(TRIM(status)) LIKE 'late%' OR LOWER(TRIM(status)) IN ('a','l'))", null, false);
        }

        $qb
            ->orderBy('date', 'DESC');

        // Pending-only filter (optional)
        if ($pendingOnly) {
            $this->applyUnreportedAttendanceFilter($qb);
            if (!$includeNotified) {
                $qb->groupStart()
                    ->where("(LOWER(TRIM(COALESCE(is_notified, ''))) NOT IN ('yes','1'))", null, false)
                ->groupEnd();
            }
        }

        if ($schoolYearFilter !== null) {
            $qb->where('school_year', $schoolYearFilter);
        }
        if ($semesterFilter !== null) {
            $qb->where('semester', $semesterFilter);
        }

        $attendanceRows = $qb->get()->getResultArray();

        $data = [
            'title'          => 'Attendance History - ' . $student['name'],
            'student'        => $student,
            'attendance'     => $attendanceRows,
            'violation_code' => $codeParam !== '' ? $codeParam : null,
            // Prefer the incident date passed from the violation list; otherwise fall back to
            // the newest attendance row in the window so the details page always shows a date.
            'violation_date'     => $incidentDate,
            'violation_notified' => null,
            'show_violation_summary' => true,
        ];

        if ($data['violation_date'] === '' && !empty($data['attendance'][0]['date'])) {
            $data['violation_date'] = substr((string)$data['attendance'][0]['date'], 0, 10);
        }

        if (!empty($data['attendance'][0]['is_notified'])) {
            $data['violation_notified'] = $data['attendance'][0]['is_notified'];
        }

        // Avoid duplicating the violation summary row when the attendance list already
        // contains the same incident date.
        if ($data['violation_date'] !== '') {
            foreach ($attendanceRows as $row) {
                $ymd = substr((string)($row['date'] ?? ''), 0, 10);
                if ($ymd === $data['violation_date']) {
                    $data['show_violation_summary'] = false;
                    break;
                }
            }
        }

        return view('attendance/view', $data);
    }

    private function dayBounds(string $ymd): array
    {
        $d = substr($ymd, 0, 10);
        return [
            $d . ' 00:00:00',
            date('Y-m-d', strtotime($d . ' +1 day')) . ' 00:00:00'
        ];
    }

    private function schoolYearForDate(string $ymd): string
    {
        $ts = strtotime($ymd);
        if ($ts === false) {
            return (string) $this->schoolYear;
        }
        $y = (int) date('Y', $ts);
        $m = (int) date('n', $ts);
        $startY = ($m >= 8) ? $y : $y - 1;
        return sprintf('%d-%d', $startY, $startY + 1);
    }

    protected function getPrimaryParentForStudent(int $studentId): ?array
    {
        $row = $this->db->table('students s')
            ->select('u.id AS user_id, u.email, u.cellphone, CONCAT(u.firstname, " ", u.lastname) AS parent_name', false)
            ->join('users u', 'u.id = s.parent_id', 'left')
            ->where('s.id', $studentId)
            ->get()->getRowArray();

        if (!$row || empty($row['user_id'])) return null;

        return [
            'user_id'     => (int) $row['user_id'],
            'email'       => (string) ($row['email'] ?? ''),
            'parent_name' => (string) ($row['parent_name'] ?? ''),
            'phone'       => $row['cellphone'] ?? null,
        ];
    }

    protected function getSecondaryParentForStudent(int $studentId): ?array
    {
        try {
            $stu = $this->db->table('students')
                ->select('id, parent_id, school_year, semester')
                ->where('id', $studentId)
                ->get()->getRowArray();

            if (!$stu) return null;
            $primaryId  = (int)($stu['parent_id'] ?? 0);
            $schoolYear = (string)($stu['school_year'] ?? '');

            $pb = $this->db->table('parents')->where('firstparent_id', $primaryId);
            if ($schoolYear !== '') {
                $pb->where('school_year', $schoolYear);
            }
            $pRow = $pb->orderBy('updated_at', 'DESC')->limit(1)->get()->getRowArray();
            if (!$pRow) return null;

            $secondId = (int)($pRow['secondparent_id'] ?? 0);
            if ($secondId > 0) {
                $u2 = $this->db->table('users')
                    ->select('id, firstname, lastname, email, cellphone')
                    ->where('id', $secondId)
                    ->get()->getRowArray();
                if ($u2 && !empty($u2['email'])) {
                    return [
                        'user_id'     => (int)$u2['id'],
                        'email'       => (string)$u2['email'],
                        'parent_name' => trim(($u2['firstname'] ?? '') . ' ' . ($u2['lastname'] ?? '')),
                        'phone'       => (string)($u2['cellphone'] ?? ''),
                    ];
                }
            }

            // Fallback to plain fields on parents row
            $fallbackEmail = (string)($pRow['secondparent_email'] ?? '');
            $fallbackPhone = (string)($pRow['secondparent_phone'] ?? '');
            $fallbackFirst = (string)($pRow['secondparent_firstname'] ?? '');
            $fallbackLast  = (string)($pRow['secondparent_lastname'] ?? '');

            if ($fallbackEmail || $fallbackPhone || $fallbackFirst || $fallbackLast) {
                return [
                    'user_id'     => $secondId ?: null,
                    'email'       => $fallbackEmail,
                    'parent_name' => trim(($fallbackFirst . ' ' . $fallbackLast)) ?: null,
                    'phone'       => $fallbackPhone,
                ];
            }
        } catch (\Throwable $e) {
            log_message('error', 'getSecondaryParentForStudent() failed: ' . $e->getMessage());
        }
        return null;
    }

    // ------------------------------------------------------------
    // RULES ENGINE (Absence/Late) — uses ONLY non-reported rows
    // ------------------------------------------------------------
    private function computeViolations(
        array $students,
        array $attendanceData,
        ?string $schoolYear = null,
        ?string $semester = null,
        ?array $weekRowsFallback = null
    ): array {
        $this->debugCompute = ['active_weeks' => 0, 'by_student' => 0];
        $schoolYear = $schoolYear ?: $this->schoolYear;
        $semester   = ($semester !== null && $semester !== '') ? (string)$semester : null;

        $studentCodeToId = [];
        foreach ($students as $stu) {
            $code = trim((string)($stu['student_code'] ?? $stu['school_id'] ?? ''));
            if ($code !== '') {
                $studentCodeToId[$code] = (int)($stu['id'] ?? 0);
            }
        }

        $byStudent = [];
        foreach ($attendanceData as $r) {
            $rawSid = $r['student_id'] ?? null;
            if (is_numeric($rawSid)) {
                $sid = (int)$rawSid;
            } else {
                $sid = $studentCodeToId[trim((string)$rawSid)] ?? 0;
            }
            if ($sid <= 0) continue;
            $ymd  = substr((string)($r['date'] ?? ''), 0, 10);
            $stat = strtolower(trim((string)$r['status']));

            if (!in_array($stat, ['absent', 'late'], true)) continue;

            $byStudent[$sid][$stat][] = $ymd;
        }
        $this->debugCompute['by_student'] = count($byStudent);

        [$syStart, $syEnd]  = $this->deriveSchoolYearBounds($schoolYear);
        // Only trust fallback rows for "active weeks" if they include non-violation statuses.
        $weekRowsSource = null;
        if (!empty($weekRowsFallback)) {
            foreach ($weekRowsFallback as $r) {
                $st = strtolower(trim((string)($r['status'] ?? '')));
                if ($st !== '' && !in_array($st, ['absent', 'late'], true)) {
                    $weekRowsSource = $weekRowsFallback;
                    break;
                }
            }
        }
        $activeWeekKeys     = $this->getActiveWeeksFromAttendanceData($syStart, $syEnd, $schoolYear, $semester, $weekRowsSource);
        $this->debugCompute['active_weeks'] = count($activeWeekKeys);
        if (empty($activeWeekKeys)) {
            log_message('debug', 'computeViolations: no active weeks', [
                'school_year' => $schoolYear,
                'semester'    => $semester,
                'rows'        => count($attendanceData),
            ]);
            return [];
        }

        $weekIndex          = array_flip($activeWeekKeys);
        $currentWeekIdx     = count($activeWeekKeys) - 1;
        // Use only the last 5 active weeks for violation calculations
        $windowStartIdx     = max(0, $currentWeekIdx - 4);
        $requiredWeeksCount = $currentWeekIdx - $windowStartIdx + 1;
        // Keep dates that fall within the last 5 active weeks
        $keepLastWeeks = function (array $dates) use ($weekIndex, $windowStartIdx, $currentWeekIdx): array {
            $out = [];
            foreach ($dates as $d) {
                $key = date('o-\WW', strtotime($d));
                if (isset($weekIndex[$key])) {
                    $idx = $weekIndex[$key];
                    $ymd = substr((string)$d, 0, 10);
                    if ($idx >= $windowStartIdx && $idx <= $currentWeekIdx) {
                        $out[] = $ymd;
                    }
                }
            }
            $out = array_values(array_unique($out));
            rsort($out);
            return $out;
        };

        $out = [];

        // Prefetch class/grade (class section) labels for all students to avoid N+1 queries
        $classByStudent = [];
        try {
            $studentIdsForClass = array_values(array_unique(array_map(
                static fn($s) => (int)($s['id'] ?? 0),
                $students
            )));

            if (!empty($studentIdsForClass)) {
                $rows = $this->db->table('student_class sc')
                    ->select('sc.student_id, sc.class_section_id, cs.class_section_name, cs.class_id, c.class_name')
                    ->join('classSection cs', 'cs.class_section_id = sc.class_section_id', 'left')
                    ->join('classes c', 'c.id = cs.class_id', 'left')
                    ->whereIn('sc.student_id', $studentIdsForClass)
                    ->where('sc.school_year', $schoolYear)
                    ->orderBy('sc.id', 'DESC') // latest assignment first
                    ->get()->getResultArray();

                // Keep the latest row per student_id
                foreach ($rows as $row) {
                    $sid = (int)($row['student_id'] ?? 0);
                    if ($sid <= 0) continue;
                    if (!isset($classByStudent[$sid])) {
                        $classByStudent[$sid] = [
                            'class_section_id'   => $row['class_section_id']   ?? null,
                            'class_section_name' => (string)($row['class_section_name'] ?? ''),
                            'class_id'           => $row['class_id']           ?? null,
                            'class_name'         => (string)($row['class_name']         ?? ''),
                        ];
                    }
                }
            }
        } catch (\Throwable $e) {
            // If anything goes wrong, fail gracefully without class labels
            log_message('debug', 'computeViolations(): class prefetch failed: ' . $e->getMessage());
        }

        $absCountLast5 = 0;
        $lateCountLast5 = 0;

        foreach ($students as $stu) {
            $sid   = (int) ($stu['id'] ?? 0);
            $name  = trim(($stu['firstname'] ?? '') . ' ' . ($stu['lastname'] ?? ''));

            $absDatesAll  = array_values(array_unique($byStudent[$sid]['absent'] ?? []));
            $lateDatesAll = array_values(array_unique($byStudent[$sid]['late']   ?? []));

            $absDates  = $keepLastWeeks($absDatesAll);
            $lateDates = $keepLastWeeks($lateDatesAll);

            if (empty($absDates) && empty($lateDates)) continue;

            $absWeekIdx  = $this->datesToWeekIndices($absDates,  $weekIndex);
            $lateWeekIdx = $this->datesToWeekIndices($lateDates, $weekIndex);
            $absCountLast5  += count($absDates);
            $lateCountLast5 += count($lateDates);

            $absenceViolation = null;
            if (!empty($absDates)) {
                $lastAbsDate = $absDates[0];

                $A2row = $this->hasNConsecutiveItems($absDates, 2, 7);
                $A3row = $this->hasNConsecutiveItems($absDates, 3, 7);
                $A4row = $this->hasNConsecutiveItems($absDates, 4, 7);

                $A_in3w2 = $this->hasNInWActiveWeeks($absWeekIdx, 2, 3);
                $A_in4w3 = $this->hasNInWActiveWeeks($absWeekIdx, 3, 4);
                $A_in5w4 = $this->hasNInWActiveWeeks($absWeekIdx, 4, 5);

                if ($A4row || $A_in5w4) {
                    $absenceViolation = [
                        'type'        => 'absence',
                        'code'        => 'ABS_4',
                        'severity'    => 4,
                        'action'      => 'expel_notify',
                        'color'       => '#ff4d4f',
                        'title'       => '4 Absences (in a row or within last 5 active weeks)',
                        'description' => 'Notify team to inform parent about expulsion and send email.',
                        'last_date'   => $lastAbsDate,
                    ];
                } elseif ($A3row || $A_in4w3) {
                    $absenceViolation = [
                        'type'        => 'absence',
                        'code'        => 'ABS_3',
                        'severity'    => 3,
                        'action'      => 'team_notify',
                        'color'       => '#fa8c16',
                        'title'       => '3 Absences (in a row or within last 4 active weeks)',
                        'description' => 'Team calls parent and sends email.',
                        'last_date'   => $lastAbsDate,
                    ];
                } elseif ($A2row || $A_in3w2) {
                    $absenceViolation = [
                        'type'        => 'absence',
                        'code'        => 'ABS_2',
                        'severity'    => 2,
                        'action'      => 'team_notify',
                        'color'       => '#fadb14',
                        'title'       => '2 Absences (in a row or within last 3 active weeks)',
                        'description' => 'Team calls parent and sends email.',
                        'last_date'   => $lastAbsDate,
                    ];
                }
            }

            $lateViolation = null;
            if (!empty($lateDates)) {
                $lastLateDate = $lateDates[0];

                $L2row   = $this->hasNConsecutiveItems($lateDates, 2, 7);
                $L3row   = $this->hasNConsecutiveItems($lateDates, 3, 7);
                $L4row   = $this->hasNConsecutiveItems($lateDates, 4, 7);

                $L_in3w2 = $this->hasNInWActiveWeeks($lateWeekIdx, 2, 3);
                $L_in4w3 = $this->hasNInWActiveWeeks($lateWeekIdx, 3, 4);

                $lateCoversAllWindowWeeks = false;
                if ($requiredWeeksCount >= 4) {
                    $lateWeeksSet = array_flip($lateWeekIdx);
                    $lateCoversAllWindowWeeks = true;
                    for ($i = $windowStartIdx; $i <= $currentWeekIdx; $i++) {
                        if (!isset($lateWeeksSet[$i])) {
                            $lateCoversAllWindowWeeks = false;
                            break;
                        }
                    }
                }

                if ($L4row || $lateCoversAllWindowWeeks) {
                    $lateViolation = [
                        'type'        => 'late',
                        'code'        => 'LATE_4',
                        'severity'    => 4,
                        'action'      => 'team_notify',
                        'color'       => '#ff4d4f',
                        'title'       => '4 Lates in a row OR in each of the last 4 active weeks',
                        'description' => 'Team calls parent and sends email.',
                        'last_date'   => $lastLateDate,
                    ];
                } else {
                    $twoLatesOneAbsWithin4 = $this->twoLatesOneAbsInWWeeks($lateWeekIdx, $absWeekIdx, 4);

                    if ($L3row || $L_in4w3 || $twoLatesOneAbsWithin4) {
                        $lateViolation = [
                            'type'        => $twoLatesOneAbsWithin4 ? 'mix' : 'late',
                            'code'        => $twoLatesOneAbsWithin4 ? 'MIX_L2A1' : 'LATE_3',
                            'severity'    => 3,
                            'action'      => 'team_notify',
                            'color'       => '#002766',
                            'title'       => $twoLatesOneAbsWithin4
                                ? '2 Lates + 1 Absence (within last 4 active weeks)'
                                : '3 Lates (in a row or within last 4 active weeks)',
                            'description' => 'Team calls parent and sends email.',
                            'last_date'   => $lastLateDate,
                        ];
                    } elseif ($L2row || $L_in3w2) {
                        $lateViolation = [
                            'type'        => 'late',
                            'code'        => 'LATE_2',
                            'severity'    => 1,
                            'action'      => 'auto_email',
                            'color'       => '#bae7ff',
                            'title'       => '2 Lates (in a row or within last 3 active weeks)',
                            'description' => 'Send automated email to parent.',
                            'last_date'   => $lastLateDate,
                        ];
                    }
                }
            }

            $chosen = $this->chooseHigherSeverity($absenceViolation, $lateViolation);

            if (!$chosen && count($absDates) === 1) {
                $chosen = [
                    'type'        => 'absence',
                    'code'        => 'ABS_1',
                    'severity'    => 1,
                    'action'      => 'auto_email',
                    'color'       => '#e6f7ff',
                    'title'       => '1 Unreported Absence',
                    'description' => 'Send automated email to parent.',
                    'last_date'   => $absDates[0],
                ];
            }

            if (!$chosen) continue;

            $parent = $this->getPrimaryParentForStudent($sid);

            // Do not skip pending violations even if a previous tracking/notification exists;
            // the view should surface all current infractions.

            [$start, $end] = $this->dayBounds($chosen['last_date']);
            $trackingQ = $this->attendanceTrackingModel
                ->where('student_id', $sid)
                ->where('school_year', $schoolYear)
                ->where('date >=', $start)
                ->where('date <',  $end)
                ->like('reason', $chosen['code'], 'both');
            if ($semester !== null) {
                $trackingQ->where('semester', $semester);
            }
            $tracking = $trackingQ->orderBy('date', 'DESC')->first();

            $isNotified = (bool)($tracking['is_notified'] ?? 0);
            $notifCnt   = (int)($tracking['notif_counter'] ?? 0);

            // Resolve class/grade label: prefer class_section_name (e.g., "3-A"),
            // fall back to classes.class_name if available.
            $cls = $classByStudent[$sid] ?? [];
            $classLabel = (string)($cls['class_section_name'] ?? '');
            if ($classLabel === '' && !empty($cls['class_name'])) {
                $classLabel = (string)$cls['class_name'];
            }

            $out[] = [
                'id'              => $sid,
                'name'            => $name,
                // Provide multiple common keys so views can pick any
                'class_name'      => $classLabel,
                'class_section_name' => (string)($cls['class_section_name'] ?? ''),
                'className'       => (string)($cls['class_name'] ?? ''),
                'class_id'        => $cls['class_id'] ?? null,
                'class_section_id'=> $cls['class_section_id'] ?? null,
                'parent_email'    => $parent['email']       ?? '',
                'parent_name'     => $parent['parent_name'] ?? '',
                'parent_phone'    => $parent['phone']       ?? null,

                'violation'       => $chosen['title'],
                'violation_code'  => $chosen['code'],
                'type'            => $chosen['type'],
                'severity'        => $chosen['severity'],
                'action'          => $chosen['action'],
                'color'           => $chosen['color'],
                'last_absence'    => $absDates[0]  ?? null,
                'last_late'       => $lateDates[0] ?? null,
                'last_date'       => $chosen['last_date'],

                'is_notified'     => $isNotified,
                'notif_counter'   => $notifCnt,

                'absences'        => $absDates,
                'lates'           => $lateDates,
            ];
        }

        $this->debugCompute['abs_last5']  = $absCountLast5;
        $this->debugCompute['late_last5'] = $lateCountLast5;

        return $out;
    }

    public function sendAutoEmails()
    {
        if (!$this->request->is('post')) {
            return $this->response->setStatusCode(405)->setJSON([
                'success' => false,
                'message' => 'Method Not Allowed'
            ]);
        }

        $globalVariantOverride = (string)($this->request->getVar('variant') ?? '');

        $classStudents = $this->studentClassModel
            ->active()
            ->where('school_year', $this->schoolYear)
            ->findAll();

        if (empty($classStudents)) {
            return $this->response->setJSON([
                'success' => true,
                'sent' => 0,
                'skipped' => 0,
                'errors' => 0,
                'details' => []
            ]);
        }

        $studentIds = array_column($classStudents, 'student_id');
        $students   = $this->studentModel
            ->whereIn('id', $studentIds)
            ->where('is_active', 1)
            ->findAll();

        $attendanceQuery = $this->attendanceDataModel
            ->whereIn('student_id', $studentIds)
            ->where('school_year', $this->schoolYear)
            ->where('date >=', (new \DateTime('today'))->modify('-5 weeks')->format('Y-m-d'));
        $this->applyUnreportedAttendanceFilter($attendanceQuery);
        $attendanceData = $attendanceQuery
            ->where("LOWER(TRIM(status)) IN ('absent','late')", null, false)
            ->orderBy('date', 'DESC')
            ->findAll();

        $violations = $this->computeViolations($students, $attendanceData, $this->schoolYear, null, $attendanceData);
        $toSend     = array_values(array_filter($violations, fn($v) => ($v['action'] ?? '') === 'auto_email'));

        $sent = 0;
        $skipped = 0;
        $errors = 0;
        $details = [];

        $getPrimaryParent = function (int $sid): array {
            return $this->getPrimaryParentForStudent($sid) ?? [];
        };
        $buildContext = function (array $v, array $parent): array {
            return [
                '{{parent_name}}'     => (string)($parent['parent_name'] ?? 'Parent/Guardian'),
                '{{student_name}}'    => (string)($v['name'] ?? ''),
                '{{incident_date}}'   => (string)($v['last_date'] ?? date('Y-m-d')),
                '{{school_phone}}'    => '978-364-0219',
                '{{voicemail_phone}}' => (string)($parent['phone'] ?? '—'),
                '{{class_time}}'      => '10:00 AM',
            ];
        };
        $pickVariant = function (string $code) use ($globalVariantOverride): string {
            if ($globalVariantOverride !== '') return $globalVariantOverride;
            return match ($code) {
                'ABS_2', 'ABS_2_IN3W' => 'no_answer',
                default               => 'default',
            };
        };
        $renderFromTemplate = function (string $code, array $context, string $variant = 'default') {
            $templateModel = $this->attendanceEmailTemplateModel ?? new \App\Models\AttendanceEmailTemplateModel();
            $row = $templateModel->getTemplate($code, $variant) ?: $templateModel->getTemplate($code, 'default');
            if (!$row) return null;
            return [strtr($row['subject'], $context), strtr($row['body_html'], $context)];
        };

        foreach ($toSend as $v) {
            $sid   = (int)($v['id'] ?? 0);
            $code  = (string)($v['violation_code'] ?? $v['code'] ?? '');
            $date  = (string)($v['last_date'] ?? '');
            $to    = trim((string)($v['parent_email'] ?? ''));
            $pname = (string)($v['parent_name'] ?? '');

            if ($sid <= 0 || !$code || !$date || !$to) {
                $errors++;
                $details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'missing data'];
                continue;
            }

            if ($this->notificationAlreadySent($sid, $code, $date, 'email', $to)) {
                $skipped++;
                $details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'duplicate'];
                continue;
            }

            $parent  = $getPrimaryParent($sid);
            $context = $buildContext($v, $parent);

            $variant = $pickVariant($code);
            $tpl     = $renderFromTemplate($code, $context, $variant);

            if (!$tpl) {
                $subject = match ($code) {
                    'ABS_1'  => 'Attendance Notice: Unreported Absence',
                    'LATE_2' => 'Attendance Notice: Repeated Lateness',
                    default  => 'Attendance Notice',
                };
                $studentName = (string)($v['name'] ?? '');
                $body = "<p>Dear " . ($pname ?: 'Parent/Guardian') . "</p>"
                    . "<p>We'd like to inform you about your student's recent attendance:</p>"
                    . "<ul>"
                    . "<li><strong>Student:</strong> " . $studentName . "</li>"
                    . "<li><strong>Issue:</strong> " . (string)($v['violation'] ?? '') . "</li>"
                    . "<li><strong>As of:</strong> " . $date . "</li>"
                    . "</ul>"
                    . "<p>If you have any questions, please contact the school office.</p>";
            } else {
                [$subject, $body] = $tpl;
            }

            $wrapped = $this->renderWithEmailLayout($subject, $body);

            $anyOk = false;
            $mailer = new EmailController();

            // Primary send
            try {
                $ok = $mailer->sendEmail($to, $subject, $wrapped);
                if ($ok) {
                    $anyOk = true;
                    $details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'sent'];
                    $this->logNotification($sid, $code, $date, 'email', $to, $subject, 'sent', 'OK');
                } else {
                    $errors++;
                    $this->logNotification($sid, $code, $date, 'email', $to, $subject, 'failed', 'sendEmail returned false');
                    $details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'failed'];
                }
            } catch (\Throwable $e) {
                $errors++;
                $this->logNotification($sid, $code, $date, 'email', $to, $subject, 'failed', $e->getMessage());
                $details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'error: ' . $e->getMessage()];
            }

            // Secondary (CC-like) send: send separate email to second parent if available
            try {
                $sec = $this->getSecondaryParentForStudent($sid);
                $to2 = trim((string)($sec['email'] ?? ''));
                if ($to2 !== '' && strcasecmp($to2, $to) !== 0) {
                    if ($this->notificationAlreadySent($sid, $code, $date, 'email', $to2)) {
                        $skipped++;
                        $details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'duplicate_cc'];
                    } else {
                        $ok2 = $mailer->sendEmail($to2, $subject, $wrapped);
                        if ($ok2) {
                            $anyOk = true;
                            $this->logNotification($sid, $code, $date, 'email', $to2, $subject, 'sent', 'OK');
                            $details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'cc_sent'];
                        } else {
                            $errors++;
                            $this->logNotification($sid, $code, $date, 'email', $to2, $subject, 'failed', 'sendEmail returned false');
                            $details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'cc_failed'];
                        }
                    }
                }
            } catch (\Throwable $e) {
                $errors++;
                $details[] = ['student_id' => $sid, 'code' => $code, 'date' => $date, 'result' => 'cc_error: ' . $e->getMessage()];
            }

            // Mark tracking notified once per student/code/date if any send succeeded
            if ($anyOk) {
                $sent++;
                $this->attendanceTrackingModel->markRuleAsNotified(
                    $sid,
                    $code,
                    $date,
                    $this->semester,
                    $this->schoolYear
                );
                // Mark all dates involved in this violation as reported+notified
                $allDates = [];
                $absDates = array_map(fn($d) => substr((string)$d, 0, 10), (array)($v['absences'] ?? []));
                $lateDates= array_map(fn($d) => substr((string)$d, 0, 10), (array)($v['lates'] ?? []));
                if (!empty($absDates)) $allDates = array_merge($allDates, $absDates);
                if (!empty($lateDates)) $allDates = array_merge($allDates, $lateDates);
                if (empty($allDates)) {
                    $allDates[] = substr($date, 0, 10);
                }
                $this->attendanceDataModel->markReportedAndNotified($sid, $allDates, $this->semester, $this->schoolYear);
            }
        }

        return $this->response->setJSON([
            'success' => true,
            'sent'    => $sent,
            'skipped' => $skipped,
            'errors'  => $errors,
            'details' => $details,
        ]);
    }


    /**
     * Wraps an email body with the app/Views/layout/email_layout.php view if present.
     * Falls back to raw body if the view is missing to avoid crashing.
     */
    private function renderWithEmailLayout(string $subject, string $bodyHtml): string
    {
        return view('emails/_wrap_layout', [
            'title'     => $subject,
            'body_html' => $bodyHtml,
        ], ['saveData' => false]);
    }

    private function chooseHigherSeverity(?array $a, ?array $b): ?array
    {
        if ($a && $b) {
            if ($a['severity'] === $b['severity']) {
                $prio = ['expel_notify' => 3, 'team_notify' => 2, 'auto_email' => 1];
                return ($prio[$a['action']] ?? 0) >= ($prio[$b['action']] ?? 0) ? $a : $b;
            }
            return $a['severity'] > $b['severity'] ? $a : $b;
        }
        return $a ?: $b;
    }

    // -------------- Helpers (dates & weeks) --------------

    private function attendanceReportedColumns(): array
    {
        if ($this->attendanceReportedColumns !== null) {
            return $this->attendanceReportedColumns;
        }
        $this->attendanceReportedColumns = [
            'is_reported' => $this->db->fieldExists('is_reported', 'attendance_data'),
            'reported'    => $this->db->fieldExists('reported', 'attendance_data'),
        ];
        return $this->attendanceReportedColumns;
    }

    private function applyUnreportedAttendanceFilter($qb): void
    {
        $cols = $this->attendanceReportedColumns();
        if (!empty($cols['is_reported'])) {
            $qb->where("(LOWER(TRIM(COALESCE(is_reported, ''))) NOT IN ('yes','1','true','y','on'))", null, false);
        }
        if (!empty($cols['reported'])) {
            $qb->where("(LOWER(TRIM(COALESCE(reported, ''))) NOT IN ('yes','1','true','y','on'))", null, false);
        }
        // Treat parent-reported rows as reported even if flags weren't updated
        $qb->where("(LOWER(COALESCE(reason, '')) NOT LIKE '%parent-reported%')", null, false)
           ->where("(LOWER(COALESCE(reason, '')) NOT LIKE '%parent reported%')", null, false);
        if ($this->db->tableExists('parent_attendance_reports')) {
            $qb->where("NOT EXISTS (
                SELECT 1 FROM parent_attendance_reports par
                WHERE par.student_id = attendance_data.student_id
                  AND DATE(par.report_date) = DATE(attendance_data.`date`)
                  AND par.type IN ('absent','late')
            )", null, false);
        }
    }

    private function getActiveWeeksFromAttendanceData(
        string $startYmd,
        string $endYmd,
        ?string $schoolYear = null,
        ?string $semester = null,
        ?array $weekRowsFallback = null
    ): array {
        $schoolYear = $schoolYear ?: $this->schoolYear;
        $semester   = ($semester !== null && $semester !== '') ? (string)$semester : null;
        $weeks = [];

        // Prefer explicit rows passed in (all statuses) to define active weeks for the window
        if (!empty($weekRowsFallback)) {
            foreach ($weekRowsFallback as $r) {
                $d = $r['date'] ?? null;
                if (!$d) continue;
                $ts = strtotime($d);
                if ($ts === false) continue;
                $ymd = date('Y-m-d', $ts);
                if ($ymd < $startYmd || $ymd > $endYmd) continue;
                $wk = date('o-\WW', $ts);
                $weeks[$wk] = true;
            }
        }

        // If still empty, fall back to open dates from attendance_data table
        if (empty($weeks)) {
            $builder = $this->db->table('attendance_data')
                ->select('DATE(`date`) AS ymd')
                ->where('school_year', $schoolYear)
                ->where('DATE(`date`) >=', $startYmd)
                ->where('DATE(`date`) <=', $endYmd)
                ->groupStart()
                ->where('reason', null)
                ->orGroupStart()
                ->notLike('reason', 'break')
                ->notLike('reason', 'cancel')
                ->groupEnd()
                ->groupEnd()
                ->groupBy('DATE(`date`)')
                ->orderBy('ymd', 'ASC');
            if ($semester !== null) {
                $builder->where('semester', $semester);
            }
            $rows = $builder->get()->getResultArray();

            foreach ($rows as $r) {
                $wk = date('o-\WW', strtotime($r['ymd']));
                $weeks[$wk] = true;
            }
        }

        $keys = array_keys($weeks);
        sort($keys, SORT_NATURAL);
        return $keys;
    }

    private function datesToWeekIndices(array $dates, array $weekIndex): array
    {
        $set = [];
        foreach ($dates as $ymd) {
            $wk = date('o-\WW', strtotime($ymd));
            if (isset($weekIndex[$wk])) {
                $set[$weekIndex[$wk]] = true;
            }
        }
        $idx = array_keys($set);
        sort($idx);
        return $idx;
    }

    private function windowWeeksForViolationCode(string $code): int
    {
        $code = strtoupper(trim($code));
        if ($code === '') {
            return 5;
        }
        if (str_starts_with($code, 'ABS_2') || str_starts_with($code, 'LATE_2')) {
            return 3;
        }
        if (str_starts_with($code, 'ABS_3') || str_starts_with($code, 'LATE_3') || str_starts_with($code, 'MIX')) {
            return 4;
        }
        if (str_starts_with($code, 'LATE_4')) {
            return 4;
        }
        if (str_starts_with($code, 'ABS_4')) {
            return 5;
        }
        return 5;
    }

    private function isoWeekStartYmd(string $weekKey): string
    {
        if (preg_match('/^(\d{4})-W(\d{2})$/', $weekKey, $m)) {
            return date('Y-m-d', strtotime($m[1] . '-W' . $m[2] . '-1'));
        }
        return date('Y-m-d');
    }

    private function activeWeekWindowStartYmd(
        string $endYmd,
        int $windowWeeks,
        string $schoolYear,
        ?string $semester
    ): string {
        $windowWeeks = max(1, $windowWeeks);
        [$syStart, $syEnd] = $this->deriveSchoolYearBounds($schoolYear ?: $this->schoolYear);
        $endBound = $endYmd;
        if ($syEnd !== '' && $endBound > $syEnd) {
            $endBound = $syEnd;
        }

        $activeWeekKeys = $this->getActiveWeeksFromAttendanceData($syStart, $endBound, $schoolYear, $semester, null);
        if (empty($activeWeekKeys)) {
            $days = ($windowWeeks * 7) - 1;
            return date('Y-m-d', strtotime($endBound . ' -' . $days . ' days'));
        }

        $activeWeekKeys = array_values($activeWeekKeys);
        $weekIndex = array_flip($activeWeekKeys);
        $endWeekKey = date('o-\WW', strtotime($endBound));
        if (!isset($weekIndex[$endWeekKey])) {
            $endWeekKey = null;
            for ($i = count($activeWeekKeys) - 1; $i >= 0; $i--) {
                $wkStart = $this->isoWeekStartYmd($activeWeekKeys[$i]);
                if ($wkStart <= $endBound) {
                    $endWeekKey = $activeWeekKeys[$i];
                    break;
                }
            }
            if ($endWeekKey === null) {
                $endWeekKey = end($activeWeekKeys);
            }
        }

        $currentIdx = $weekIndex[$endWeekKey] ?? (count($activeWeekKeys) - 1);
        $windowStartIdx = max(0, $currentIdx - ($windowWeeks - 1));
        $startWeekKey = $activeWeekKeys[$windowStartIdx] ?? $activeWeekKeys[0];
        return $this->isoWeekStartYmd($startWeekKey);
    }

    private function hasNConsecutiveItems(array $datesDesc, int $n, int $daysApart): bool
    {
        $N = count($datesDesc);
        if ($N < $n) return false;

        $dates = $datesDesc;
        sort($dates); // asc
        $run = 1;
        for ($i = 1; $i < $N; $i++) {
            $prev = new \DateTime($dates[$i - 1]);
            $curr = new \DateTime($dates[$i]);
            $diff = (int)$prev->diff($curr)->format('%a');
            if ($diff === $daysApart) {
                $run++;
                if ($run >= $n) return true;
            } else {
                $run = 1;
            }
        }
        return false;
    }

    private function hasNInWActiveWeeks(array $weekIdx, int $n, int $w): bool
    {
        $N = count($weekIdx);
        if ($N < $n) return false;

        $l = 0;
        for ($r = 0; $r < $N; $r++) {
            while ($weekIdx[$r] - $weekIdx[$l] > ($w - 1)) {
                $l++;
            }
            if (($r - $l + 1) >= $n) return true;
        }
        return false;
    }

    private function twoLatesOneAbsInWWeeks(array $lateIdx, array $absIdx, int $w): bool
    {
        if (count($lateIdx) < 2 || count($absIdx) < 1) return false;

        $L = $lateIdx;
        $A = $absIdx;
        $iL1 = 0;
        $iL2 = 1;
        $iA = 0;

        while ($iL2 < count($L)) {
            $windowStart = $L[$iL1];
            $windowEnd   = $windowStart + ($w - 1);

            if ($L[$iL2] > $windowEnd) {
                $iL1++;
                $iL2 = $iL1 + 1;
                continue;
            }

            while ($iA < count($A) && $A[$iA] < $windowStart) $iA++;
            if ($iA < count($A) && $A[$iA] <= $windowEnd) return true;

            $iL2++;
            if ($iL2 >= count($L)) {
                $iL1++;
                $iL2 = $iL1 + 1;
            }
        }
        return false;
    }

    /**
     * True only if the student has a late in EVERY active week from week 0 → currentWeekIdx.
     * Also requires at least 4 active weeks to have passed (guards single-week false positives).
     */
    private function latesCoverAllPassedWeeks(array $lateIdx, int $currentWeekIdx, int $minWeeks = 4): bool
    {
        if ($currentWeekIdx < 0) return false;                 // no active weeks yet
        $totalWeeksPassed = $currentWeekIdx + 1;               // weeks are 0-based
        if ($totalWeeksPassed < $minWeeks) return false;       // don’t escalate before 4 weeks total
        if (count($lateIdx) < $totalWeeksPassed) return false; // not enough late weeks to cover all

        $set = array_flip($lateIdx);                           // fast lookup
        for ($i = 0; $i <= $currentWeekIdx; $i++) {
            if (!isset($set[$i])) return false;                // any gap breaks the condition
        }
        return true;
    }

    private function alreadyReported(
        int $studentId,
        string $code,
        string $lastDate,
        ?string $semester = null,
        ?string $schoolYear = null
    ): bool {
        $semester   = ($semester !== null && $semester !== '') ? (string)$semester : null;
        $schoolYear = $schoolYear ?: $this->schoolYear;
        try {
            $bounds = $this->dayBounds($lastDate);
            $start  = $bounds[0];
            $end    = $bounds[1];

            $q = $this->attendanceTrackingModel
                ->where('student_id', $studentId)
                ->where('school_year', $schoolYear)
                ->where('date >=', $start)
                ->where('date <',  $end)
                ->like('reason', $code, 'both');
            if ($semester !== null) {
                $q->where('semester', $semester);
            }

            $row = $q->orderBy('date', 'DESC')->first();

            if (!$row) {
                $q2 = $this->attendanceTrackingModel
                    ->where('student_id', $studentId)
                    ->where('school_year', $schoolYear)
                    ->where('date >=', $start)
                    ->where('date <',  $end);
                if ($semester !== null) {
                    $q2->where('semester', $semester);
                }
                $row = $q2->orderBy('date', 'DESC')->first();
            }

            return $row ? (bool) ($row['is_notified'] ?? 0) : false;
        } catch (\Throwable $e) {
            log_message('debug', 'alreadyReported() error: ' . $e->getMessage());
            return false;
        }
    }

    public function notifiedViolationsView()
    {
        $syParam = $this->request->getGet('school_year');
        $semParam = $this->request->getGet('semester');
        $schoolYear = (is_string($syParam) && $syParam !== '') ? (string)$syParam : (string)$this->schoolYear;
        $semester   = (is_string($semParam) && $semParam !== '') ? (string)$semParam : (string)$this->semester;

        // 1️⃣ Get all notified records for this term from attendance_tracking
        $trackingData = $this->attendanceTrackingModel
            ->where('school_year', $schoolYear)
            ->where('semester', $semester)
            ->groupStart()
                ->where('is_notified', 1)
                ->orWhere('is_notified', '1')
                ->orWhere("LOWER(TRIM(COALESCE(is_notified, ''))) =", 'yes')
            ->groupEnd()
            ->orderBy('date', 'DESC')
            ->findAll();

        // 🔄 Fallback: if none found for the configured term, auto-switch to the latest notified term
        if (empty($trackingData)) {
            $latest = $this->attendanceTrackingModel
                ->select('school_year, semester')
                ->groupStart()
                    ->where('is_notified', 1)
                    ->orWhere('is_notified', '1')
                    ->orWhere("LOWER(TRIM(COALESCE(is_notified, ''))) =", 'yes')
                ->groupEnd()
                ->orderBy('date', 'DESC')
                ->orderBy('id', 'DESC')
                ->first();

            if ($latest) {
                $schoolYear = (string)($latest['school_year'] ?? $schoolYear);
                $semester   = (string)($latest['semester']   ?? $semester);

                $trackingData = $this->attendanceTrackingModel
                    ->where('school_year', $schoolYear)
                    ->where('semester', $semester)
                    ->groupStart()
                        ->where('is_notified', 1)
                        ->orWhere('is_notified', '1')
                        ->orWhere("LOWER(TRIM(COALESCE(is_notified, ''))) =", 'yes')
                    ->groupEnd()
                    ->orderBy('date', 'DESC')
                    ->findAll();
            }
        }

        // If no notified entries, show empty view
        if (empty($trackingData)) {
            return view('attendance/violations_notified', [
                'title'        => 'Notified Attendance Violations',
                'students'     => [],
                'school_year'  => $schoolYear,
                'semester'     => $semester,
                'show_actions' => false,
            ]);
        }

        // 2️⃣ Collect unique student IDs from tracking table
        $studentIds = array_unique(array_column($trackingData, 'student_id'));

        // 3️⃣ Get student records for those IDs
        $students = $this->studentModel
            ->whereIn('id', $studentIds)
            ->where('is_active', 1)
            ->findAll();

        $activeIdSet = array_flip(array_map(
            static fn($row) => (int)($row['id'] ?? 0),
            $students
        ));
        $trackingData = array_values(array_filter(
            $trackingData,
            static fn($row) => isset($activeIdSet[(int)($row['student_id'] ?? 0)])
        ));
        $studentIds = array_keys($activeIdSet);

        if (empty($trackingData)) {
            return view('attendance/violations_notified', [
                'title'        => 'Notified Attendance Violations',
                'students'     => [],
                'school_year'  => $schoolYear,
                'semester'     => $semester,
                'show_actions' => false,
            ]);
        }

        // 4️⃣ Build a map for faster merge
        $studentMap = [];
        foreach ($students as $s) {
            $studentMap[$s['id']] = $s;
        }

        // 4.5️⃣ Prefetch latest class/grade label per student for the selected school year
        $classByStudent = [];
        try {
            $rows = $this->db->table('student_class sc')
                ->select('sc.student_id, sc.class_section_id, cs.class_section_name, cs.class_id, c.class_name')
                ->join('classSection cs', 'cs.class_section_id = sc.class_section_id', 'left')
                ->join('classes c', 'c.id = cs.class_id', 'left')
                ->whereIn('sc.student_id', $studentIds)
                ->where('sc.school_year', $schoolYear)
                ->orderBy('sc.id', 'DESC') // latest assignment first
                ->get()->getResultArray();

            foreach ($rows as $row) {
                $sid = (int)($row['student_id'] ?? 0);
                if ($sid <= 0) continue;
                if (!isset($classByStudent[$sid])) {
                    $classByStudent[$sid] = [
                        'class_section_id'   => $row['class_section_id']   ?? null,
                        'class_section_name' => (string)($row['class_section_name'] ?? ''),
                        'class_id'           => $row['class_id']           ?? null,
                        'class_name'         => (string)($row['class_name']         ?? ''),
                    ];
                }
            }
        } catch (\Throwable $e) {
            // ignore enrichment failure
        }

        // 5️⃣ Merge tracking info with student info (and class/grade when available)
        $mergedData = [];
        foreach ($trackingData as $row) {
            $sid = (int)($row['student_id'] ?? 0);
            if (isset($studentMap[$sid])) {
                $merged = array_merge($studentMap[$sid], $row);
                // attach grade/class labels for view consumption
                if (isset($classByStudent[$sid])) {
                    $merged['class_section_name'] = (string)($classByStudent[$sid]['class_section_name'] ?? '');
                    $merged['class_name']         = (string)($classByStudent[$sid]['class_name'] ?? '');
                } else {
                    // fallback to registration_grade if present
                    if (!empty($merged['registration_grade'])) {
                        $merged['grade'] = (string)$merged['registration_grade'];
                    }
                }
                $mergedData[] = $merged;
            } else {
                // Skip rows with no active student record
                continue;
            }
        }

        // 6️⃣ Enrich with primary parent contact so the view can display inline info
        foreach ($mergedData as &$row) {
            $sid = (int)($row['student_id'] ?? $row['id'] ?? 0);
            if ($sid > 0) {
                try {
                    $p = $this->getPrimaryParentForStudent($sid);
                    if ($p) {
                        $row['parent_name']  = $p['parent_name'] ?? '';
                        $row['parent_email'] = $p['email']       ?? '';
                        $row['parent_phone'] = $p['phone']       ?? '';
                    }
                } catch (\Throwable $e) {
                    // ignore enrichment failure; view will fallback
                }
            }
        }
        unset($row);

        // 7️⃣ Send data to view
        return view('attendance/violations_notified', [
            'title'        => 'Notified Attendance Violations',
            'students'     => $mergedData, // tracking + students + parent info
            'school_year'  => $schoolYear,
            'semester'     => $semester,
        ]);
    }

    private function deriveSchoolYearBounds(string $schoolYear): array
    {
        if (!preg_match('/^(\d{4})-(\d{4})$/', $schoolYear, $m)) {
            $y      = (int) date('Y');
            $startY = (int) (date('n') >= 8 ? $y : $y - 1);
            return [sprintf('%d-08-01', $startY), sprintf('%d-07-31', $startY + 1)];
        }
        $startY = (int) $m[1];
        $endY   = (int) $m[2];
        return [sprintf('%d-08-01', $startY), sprintf('%d-07-31', $endY)];
    }

    private function buildTemplateContext(array $v, ?array $parent = null): array
    {
        $studentName = (string)($v['name'] ?? '');
        $incident    = (string)($v['last_date'] ?? date('Y-m-d'));
        $p = $parent ?: $this->getPrimaryParentForStudent((int)($v['id'] ?? 0)) ?? [];

        return [
            '{{parent_name}}'     => (string)($p['parent_name'] ?? 'Parent/Guardian'),
            '{{student_name}}'    => $studentName,
            '{{incident_date}}'   => $incident,
            '{{school_phone}}'    => '978-364-0219',
            '{{voicemail_phone}}' => (string)($p['phone'] ?? '—'),
            '{{class_time}}'      => '10:00 AM',
        ];
    }

    private function renderTemplate(string $code, string $variant, array $context): ?array
    {
        $row = $this->attendanceEmailTemplateModel->getTemplate($code, $variant);
        if (!$row) {
            $row = $this->attendanceEmailTemplateModel->getTemplate($code, 'default');
        }
        if (!$row) return null;

        // CI4 won't interpolate placeholders automatically
        log_message('debug', 'Email template resolved: code=' . $code . ' variant=' . ($row['variant'] ?? 'unknown'));

        $subject = strtr($row['subject'], $context);
        $body    = strtr($row['body_html'], $context);
        return [$subject, $body];
    }

    private function buildFallbackTemplate(string $code, string $variant, array $context): array
    {
        $studentName = (string) ($context['{{student_name}}'] ?? 'the student');
        $incident    = (string) ($context['{{incident_date}}'] ?? date('Y-m-d'));
        $parentName  = (string) ($context['{{parent_name}}'] ?? 'Parent/Guardian');
        $phone       = (string) ($context['{{school_phone}}'] ?? 'the school office');
        $voicemail   = (string) ($context['{{voicemail_phone}}'] ?? 'your voicemail');

        $subject = match (true) {
            str_starts_with($code, 'ABS_1')  => 'Attendance Notice: Unreported Absence',
            str_starts_with($code, 'ABS_2')  => 'Attendance Follow-Up: Two Consecutive Absences',
            str_starts_with($code, 'ABS_3')  => 'Attendance Follow-Up: Three Absences',
            str_starts_with($code, 'LATE_2') => 'Attendance Notice: Repeated Lateness',
            str_starts_with($code, 'LATE_3'),
            str_starts_with($code, 'LATE_4'),
            str_starts_with($code, 'MIX')    => 'Attendance Follow-Up: Repeated Lateness and Absence',
            default                          => 'Attendance Notice',
        };

        $intro = "<p>Insha Allah this email finds you well";
        if ($variant === 'answered') {
            $intro .= ", <strong>{$parentName}</strong>. Jazakum Allahu khayran for taking the time to speak with us today.</p>";
        } elseif ($variant === 'no_answer') {
            $intro .= ".</p><p>We tried to reach you but could not connect and left a voice message at <strong>{$voicemail}</strong>.</p>";
        } else {
            $intro .= ".</p>";
        }

        $details = match (true) {
            str_starts_with($code, 'ABS_1')
                => "<p>This is to let you know that <strong>{$studentName}</strong> was absent on <strong>{$incident}</strong> without prior notice.</p>",
            str_starts_with($code, 'ABS_2')
                => "<p>This is a reminder that <strong>{$studentName}</strong> has had repeated absences, most recently on <strong>{$incident}</strong>, without prior notice.</p>",
            str_starts_with($code, 'ABS_3')
                => "<p>This is a reminder that <strong>{$studentName}</strong> has been absent three times, most recently on <strong>{$incident}</strong>, without prior notice.</p>",
            str_starts_with($code, 'LATE_2')
                => "<p>This is a reminder that <strong>{$studentName}</strong> has been late multiple times, most recently on <strong>{$incident}</strong>.</p>",
            str_starts_with($code, 'LATE_3'),
            str_starts_with($code, 'LATE_4')
                => "<p>This is a follow-up that <strong>{$studentName}</strong> has had repeated lateness concerns, most recently on <strong>{$incident}</strong>.</p>",
            str_starts_with($code, 'MIX')
                => "<p>This is a follow-up that <strong>{$studentName}</strong> has had a mix of repeated lateness and absence concerns, most recently on <strong>{$incident}</strong>.</p>",
            default
                => "<p>We'd like to inform you about <strong>{$studentName}</strong>'s recent attendance concern dated <strong>{$incident}</strong>.</p>",
        };

        $closing = "<p>If your child will be absent or late due to illness or family commitments, please let us know ahead of time.</p>"
            . "<p>You can email/call/text us at <strong>{$phone}</strong>.</p>";

        return [$subject, $intro . $details . $closing];
    }


    public function compose()
    {
        $sid     = (int)($this->request->getGet('student_id') ?? 0);
        $code    = (string)($this->request->getGet('code') ?? 'ABS_1');
        $variant = (string)($this->request->getGet('variant') ?? 'default');
        $incident = (string)($this->request->getGet('incident_date') ?? '');

        if ($sid <= 0) {
            return redirect()->to(site_url('attendance/violations'))
                ->with('error', 'Missing student_id.');
        }

        $student   = $this->studentModel->find($sid) ?? [];
        $parent    = $this->getPrimaryParentForStudent($sid);
        $secondary = $this->getSecondaryParentForStudent($sid);

        // Prefer incident date from query if valid (YYYY-MM-DD), else today
        $lastDate = (preg_match('/^\d{4}-\d{2}-\d{2}$/', $incident)) ? $incident : date('Y-m-d');
        $violation = [
            'id'         => $sid,
            'name'       => trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? '')),
            'last_date'  => $lastDate,
            'violation'  => $code,
            'violation_code' => $code,
        ];
        $ctx = $this->buildTemplateContext($violation, $parent);
        $rendered = $this->renderTemplate($code, $variant, $ctx);

        if (!$rendered) {
            log_message('warning', 'Attendance email template missing; using fallback compose body. code=' . $code . ' variant=' . $variant);
            $rendered = $this->buildFallbackTemplate($code, $variant, $ctx);
        }

        [$subject, $body] = $rendered;

        return view('attendance/compose_email', [
            'title'        => 'Compose Attendance Email',
            'student_id'   => $sid,
            'student_name' => $violation['name'],
            'parent_email' => (string)($parent['email'] ?? ''),
            'parent_name'  => (string)($parent['parent_name'] ?? ''),
            'secondary_email' => (string)($secondary['email'] ?? ''),
            'secondary_name'  => (string)($secondary['parent_name'] ?? ''),
            'code'         => $code,
            'variant'      => $variant,
            'subject'      => $subject,
            'body_html'    => $body,
            'semester'     => $this->semester,
            'school_year'  => $this->schoolYear,
            'incident_date'=> $lastDate,
        ]);
    }

    public function sendEmailManual()
{
    if (! $this->request->is('post')) {
        return redirect()->to(site_url('attendance/violations'));
    }

    // Validate
    $rules = [
        'student_id'    => 'required|is_natural_no_zero',
        'to'            => 'required|valid_email',
        'subject'       => 'required|string',
        // We are accepting PLAIN TEXT in this field name (kept as body_html for compatibility)
        'body_html'     => 'required|string',
        'code'          => 'required|string',
        'variant'       => 'permit_empty|string',
        'incident_date' => 'permit_empty|valid_date[Y-m-d]',
        'return_url'    => 'permit_empty|string',
    ];
    if (! $this->validate($rules)) {
        return redirect()->back()->withInput()
            ->with('error', implode(' ', $this->validator->getErrors()));
    }

    // Inputs
    $sid       = (int) $this->request->getPost('student_id');
    $to        = trim((string) $this->request->getPost('to'));
    $subject   = (string) $this->request->getPost('subject');
    $plainBody = (string) $this->request->getPost('body_html'); // plain text by design
    $code      = (string) $this->request->getPost('code');
    $variant   = (string) ($this->request->getPost('variant') ?? 'default');
    $returnUrl = (string) ($this->request->getPost('return_url') ?? site_url('attendance/violations'));

    // Normalize incident date strictly to YYYY-MM-DD; do NOT invent "today"
    $ymdRaw = (string) ($this->request->getPost('incident_date') ?? '');
    $ymd    = $ymdRaw !== '' ? substr($ymdRaw, 0, 10) : ''; // '' means unknown

    // Treat body_html as HTML when it contains tags; otherwise convert plain text to basic HTML
    // This prevents recipients from seeing raw tags when composing with the rich-text editor.
    if ($plainBody !== '' && preg_match('/<[^>]+>/', $plainBody)) {
        $safeHtmlBody = $plainBody; // HTML already
    } elseif ($plainBody !== '' && stripos($plainBody, '&lt;') !== false) {
        // Hidden field might contain escaped HTML if JS didn’t run; decode and re-check
        $decoded = html_entity_decode($plainBody, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
        $safeHtmlBody = preg_match('/<[^>]+>/', $decoded) ? $decoded : nl2br(esc($plainBody), false);
    } else {
        // Plain text fallback: escape + line breaks
        $safeHtmlBody = nl2br(esc($plainBody), false);
    }

    // Wrap with your email layout
    $wrapped = $this->renderWithEmailLayout($subject, $safeHtmlBody);

    // Resolve term (prefer controller properties if set)
    $semester   = $this->semester   ?? (new \App\Models\ConfigurationModel())->getConfig('semester');
    $schoolYear = $this->schoolYear ?? (new \App\Models\ConfigurationModel())->getConfig('school_year');

    try {
        // Send to primary and (if available) to second parent as separate email
        $mailer = new EmailController();
        $anyOk = false;

        // Primary
        $ok = $mailer->sendEmail($to, $subject, $wrapped);
        $this->logNotification(
            $sid,
            $code,
            $ymd,
            'email',
            $to,
            $subject,
            $ok ? 'sent' : 'failed',
            $ok ? 'OK' : 'sendEmail returned false'
        );
        if ($ok) {
            $anyOk = true;
        }

        // Secondary
        $sec = $this->getSecondaryParentForStudent($sid);
        $to2 = trim((string)($sec['email'] ?? ''));
        if ($to2 !== '' && strcasecmp($to2, $to) !== 0) {
            if (! $this->notificationAlreadySent($sid, $code, $ymd, 'email', $to2)) {
                $ok2 = $mailer->sendEmail($to2, $subject, $wrapped);
                $this->logNotification(
                    $sid,
                    $code,
                    $ymd,
                    'email',
                    $to2,
                    $subject,
                    $ok2 ? 'sent' : 'failed',
                    $ok2 ? 'OK' : 'sendEmail returned false'
                );
                if ($ok2) {
                    $anyOk = true;
                }
            }
        }

        if (! $anyOk) {
            return redirect()->back()->withInput()->with('error', 'Failed to send email.');
        }

        // === Post-send bookkeeping ===

        // 1) TRACKING TABLE: mark notified + bump counter
        $this->attendanceTrackingModel->markRuleAsNotified(
            $sid,
            $code,
            $ymd !== '' ? $ymd : date('Y-m-d'), // if not supplied, use today as a fallback for the tracking row only
            $semester,
            $schoolYear
        );

        // 2) ATTENDANCE DATA: mark all dates in the violation as reported + notified
        $violationDates = $this->getViolationDatesForStudent($sid, $code, $ymd);
        $this->attendanceDataModel->markReportedAndNotified($sid, $violationDates, $semester, $schoolYear);

        // Queue comment modal on the violations page
        try {
            $stu = $this->studentModel->find($sid) ?: [];
            $stuName = trim(($stu['firstname'] ?? '') . ' ' . ($stu['lastname'] ?? ''));
            session()->set('open_comment_modal', [
                'student_id'   => $sid,
                'student_name' => $stuName,
                'code'         => $code,
                'incident_date'=> $ymd !== '' ? $ymd : date('Y-m-d'),
                'to'           => $to,
                'subject'      => $subject,
                'variant'      => $variant,
            ]);
        } catch (\Throwable $e) {
            log_message('debug', 'Could not set open_comment_modal: ' . $e->getMessage());
        }

        $msg = 'Email sent to ' . esc($to);
        if (!empty($to2) && strcasecmp($to2, $to) !== 0) {
            $msg .= ' (CC ' . esc($to2) . ')';
        }
        return redirect()->to($returnUrl)->with('message', $msg);
    } catch (\Throwable $e) {
        $this->logNotification($sid, $code, $ymd, 'email', $to, $subject, 'failed', $e->getMessage());
        return redirect()->back()->withInput()->with('error', 'Error: ' . $e->getMessage());
    }
}


    /**
     * Convert user-entered plain text to safe HTML.
     * - Escapes all HTML
     * - Splits on blank lines into <p> paragraphs
     * - Converts single newlines to <br>
     * - Auto-links http/https URLs
     */
    private function plainTextToHtml(string $text): string
    {
        // Normalize line endings and trim
        $text = preg_replace("/\r\n?/", "\n", trim($text));

        if ($text === '') {
            return '<p></p>';
        }

        // Escape everything first
        $escaped = htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');

        // Auto-link URLs (http/https)
        $escaped = preg_replace_callback(
            '#\bhttps?://[^\s<]+#i',
            static function ($m) {
                $url = $m[0];
                $urlAttr = htmlspecialchars($url, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
                return '<a href="' . $urlAttr . '" target="_blank" rel="noopener noreferrer">' . $url . '</a>';
            },
            $escaped
        );

        // Split into paragraphs by blank lines
        $paragraphs = preg_split("/\n{2,}/", $escaped);

        // Turn single newlines into <br> inside each paragraph
        $paragraphs = array_map(static function ($p) {
            $p = preg_replace("/\n/", "<br>", $p);
            return '<p>' . $p . '</p>';
        }, $paragraphs);

        return implode("\n", $paragraphs);
    }

    private function renderAutoEmailFor(array $v): ?array
    {
        // $v is each element in $toSend (from your sendAutoEmails)
        $code  = (string)($v['violation_code'] ?? $v['code'] ?? '');
        $sid   = (int)($v['id'] ?? 0);
        $p     = $this->getPrimaryParentForStudent($sid);

        $ctx   = $this->buildTemplateContext($v, $p);
        $tpl   = $this->renderTemplate($code, 'default', $ctx);

        if (!$tpl) {
            // fallback generic
            $subject = ($code === 'ABS_1') ? 'Attendance Notice: Unreported Absence'
                : (($code === 'LATE_2') ? 'Attendance Notice: Repeated Lateness' : 'Attendance Notice');
            $html = "<p>Dear " . esc($p['parent_name'] ?? 'Parent/Guardian') . ",</p>"
                . "<p>We'd like to inform you about your student's recent attendance:</p>"
                . "<ul>"
                . "<li><strong>Student:</strong> " . esc((string)($v['name'] ?? '')) . "</li>"
                . "<li><strong>Issue:</strong> " . esc((string)($v['violation'] ?? '')) . "</li>"
                . "<li><strong>As of:</strong> " . esc((string)($v['last_date'] ?? '')) . "</li>"
                . "</ul>"
                . "<p>If you have any questions, please contact the school office.</p>";
            return [$subject, $html];
        }
        return $tpl;
    }

    public function parentsInfo()
    {
        $studentId = (int)($this->request->getGet('student_id') ?? 0);
        if ($studentId <= 0) {
            return $this->response->setStatusCode(422)->setJSON(['error' => 'Missing student_id']);
        }

        try {
            $stu = $this->db->table('students')
                ->select('id, parent_id, school_year, semester')
                ->where('id', $studentId)
                ->get()->getRowArray();

            if (!$stu) {
                return $this->response->setStatusCode(404)->setJSON(['error' => 'Student not found']);
            }

            $primaryId  = (int)$stu['parent_id'];
            $schoolYear = $stu['school_year'] ?: ($this->schoolYear ?? null);

            $u1 = $this->db->table('users')
                ->select('id, firstname, lastname, email, cellphone')
                ->where('id', $primaryId)
                ->get()->getRowArray();

            $primary = $u1 ? [
                'id'        => (int)$u1['id'],
                'firstname' => (string)$u1['firstname'],
                'lastname'  => (string)$u1['lastname'],
                'email'     => (string)$u1['email'],
                'cellphone' => (string)$u1['cellphone'],
            ] : null;

            $pb = $this->db->table('parents')
                ->where('firstparent_id', $primaryId);

            if (!empty($schoolYear)) {
                $pb->where('school_year', $schoolYear);
            }

            $pRow = $pb->orderBy('updated_at', 'DESC')->limit(1)->get()->getRowArray();

            $secondary = null;
            if ($pRow) {
                $secondId = (int)($pRow['secondparent_id'] ?? 0);

                if ($secondId > 0) {
                    $u2 = $this->db->table('users')
                        ->select('id, firstname, lastname, email, cellphone')
                        ->where('id', $secondId)
                        ->get()->getRowArray();

                    if ($u2) {
                        $secondary = [
                            'id'        => (int)$u2['id'],
                            'firstname' => (string)$u2['firstname'],
                            'lastname'  => (string)$u2['lastname'],
                            'email'     => (string)$u2['email'],
                            'cellphone' => (string)$u2['cellphone'],
                        ];
                    }
                }

                if (!$secondary) {
                    $fallbackEmail = (string)($pRow['secondparent_email'] ?? '');
                    $fallbackPhone = (string)($pRow['secondparent_phone'] ?? '');
                    $fallbackFirst = (string)($pRow['secondparent_firstname'] ?? '');
                    $fallbackLast  = (string)($pRow['secondparent_lastname'] ?? '');

                    if ($fallbackEmail || $fallbackPhone || $fallbackFirst || $fallbackLast) {
                        $secondary = [
                            'id'        => $secondId ?: null,
                            'firstname' => $fallbackFirst,
                            'lastname'  => $fallbackLast,
                            'email'     => $fallbackEmail,
                            'cellphone' => $fallbackPhone,
                        ];
                    }
                }
            }

            return $this->response->setJSON([
                'primary'   => $primary,
                'secondary' => $secondary,
            ]);
        } catch (\Throwable $e) {
            log_message('error', 'parentsInfo() failed: ' . $e->getMessage());
            return $this->response->setStatusCode(500)->setJSON(['error' => 'Server error']);
        }
    }

    /**
     * Collect all dates involved in a violation for a student.
     * Uses provided incident date and recent attendance rows to gather unreported absences/lates.
     */
    private function getViolationDatesForStudent(int $studentId, string $code, ?string $incidentDate = null): array
    {
        $code = strtoupper(trim($code));
        $statuses = ['absent', 'late'];
        if (str_starts_with($code, 'ABS')) {
            $statuses = ['absent'];
        } elseif (str_starts_with($code, 'LATE')) {
            $statuses = ['late'];
        } elseif (str_starts_with($code, 'MIX')) {
            $statuses = ['absent', 'late'];
        }

        return $this->attendanceDataModel->getRecentUnreportedDates(
            $studentId,
            $statuses,
            $incidentDate,
            35,
            $this->semester,
            $this->schoolYear
        );
    }

    /**
     * Save a comment/note after notifying a parent about an attendance case.
     * Posts from the modal in violations_pending view.
     */
    public function saveNotificationNote()
    {
        if (! $this->request->is('post')) {
            return redirect()->to(site_url('attendance/violations'));
        }

        $rules = [
            'student_id'    => 'required|is_natural_no_zero',
            'code'          => 'required|string',
            'note'          => 'required|string',
            'incident_date' => 'permit_empty|valid_date[Y-m-d]',
            'return_url'    => 'permit_empty|string',
            'to'            => 'permit_empty|valid_email',
            'subject'       => 'permit_empty|string',
            'variant'       => 'permit_empty|string',
        ];
        if (! $this->validate($rules)) {
            return redirect()->back()->withInput()->with('error', implode(' ', $this->validator->getErrors()));
        }

        $sid       = (int) $this->request->getPost('student_id');
        $code      = (string) $this->request->getPost('code');
        $note      = trim((string) $this->request->getPost('note'));
        $ymdRaw    = (string) ($this->request->getPost('incident_date') ?? '');
        $ymd       = $ymdRaw !== '' ? substr($ymdRaw, 0, 10) : '';
        $returnUrl = (string) ($this->request->getPost('return_url') ?? site_url('attendance/violations'));

        // Find corresponding tracking row for that student/day (prefer matching reason/code)
        try {
            $day  = $ymd !== '' ? $ymd : date('Y-m-d');
            [$start, $end] = $this->dayBounds($day);

            $qb = $this->attendanceTrackingModel
                ->where('student_id', $sid)
                ->where('semester', $this->semester)
                ->where('school_year', $this->schoolYear)
                ->where('date >=', $start)
                ->where('date <',  $end)
                ->orderBy('date', 'DESC');

            // Prefer a row whose reason contains the code
            $row = $this->attendanceTrackingModel
                ->where('student_id', $sid)
                ->where('semester', $this->semester)
                ->where('school_year', $this->schoolYear)
                ->where('date >=', $start)
                ->where('date <',  $end)
                ->like('reason', $code, 'both')
                ->orderBy('date', 'DESC')
                ->first();

            if (! $row) {
                $row = $qb->first();
            }

            if ($row && !empty($row['id'])) {
                $updates = [
                    'note'        => $note,
                    'is_notified' => 1,
                    'updated_at'  => date('Y-m-d H:i:s'),
                ];
                // Ensure counter exists
                $this->attendanceTrackingModel->builder()
                    ->where('id', (int)$row['id'])
                    ->set('notif_counter', 'COALESCE(notif_counter,0)+1', false)
                    ->update();
                $this->attendanceTrackingModel->update((int)$row['id'], $updates);
            } else {
                // No row exists for that day — create a minimal one to hold the note
                $this->attendanceTrackingModel->insert([
                    'student_id'    => $sid,
                    'date'          => $start,
                    'is_reported'   => 0,
                    'reason'        => $code,
                    'is_notified'   => 1,
                    'notif_counter' => 1,
                    'semester'      => $this->semester,
                    'school_year'   => $this->schoolYear,
                    'note'          => $note,
                    'created_at'    => date('Y-m-d H:i:s'),
                    'updated_at'    => date('Y-m-d H:i:s'),
                ]);
            }

            // One-time modal context — clear so it won't re-open on refresh
            try { session()->remove('open_comment_modal'); } catch (\Throwable $e) {}

            return redirect()->to($returnUrl)->with('message', 'Note saved.');
        } catch (\Throwable $e) {
            return redirect()->back()->withInput()->with('error', 'Failed to save note: ' . $e->getMessage());
        }
    }
}
