<?php

namespace App\Services\Reports\ReportCards;

use App\Models\Calendar;
use App\Models\AttendanceData;
use App\Models\AttendanceRecord;
use App\Models\ClassSection;
use App\Models\Configuration;
use App\Models\ReportCardAcknowledgement;
use App\Models\ScoreComment;
use App\Models\SemesterScore;
use App\Models\Student;
use App\Models\StudentClass;
use App\Models\TeacherClass;
use App\Models\User;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;

class ReportCardService
{
    private string $schoolYear;
    private string $semester;

    public function __construct()
    {
        $this->schoolYear = trim((string) (Configuration::getConfig('school_year') ?? ''));
        $this->semester = trim((string) (Configuration::getConfig('semester') ?? ''));
    }

    public function meta(?string $schoolYear, ?string $semester): array
    {
        $year = trim((string) ($schoolYear ?? $this->schoolYear));
        $sem = trim((string) ($semester ?? $this->semester));

        if ($year === '') {
            $yr = DB::table('classSection')->select('school_year')->orderBy('school_year', 'DESC')->first();
            $year = (string) ($yr->school_year ?? '');
        }

        $schoolYears = [];
        try {
            $q1 = DB::table('classSection')
                ->select(DB::raw('DISTINCT school_year'))
                ->whereNotNull('school_year')
                ->orderBy('school_year', 'DESC')
                ->get();
            $schoolYears = array_values(array_filter(array_map(static fn ($r) => (string) ($r->school_year ?? ''), $q1->all())));
        } catch (\Throwable $e) {
        }
        try {
            $q2 = DB::table('semester_scores')
                ->select(DB::raw('DISTINCT school_year'))
                ->whereNotNull('school_year')
                ->orderBy('school_year', 'DESC')
                ->get();
            foreach ($q2->all() as $r) {
                $val = (string) ($r->school_year ?? '');
                if ($val !== '' && !in_array($val, $schoolYears, true)) {
                    $schoolYears[] = $val;
                }
            }
        } catch (\Throwable $e) {
        }
        rsort($schoolYears);

        $semesters = [];
        try {
            $rs = DB::table('semester_scores')
                ->select(DB::raw('DISTINCT semester'))
                ->where('school_year', $year)
                ->whereNotNull('semester')
                ->orderBy('semester', 'ASC')
                ->get();
            $semesters = array_values(array_filter(array_map(static fn ($r) => (string) ($r->semester ?? ''), $rs->all())));
        } catch (\Throwable $e) {
        }
        try {
            $rs2 = DB::table('student_class')
                ->select(DB::raw('DISTINCT semester'))
                ->where('school_year', $year)
                ->whereNotNull('semester')
                ->orderBy('semester', 'ASC')
                ->get();
            foreach ($rs2->all() as $r) {
                $val = (string) ($r->semester ?? '');
                if ($val !== '' && !in_array($val, $semesters, true)) {
                    $semesters[] = $val;
                }
            }
        } catch (\Throwable $e) {
        }

        $defaults = array_values(array_filter([(string) ($this->semester ?? ''), 'Fall', 'Spring'], static fn ($v) => $v !== ''));
        foreach ($defaults as $d) {
            if (!in_array($d, $semesters, true)) {
                $semesters[] = $d;
            }
        }
        $semesters = array_values(array_unique($semesters));
        if ($sem === '' && !empty($semesters)) {
            $sem = $semesters[0];
        } elseif ($sem !== '' && !in_array($sem, $semesters, true)) {
            array_unshift($semesters, $sem);
            $semesters = array_values(array_unique($semesters));
        }

        $students = [];
        try {
            $builder = DB::table('semester_scores as ss')
                ->select('s.id', 's.firstname', 's.lastname', DB::raw('GROUP_CONCAT(DISTINCT cs.class_section_name ORDER BY cs.class_section_name SEPARATOR ", ") AS class_section_name'))
                ->join('students as s', 's.id', '=', 'ss.student_id')
                ->leftJoin('student_class as sc', function ($join) {
                    $join->on('sc.student_id', '=', 's.id')
                        ->on('sc.school_year', '=', 'ss.school_year');
                })
                ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
                ->where('s.is_active', 1)
                ->where('ss.school_year', $year);
            if ($sem !== '') {
                $this->applySemesterFilter($builder, $sem, 'ss.semester');
            }
            $stuRows = $builder
                ->groupBy('s.id', 's.firstname', 's.lastname')
                ->orderBy('class_section_name', 'ASC')
                ->orderBy('s.firstname', 'ASC')
                ->orderBy('s.lastname', 'ASC')
                ->get()
                ->map(fn ($r) => (array) $r)
                ->all();
            if (!empty($stuRows)) {
                $students = $stuRows;
            }
        } catch (\Throwable $e) {
        }
        if (empty($students)) {
            $fallback = Student::query()
                ->select('students.id', 'students.firstname', 'students.lastname', DB::raw('GROUP_CONCAT(DISTINCT cs.class_section_name ORDER BY cs.class_section_name SEPARATOR ", ") AS class_section_name'))
                ->leftJoin('student_class as sc', 'sc.student_id', '=', 'students.id')
                ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id');
            if ($year !== '') {
                $fallback->where('sc.school_year', $year);
            }
            $students = $fallback
                ->where('students.is_active', 1)
                ->groupBy('students.id', 'students.firstname', 'students.lastname')
                ->orderBy('class_section_name', 'ASC')
                ->orderBy('students.firstname', 'ASC')
                ->orderBy('students.lastname', 'ASC')
                ->get()
                ->map(fn ($r) => (array) $r)
                ->all();
        }

        $classSections = [];
        try {
            $builder = DB::table('classSection as cs')
                ->select('cs.id', 'cs.class_section_id', 'cs.class_section_name', 'cs.school_year')
                ->join('student_class as sc', 'sc.class_section_id', '=', 'cs.class_section_id')
                ->join('students as s', 's.id', '=', 'sc.student_id')
                ->where('s.is_active', 1)
                ->where('sc.school_year', $year);
            $classSections = $builder
                ->groupBy('cs.id', 'cs.class_section_id', 'cs.class_section_name', 'cs.school_year')
                ->orderBy('cs.class_section_name', 'ASC')
                ->get()
                ->map(fn ($r) => (array) $r)
                ->all();

            if (empty($classSections) && $sem !== '') {
                $classSections = DB::table('classSection as cs')
                    ->select('cs.id', 'cs.class_section_id', 'cs.class_section_name', 'cs.school_year')
                    ->join('student_class as sc', 'sc.class_section_id', '=', 'cs.class_section_id')
                    ->join('students as s', 's.id', '=', 'sc.student_id')
                    ->where('s.is_active', 1)
                    ->where('sc.school_year', $year)
                    ->groupBy('cs.id', 'cs.class_section_id', 'cs.class_section_name', 'cs.school_year')
                    ->orderBy('cs.class_section_name', 'ASC')
                    ->get()
                    ->map(fn ($r) => (array) $r)
                    ->all();
            }
        } catch (\Throwable $e) {
        }

        return [
            'schoolYears' => $schoolYears,
            'selectedYear' => $year,
            'semesters' => $semesters,
            'selectedSemester' => $sem,
            'students' => $students,
            'classSections' => array_map(static fn ($r) => [
                'id' => (int) ($r['id'] ?? 0),
                'class_section_id' => (int) ($r['class_section_id'] ?? ($r['id'] ?? 0)),
                'class_section_name' => (string) ($r['class_section_name'] ?? 'Section'),
                'school_year' => (string) ($r['school_year'] ?? ''),
            ], $classSections),
        ];
    }

    public function completeness(string $year, string $sem, int $sectionInput): array
    {
        $year = trim($year);
        $sem = trim($sem);

        if ($year === '' || $sectionInput <= 0) {
            return [
                'ok' => false,
                'message' => 'Missing school year or class section.',
            ];
        }

        $sectionCode = $sectionInput;
        $sectionId = null;
        $sectionName = '';
        try {
            $row = DB::table('classSection')
                ->select('id', 'class_section_id', 'class_section_name')
                ->where('class_section_id', $sectionInput)
                ->limit(1)
                ->first();
            if ($row) {
                $sectionId = (int) $row->id;
                $sectionCode = (int) $row->class_section_id;
                $sectionName = (string) ($row->class_section_name ?? '');
            } else {
                $row = DB::table('classSection')
                    ->select('id', 'class_section_id', 'class_section_name')
                    ->where('id', $sectionInput)
                    ->limit(1)
                    ->first();
                if ($row) {
                    $sectionId = (int) $row->id;
                    $sectionCode = (int) $row->class_section_id;
                    $sectionName = (string) ($row->class_section_name ?? '');
                }
            }
        } catch (\Throwable $e) {
        }

        if ($sectionName === '') {
            $sectionName = (string) ($this->resolveClassName($sectionCode) ?? '');
        }

        $students = $this->fetchStudentsByClass($sectionCode, $year);
        if (empty($students) && $sectionId && $sectionId !== $sectionCode) {
            $students = $this->fetchStudentsByClass($sectionId, $year);
        }
        $rosterSemester = $sem;
        $usedFallback = false;
        if (empty($students) && $sem !== '') {
            $students = $this->fetchStudentsByClass($sectionCode, $year);
            if (empty($students) && $sectionId && $sectionId !== $sectionCode) {
                $students = $this->fetchStudentsByClass($sectionId, $year);
            }
            $rosterSemester = '';
            $usedFallback = !empty($students);
        }

        if (empty($students)) {
            return [
                'ok' => true,
                'message' => 'No students found for the selected class and year.',
                'summary' => [
                    'total' => 0,
                    'complete' => 0,
                    'incomplete' => 0,
                    'warnings' => 0,
                ],
                'students' => [],
                'roster_semester' => $rosterSemester,
                'used_fallback' => $usedFallback,
            ];
        }

        $studentIds = array_values(array_filter(array_map(
            static fn ($s) => (int) ($s['id'] ?? 0),
            $students
        ), static fn ($v) => $v > 0));

        if (empty($studentIds)) {
            return [
                'ok' => false,
                'message' => 'No valid students found.',
            ];
        }

        $scoreRows = SemesterScore::query()
            ->whereIn('student_id', $studentIds)
            ->where('school_year', $year);
        if ($sem !== '') {
            $this->applySemesterFilter($scoreRows, $sem, 'semester');
        }
        $scoreRows = $scoreRows
            ->orderBy('updated_at', 'DESC')
            ->orderBy('id', 'DESC')
            ->get()
            ->map(fn ($r) => $r->toArray())
            ->all();

        $scoreByStudent = [];
        foreach ($scoreRows as $row) {
            $sid = (int) ($row['student_id'] ?? 0);
            if ($sid > 0 && !isset($scoreByStudent[$sid])) {
                $scoreByStudent[$sid] = $row;
            }
        }

        $semLower = strtolower(trim($sem));
        $isFirst = in_array($semLower, ['fall', 'first', 'semester 1', '1'], true);
        $isSecond = in_array($semLower, ['spring', 'second', 'semester 2', '2'], true);

        $firstScoreByStudent = [];
        if ($isSecond) {
            $otherRows = SemesterScore::query()
                ->whereIn('student_id', $studentIds)
                ->where('school_year', $year);
            if ($sem !== '') {
                $this->applySemesterExclusion($otherRows, $sem, 'semester');
            }
            $otherRows = $otherRows
                ->orderBy('updated_at', 'DESC')
                ->orderBy('id', 'DESC')
                ->get()
                ->map(fn ($r) => $r->toArray())
                ->all();
            foreach ($otherRows as $row) {
                $sid = (int) ($row['student_id'] ?? 0);
                if ($sid > 0 && !isset($firstScoreByStudent[$sid])) {
                    $firstScoreByStudent[$sid] = $row;
                }
            }
        }

        $examCommentTypes = $isSecond
            ? ['final']
            : ($isFirst ? ['midterm'] : ['midterm', 'final']);
        $commentTypes = array_merge($examCommentTypes, ['ptap', 'attendance', 'attendance_comment']);
        $hasCommentReview = DB::getSchemaBuilder()->hasColumn('score_comments', 'comment_review');
        $commentSelect = $hasCommentReview
            ? ['student_id', 'score_type', 'comment', 'comment_review']
            : ['student_id', 'score_type', 'comment'];
        $commentBuilder = ScoreComment::query()
            ->select($commentSelect)
            ->whereIn('student_id', $studentIds)
            ->where('school_year', $year)
            ->whereIn('score_type', $commentTypes);
        if ($sem !== '') {
            $this->applySemesterFilter($commentBuilder, $sem, 'semester');
        }
        $commentRows = [];
        try {
            $commentRows = $commentBuilder->get()->map(fn ($r) => $r->toArray())->all();
        } catch (\Throwable $e) {
        }

        $commentsByStudent = [];
        foreach ($commentRows as $row) {
            $sid = (int) ($row['student_id'] ?? 0);
            if ($sid <= 0) {
                continue;
            }
            $typeRaw = strtolower(trim((string) ($row['score_type'] ?? '')));
            if ($typeRaw === '') {
                continue;
            }
            if ($typeRaw === 'attendance_comment') {
                $typeRaw = 'attendance';
            }
            $reviewVal = $hasCommentReview ? trim((string) ($row['comment_review'] ?? '')) : '';
            $commentVal = $hasCommentReview ? $reviewVal : trim((string) ($row['comment'] ?? ''));
            if ($commentVal === '') {
                continue;
            }
            $commentsByStudent[$sid][$typeRaw] = $commentVal;
        }

        $isNumeric = static fn ($v) => $v !== null && $v !== '' && is_numeric($v);

        $ackMap = [];
        try {
            $ackRows = ReportCardAcknowledgement::query()
                ->whereIn('student_id', $studentIds)
                ->where('school_year', $year)
                ->where('semester', $sem)
                ->orderBy('updated_at', 'DESC')
                ->get()
                ->map(fn ($r) => $r->toArray())
                ->all();
            foreach ($ackRows as $row) {
                $sid = (int) ($row['student_id'] ?? 0);
                if ($sid > 0 && !isset($ackMap[$sid])) {
                    $ackMap[$sid] = $row;
                }
            }
        } catch (\Throwable $e) {
            $ackMap = [];
        }

        $results = [];
        $completeCount = 0;
        $warningCount = 0;
        foreach ($students as $student) {
            $sid = (int) ($student['id'] ?? 0);
            $name = trim(((string) ($student['firstname'] ?? '')) . ' ' . ((string) ($student['lastname'] ?? '')));
            $missing = [];
            $warnings = [];

            if ($sectionName === '') {
                $missing[] = 'Grade/Section';
            }

            $score = $sid > 0 ? ($scoreByStudent[$sid] ?? null) : null;
            $examScore = null;
            $attendanceScore = null;
            $ptapScore = null;
            $semesterScore = null;
            $hasAllComponents = false;

            if (!$score) {
                $missing[] = 'Score record';
            } else {
                $attendanceScore = $score['attendance_score'] ?? null;
                $ptapScore = $score['ptap_score'] ?? null;
                $semesterScore = $score['semester_score'] ?? null;

                $components = [
                    $score['homework_avg'] ?? null,
                    $score['quiz_avg'] ?? null,
                    $score['project_avg'] ?? null,
                    $score['test_avg'] ?? null,
                ];
                $hasAllComponents = !in_array(false, array_map($isNumeric, $components), true);

                if ($isSecond) {
                    $examScore = $score['final_exam_score'] ?? null;
                    if (!$isNumeric($examScore) && $isNumeric($score['midterm_exam_score'] ?? null)) {
                        $examScore = $score['midterm_exam_score'];
                        $warnings[] = 'Final exam stored as midterm';
                    }
                } elseif ($isFirst) {
                    $examScore = $score['midterm_exam_score'] ?? null;
                } else {
                    $examScore = $score['final_exam_score'] ?? ($score['midterm_exam_score'] ?? null);
                }

                if (!$isNumeric($examScore)) {
                    $missing[] = $isSecond ? 'Final exam score' : ($isFirst ? 'Midterm exam score' : 'Exam score');
                }
                if (!$isNumeric($attendanceScore)) {
                    $missing[] = 'Attendance score';
                }
                if (!$isNumeric($ptapScore)) {
                    if ($hasAllComponents) {
                        $warnings[] = 'PTAP computed from components';
                    } else {
                        $missing[] = 'PTAP score';
                    }
                }

                $canComputeSemester = $isNumeric($examScore) && $isNumeric($attendanceScore) && ($isNumeric($ptapScore) || $hasAllComponents);
                if (!$isNumeric($semesterScore)) {
                    if ($canComputeSemester) {
                        $warnings[] = 'Semester score computed';
                    } else {
                        $missing[] = 'Semester score';
                    }
                }

                if ($isSecond) {
                    $firstScore = $firstScoreByStudent[$sid]['semester_score'] ?? null;
                    if (!$isNumeric($firstScore)) {
                        $missing[] = 'First semester score';
                    }
                }
            }

            $commentKey = $isSecond ? 'final' : ($isFirst ? 'midterm' : '');
            $commentLabel = $isSecond ? 'Final' : ($isFirst ? 'Midterm' : 'Exam');
            $commentSet = $commentsByStudent[$sid] ?? [];
            $examComment = $commentKey !== ''
                ? ($commentSet[$commentKey] ?? '')
                : ($commentSet['final'] ?? ($commentSet['midterm'] ?? ''));
            if (trim((string) $examComment) === '') {
                $missing[] = $commentLabel . ' comment';
            }
            if (trim((string) ($commentSet['ptap'] ?? '')) === '') {
                $missing[] = 'PTAP comment';
            }
            if (trim((string) ($commentSet['attendance'] ?? '')) === '') {
                $missing[] = 'Attendance comment';
            }

            $isComplete = empty($missing);
            if ($isComplete) {
                $completeCount++;
            }
            if (!empty($warnings)) {
                $warningCount++;
            }

            $ack = $sid > 0 ? ($ackMap[$sid] ?? null) : null;
            $results[] = [
                'id' => $sid,
                'name' => $name !== '' ? $name : 'N/A',
                'missing' => $missing,
                'warnings' => $warnings,
                'complete' => $isComplete,
                'viewed_at' => $ack['viewed_at'] ?? null,
                'signed_at' => $ack['signed_at'] ?? null,
                'signed_name' => $ack['signed_name'] ?? null,
            ];
        }

        $total = count($results);
        $incomplete = $total - $completeCount;

        return [
            'ok' => true,
            'summary' => [
                'total' => $total,
                'complete' => $completeCount,
                'incomplete' => $incomplete,
                'warnings' => $warningCount,
            ],
            'students' => $results,
            'class_section_name' => $sectionName,
            'class_section_id' => $sectionCode,
            'school_year' => $year,
            'semester' => $sem,
            'roster_semester' => $rosterSemester,
            'used_fallback' => $usedFallback,
        ];
    }

    public function acknowledgement(int $studentId, string $schoolYear, string $semester): array
    {
        $row = null;
        try {
            $row = ReportCardAcknowledgement::query()
                ->where('student_id', $studentId)
                ->where('school_year', $schoolYear)
                ->where('semester', $semester)
                ->orderBy('updated_at', 'DESC')
                ->first();
        } catch (\Throwable $e) {
            // allow missing table or other DB errors to bubble as empty acknowledgement
        }

        return [
            'student_id' => $studentId,
            'school_year' => $schoolYear,
            'semester' => $semester,
            'viewed_at' => $row?->viewed_at,
            'signed_at' => $row?->signed_at,
            'signed_name' => $row?->signed_name,
        ];
    }

    public function generateSingleReport(int $studentId, array $params): array
    {
        $schoolYear = (string) ($params['school_year'] ?? $this->schoolYear);
        $semester = (string) ($params['semester'] ?? $this->semester);
        $reportDate = $this->sanitizeReportDate((string) ($params['report_date'] ?? ''));
        $data = $this->prepareStudentReportData($studentId, $schoolYear, $semester);

        if (!$data) {
            return ['ok' => false, 'message' => 'Student not found or missing scores.'];
        }

        $data['report_date'] = $reportDate['iso'];
        $data['report_date_display'] = $reportDate['display'];

        $pdf = new \FPDF('P', 'mm', 'Letter');
        $this->formatReportPDF($pdf, $data);

        $filename = 'ReportCard_' . ($data['student']['lastname'] ?? 'student') . '.pdf';

        $pdfContent = $pdf->Output('S');

        return [
            'ok' => true,
            'pdf' => $pdfContent,
            'filename' => $filename,
        ];
    }

    public function generateClassReport(int $classSectionId, array $params): array
    {
        $schoolYear = (string) ($params['school_year'] ?? $this->schoolYear);
        $semester = (string) ($params['semester'] ?? $this->semester);
        $reportDate = $this->sanitizeReportDate((string) ($params['report_date'] ?? ''));

        $q = SemesterScore::query()
            ->select('semester_scores.*')
            ->join('students as s', 's.id', '=', 'semester_scores.student_id')
            ->where('s.is_active', 1)
            ->where('semester_scores.class_section_id', $classSectionId);
        if (!empty($schoolYear)) {
            $q->where('semester_scores.school_year', $schoolYear);
        }
        if (!empty($semester)) {
            $this->applySemesterFilter($q, $semester, 'semester_scores.semester');
        }
        $scores = $q
            ->orderBy('s.firstname', 'ASC')
            ->orderBy('s.lastname', 'ASC')
            ->get()
            ->map(fn ($r) => $r->toArray())
            ->all();

        if (!$scores) {
            return ['ok' => false, 'message' => 'No students found for this class section.'];
        }

        $pdf = new \FPDF('P', 'mm', 'Letter');

        foreach ($scores as $score) {
            $studentId = (int) ($score['student_id'] ?? 0);
            $data = $this->prepareStudentReportData($studentId, $schoolYear, $semester);
            if ($data) {
                $data['report_date'] = $reportDate['iso'];
                $data['report_date_display'] = $reportDate['display'];
                $this->formatReportPDF($pdf, $data);
            }
        }

        $filename = 'ClassReport_Section_' . $classSectionId . '.pdf';
        $pdfContent = $pdf->Output('S');

        return [
            'ok' => true,
            'pdf' => $pdfContent,
            'filename' => $filename,
        ];
    }

    private function resolveClassName(int $classId): ?string
    {
        $tables = [
            ['classSection', 'class_section_id'],
            ['classSection', 'id'],
            ['class_sections', 'class_section_id'],
            ['class_sections', 'id'],
        ];

        foreach ($tables as [$table, $pk]) {
            $result = DB::table($table)
                ->select('class_section_name')
                ->where($pk, $classId)
                ->first();
            if ($result) {
                return $result->class_section_name ?? null;
            }
        }

        return null;
    }

    private function fetchStudentsByClass(int $sectionId, ?string $schoolYear): array
    {
        $query = StudentClass::query()
            ->select('s.id', 's.firstname', 's.lastname', 's.registration_grade', 's.gender')
            ->join('students as s', 's.id', '=', 'student_class.student_id')
            ->where('student_class.class_section_id', $sectionId)
            ->where('s.is_active', 1)
            ->orderBy('s.firstname', 'ASC')
            ->orderBy('s.lastname', 'ASC');

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

        return $query->get()->map(fn ($r) => (array) $r)->all();
    }

    private function normalizeSemester(?string $input): string
    {
        $s = strtolower(trim((string) $input));
        if ($s === 'fall' || $s === 'first' || str_contains($s, 'fall') || str_contains($s, '1')) {
            return 'fall';
        }
        if ($s === 'spring' || $s === 'second' || str_contains($s, 'spring') || str_contains($s, '2')) {
            return 'spring';
        }
        return '';
    }

    private function semesterVariants(?string $semester): array
    {
        $raw = trim((string) $semester);
        if ($raw === '') {
            return [];
        }

        $norm = $this->normalizeSemester($raw);
        if ($norm === 'fall') {
            $vals = ['Fall', 'fall', 'First', 'first', 'Semester 1', 'semester 1', 'Semester1', 'semester1', 'Sem 1', 'sem 1', '1', '01', 'S1', 's1'];
        } elseif ($norm === 'spring') {
            $vals = ['Spring', 'spring', 'Second', 'second', 'Semester 2', 'semester 2', 'Semester2', 'semester2', 'Sem 2', 'sem 2', '2', '02', 'S2', 's2'];
        } else {
            $vals = [$raw];
        }
        $vals[] = $raw;

        return array_values(array_unique($vals));
    }

    private function applySemesterFilter($builder, ?string $semester, string $field = 'semester'): void
    {
        $vals = $this->semesterVariants($semester);
        if (!empty($vals)) {
            $builder->whereIn($field, $vals);
        }
    }

    private function applySemesterExclusion($builder, ?string $semester, string $field = 'semester'): void
    {
        $vals = $this->semesterVariants($semester);
        if (!empty($vals)) {
            $builder->whereNotIn($field, $vals);
        }
    }

    private function sanitizeReportDate(?string $input): array
    {
        $input = trim((string) $input);
        if ($input === '') {
            $input = date('Y-m-d');
        }

        $display = $input;
        $iso = $input;
        try {
            $dt = new \DateTime($input);
            $iso = $dt->format('Y-m-d');
            $display = $dt->format('M d, Y');
        } catch (\Throwable $e) {
        }

        return [
            'iso' => $iso,
            'display' => $display,
        ];
    }

    private function resolveAnchorSunday(): string
    {
        $tzName = (string) (config('School')->attendance['timezone'] ?? config('app.timezone'));
        try {
            $tzObj = new \DateTimeZone($tzName ?: 'UTC');
        } catch (\Throwable $e) {
            $tzObj = new \DateTimeZone('UTC');
        }
        try {
            $now = new \DateTime('now', $tzObj);
        } catch (\Throwable $e) {
            $now = new \DateTime('now');
        }
        $weekday = (int) $now->format('w');
        if ($weekday === 0) {
            return $now->format('Y-m-d');
        }
        return $now->modify('next sunday')->format('Y-m-d');
    }

    private function listSundays(string $startDate, string $endDate): array
    {
        try {
            $start = new \DateTimeImmutable($startDate);
        } catch (\Throwable $e) {
            return [];
        }
        try {
            $end = new \DateTimeImmutable($endDate);
        } catch (\Throwable $e) {
            return [];
        }

        $sundays = [];
        $cursor = $start;
        $w = (int) $cursor->format('w');
        if ($w !== 0) {
            $cursor = $cursor->modify('next sunday');
        }

        while ($cursor <= $end) {
            $sundays[] = $cursor->format('Y-m-d');
            $cursor = $cursor->modify('+7 days');
        }
        return $sundays;
    }

    private function prepareStudentReportData(int $studentId, ?string $schoolYear = null, ?string $semester = null)
    {
        $student = Student::query()->find($studentId);
        if (!$student || (int) ($student->is_active ?? 1) !== 1) {
            return null;
        }

        if (function_exists('attendance_comment_from_score')) {
            // optional helper available
        }

        $refYear = $schoolYear ?: ($this->schoolYear ?? '');
        $refSemester = $semester ?: ($this->semester ?? '');

        $grade = StudentClass::getClassSectionsByStudentId($studentId, $refYear);

        $qb = SemesterScore::query()->where('student_id', $studentId);
        if ($refYear !== '') {
            $qb->where('school_year', $refYear);
        }
        if ($refSemester !== '') {
            $this->applySemesterFilter($qb, $refSemester, 'semester');
        }
        $score = $qb->orderBy('updated_at', 'DESC')->orderBy('id', 'DESC')->first();
        if (!$score) {
            return null;
        }
        $score = $score->toArray();

        $sectionCode = (int) ($score['class_section_id'] ?? 0);
        $sectionId = null;

        if ($sectionCode > 0) {
            $row = DB::table('classSection')
                ->select('id', 'class_section_id', 'class_section_name')
                ->where('class_section_id', $sectionCode)
                ->limit(1)
                ->first();
            if ($row) {
                $sectionId = (int) $row->id;
                $sectionCode = (int) $row->class_section_id;
            } else {
                $row2 = DB::table('classSection')
                    ->select('id', 'class_section_id', 'class_section_name')
                    ->where('id', $sectionCode)
                    ->limit(1)
                    ->first();
                if ($row2) {
                    $sectionId = (int) $row2->id;
                    $sectionCode = (int) $row2->class_section_id;
                }
            }
        }

        if (!$sectionId || $sectionCode <= 0) {
            try {
                $scRow = DB::table('student_class')
                    ->select('class_section_id')
                    ->where('student_id', $studentId)
                    ->where('school_year', (string) ($score['school_year'] ?? $refYear))
                    ->orderByRaw('COALESCE(updated_at, created_at) DESC')
                    ->limit(1)
                    ->first();
                if (!empty($scRow->class_section_id)) {
                    $sectionCode = (int) $scRow->class_section_id;
                    $row = DB::table('classSection')
                        ->select('id')
                        ->where('class_section_id', $sectionCode)
                        ->limit(1)
                        ->first();
                    if ($row) {
                        $sectionId = (int) $row->id;
                    }
                }
            } catch (\Throwable $e) {
            }
        }

        $teacherName = 'N/A';
        $teacherNamesList = [];
        $taNames = [];

        if ($sectionCode > 0) {
            $names = $this->resolveTeacherAndTAs($sectionCode, $refYear, $refSemester);
            $teacherName = $names['teacher_name'];
            $taNames = $names['ta_names'];
            $teacherNamesList = $names['teacher_names'] ?? [];
        }
        if (($teacherName === 'N/A' || empty($taNames)) && $sectionId) {
            $names = $this->resolveTeacherAndTAs($sectionId, $refYear, $refSemester);
            if ($teacherName === 'N/A') {
                $teacherName = $names['teacher_name'];
            }
            if (empty($taNames)) {
                $taNames = $names['ta_names'];
            }
            if (empty($teacherNamesList) && !empty($names['teacher_names'])) {
                $teacherNamesList = $names['teacher_names'];
            }
        }

        $commentRows = ScoreComment::query()
            ->select('student_id', 'score_type', 'comment', 'comment_review')
            ->where('student_id', $studentId)
            ->where('school_year', $refYear)
            ->whereIn('score_type', ['final', 'midterm', 'ptap', 'attendance', 'attendance_comment'])
            ->orderBy('created_at', 'DESC')
            ->get()
            ->map(fn ($r) => $r->toArray())
            ->all();

        $commentMap = [];
        foreach ($commentRows as $row) {
            $typeRaw = strtolower(trim((string) ($row['score_type'] ?? '')));
            if ($typeRaw === '') {
                continue;
            }
            if ($typeRaw === 'attendance_comment') {
                $typeRaw = 'attendance';
            }
            $commentVal = trim((string) ($row['comment_review'] ?? $row['comment'] ?? ''));
            if ($commentVal === '') {
                continue;
            }
            if (!isset($commentMap[$typeRaw])) {
                $commentMap[$typeRaw] = $commentVal;
            }
        }

        if (isset($commentMap['attendance']) && !isset($score['attendance_comment'])) {
            $score['attendance_comment'] = $commentMap['attendance'];
        }

        $att = $score['attendance_score'] ?? null;
        $attendanceCommentVal = $commentMap['attendance'] ?? $score['attendance_comment'] ?? null;
        if ((!is_string($attendanceCommentVal) || trim($attendanceCommentVal) === '') && is_numeric($att) && function_exists('attendance_comment_from_score')) {
            $autoAttendance = attendance_comment_from_score((float) $att, (string) ($student->firstname ?? ''));
            if (is_string($autoAttendance) && $autoAttendance !== '') {
                $commentMap['attendance'] = $autoAttendance;
                if (!isset($score['attendance_comment'])) {
                    $score['attendance_comment'] = $autoAttendance;
                }
            }
        }

        $totalSemesterDays = null;
        try {
            $normSemester = $this->normalizeSemester($refSemester);
            if ($normSemester !== '') {
                $events = Calendar::getEventsBySchoolYearAndSemester($refYear, $normSemester);
                $dates = array_values(array_filter(array_map(static fn ($r) => (string) ($r['date'] ?? ''), $events)));
                if (!empty($dates)) {
                    $start = min($dates);
                    $end = max($dates);
                    $sundays = $this->listSundays($start, $end);
                    $totalSemesterDays = count($sundays);
                }
            }
        } catch (\Throwable $e) {
        }

        if ($totalSemesterDays === null) {
            try {
                $attRows = AttendanceRecord::query()
                    ->select('total_attendance')
                    ->where('student_id', $studentId)
                    ->where('school_year', $refYear)
                    ->orderBy('created_at', 'DESC')
                    ->limit(1)
                    ->get()
                    ->map(fn ($r) => $r->toArray())
                    ->all();
                $vals = array_values(array_filter(array_map(
                    static fn ($r) => isset($r['total_attendance']) ? (int) $r['total_attendance'] : null,
                    $attRows
                ), static fn ($v) => $v !== null));
                if (!empty($vals)) {
                    $totalSemesterDays = max($vals);
                }
            } catch (\Throwable $e) {
            }
        }

        $attendanceTotals = [
            'total_absences' => null,
            'total_tardies' => null,
        ];
        try {
            $absences = AttendanceData::query()
                ->where('student_id', $studentId)
                ->where('school_year', $refYear)
                ->where('status', 'Absent');
            $attendanceTotals['total_absences'] = $absences->count();

            $lates = AttendanceData::query()
                ->where('student_id', $studentId)
                ->where('school_year', $refYear)
                ->where('status', 'Late');
            $attendanceTotals['total_tardies'] = $lates->count();
        } catch (\Throwable $e) {
        }

        if ($attendanceTotals['total_absences'] !== null) {
            $score['total_absences'] = $attendanceTotals['total_absences'];
        }
        if ($attendanceTotals['total_tardies'] !== null) {
            $score['total_tardies'] = $attendanceTotals['total_tardies'];
        }

        return [
            'student' => $student->toArray(),
            'score' => $score,
            'grade' => $grade,
            'teacher_name' => $teacherName,
            'teacher_names' => $teacherNamesList,
            'ta_names' => $taNames,
            'comment_map' => $commentMap,
            'school_year' => $refYear,
            'semester' => $refSemester,
            'class_section_id' => $sectionCode,
            'total_attendance_days' => $totalSemesterDays,
        ];
    }

    private function resolveTeacherAndTAs(int $sectionIdOrCode, ?string $schoolYear = null, ?string $semester = null): array
    {
        $teacherName = 'N/A';
        $teacherNames = [];
        $taNames = [];

        $withPrefix = static function (array $row): string {
            $first = trim((string) ($row['firstname'] ?? ''));
            $last = trim((string) ($row['lastname'] ?? ''));
            $name = trim($last . ', ' . $first, ' ,');
            return $name !== '' ? $name : 'N/A';
        };
        $normalizePos = static function (?string $pos): string {
            $pos = strtolower(trim((string) $pos));
            if ($pos === 'ta' || $pos === 'assistant') {
                return 'ta';
            }
            return 'main';
        };

        $consume = static function (array $rows) use (&$teacherName, &$teacherNames, &$taNames, $normalizePos, $withPrefix) {
            foreach ($rows as $row) {
                $pos = $normalizePos($row['position'] ?? '');
                $name = $withPrefix($row);
                if ($pos === 'ta') {
                    $taNames[] = $name;
                } else {
                    $teacherNames[] = $name;
                    if ($teacherName === 'N/A') {
                        $teacherName = $name;
                    }
                }
            }
        };

        $runner = function (array $ids, string $yr, string $sm) use (&$consume) {
            return TeacherClass::query()
                ->select('teacher_class.position', 'users.firstname', 'users.lastname')
                ->join('users', 'users.id', '=', 'teacher_class.teacher_id')
                ->whereIn('teacher_class.class_section_id', $ids)
                ->when($yr !== '', fn ($q) => $q->where('teacher_class.school_year', $yr))
                ->when($sm !== '', fn ($q) => $q->where('teacher_class.semester', $sm))
                ->orderBy('teacher_class.position', 'ASC')
                ->get()
                ->map(fn ($r) => (array) $r)
                ->all();
        };

        $ids = [$sectionIdOrCode];
        $rows = $runner($ids, (string) $schoolYear, (string) $semester);
        if (!empty($rows)) {
            $consume($rows);
        }

        $teacherNames = array_values(array_filter(array_unique($teacherNames)));
        $taNames = array_values(array_filter(array_unique($taNames)));

        return [
            'teacher_name' => $teacherName,
            'teacher_names' => $teacherNames,
            'ta_names' => $taNames,
        ];
    }

    private function formatReportPDF(\FPDF $pdf, array $data): void
    {
        $w = $pdf->GetPageWidth();
        $pdf->SetMargins(10, 10, 10);
        $bottomMarginVal = 3;
        $pdf->SetAutoPageBreak(true, $bottomMarginVal);
        $pdf->AddPage('P', 'Letter');

        $leftMargin = 10;

        $leftLogo = $this->firstExisting([
            public_path('images/Isgl_logo.png'),
            public_path('images/isgl_logo.png'),
            public_path('assets/images/isgl_logo.png'),
            public_path('assets/images/isgl.png'),
        ]);
        $rightLogo = $this->firstExisting([
            public_path('images/logo.png'),
            public_path('assets/images/logo.png'),
            public_path('assets/images/school_logo.png'),
        ]);

        if ($leftLogo) {
            $pdf->Image($leftLogo, 5, 5, 32);
        }
        if ($rightLogo) {
            $pdf->Image($rightLogo, $w - 33, 5, 32);
        }

        $pdf->SetFont('Times', 'B', 24);
        $pdf->Cell(0, 12, 'Al Rahma Sunday School', 0, 1, 'C');
        $pdf->Cell(0, 12, 'Student Report Card', 0, 1, 'C');
        $pdf->Ln(4);

        $numFmt = static function ($val) {
            if ($val === null || $val === '') {
                return 'N/A';
            }
            if (is_numeric($val)) {
                return number_format((float) $val, 1);
            }
            return (string) $val;
        };
        $splitText = static function ($text, int $limit = 55): array {
            $text = trim((string) $text);
            if ($text === '') {
                return [' ', ''];
            }
            $lines = explode("\n", wordwrap($text, $limit));
            $lines[] = '';
            return $lines;
        };
        $drawLabeledCell = static function (\FPDF $pdf, string $label, string $value, float $width = 95) {
            $pdf->SetFont('Helvetica', 'B', 11);
            $pdf->Cell($width, 10, $label, 1, 0);
            $pdf->SetFont('Helvetica', '', 12);
            $pdf->Cell($width, 10, $value, 1, 1);
        };
        $firstNonNull = static function (...$items) {
            foreach ($items as $item) {
                if ($item !== null) {
                    return $item;
                }
            }
            return null;
        };
        $wrapLinesToWidth = static function (\FPDF $pdf, string $text, float $width) {
            $text = trim((string) $text);
            if ($text === '') {
                return [' '];
            }
            if ($width <= 0) {
                return [$text];
            }
            $segments = preg_split("/\\r?\\n+/", $text);
            $lines = [];
            foreach ($segments as $seg) {
                $seg = trim($seg);
                if ($seg === '') {
                    $lines[] = ' ';
                    continue;
                }
                $words = preg_split('/\\s+/u', $seg);
                $current = '';
                foreach ($words as $word) {
                    $trial = trim($current === '' ? $word : ($current . ' ' . $word));
                    if ($pdf->GetStringWidth($trial) <= $width) {
                        $current = $trial;
                    } else {
                        if ($current !== '') {
                            $lines[] = $current;
                        }
                        $current = $word;
                    }
                }
                if ($current !== '') {
                    $lines[] = $current;
                }
            }
            return $lines;
        };
        $draw3ColumnRow = static function (\FPDF $pdf, string $label, $score, $feedback) use ($wrapLinesToWidth) {
            $w1 = 55;
            $w2 = 30;
            $w3 = 110;
            $commentWrapW = $w3 - 4;
            $lineHeight = 6;

            $pdf->SetFont('Helvetica', '', 11);
            $cellMargin = 1.0;
            $wrapWidth = max(1, $commentWrapW - (2 * $cellMargin));
            $lines = $wrapLinesToWidth($pdf, (string) $feedback, $wrapWidth);
            if (trim((string) $feedback) !== '') {
                $lines[] = ' ';
            }
            $linesCount = max(1, count($lines));
            $rowHeight = max($lineHeight, $linesCount * $lineHeight);
            $textOffsetY = ($rowHeight - ($linesCount * $lineHeight)) / 2;

            $startX = $pdf->GetX();
            $startY = $pdf->GetY();
            $labelPad = 2;

            $pdf->SetFont('Helvetica', 'B', 11);
            $pdf->SetXY($startX + $labelPad, $startY + $textOffsetY);
            $pdf->Cell($w1 - $labelPad, $lineHeight, $label, 0, 0);

            $pdf->SetFont('Helvetica', '', 12);
            $scoreStr = is_numeric($score) ? number_format((float) $score, 1) : ((string) $score !== '' ? (string) $score : 'N/A');
            $pdf->SetXY($startX + $w1, $startY + $textOffsetY);
            $pdf->Cell($w2, $lineHeight, $scoreStr, 0, 0, 'C');

            $commentX = $pdf->GetX();
            $pdf->SetFont('Helvetica', '', 11);
            $pdf->SetXY($commentX + 2, $startY + $textOffsetY);
            $pdf->MultiCell($commentWrapW, $lineHeight, implode("\n", $lines), 0, 'L');

            $pdf->SetXY($startX, $startY + $rowHeight);
        };
        $drawInfoRows = static function (\FPDF $pdf, array $rows, float $height = 12, ?float $startY = null) use ($leftMargin) {
            $leftW = 130;
            $rightW = 65;
            $startX = $leftMargin;
            $currentY = $startY !== null ? $startY : $pdf->GetY();
            $lineH = 6;
            $wrapLines = static function (string $text, float $width) use ($pdf) {
                $text = trim($text);
                if ($text === '') {
                    return [' '];
                }
                $segments = preg_split('/\\r?\\n+/', $text);
                $out = [];
                foreach ($segments as $seg) {
                    $seg = trim($seg);
                    if ($seg === '') {
                        $out[] = ' ';
                        continue;
                    }
                    $words = preg_split('/\\s+/u', $seg);
                    $current = '';
                    foreach ($words as $word) {
                        $trial = $current === '' ? $word : ($current . ' ' . $word);
                        if ($pdf->GetStringWidth($trial) <= ($width - 2)) {
                            $current = $trial;
                        } else {
                            if ($current !== '') {
                                $out[] = $current;
                            }
                            $current = $word;
                        }
                    }
                    if ($current !== '') {
                        $out[] = $current;
                    }
                }
                return !empty($out) ? $out : [' '];
            };

            if ($startY !== null) {
                $pdf->SetY($startY);
            }
            foreach ($rows as $row) {
                if (!is_array($row) || count($row) < 4) {
                    continue;
                }
                [$labelLeft, $valueLeft, $labelRight, $valueRight] = $row;
                $y = $currentY;
                $labelY = $y + 2;
                $isTeacherRow = stripos($labelLeft, 'teacher') !== false;
                if ($isTeacherRow) {
                    $labelY += 1;
                }
                $leftLines = $isTeacherRow
                    ? array_values(array_filter(array_map('rtrim', explode("\n", (string) $valueLeft)), static fn ($v) => $v !== ''))
                    : $wrapLines((string) $valueLeft, $leftW - 6);
                $rightLines = $wrapLines((string) $valueRight, $rightW - 6);
                $rowHeight = max($height, max(count($leftLines), count($rightLines)) * $lineH + 4);
                if (stripos($labelLeft, 'teacher') !== false && count($leftLines) > 1) {
                    $rowHeight += ($lineH / 4);
                }
                $pdf->SetXY($startX, $y);
                $pdf->Rect($startX, $y, $leftW, $rowHeight);
                $pdf->Rect($startX + $leftW, $y, $rightW, $rowHeight);
                $pdf->SetFont('Helvetica', 'B', 11);
                $pdf->SetXY($startX + 3, $labelY);
                $pdf->Write($lineH - 1, $labelLeft);
                $pdf->SetFont('Arial', '', 11);
                $labelLeftWidth = $pdf->GetStringWidth($labelLeft);
                $pad = 2;
                $valLeftWidth = $leftW - 6 - $labelLeftWidth - $pad;
                if ($valLeftWidth < 10) {
                    $valLeftWidth = $leftW - 6 - $pad;
                }
                $pdf->SetXY($startX + 3 + $labelLeftWidth + $pad, $labelY);
                if (count($leftLines) === 1) {
                    $pdf->Write($lineH - 1, $leftLines[0]);
                } else {
                    $pdf->MultiCell($valLeftWidth, $lineH, implode("\n", $leftLines), 0, 'L');
                }
                $pdf->SetFont('Helvetica', 'B', 11);
                $pdf->SetXY($startX + $leftW + 3, $labelY);
                $pdf->Write($lineH - 1, $labelRight);
                $pdf->SetFont('Arial', '', 11);
                $labelRightWidth = $pdf->GetStringWidth($labelRight);
                $valRightWidth = $rightW - 6 - $labelRightWidth - $pad;
                if ($valRightWidth < 10) {
                    $valRightWidth = $rightW - 6 - $pad;
                }
                $pdf->SetXY($startX + $leftW + 3 + $labelRightWidth + $pad, $labelY);
                if (count($rightLines) === 1) {
                    $pdf->Write($lineH - 1, $rightLines[0]);
                } else {
                    $pdf->MultiCell($valRightWidth, $lineH, implode("\n", $rightLines), 0, 'L');
                }
                $currentY += $rowHeight;
                $pdf->SetY($currentY);
            }
            return $currentY;
        };

        $semRaw = (string) ($data['score']['semester'] ?? ($data['selected_semester'] ?? ''));
        $semNum = null;
        $semFriendly = 'Second Semester';
        $s = strtolower(trim($semRaw));
        if ($s === 'fall' || $s === 'first' || $s === 'semester 1' || $s === '1') {
            $semNum = 1;
            $semFriendly = 'First Semester';
        } elseif ($s === 'spring' || $s === 'second' || $s === 'semester 2' || $s === '2') {
            $semNum = 2;
            $semFriendly = 'Second Semester';
        }

        $teacherNamesList = $data['teacher_names'] ?? [];
        if (empty($teacherNamesList) && !empty($data['teacher_name'])) {
            $teacherNamesList = [trim((string) $data['teacher_name'])];
        }
        $teacherNamesList = array_values(array_filter(
            array_map('trim', $teacherNamesList),
            static fn ($v) => $v !== '' && strcasecmp($v, 'N/A') !== 0
        ));
        $tas = array_values(array_filter(
            array_map(static fn ($v) => trim((string) $v), $data['ta_names'] ?? []),
            static fn ($v) => $v !== '' && strcasecmp($v, 'N/A') !== 0
        ));
        $allTeacherNames = array_values(array_unique(array_filter(
            array_merge($teacherNamesList, $tas),
            static fn ($v) => $v !== ''
        )));
        $teacherNameLines = [];
        $teacherCount = count($allTeacherNames);
        for ($i = 0; $i < $teacherCount; $i += 2) {
            $pair = array_slice($allTeacherNames, $i, 2);
            $isLastPair = ($i + 2) >= $teacherCount;
            if (count($pair) === 2) {
                $teacherNameLines[] = implode($isLastPair ? ' & ' : ', ', $pair);
            } else {
                $teacherNameLines[] = $pair[0];
            }
        }
        if (empty($teacherNameLines)) {
            $teacherNameLines[] = 'N/A';
        }
        $teacherNames = implode("\n", $teacherNameLines);

        $schoolYear = (string) ($data['score']['school_year'] ?? ($data['score']['year'] ?? 'N/A'));
        if (preg_match('/^(\\d{4})-(\\d{4})$/', $schoolYear, $m)) {
            $schoolYear = $m[1] . '/' . $m[2];
        }
        $studentName = trim(($data['student']['firstname'] ?? '') . ' ' . ($data['student']['lastname'] ?? ''));
        $gradeLabel = (string) ($data['grade'] ?? ($data['class_section_name'] ?? 'N/A'));
        $today = (string) ($data['report_date_display'] ?? date('m-d-Y'));
        $firstSemScore = $data['first_semester_score'] ?? null;
        $secondSemScore = $data['second_semester_score'] ?? ($data['total_score'] ?? null);
        $finalAverage = $data['final_average'] ?? null;
        $comments = $data['comments'] ?? [];

        $infoRow = function ($labelLeft, $valueLeft, $labelRight, $valueRight, $height = 12) use ($pdf) {
            $x = $pdf->GetX();
            $y = $pdf->GetY();
            $leftW = 115;
            $rightW = 68;
            $pdf->Rect($x, $y, $leftW, $height);
            $pdf->Rect($x + $leftW, $y, $rightW, $height);
            $pdf->SetFont('Helvetica', 'B', 11);
            $pdf->SetXY($x + 3, $y + 3);
            $pdf->Write(5, $labelLeft);
            $pdf->SetFont('Arial', '', 11);
            $pdf->Write(5, $valueLeft);
            $pdf->SetFont('Helvetica', 'B', 11);
            $pdf->SetXY($x + $leftW + 3, $y + 3);
            $pdf->Write(5, $labelRight);
            $pdf->SetFont('Arial', '', 11);
            $pdf->Write(5, $valueRight);
            $pdf->Ln($height);
        };

        $afterInfoY = $drawInfoRows($pdf, [
            ["Teachers' Names: ", '  ' . ($teacherNames !== '' ? $teacherNames : 'N/A'), 'School Year: ', '  ' . $schoolYear],
            ['Student Name: ', '  ' . ($studentName !== '' ? $studentName : 'N/A'), 'Grade: ', '  ' . ($gradeLabel !== '' ? $gradeLabel : 'N/A')],
            ['Term: ', '  ' . $semFriendly, 'Date: ', '  ' . $today],
        ]);
        $pdf->SetY($afterInfoY);

        $pdf->Ln(2);
        $finalExamScore = $data['final_exam_score'] ?? null;
        if ($finalExamScore === null) {
            if ($semNum === 1) {
                $finalExamScore = $data['score']['midterm_exam_score'] ?? null;
            } elseif ($semNum === 2) {
                $finalExamScore = $data['score']['final_exam_score'] ?? ($data['score']['midterm_exam_score'] ?? null);
            } else {
                $finalExamScore = $data['score']['final_exam_score'] ?? ($data['score']['midterm_exam_score'] ?? null);
            }
        }
        $ptapScore = $data['ptap'] ?? $data['score']['ptap_score'] ?? null;
        $attendance = $data['score']['attendance_score'] ?? null;

        $examLabel = ($semNum === 1) ? 'Midterm Exam*' : 'Final Exam*';
        $pdf->SetFont('Helvetica', 'B', 11);
        $headerX = $pdf->GetX();
        $headerY = $pdf->GetY();
        $pdf->Cell(55, 15, 'Subject', 1, 0, 'C');
        $scoreHeader = $semNum === 1 ? "1st Semester\nScore (/100)" : "2nd Semester\nScore (/100)";
        $pdf->SetXY($headerX + 55, $headerY);
        $pdf->MultiCell(30, 7.5, $scoreHeader, 1, 'C');
        $pdf->SetXY($headerX + 85, $headerY);
        $pdf->Cell(110, 15, "Teachers' Comments", 1, 1, 'C');

        $examComment = $semNum === 1
            ? $firstNonNull($comments['midterm'] ?? null, $data['score']['midterm_comment'] ?? null, $data['score']['comment'] ?? null)
            : $firstNonNull($comments['final'] ?? null, $data['score']['final_comment'] ?? null, $data['score']['comment'] ?? null);
        $ptapComment = $firstNonNull($comments['ptap'] ?? null, $data['score']['ptap_comment'] ?? null);
        $attendanceComment = $firstNonNull(
            $comments['attendance'] ?? null,
            $comments['attendance_comment'] ?? null,
            $data['score']['attendance_comment'] ?? null
        );

        $pdf->Ln(2);
        $draw3ColumnRow($pdf, $examLabel, $numFmt($finalExamScore), $examComment ?? '');
        $draw3ColumnRow($pdf, 'PTAP**', $numFmt($ptapScore), $ptapComment ?? '');
        $attendanceComment = $firstNonNull(
            $comments['attendance'] ?? null,
            $comments['attendance_comment'] ?? null,
            $data['score']['attendance_comment'] ?? null
        );
        $draw3ColumnRow($pdf, 'Attendance***', $numFmt($attendance), $attendanceComment ?? '');

        $rowsStartX = $headerX;
        $rowsStartY = $headerY + 15;
        $rowsEndY = $pdf->GetY();
        $blockHeight = $rowsEndY - $rowsStartY;
        $blockWidth = 195;
        $pdf->Rect($rowsStartX, $rowsStartY, $blockWidth, $blockHeight);
        $pdf->Line($rowsStartX + 55, $rowsStartY, $rowsStartX + 55, $rowsStartY + $blockHeight);
        $pdf->Line($rowsStartX + 85, $rowsStartY, $rowsStartX + 85, $rowsStartY + $blockHeight);

        $totalDays = $data['total_attendance_days'] ?? $data['score']['total_semester_days'] ?? $data['score']['total_days'] ?? '-';
        $absences = $data['score']['total_absences'] ?? $data['score']['absences'] ?? '0';
        $tardies = $data['score']['total_tardies'] ?? $data['score']['tardies'] ?? '0';

        $pdf->Ln(2);
        $statW = 65;
        $statH = 12;
        $startX = $pdf->GetX();
        $startY = $pdf->GetY();
        $stats = [
            ['Total Semester Days: ', (string) $totalDays],
            ['Total Absences: ', (string) $absences],
            ['Tardies (Unjustified): ', (string) $tardies],
        ];
        foreach ($stats as $i => [$lbl, $val]) {
            $x = $startX + ($i * $statW);
            $pdf->Rect($x, $startY, $statW, $statH);
            $pdf->SetXY($x + 2, $startY + 3);
            $pdf->SetFont('Helvetica', 'B', 11);
            $pdf->Write(5, $lbl . ' ');
            $pdf->SetFont('Helvetica', '', 11);
            $pdf->Write(5, $val);
        }
        $pdf->SetY($startY + $statH);

        $effectiveSecond = $secondSemScore ?? $data['score']['semester_score'] ?? $data['total_score'] ?? null;
        $finalScoreVal = $finalAverage;
        if (!is_numeric($finalScoreVal)) {
            $finalScoreVal = (is_numeric($firstSemScore) && is_numeric($effectiveSecond))
                ? number_format(((float) $firstSemScore + (float) $effectiveSecond) / 2, 1)
                : $numFmt($effectiveSecond);
        }
        $gradeCellWidth = 65;
        $gradeCellHeight = 12;
        $gradeX = $pdf->GetX();
        $gradeY = $pdf->GetY();
        $gradePad = max(0, 2 - (4 * 0.3528));

        $drawGradeCell = static function (\FPDF $pdf, float $x, float $y, float $w, float $h, string $label, string $value, float $pad) {
            $pdf->Rect($x, $y, $w, $h);
            $pdf->SetXY($x + $pad, $y + 3);
            $pdf->SetFont('Helvetica', 'B', 11);
            $pdf->Write(5, $label . ' ');
            $labelWidth = $pdf->GetStringWidth($label . ' ');
            $pdf->SetFont('Helvetica', '', 12);
            $pdf->SetXY($x + 2 + $labelWidth, $y + 3);
            $pdf->Write(5, $value);
        };

        if ($semNum === 2) {
            $firstLabel = ' 1st Semester Grade:';
            $firstValue = $numFmt($firstSemScore) . '/100';
            $secondLabel = '  2nd Semester Grade:';
            $secondValue = $numFmt($effectiveSecond) . '/100';
            $drawGradeCell($pdf, $gradeX, $gradeY, $gradeCellWidth, $gradeCellHeight, $firstLabel, $firstValue, $gradePad);
            $drawGradeCell($pdf, $gradeX + $gradeCellWidth, $gradeY, $gradeCellWidth, $gradeCellHeight, $secondLabel, $secondValue, $gradePad);
        } else {
            $gradeLabel = ' 1st Semester Grade:';
            $gradeValue = $numFmt($effectiveSecond) . '/100';
            $drawGradeCell($pdf, $gradeX, $gradeY, $gradeCellWidth, $gradeCellHeight, $gradeLabel, $gradeValue, $gradePad);
        }
        $pdf->SetY($gradeY + $gradeCellHeight);

        $finalScoreRowHeight = 12;
        $finalScoreEndY = null;
        if ($semNum === 2) {
            $finalLabel = 'Final Score****:';
            $finalValue = (is_string($finalScoreVal) ? $finalScoreVal : $numFmt($finalScoreVal)) . '/100';
            $finalCellWidth = 65;
            $finalCellHeight = $finalScoreRowHeight;
            $finalX = $pdf->GetX();
            $finalY = $pdf->GetY();
            $pdf->Rect($finalX, $finalY, $finalCellWidth, $finalCellHeight);
            $finalPad = max(0, 2 - (4 * 0.3528));
            $pdf->SetXY($finalX + $finalPad, $finalY + 3);
            $pdf->SetFont('Helvetica', 'B', 11);
            $pdf->Write(5, '  ' . $finalLabel . ' ');
            $finalLabelWidth = $pdf->GetStringWidth('  ' . $finalLabel . ' ');
            $pdf->SetFont('Helvetica', '', 12);
            $pdf->SetXY($finalX + 2 + $finalLabelWidth, $finalY + 3);
            $pdf->Write(5, $finalValue);
            $pdf->Ln($finalCellHeight);
            $finalScoreEndY = $finalY + $finalCellHeight;
        }
        $scoresEndY = $pdf->GetY();

        $examLegendLabel = ($semNum === 1) ? 'Midterm Exam' : 'Final Exam';
        $legendLines = [
            "(*) {$examLegendLabel} (Weight: 60% of semester score)",
            "(**) PTAP - Participation, Tests, Assignments, Projects (Weight: 20%)",
            "(***) Attendance = ({$totalDays} - Total Absences + One Sick Day) / {$totalDays} ) x 100 (Weight: 20%)",
        ];
        if ($semNum === 2) {
            $legendLines[] = "(****) Final Score = (1st Semester Score + 2nd Semester Score) / 2";
        }
        $legendLines[] = ['text' => 'If you have any questions, please contact the school via email at alrahma.isgl@gmail.com', 'bold' => true];
        $lineHeight = 4;
        $legendHeight = count($legendLines) * $lineHeight;
        $bottomPad = 0.1;

        $targetY = $pdf->GetPageHeight() - $bottomMarginVal - $bottomPad - $legendHeight;
        $targetY = max($pdf->GetY(), $targetY);
        $legendY = $targetY;

        $marginL = 10;
        $sigWidth = 40;
        $sigHeight = $finalScoreRowHeight;
        $sigX = $w - $sigWidth - 15;
        $legendWidth = $sigX - $marginL - 5;

        $pdf->SetXY($marginL, $legendY);
        foreach ($legendLines as $line) {
            $isBold = is_array($line) ? ($line['bold'] ?? false) : false;
            $text = is_array($line) ? ($line['text'] ?? '') : $line;
            $pdf->SetFont('Arial', $isBold ? 'B' : '', 8);
            $pdf->MultiCell($legendWidth, $lineHeight, $text, 0, 'L');
        }
        $legendEndY = $pdf->GetY();

        $pdf->SetFont('Times', 'BU', 16);
        $labelHeight = $finalScoreRowHeight;

        $sigPath = $this->firstExisting([
            public_path('images/signature.png'),
            public_path('assets/images/signature.png'),
            storage_path('report_assets/signature.png'),
        ]);
        $signatureBlockHeight = $labelHeight + ($sigPath ? $sigHeight : 0);
        $anchorEndY = ($semNum === 2 && $finalScoreEndY !== null) ? $finalScoreEndY : $scoresEndY;
        $sigGap = 2;
        $sigY = $anchorEndY + $sigGap;

        $pdf->SetXY($sigX, $sigY);
        $pdf->Cell($sigWidth + 5, $labelHeight, 'School Official', 0, 0, 'R');

        $signatureEndY = $sigY + $labelHeight;
        if ($sigPath) {
            $imgY = $sigY + $labelHeight;
            $pdf->Image($sigPath, $sigX + 3, $imgY - 2, $sigWidth + 2, $sigHeight + 5);
            $signatureEndY = $imgY + $sigHeight;
        }

        $pdf->SetY(max($legendEndY, $signatureEndY) + 4);
    }

    private function firstExisting(array $paths): ?string
    {
        foreach ($paths as $path) {
            if (is_string($path) && file_exists($path)) {
                return $path;
            }
        }
        return null;
    }
}
