<?php

namespace App\Services;

use App\Models\AttendanceTracking;
use App\Models\StudentClass;
use App\Models\Student;
use App\Models\Configuration;
use App\Models\AttendanceData;
use App\Models\ParentNotification;
use App\Models\AttendanceEmailTemplate;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Throwable;

class AttendanceTrackingService
{
    protected string $semester;
    protected string $schoolYear;
    protected array $debugCompute = [];
    protected ?array $attendanceReportedColumns = null;

    public function __construct(
        protected Student $studentModel,
        protected StudentClass $studentClassModel,
        protected AttendanceData $attendanceDataModel,
        protected AttendanceTracking $attendanceTrackingModel,
        protected Configuration $configModel,
        protected ParentNotification $notificationModel,
        protected AttendanceEmailTemplate $attendanceEmailTemplateModel,
        protected AttendanceMailerService $attendanceMailerService
    ) {
        $this->semester = (string) ($this->configModel->getConfig('semester') ?? '');
        $this->schoolYear = (string) ($this->configModel->getConfig('school_year') ?? '');
    }

    public function getPendingViolations(?string $schoolYearParam = null, ?string $semesterParam = null): array
    {
        $schoolYear = filled($schoolYearParam) ? (string) $schoolYearParam : (string) $this->schoolYear;
        $semester   = filled($semesterParam) ? (string) $semesterParam : null; // disabled like CI4

        $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,
        ];

        $classStudentsQuery = $this->studentClassModel->query();
        if (method_exists($this->studentClassModel, 'scopeActive')) {
            $classStudentsQuery->active();
        }

        $classStudents = $classStudentsQuery
            ->where('school_year', $schoolYear)
            ->get()
            ->toArray();

        $debugInfo['class_students'] = count($classStudents);

        if (empty($classStudents)) {
            $latestTerm = DB::table('student_class')
                ->select('school_year', 'semester')
                ->orderByDesc('id')
                ->first();

            if ($latestTerm && !empty($latestTerm->school_year)) {
                $schoolYear = (string) $latestTerm->school_year;
                if (!empty($latestTerm->semester)) {
                    $semester = (string) $latestTerm->semester;
                }

                $classStudentsQuery = $this->studentClassModel->query();
                if (method_exists($this->studentClassModel, 'scopeActive')) {
                    $classStudentsQuery->active();
                }

                $classStudents = $classStudentsQuery
                    ->where('school_year', $schoolYear)
                    ->get()
                    ->toArray();
            }
        }

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

        if (empty($studentIds) && empty($studentCodes)) {
            $attendanceStudents = DB::table('attendance_data')
                ->selectRaw('DISTINCT student_id')
                ->when(!empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear))
                ->orderBy('student_id')
                ->get();

            foreach ($attendanceStudents as $r) {
                $raw = $r->student_id ?? null;
                if (is_numeric($raw)) {
                    $studentIds[] = (int) $raw;
                } elseif (is_string($raw) && $raw !== '') {
                    $studentCodes[] = trim($raw);
                }
            }
        }

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

        if (!empty($studentIds)) {
            $inactiveRows = $this->studentModel->query()
                ->select('id')
                ->whereIn('id', $studentIds)
                ->where('is_active', 0)
                ->get()
                ->toArray();

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

        if (!empty($studentCodes)) {
            $inactiveCodeRows = $this->studentModel->query()
                ->select('school_id')
                ->whereIn('school_id', $studentCodes)
                ->where('is_active', 0)
                ->get()
                ->toArray();

            $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 [
                'students' => [],
                'school_year' => $schoolYear,
                'semester' => $semester,
                'error' => 'No student identifiers found for the current school year.',
                'debug' => $debugInfo,
            ];
        }

        $students = [];
        if (!empty($studentIds)) {
            $students = $this->studentModel->query()
                ->whereIn('id', $studentIds)
                ->where('is_active', 1)
                ->get()
                ->toArray();
        }

        if (!empty($studentCodes)) {
            $byCode = $this->studentModel->query()
                ->whereIn('school_id', $studentCodes)
                ->where('is_active', 1)
                ->get()
                ->toArray();

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

        $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)) ?: random_int(1000000, 1999999);
                $students[] = [
                    'id' => $virtualId,
                    'school_id' => $code,
                    'firstname' => $code,
                    'lastname' => '',
                    'parent_id' => 0,
                    'class_name' => '',
                ];
            }
        }

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

        $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) {
            if (is_numeric($sidOrCode)) {
                $sid = (int) $sidOrCode;
                if ($sid > 0 && !isset($existingIds[$sid])) {
                    $students[] = [
                        'id' => $sid,
                        'school_id' => (string) $sid,
                        'firstname' => (string) $sid,
                        'lastname' => '',
                        'parent_id' => 0,
                        'class_name' => '',
                    ];
                    $existingIds[$sid] = true;
                }
            } else {
                $code = (string) $sidOrCode;
                if ($code !== '' && !isset($studentCodeToId[$code])) {
                    $virtualId = abs(crc32($code)) ?: random_int(2000000, 2999999);
                    $students[] = [
                        'id' => $virtualId,
                        'school_id' => $code,
                        'firstname' => $code,
                        'lastname' => '',
                        'parent_id' => 0,
                        'class_name' => '',
                    ];
                    $studentCodeToId[$code] = $virtualId;
                    $existingIds[$virtualId] = true;
                }
            }
        }

        $today = Carbon::today();
        [$syStart, $syEnd] = $this->deriveSchoolYearBounds($schoolYear);
        $start = Carbon::parse($syStart)->startOfDay();
        $rangeEnd = Carbon::parse(min($syEnd, $today->format('Y-m-d')))->startOfDay();
        $endEx = $rangeEnd->copy()->addDay();

        $studentCodes = array_keys($studentCodeToId);

        $qb = $this->attendanceDataModel->query()
            ->select([
                'id',
                'student_id',
                'class_id',
                'class_section_id',
                'date',
                'status',
                'reason',
                'school_year',
                'semester',
                'is_reported',
                'is_notified',
            ])
            ->when(!empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear))
            ->when(!empty($semester), fn ($q) => $q->where('semester', $semester))
            ->where(function ($q) use ($studentIds, $studentCodes) {
                if (!empty($studentIds)) {
                    $q->whereIn('student_id', $studentIds);
                }
                if (!empty($studentCodes)) {
                    if (!empty($studentIds)) {
                        $q->orWhereIn('student_id', $studentCodes);
                    } else {
                        $q->whereIn('student_id', $studentCodes);
                    }
                }
            });

        $this->applyUnreportedAttendanceFilter($qb);

        $qb->whereRaw("(LOWER(TRIM(COALESCE(is_notified, ''))) NOT IN ('yes','1'))")
            ->whereRaw("LOWER(TRIM(status)) IN ('absent','late')")
            ->where('date', '>=', $start->format('Y-m-d'))
            ->where('date', '<', $endEx->format('Y-m-d'));

        $termRows = $qb->orderByDesc('date')->get()->toArray();

        if (empty($termRows)) {
            $latestAttendanceTerm = DB::table('attendance_data')
                ->select('school_year', 'semester', 'date')
                ->when(!empty($studentIds), fn ($q) => $q->whereIn('student_id', $studentIds))
                ->orderByDesc('date')
                ->first();

            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 = Carbon::parse($syStart)->startOfDay();
                $rangeEnd = Carbon::parse(min($syEnd, $today->format('Y-m-d')))->startOfDay();
                $endEx = $rangeEnd->copy()->addDay();

                $qb = $this->attendanceDataModel->query()
                    ->select([
                        'id',
                        'student_id',
                        'class_id',
                        'class_section_id',
                        'date',
                        'status',
                        'reason',
                        'school_year',
                        'semester',
                        'is_reported',
                        'is_notified',
                    ])
                    ->when(!empty($schoolYear), fn ($q) => $q->where('school_year', $schoolYear))
                    ->when(!empty($semester), fn ($q) => $q->where('semester', $semester))
                    ->where(function ($q) use ($studentIds, $studentCodes) {
                        if (!empty($studentIds)) {
                            $q->whereIn('student_id', $studentIds);
                        }
                        if (!empty($studentCodes)) {
                            if (!empty($studentIds)) {
                                $q->orWhereIn('student_id', $studentCodes);
                            } else {
                                $q->whereIn('student_id', $studentCodes);
                            }
                        }
                    });

                $this->applyUnreportedAttendanceFilter($qb);

                $qb->whereRaw("(LOWER(TRIM(COALESCE(is_notified, ''))) NOT IN ('yes','1'))")
                    ->whereRaw("LOWER(TRIM(status)) IN ('absent','late')")
                    ->where('date', '>=', $start->format('Y-m-d'))
                    ->where('date', '<', $endEx->format('Y-m-d'));

                $termRows = $qb->orderByDesc('date')->get()->toArray();
            }

            if (empty($termRows)) {
                return [
                    'students' => [],
                    'school_year' => $schoolYear,
                    'semester' => $semester,
                    'debug' => $debugInfo,
                ];
            }
        }

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

            try {
                $d = Carbon::parse($dStr);
            } catch (Throwable) {
                continue;
            }

            if ($d->lt($start) || !$d->lt($endEx)) {
                continue;
            }

            $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) {
                    $sid = abs(crc32($code)) ?: random_int(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'] = $d->format('Y-m-d');
            $attendanceRows[] = $row;
        }

        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)) {
            return [
                'students' => [],
                'school_year' => $schoolYear,
                'semester' => $semester,
                'debug' => $debugInfo,
            ];
        }

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

        $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 [
            'students' => $violations,
            'school_year' => $schoolYear,
            'semester' => $semester,
            'debug' => $debugInfo,
        ];
    }

    public function record(array $data): array
    {
        $sid = (int) $data['student_id'];
        $ymd = substr((string) $data['date'], 0, 10);
        $semester = $data['semester'] ?? $this->semester;
        $schoolYear = $data['school_year'] ?? $this->schoolYear;
        $subject = $data['subject_type'] ?? 'Absent';

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

        if (!$parentEmail) {
            $parent = $this->getPrimaryParentForStudent($sid) ?? [];
            $parentEmail = $parent['email'] ?? '';
            $parentName = $parent['parent_name'] ?? '';
        }

        if (!$parentEmail) {
            return [
                'success' => false,
                'message' => 'No parent email found for this student.',
                'status' => 422,
            ];
        }

        [$start, $end] = $this->dayBounds($ymd);

        $row = $this->attendanceTrackingModel->query()
            ->where('student_id', $sid)
            ->where('semester', $semester)
            ->where('school_year', $schoolYear)
            ->where('date', '>=', $start)
            ->where('date', '<', $end)
            ->orderByDesc('date')
            ->first();

        $row = $row?->toArray();

        $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 = method_exists($this->studentClassModel, 'getClassSectionsByStudentId')
            ? $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' => now()->toDateTimeString(),
            'date' => $ymd,
            'subject' => $subject,
            'consequence' => $consequence ?: 'follow_up',
        ];

        try {
            $this->attendanceMailerService->queueAttendanceEvent($payload);

            if ($row && !empty($row['id']) && method_exists($this->attendanceTrackingModel, 'markAsNotified')) {
                $this->attendanceTrackingModel->markAsNotified((int) $row['id']);
            }

            return [
                'success' => true,
                'message' => 'Notification queued.',
                'data' => $payload,
            ];
        } catch (Throwable $e) {
            Log::error('Attendance record queue failed: ' . $e->getMessage());

            return [
                'success' => false,
                'message' => $e->getMessage(),
                'status' => 500,
            ];
        }
    }

    public function getStudentCase(
        int $studentId,
        ?string $codeParam = null,
        ?string $incidentDate = null,
        bool $includeNotified = false,
        bool $pendingOnly = false,
        ?string $schoolYearFilter = null,
        ?string $semesterFilter = null
    ): array {
        $student = $this->studentModel->query()->find($studentId);
        if (!$student) {
            return [
                'success' => false,
                'message' => 'Student not found',
                'status' => 404,
            ];
        }

        $student = $student->toArray();
        $student['name'] = trim(($student['firstname'] ?? '') . ' ' . ($student['lastname'] ?? ''));

        $codeParam = strtoupper((string) ($codeParam ?? ''));
        $incidentDate = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) $incidentDate) ? (string) $incidentDate : '';

        $statusFilter = ['absent', 'late'];
        if (Str::startsWith($codeParam, 'ABS')) {
            $statusFilter = ['absent'];
        } elseif (Str::startsWith($codeParam, 'LATE')) {
            $statusFilter = ['late'];
        } elseif (Str::startsWith($codeParam, 'MIX')) {
            $statusFilter = ['absent', 'late'];
        }

        $schoolYear = $schoolYearFilter
            ?? ($incidentDate !== '' ? $this->schoolYearForDate($incidentDate) : (string) $this->schoolYear);

        $semester = $semesterFilter;
        if ($semester === null && $codeParam === '' && $incidentDate === '') {
            $semester = (string) $this->semester;
        }

        [$syStart, $syEnd] = $this->deriveSchoolYearBounds($schoolYear ?: date('Y'));
        $today = Carbon::today();
        $start = Carbon::parse($syStart)->startOfDay();
        $rangeEnd = Carbon::parse(min($syEnd, $today->format('Y-m-d')))->startOfDay();

        $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->query()
            ->select([
                'id',
                'student_id',
                'class_id',
                'class_section_id',
                'date',
                'status',
                'reason',
                'school_year',
                'semester',
                'is_reported',
                'is_notified',
            ])
            ->where(function ($q) use ($studentIdValues, $studentCodeValues) {
                $q->whereIn('student_id', $studentIdValues);
                if (!empty($studentCodeValues)) {
                    $q->orWhereIn('student_id', $studentCodeValues);
                }
            })
            ->where('date', '>=', max($start->format('Y-m-d'), $startYmd))
            ->where('date', '<', $this->dayBounds($endYmd)[1]);

        if ($statusFilter === ['absent']) {
            $qb->whereRaw("(LOWER(TRIM(status)) LIKE 'abs%' OR LOWER(TRIM(status)) = 'a')");
        } elseif ($statusFilter === ['late']) {
            $qb->whereRaw("(LOWER(TRIM(status)) LIKE 'late%' OR LOWER(TRIM(status)) = 'l')");
        } else {
            $qb->whereRaw("(LOWER(TRIM(status)) LIKE 'abs%' OR LOWER(TRIM(status)) LIKE 'late%' OR LOWER(TRIM(status)) IN ('a','l'))");
        }

        if ($pendingOnly) {
            $this->applyUnreportedAttendanceFilter($qb);

            if (!$includeNotified) {
                $qb->whereRaw("(LOWER(TRIM(COALESCE(is_notified, ''))) NOT IN ('yes','1'))");
            }
        }

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

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

        $attendanceRows = $qb->orderByDesc('date')->get()->toArray();

        $data = [
            'student' => $student,
            'attendance' => $attendanceRows,
            'violation_code' => $codeParam !== '' ? $codeParam : null,
            'violation_date' => $incidentDate,
            'violation_notified' => null,
            'show_violation_summary' => true,
            'school_year' => $schoolYear,
            'semester' => $semester,
        ];

        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'];
        }

        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 [
            'success' => true,
            'data' => $data,
        ];
    }

    public function sendAutoEmails(?string $variantOverride = null): array
    {
        $classStudentsQuery = $this->studentClassModel->query();
        if (method_exists($this->studentClassModel, 'scopeActive')) {
            $classStudentsQuery->active();
        }

        $classStudents = $classStudentsQuery
            ->where('school_year', $this->schoolYear)
            ->get()
            ->toArray();

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

        $studentIds = array_column($classStudents, 'student_id');

        $students = $this->studentModel->query()
            ->whereIn('id', $studentIds)
            ->where('is_active', 1)
            ->get()
            ->toArray();

        $attendanceQuery = $this->attendanceDataModel->query()
            ->whereIn('student_id', $studentIds)
            ->where('school_year', $this->schoolYear)
            ->where('date', '>=', Carbon::today()->subWeeks(5)->format('Y-m-d'));

        $this->applyUnreportedAttendanceFilter($attendanceQuery);

        $attendanceData = $attendanceQuery
            ->whereRaw("LOWER(TRIM(status)) IN ('absent','late')")
            ->orderByDesc('date')
            ->get()
            ->toArray();

        $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 = [];

        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 = $this->getPrimaryParentForStudent($sid);
            $context = [
                '{{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',
            ];

            $variant = $variantOverride ?: match ($code) {
                'ABS_2', 'ABS_2_IN3W' => 'no_answer',
                default => 'default',
            };

            $tpl = $this->renderTemplate($code, $variant, $context);

            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;

            try {
                $ok = $this->attendanceMailerService->send($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', 'send 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()];
            }

            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 = $this->attendanceMailerService->send($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', 'send 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()];
            }

            if ($anyOk) {
                $sent++;

                if (method_exists($this->attendanceTrackingModel, 'markRuleAsNotified')) {
                    $this->attendanceTrackingModel->markRuleAsNotified(
                        $sid,
                        $code,
                        $date,
                        $this->semester,
                        $this->schoolYear
                    );
                }

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

                if (method_exists($this->attendanceDataModel, 'markReportedAndNotified')) {
                    $this->attendanceDataModel->markReportedAndNotified(
                        $sid,
                        $allDates,
                        $this->semester,
                        $this->schoolYear
                    );
                }
            }
        }

        return [
            'success' => true,
            'sent' => $sent,
            'skipped' => $skipped,
            'errors' => $errors,
            'details' => $details,
        ];
    }

    public function getNotifiedViolations(?string $schoolYearParam = null, ?string $semesterParam = null): array
    {
        $schoolYear = filled($schoolYearParam) ? (string) $schoolYearParam : (string) $this->schoolYear;
        $semester = filled($semesterParam) ? (string) $semesterParam : (string) $this->semester;

        $trackingData = $this->attendanceTrackingModel->query()
            ->where('school_year', $schoolYear)
            ->where('semester', $semester)
            ->where(function ($q) {
                $q->where('is_notified', 1)
                    ->orWhere('is_notified', '1')
                    ->orWhereRaw("LOWER(TRIM(COALESCE(is_notified, ''))) = 'yes'");
            })
            ->orderByDesc('date')
            ->get()
            ->toArray();

        if (empty($trackingData)) {
            $latest = $this->attendanceTrackingModel->query()
                ->select(['school_year', 'semester'])
                ->where(function ($q) {
                    $q->where('is_notified', 1)
                        ->orWhere('is_notified', '1')
                        ->orWhereRaw("LOWER(TRIM(COALESCE(is_notified, ''))) = 'yes'");
                })
                ->orderByDesc('date')
                ->orderByDesc('id')
                ->first();

            if ($latest) {
                $schoolYear = (string) ($latest->school_year ?? $schoolYear);
                $semester = (string) ($latest->semester ?? $semester);

                $trackingData = $this->attendanceTrackingModel->query()
                    ->where('school_year', $schoolYear)
                    ->where('semester', $semester)
                    ->where(function ($q) {
                        $q->where('is_notified', 1)
                            ->orWhere('is_notified', '1')
                            ->orWhereRaw("LOWER(TRIM(COALESCE(is_notified, ''))) = 'yes'");
                    })
                    ->orderByDesc('date')
                    ->get()
                    ->toArray();
            }
        }

        if (empty($trackingData)) {
            return [
                'students' => [],
                'school_year' => $schoolYear,
                'semester' => $semester,
                'show_actions' => false,
            ];
        }

        $studentIds = array_unique(array_map(fn ($r) => (int) ($r['student_id'] ?? 0), $trackingData));

        $students = $this->studentModel->query()
            ->whereIn('id', $studentIds)
            ->where('is_active', 1)
            ->get()
            ->toArray();

        $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 [
                'students' => [],
                'school_year' => $schoolYear,
                'semester' => $semester,
                'show_actions' => false,
            ];
        }

        $studentMap = [];
        foreach ($students as $s) {
            $studentMap[$s['id']] = $s;
        }

        $classByStudent = [];
        try {
            $rows = DB::table('student_class as sc')
                ->select([
                    'sc.student_id',
                    'sc.class_section_id',
                    'cs.class_section_name',
                    'cs.class_id',
                    'c.class_name',
                ])
                ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
                ->leftJoin('classes as c', 'c.id', '=', 'cs.class_id')
                ->whereIn('sc.student_id', $studentIds)
                ->where('sc.school_year', $schoolYear)
                ->orderByDesc('sc.id')
                ->get();

            foreach ($rows as $row) {
                $sid = (int) ($row->student_id ?? 0);
                if ($sid > 0 && !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) {
        }

        $mergedData = [];
        foreach ($trackingData as $row) {
            $sid = (int) ($row['student_id'] ?? 0);
            if (!isset($studentMap[$sid])) {
                continue;
            }

            $merged = array_merge($studentMap[$sid], $row);
            if (isset($classByStudent[$sid])) {
                $merged['class_section_name'] = (string) ($classByStudent[$sid]['class_section_name'] ?? '');
                $merged['class_name'] = (string) ($classByStudent[$sid]['class_name'] ?? '');
            } elseif (!empty($merged['registration_grade'])) {
                $merged['grade'] = (string) $merged['registration_grade'];
            }

            try {
                $p = $this->getPrimaryParentForStudent($sid);
                if ($p) {
                    $merged['parent_name'] = $p['parent_name'] ?? '';
                    $merged['parent_email'] = $p['email'] ?? '';
                    $merged['parent_phone'] = $p['phone'] ?? '';
                }
            } catch (Throwable) {
            }

            $mergedData[] = $merged;
        }

        return [
            'students' => $mergedData,
            'school_year' => $schoolYear,
            'semester' => $semester,
        ];
    }

    public function compose(int $studentId, string $code = 'ABS_1', string $variant = 'default', ?string $incidentDate = null): array
    {
        if ($studentId <= 0) {
            return [
                'success' => false,
                'message' => 'Missing student_id.',
                'status' => 422,
            ];
        }

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

        $lastDate = preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) $incidentDate)
            ? $incidentDate
            : date('Y-m-d');

        $violation = [
            'id' => $studentId,
            '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) {
            return [
                'success' => false,
                'message' => "Template not found for {$code} ({$variant}).",
                'status' => 404,
            ];
        }

        [$subject, $body] = $rendered;

        return [
            'success' => true,
            'data' => [
                'student_id' => $studentId,
                '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(array $data): array
    {
        $sid = (int) $data['student_id'];
        $to = trim((string) $data['to']);
        $subject = (string) $data['subject'];
        $bodyInput = (string) $data['body_html'];
        $code = (string) $data['code'];
        $variant = (string) ($data['variant'] ?? 'default');

        $ymdRaw = (string) ($data['incident_date'] ?? '');
        $ymd = $ymdRaw !== '' ? substr($ymdRaw, 0, 10) : '';

        if ($bodyInput !== '' && preg_match('/<[^>]+>/', $bodyInput)) {
            $safeHtmlBody = $bodyInput;
        } elseif ($bodyInput !== '' && stripos($bodyInput, '&lt;') !== false) {
            $decoded = html_entity_decode($bodyInput, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
            $safeHtmlBody = preg_match('/<[^>]+>/', $decoded)
                ? $decoded
                : nl2br(e($bodyInput), false);
        } else {
            $safeHtmlBody = nl2br(e($bodyInput), false);
        }

        $wrapped = $this->renderWithEmailLayout($subject, $safeHtmlBody);
        $semester = $this->semester ?: (string) ($this->configModel->getConfig('semester') ?? '');
        $schoolYear = $this->schoolYear ?: (string) ($this->configModel->getConfig('school_year') ?? '');

        try {
            $anyOk = false;

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

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

            if (!$anyOk) {
                return [
                    'success' => false,
                    'message' => 'Failed to send email.',
                    'status' => 500,
                ];
            }

            if (method_exists($this->attendanceTrackingModel, 'markRuleAsNotified')) {
                $this->attendanceTrackingModel->markRuleAsNotified(
                    $sid,
                    $code,
                    $ymd !== '' ? $ymd : date('Y-m-d'),
                    $semester,
                    $schoolYear
                );
            }

            $violationDates = $this->getViolationDatesForStudent($sid, $code, $ymd);
            if (method_exists($this->attendanceDataModel, 'markReportedAndNotified')) {
                $this->attendanceDataModel->markReportedAndNotified(
                    $sid,
                    $violationDates,
                    $semester,
                    $schoolYear
                );
            }

            return [
                'success' => true,
                'message' => 'Email sent successfully.',
                'data' => [
                    'student_id' => $sid,
                    'to' => $to,
                    'secondary_to' => $to2 ?? null,
                    'subject' => $subject,
                    'variant' => $variant,
                    'incident_date' => $ymd,
                ],
            ];
        } catch (Throwable $e) {
            $this->logNotification($sid, $code, $ymd, 'email', $to, $subject, 'failed', $e->getMessage());

            return [
                'success' => false,
                'message' => 'Error: ' . $e->getMessage(),
                'status' => 500,
            ];
        }
    }

    public function parentsInfo(int $studentId): array
    {
        if ($studentId <= 0) {
            return [
                'success' => false,
                'message' => 'Missing student_id',
                'status' => 422,
            ];
        }

        try {
            $stu = DB::table('students')
                ->select('id', 'parent_id', 'school_year', 'semester')
                ->where('id', $studentId)
                ->first();

            if (!$stu) {
                return [
                    'success' => false,
                    'message' => 'Student not found',
                    'status' => 404,
                ];
            }

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

            $u1 = DB::table('users')
                ->select('id', 'firstname', 'lastname', 'email', 'cellphone')
                ->where('id', $primaryId)
                ->first();

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

            $pb = DB::table('parents')->where('firstparent_id', $primaryId);
            if (!empty($schoolYear)) {
                $pb->where('school_year', $schoolYear);
            }

            $pRow = $pb->orderByDesc('updated_at')->first();

            $secondary = null;
            if ($pRow) {
                $secondId = (int) ($pRow->secondparent_id ?? 0);

                if ($secondId > 0) {
                    $u2 = DB::table('users')
                        ->select('id', 'firstname', 'lastname', 'email', 'cellphone')
                        ->where('id', $secondId)
                        ->first();

                    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 [
                'success' => true,
                'data' => [
                    'primary' => $primary,
                    'secondary' => $secondary,
                ],
            ];
        } catch (Throwable $e) {
            Log::error('parentsInfo() failed: ' . $e->getMessage());

            return [
                'success' => false,
                'message' => 'Server error',
                'status' => 500,
            ];
        }
    }

    public function saveNotificationNote(array $data): array
    {
        $sid = (int) $data['student_id'];
        $code = (string) $data['code'];
        $note = trim((string) $data['note']);
        $ymdRaw = (string) ($data['incident_date'] ?? '');
        $ymd = $ymdRaw !== '' ? substr($ymdRaw, 0, 10) : '';

        try {
            $day = $ymd !== '' ? $ymd : date('Y-m-d');
            [$start, $end] = $this->dayBounds($day);

            $row = $this->attendanceTrackingModel->query()
                ->where('student_id', $sid)
                ->where('semester', $this->semester)
                ->where('school_year', $this->schoolYear)
                ->where('date', '>=', $start)
                ->where('date', '<', $end)
                ->where('reason', 'like', '%' . $code . '%')
                ->orderByDesc('date')
                ->first();

            if (!$row) {
                $row = $this->attendanceTrackingModel->query()
                    ->where('student_id', $sid)
                    ->where('semester', $this->semester)
                    ->where('school_year', $this->schoolYear)
                    ->where('date', '>=', $start)
                    ->where('date', '<', $end)
                    ->orderByDesc('date')
                    ->first();
            }

            if ($row && !empty($row->id)) {
                DB::table($this->attendanceTrackingModel->getTable())
                    ->where('id', (int) $row->id)
                    ->update([
                        'notif_counter' => DB::raw('COALESCE(notif_counter,0)+1'),
                        'note' => $note,
                        'is_notified' => 1,
                        'updated_at' => now(),
                    ]);
            } else {
                $this->attendanceTrackingModel->query()->create([
                    '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' => now(),
                    'updated_at' => now(),
                ]);
            }

            return [
                'success' => true,
                'message' => 'Note saved.',
            ];
        } catch (Throwable $e) {
            return [
                'success' => false,
                'message' => 'Failed to save note: ' . $e->getMessage(),
                'status' => 500,
            ];
        }
    }

    protected function notificationAlreadySent(
        int $studentId,
        string $code,
        string $ymd,
        string $channel = 'email',
        ?string $to = null
    ): bool {
        try {
            if (method_exists($this->notificationModel, 'hasSent')) {
                return (bool) $this->notificationModel->hasSent($studentId, $code, $ymd, $channel, $to);
            }

            $query = $this->notificationModel->query()
                ->where('student_id', $studentId)
                ->where('code', $code)
                ->where('incident_date', $ymd)
                ->where('channel', $channel);

            if (!empty($to)) {
                $query->where('to_address', $to);
            }

            return $query->exists();
        } catch (Throwable $e) {
            Log::debug('notificationAlreadySent(): ' . $e->getMessage());
            return false;
        }
    }

    protected function logNotification(
        int $studentId,
        string $code,
        string $ymd,
        string $channel,
        ?string $to,
        string $subject,
        string $status,
        string $response = ''
    ): void {
        try {
            $where = [
                'student_id' => $studentId,
                'code' => $code,
                'incident_date' => $ymd,
                'channel' => $channel,
            ];

            if (!empty($to)) {
                $where['to_address'] = $to;
            }

            $existing = $this->notificationModel->query()
                ->where($where)
                ->orderByDesc('id')
                ->first();

            if ($existing && !empty($existing->id)) {
                $existing->update([
                    'subject' => $subject,
                    'status' => $status,
                    'response' => $response,
                    'semester' => $this->semester,
                    'school_year' => $this->schoolYear,
                    'updated_at' => now(),
                ]);
            } else {
                $this->notificationModel->query()->create([
                    '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::debug('logNotification(): ' . $e->getMessage());
        }
    }

    protected function getPrimaryParentForStudent(int $studentId): ?array
    {
        $row = DB::table('students as s')
            ->selectRaw('u.id AS user_id, u.email, u.cellphone, CONCAT(u.firstname, " ", u.lastname) AS parent_name')
            ->leftJoin('users as u', 'u.id', '=', 's.parent_id')
            ->where('s.id', $studentId)
            ->first();

        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 = DB::table('students')
                ->select('id', 'parent_id', 'school_year', 'semester')
                ->where('id', $studentId)
                ->first();

            if (!$stu) {
                return null;
            }

            $primaryId = (int) ($stu->parent_id ?? 0);
            $schoolYear = (string) ($stu->school_year ?? '');

            $pb = DB::table('parents')->where('firstparent_id', $primaryId);
            if ($schoolYear !== '') {
                $pb->where('school_year', $schoolYear);
            }

            $pRow = $pb->orderByDesc('updated_at')->first();
            if (!$pRow) {
                return null;
            }

            $secondId = (int) ($pRow->secondparent_id ?? 0);
            if ($secondId > 0) {
                $u2 = DB::table('users')
                    ->select('id', 'firstname', 'lastname', 'email', 'cellphone')
                    ->where('id', $secondId)
                    ->first();

                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 ?? ''),
                    ];
                }
            }

            $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::error('getSecondaryParentForStudent() failed: ' . $e->getMessage());
        }

        return null;
    }

    protected 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;
            $sid = is_numeric($rawSid)
                ? (int) $rawSid
                : ($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);
        $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)) {
            return [];
        }

        $weekIndex = array_flip($activeWeekKeys);
        $currentWeekIdx = count($activeWeekKeys) - 1;
        $windowStartIdx = max(0, $currentWeekIdx - 4);
        $requiredWeeksCount = $currentWeekIdx - $windowStartIdx + 1;

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

        $classByStudent = [];
        try {
            $studentIdsForClass = array_values(array_unique(array_map(
                static fn ($s) => (int) ($s['id'] ?? 0),
                $students
            )));

            if (!empty($studentIdsForClass)) {
                $rows = DB::table('student_class as sc')
                    ->select([
                        'sc.student_id',
                        'sc.class_section_id',
                        'cs.class_section_name',
                        'cs.class_id',
                        'c.class_name',
                    ])
                    ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
                    ->leftJoin('classes as c', 'c.id', '=', 'cs.class_id')
                    ->whereIn('sc.student_id', $studentIdsForClass)
                    ->where('sc.school_year', $schoolYear)
                    ->orderByDesc('sc.id')
                    ->get();

                foreach ($rows as $row) {
                    $sid = (int) ($row->student_id ?? 0);
                    if ($sid > 0 && !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) {
            Log::debug('computeViolations(): class prefetch failed: ' . $e->getMessage());
        }

        $out = [];
        $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);

            [$start, $end] = $this->dayBounds($chosen['last_date']);
            $trackingQ = $this->attendanceTrackingModel->query()
                ->where('student_id', $sid)
                ->where('school_year', $schoolYear)
                ->where('date', '>=', $start)
                ->where('date', '<', $end)
                ->where('reason', 'like', '%' . $chosen['code'] . '%');

            if ($semester !== null) {
                $trackingQ->where('semester', $semester);
            }

            $tracking = $trackingQ->orderByDesc('date')->first();
            $tracking = $tracking?->toArray() ?? [];

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

            $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,
                '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;
    }

    protected function renderWithEmailLayout(string $subject, string $bodyHtml): string
    {
        return view('emails._wrap_layout', [
            'title' => $subject,
            'body_html' => $bodyHtml,
        ])->render();
    }

    protected 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;
    }

    protected function attendanceReportedColumns(): array
    {
        if ($this->attendanceReportedColumns !== null) {
            return $this->attendanceReportedColumns;
        }

        $table = $this->attendanceDataModel->getTable();

        $this->attendanceReportedColumns = [
            'is_reported' => \Schema::hasColumn($table, 'is_reported'),
            'reported' => \Schema::hasColumn($table, 'reported'),
        ];

        return $this->attendanceReportedColumns;
    }

    protected function applyUnreportedAttendanceFilter(Builder $qb): void
    {
        $cols = $this->attendanceReportedColumns();

        if (!empty($cols['is_reported'])) {
            $qb->whereRaw("(LOWER(TRIM(COALESCE(is_reported, ''))) NOT IN ('yes','1','true','y','on'))");
        }

        if (!empty($cols['reported'])) {
            $qb->whereRaw("(LOWER(TRIM(COALESCE(reported, ''))) NOT IN ('yes','1','true','y','on'))");
        }

        $qb->whereRaw("(LOWER(COALESCE(reason, '')) NOT LIKE '%parent-reported%')")
            ->whereRaw("(LOWER(COALESCE(reason, '')) NOT LIKE '%parent reported%')");

        if (\Schema::hasTable('parent_attendance_reports')) {
            $table = $this->attendanceDataModel->getTable();

            $qb->whereRaw("NOT EXISTS (
                SELECT 1 FROM parent_attendance_reports par
                WHERE par.student_id = {$table}.student_id
                  AND DATE(par.report_date) = DATE({$table}.date)
                  AND par.type IN ('absent','late')
            )");
        }
    }

    protected 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',
        ];
    }

    protected 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 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)];
    }

    protected 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',
        ];
    }

    protected function renderTemplate(string $code, string $variant, array $context): ?array
    {
        $row = null;

        if (method_exists($this->attendanceEmailTemplateModel, 'getTemplate')) {
            $row = $this->attendanceEmailTemplateModel->getTemplate($code, $variant)
                ?: $this->attendanceEmailTemplateModel->getTemplate($code, 'default');
        } else {
            $row = $this->attendanceEmailTemplateModel->query()
                ->where('code', $code)
                ->where('variant', $variant)
                ->where('is_active', 1)
                ->first();

            if (!$row) {
                $row = $this->attendanceEmailTemplateModel->query()
                    ->where('code', $code)
                    ->where('variant', 'default')
                    ->where('is_active', 1)
                    ->first();
            }

            $row = $row?->toArray();
        }

        if (!$row) {
            return null;
        }

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

        return [$subject, $body];
    }

    protected function getViolationDatesForStudent(int $studentId, string $code, ?string $incidentDate = null): array
    {
        $code = strtoupper(trim($code));
        $statuses = ['absent', 'late'];

        if (Str::startsWith($code, 'ABS')) {
            $statuses = ['absent'];
        } elseif (Str::startsWith($code, 'LATE')) {
            $statuses = ['late'];
        } elseif (Str::startsWith($code, 'MIX')) {
            $statuses = ['absent', 'late'];
        }

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

        return $this->attendanceDataModel->query()
            ->where('student_id', $studentId)
            ->whereIn(DB::raw('LOWER(TRIM(status))'), $statuses)
            ->when($incidentDate, fn ($q) => $q->where('date', '<=', $incidentDate))
            ->orderByDesc('date')
            ->limit(35)
            ->pluck('date')
            ->map(fn ($d) => substr((string) $d, 0, 10))
            ->unique()
            ->values()
            ->all();
    }

    protected 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 = [];

        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 (empty($weeks)) {
            $rows = DB::table('attendance_data')
                ->selectRaw('DATE(date) AS ymd')
                ->where('school_year', $schoolYear)
                ->whereRaw('DATE(date) >= ?', [$startYmd])
                ->whereRaw('DATE(date) <= ?', [$endYmd])
                ->where(function ($q) {
                    $q->whereNull('reason')
                        ->orWhere(function ($q2) {
                            $q2->where('reason', 'not like', '%break%')
                                ->where('reason', 'not like', '%cancel%');
                        });
                })
                ->when($semester !== null, fn ($q) => $q->where('semester', $semester))
                ->groupBy(DB::raw('DATE(date)'))
                ->orderBy('ymd')
                ->get();

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

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

        return $keys;
    }

    protected 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;
    }

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

    protected 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');
    }

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

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

        $dates = $datesDesc;
        sort($dates);

        $run = 1;
        for ($i = 1; $i < $N; $i++) {
            $prev = Carbon::parse($dates[$i - 1]);
            $curr = Carbon::parse($dates[$i]);
            $diff = $prev->diffInDays($curr);

            if ($diff === $daysApart) {
                $run++;
                if ($run >= $n) return true;
            } else {
                $run = 1;
            }
        }

        return false;
    }

    protected 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;
    }

    protected 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;
    }
}