<?php
/**
 * AIEngine — AI-powered student performance ranking and recommendation engine.
 *
 * Aligned with:
 *   - Nigerian National Policy on Education (NPE) / NERDC curriculum standards
 *   - WAEC/NECO grading scales (SS 1–3)
 *   - NERDC Continuous Assessment guidelines (JSS 1–3)
 *   - International best-practice: weighted CA/Exam split, linear-regression trend,
 *     absolute + relative subject profiling, multi-factor at-risk detection.
 *
 * School score structure (matches results table columns):
 *   exam_score        max 70  → 70% weight (main examination)
 *   class_performance max 15  → 15% weight (class exercises / assignments)
 *   test_score        max 10  → 10% weight (CA test / mid-term test)
 *   weekly_assessment max  5  →  5% weight (weekly/periodic assessment)
 *   Total max = 100  → performance_score is already on a 0–100 scale.
 *
 * Grading scales:
 *   SS 1/2/3  (WAEC/NECO):
 *     A1 75–100 | B2 70–74 | B3 65–69 | C4 60–64 | C5 55–59 | C6 50–54
 *     D7 45–49  | E8 40–44 | F9  0–39
 *   JSS 1/2/3 (NERDC):
 *     Distinction 75–100 | Credit 60–74 | Merit 50–59 | Pass 40–49 | Fail 0–39
 *   Primary (NERDC):
 *     Excellent 80–100 | Very Good 70–79 | Good 60–69 | Average 50–59
 *     Below Average 40–49 | Fail 0–39
 *
 * Pass mark (Nigerian standard): 50 for promotion; 40 is the minimum acceptable.
 */
class AIEngine
{
    /** @var mysqli */
    private mysqli $con;

    // -------------------------------------------------------------------------
    // Nigerian curriculum constants
    // -------------------------------------------------------------------------

    /** Minimum score to pass / be promoted (Nigerian standard: 50%) */
    private const PASS_MARK = 50;

    /** Score below which a student is considered at risk (Nigerian: below pass mark) */
    private const AT_RISK_THRESHOLD = 50;

    /** Score below which a student is in critical danger of failure */
    private const CRITICAL_THRESHOLD = 40;

    /** Score below which a student needs immediate intervention */
    private const URGENT_THRESHOLD = 30;

    public function __construct(mysqli $con)
    {
        $this->con = $con;
    }

    // -------------------------------------------------------------------------
    // Pure computation methods
    // -------------------------------------------------------------------------

    /**
     * Compute base score from raw score components.
     *
     * School score structure (matches the results table columns exactly):
     *   exam_score        max 70  → 70% weight (main examination)
     *   class_performance max 15  → 15% weight (class exercises / assignments)
     *   test_score        max 10  → 10% weight (CA test / mid-term test)
     *   weekly_assessment max  5  →  5% weight (weekly/periodic assessment)
     *   ─────────────────────────────────────────────────────────────────
     *   Total max = 100  → score is already on a 0–100 scale, no normalisation needed.
     *
     * Each component is clamped to its maximum to guard against data entry errors.
     *
     * @param array $scores Keys: exam_score, test_score, class_performance, weekly_assessment
     * @return float Base score in [0, 100]
     */
    public function computeBaseScore(array $scores): float
    {
        $exam = min((float) ($scores['exam_score'] ?? 0), 70.0);
        $class = min((float) ($scores['class_performance'] ?? 0), 15.0);
        $test = min((float) ($scores['test_score'] ?? 0), 10.0);
        $week = min((float) ($scores['weekly_assessment'] ?? 0), 5.0);

        return $exam + $class + $test + $week;
    }

    /**
     * Compute trend score using weighted linear regression over per-term averages.
     *
     * Linear regression gives a more accurate picture of trajectory than a simple
     * first-vs-last delta, especially when there are 3 terms of data.
     * Returns the slope × (n-1) so the result is on the same scale as a raw delta.
     *
     * Returns 0.0 when fewer than 2 data points are available.
     *
     * @param float[] $termAverages Ordered ascending by term (earliest first)
     * @return float Signed trend value (positive = improving, negative = declining)
     */
    public function computeTrendScore(array $termAverages): float
    {
        $n = count($termAverages);
        if ($n < 2) {
            return 0.0;
        }

        // Weighted linear regression: more recent terms carry higher weight
        $weights = [];
        for ($i = 0; $i < $n; $i++) {
            $weights[$i] = $i + 1; // weight 1, 2, 3 … (most recent = highest)
        }
        $wSum = array_sum($weights);
        $xBar = 0.0;
        $yBar = 0.0;
        for ($i = 0; $i < $n; $i++) {
            $xBar += $weights[$i] * $i;
            $yBar += $weights[$i] * $termAverages[$i];
        }
        $xBar /= $wSum;
        $yBar /= $wSum;

        $num = 0.0;
        $den = 0.0;
        for ($i = 0; $i < $n; $i++) {
            $num += $weights[$i] * ($i - $xBar) * ($termAverages[$i] - $yBar);
            $den += $weights[$i] * ($i - $xBar) ** 2;
        }

        $slope = ($den != 0) ? $num / $den : 0.0;

        // Scale slope to a meaningful delta: slope × (n-1) terms
        return round($slope * ($n - 1), 2);
    }

    /**
     * Compute final performance score.
     *
     * The base score (0–100) is the primary signal.
     * The trend modifier is capped at ±5 points to prevent trend from
     * overriding the actual score (international best practice: trend is
     * a secondary signal, not a primary one).
     *
     * @param float $baseScore  Base score in [0, 100]
     * @param float $trendScore Signed trend value
     * @return float Performance score in [0, 100]
     */
    public function computePerformanceScore(float $baseScore, float $trendScore): float
    {
        // Cap trend modifier at ±5 points
        $modifier = max(-5.0, min(5.0, $trendScore / 10.0));
        $score = $baseScore + $modifier;
        return (float) max(0.0, min(100.0, round($score, 2)));
    }

    /**
     * Assign a performance band based on score and class level.
     *
     * SS 1/2/3  → WAEC/NECO 9-point scale
     * JSS 1/2/3 → NERDC 5-point scale (Distinction/Credit/Merit/Pass/Fail)
     * Primary   → NERDC 6-point scale
     *
     * @param float  $score Performance score in [0, 100]
     * @param string $class Class name e.g. "JSS 1", "SS 2", "Primary 5"
     * @return string Grade/band label
     */
    public function assignBand(float $score, string $class = ''): string
    {
        $classUpper = strtoupper(trim($class));

        // SS 1–3: WAEC/NECO grading scale
        if (preg_match('/^SS\s*[123]$/', $classUpper)) {
            if ($score >= 75)
                return 'A1';
            if ($score >= 70)
                return 'B2';
            if ($score >= 65)
                return 'B3';
            if ($score >= 60)
                return 'C4';
            if ($score >= 55)
                return 'C5';
            if ($score >= 50)
                return 'C6';
            if ($score >= 45)
                return 'D7';
            if ($score >= 40)
                return 'E8';
            return 'F9';
        }

        // JSS 1–3: NERDC grading scale
        if (preg_match('/^JSS\s*[123]$/', $classUpper) || preg_match('/^JS\s*[123]$/', $classUpper)) {
            if ($score >= 75)
                return 'Distinction';
            if ($score >= 60)
                return 'Credit';
            if ($score >= 50)
                return 'Merit';
            if ($score >= 40)
                return 'Pass';
            return 'Fail';
        }

        // Primary: NERDC primary grading scale
        if (preg_match('/^(PRIMARY|PRIM|PRI)\s*[1-6]$/', $classUpper)) {
            if ($score >= 80)
                return 'Excellent';
            if ($score >= 70)
                return 'Very Good';
            if ($score >= 60)
                return 'Good';
            if ($score >= 50)
                return 'Average';
            if ($score >= 40)
                return 'Below Average';
            return 'Fail';
        }

        // Fallback: generic Nigerian pass/fail scale
        if ($score >= 75)
            return 'A';
        if ($score >= 60)
            return 'B';
        if ($score >= 50)
            return 'C';
        if ($score >= 40)
            return 'D';
        return 'F';
    }

    /**
     * Determine if a band represents a pass under Nigerian curriculum.
     *
     * @param string $band
     * @return bool
     */
    public function isBandPass(string $band): bool
    {
        $failBands = ['F9', 'Fail', 'F'];
        return !in_array($band, $failBands, true);
    }

    /**
     * Compute subject strength profile for a student.
     *
     * Uses both relative comparison (vs class average) and absolute threshold
     * (Nigerian pass mark = 50) for a more accurate picture.
     *
     * @param array $studentSubjectScores  ['SubjectName' => float score, ...]
     * @param array $classAverages         ['SubjectName' => float avg, ...]
     * @return array ['strengths' => [...], 'improvements' => [...]]
     */
    public function computeSubjectStrengthProfile(array $studentSubjectScores, array $classAverages): array
    {
        $above = [];
        $below = [];

        foreach ($studentSubjectScores as $subject => $studentScore) {
            $classAvg = (float) ($classAverages[$subject] ?? 0);
            $diff = (float) $studentScore - $classAvg;

            $entry = [
                'subject' => $subject,
                'student_score' => (float) $studentScore,
                'class_avg' => $classAvg,
                'diff' => round($diff, 2),
                'below_pass' => $studentScore < self::PASS_MARK,
            ];

            // Strength: above class average AND above pass mark
            if ($diff > 0 && $studentScore >= self::PASS_MARK) {
                $above[] = $entry;
            } else {
                // Improvement needed: below class average OR below pass mark
                $below[] = $entry;
            }
        }

        // Sort strengths descending by diff; improvements: below-pass first, then by diff ascending
        usort($above, fn($a, $b) => $b['diff'] <=> $a['diff']);
        usort($below, function ($a, $b) {
            if ($a['below_pass'] !== $b['below_pass']) {
                return $a['below_pass'] ? -1 : 1; // below-pass subjects first
            }
            return $a['diff'] <=> $b['diff'];
        });

        return [
            'strengths' => array_slice($above, 0, 3),
            'improvements' => array_slice($below, 0, 3),
        ];
    }

    /**
     * Detect whether a student is at risk using Nigerian curriculum thresholds.
     *
     * PRIMARY signal: base_score (actual exam + CA marks, no trend modifier).
     * This ensures a student who scored 46 raw but has a positive trend is still
     * correctly flagged — their actual marks are below the pass mark regardless of trajectory.
     *
     * SECONDARY signal: trend_history adds context to the reason message and catches
     * students who are currently passing but consistently declining.
     *
     * Risk levels (based on base_score):
     *   Critical → base_score < 30  (urgent intervention required)
     *   High     → base_score < 40  (below NERDC minimum acceptable / WAEC E8 boundary)
     *   Moderate → base_score < 50  (below Nigerian pass mark)
     *   Moderate → base_score >= 50 but 3 consecutive term declines (early warning)
     *
     * @param float   $baseScore    Raw score (exam + CA, no trend modifier) in [0, 100]
     * @param float[] $trendHistory Ordered array of per-term averages (ascending)
     * @return array ['isAtRisk' => bool, 'riskLevel' => string, 'reason' => string]
     */
    public function detectAtRisk(float $baseScore, array $trendHistory): array
    {
        $trendScore = $this->computeTrendScore($trendHistory);

        // Trigger 1: Critical — raw marks below 30 (urgent)
        if ($baseScore < self::URGENT_THRESHOLD) {
            return [
                'isAtRisk' => true,
                'riskLevel' => 'Critical',
                'reason' => sprintf(
                    'Actual score critically low (%.1f%% — below 30%%). Immediate intervention required.',
                    $baseScore
                ),
            ];
        }

        // Trigger 2: High — raw marks below 40 (NERDC minimum / WAEC E8 boundary)
        if ($baseScore < self::CRITICAL_THRESHOLD) {
            $extra = $trendScore < 0 ? ' Declining trend makes recovery harder.' : '';
            return [
                'isAtRisk' => true,
                'riskLevel' => 'High',
                'reason' => sprintf(
                    'Actual score below minimum acceptable threshold (%.1f%% — below 40%%). At risk of failure.%s',
                    $baseScore,
                    $extra
                ),
            ];
        }

        // Trigger 3: Moderate — raw marks below Nigerian pass mark (50)
        if ($baseScore < self::PASS_MARK) {
            $reason = sprintf('Actual score below Nigerian pass mark (%.1f%% — below 50%%).', $baseScore);
            if ($trendScore < -2) {
                $reason .= ' Declining trend detected — risk of further deterioration.';
            } elseif ($trendScore > 2) {
                $reason .= ' Improving trend — early support may help student reach pass mark.';
            }
            return [
                'isAtRisk' => true,
                'riskLevel' => 'Moderate',
                'reason' => $reason,
            ];
        }

        // Trigger 4: Currently passing but 3 consecutive term declines (early warning)
        if (count($trendHistory) >= 3) {
            $consecutiveDeclines = 0;
            for ($i = 1; $i < count($trendHistory); $i++) {
                if ($trendHistory[$i] < $trendHistory[$i - 1]) {
                    $consecutiveDeclines++;
                } else {
                    $consecutiveDeclines = 0;
                }
            }
            if ($consecutiveDeclines >= 2) {
                return [
                    'isAtRisk' => true,
                    'riskLevel' => 'Moderate',
                    'reason' => sprintf(
                        'Currently passing (%.1f%%) but consistent decline across 3 consecutive terms. '
                        . 'Early intervention recommended before score drops below pass mark.',
                        $baseScore
                    ),
                ];
            }
        }

        return ['isAtRisk' => false, 'riskLevel' => '', 'reason' => ''];
    }

    /**
     * Rank students using standard competition ranking (ties share rank, next rank skips).
     *
     * @param array $students Each element must have 'performance_score' key
     * @return array Same students with 'rank' key added, sorted descending
     */
    public function rankStudents(array $students): array
    {
        // Primary: base_score descending (actual marks, no trend modifier)
        // Tie-break: student_name ascending (alphabetical — standard Nigerian school practice)
        usort($students, function ($a, $b) {
            $scoreCmp = $b['base_score'] <=> $a['base_score'];
            if ($scoreCmp !== 0)
                return $scoreCmp;
            return strcmp($a['student_name'] ?? '', $b['student_name'] ?? '');
        });

        $rank = 1;
        $count = 0;
        $prevScore = null;

        foreach ($students as &$student) {
            $count++;
            if ($prevScore !== null && (float) $student['base_score'] < (float) $prevScore) {
                $rank = $count;
            }
            $student['rank'] = $rank;
            $prevScore = $student['base_score'];
        }
        unset($student);

        return $students;
    }

    // -------------------------------------------------------------------------
    // Recommendation methods (Nigerian curriculum language & context)
    // -------------------------------------------------------------------------

    /**
     * Sort a subject-average map with deterministic tie-breaking.
     *
     * Tie-break order:
     *   1. Average score (primary — ascending for lowest, descending for highest)
     *   2. Student count (more students = more representative — descending)
     *   3. Subject name (alphabetical — deterministic final tie-break)
     *
     * @param array $subjectAvgs   ['SubjectName' => float avg]
     * @param array $subjectCounts ['SubjectName' => int count]  (optional)
     * @param bool  $ascending     true = lowest first, false = highest first
     * @return array Sorted copy of $subjectAvgs
     */
    private function sortSubjectAverages(array $subjectAvgs, array $subjectCounts = [], bool $ascending = true): array
    {
        uksort($subjectAvgs, function ($a, $b) use ($subjectAvgs, $subjectCounts, $ascending) {
            $avgA = (float) ($subjectAvgs[$a] ?? 0);
            $avgB = (float) ($subjectAvgs[$b] ?? 0);

            // 1. Compare averages (rounded to 2dp to avoid float noise)
            $roundedA = round($avgA, 2);
            $roundedB = round($avgB, 2);
            if ($roundedA !== $roundedB) {
                return $ascending
                    ? ($roundedA <=> $roundedB)
                    : ($roundedB <=> $roundedA);
            }

            // 2. Tie-break: more students = more representative (descending)
            $countA = (int) ($subjectCounts[$a] ?? 0);
            $countB = (int) ($subjectCounts[$b] ?? 0);
            if ($countA !== $countB) {
                return $countB <=> $countA; // higher count first
            }

            // 3. Final tie-break: alphabetical
            return strcmp($a, $b);
        });

        return $subjectAvgs;
    }

    /**
     * Generate a parent-facing recommendation in Nigerian school context.
     *
     * @param array $studentData Keys: student_name, gender, performance_score,
     *                           trend_score, is_at_risk, risk_level,
     *                           strength_subjects, improvement_subjects, ai_band
     * @return string Recommendation text
     */
    public function generateParentRecommendation(array $studentData): string
    {
        $name = $studentData['student_name'] ?? 'Your child';
        $score = (float) ($studentData['base_score'] ?? 0);  // actual marks, no trend modifier
        $trend = (float) ($studentData['trend_score'] ?? 0);
        $isAtRisk = (bool) ($studentData['is_at_risk'] ?? false);
        $riskLevel = $studentData['risk_level'] ?? '';
        $band = $studentData['ai_band'] ?? '';
        $strengths = $studentData['strength_subjects'] ?? [];
        $improvements = $studentData['improvement_subjects'] ?? [];

        $gender = strtolower($studentData['gender'] ?? '');
        $pronoun = $gender === 'female' ? 'her' : 'his';
        $subject = $gender === 'female' ? 'She' : 'He';
        $object = $gender === 'female' ? 'her' : 'him';

        // Critical / High risk
        if ($isAtRisk && in_array($riskLevel, ['Critical', 'High'])) {
            $weakSubject = !empty($improvements) ? $improvements[0]['subject'] : 'several subjects';
            return "{$name} is currently facing serious academic challenges with a performance score of "
                . round($score, 1) . "% (Grade: {$band}). "
                . "{$subject} is particularly struggling in {$weakSubject}. "
                . "We strongly recommend an urgent meeting with {$pronoun} class teacher and subject teachers "
                . "to develop a personalised remediation plan. "
                . "Daily supervised study and extra lessons are advised to prevent failure at the end of term.";
        }

        // Moderate risk (below pass mark)
        if ($isAtRisk && $riskLevel === 'Moderate') {
            $weakSubject = !empty($improvements) ? $improvements[0]['subject'] : 'some subjects';
            return "{$name}'s current performance of " . round($score, 1) . "% (Grade: {$band}) "
                . "is below the required pass mark of 50%. "
                . "{$subject} needs additional support, especially in {$weakSubject}. "
                . "Please ensure {$object} attends all classes, completes assignments on time, "
                . "and dedicates extra study time at home. Consider enrolling {$object} in after-school lessons.";
        }

        // Passing but declining
        if (!$isAtRisk && $trend < -2) {
            $weakSubject = !empty($improvements) ? $improvements[0]['subject'] : 'some subjects';
            return "{$name} is currently passing with " . round($score, 1) . "% (Grade: {$band}), "
                . "but {$pronoun} performance has been declining this term, particularly in {$weakSubject}. "
                . "Encourage {$object} to review class notes regularly and seek help from teachers early "
                . "before the decline affects {$pronoun} final grade.";
        }

        // Improving
        if ($trend > 2) {
            $topSubject = !empty($strengths) ? $strengths[0]['subject'] : 'several subjects';
            return "{$name} is making excellent progress with " . round($score, 1) . "% (Grade: {$band})! "
                . "{$subject} has shown notable improvement this term, especially in {$topSubject}. "
                . "Keep encouraging {$object} to maintain this positive momentum by staying consistent "
                . "with assignments and revision. Well done!";
        }

        // Stable and passing
        $topSubject = !empty($strengths) ? $strengths[0]['subject'] : 'most subjects';
        return "{$name} is performing steadily at " . round($score, 1) . "% (Grade: {$band}), "
            . "showing consistency in {$topSubject}. "
            . "Encourage {$object} to aim higher by dedicating extra revision time "
            . "and actively participating in class. With a little more effort, {$subject} can achieve a distinction.";
    }

    /**
     * Generate class-level recommendations for a teacher (Nigerian context).
     *
     * @param array $classData Keys: class, section, students, subject_averages
     * @return array Array of recommendation strings
     */
    public function generateTeacherRecommendations(array $classData): array
    {
        $recommendations = [];
        $students = $classData['students'] ?? [];
        $subjectAverages = $classData['subject_averages'] ?? [];
        $class = $classData['class'] ?? '';
        $section = $classData['section'] ?? '';

        if (empty($students)) {
            return ['No student data available for this class.'];
        }

        $total = count($students);
        $atRisk = array_filter($students, fn($s) => $s['is_at_risk'] ?? false);
        $failing = array_filter($students, fn($s) => ($s['base_score'] ?? 0) < self::PASS_MARK);
        $critical = array_filter($students, fn($s) => ($s['base_score'] ?? 0) < self::CRITICAL_THRESHOLD);
        $declining = array_filter($students, fn($s) => ($s['trend_score'] ?? 0) < -2);

        // Critical cases
        if (!empty($critical)) {
            $names = implode(', ', array_map(fn($s) => $s['student_name'] ?? 'Unknown', array_slice($critical, 0, 3)));
            $more = count($critical) > 3 ? ' and ' . (count($critical) - 3) . ' others' : '';
            $recommendations[] = "URGENT: " . count($critical) . " student(s) in {$class} {$section} are critically below "
                . "the minimum acceptable score (40%): {$names}{$more}. "
                . "Immediate remediation sessions and parent notifications are required per NERDC guidelines.";
        }

        // Below pass mark
        $failPct = $total > 0 ? round((count($failing) / $total) * 100) : 0;
        if ($failPct > 0) {
            $recommendations[] = "{$failPct}% of students in {$class} {$section} are below the Nigerian pass mark of 50%. "
                . "Consider organising targeted revision classes and continuous assessment review sessions "
                . "before the end-of-term examination.";
        }

        // Declining trend students
        if (!empty($declining)) {
            $names = implode(', ', array_map(fn($s) => $s['student_name'] ?? 'Unknown', array_slice($declining, 0, 3)));
            $more = count($declining) > 3 ? ' and others' : '';
            $recommendations[] = "The following students show a declining performance trend in {$class} {$section}: "
                . "{$names}{$more}. Individual attention and early intervention are recommended.";
        }

        // Weakest subject (deterministic tie-breaking: more students first, then alphabetical)
        if (!empty($subjectAverages)) {
            $sortedSubjects = $subjectAverages;
            uksort($sortedSubjects, function ($a, $b) use ($sortedSubjects) {
                $diff = round($sortedSubjects[$a], 2) <=> round($sortedSubjects[$b], 2);
                if ($diff !== 0)
                    return $diff; // ascending: lowest first
                return strcmp($a, $b);         // alphabetical tie-break
            });
            $lowestSubject = array_key_first($sortedSubjects);
            $lowestAvg = round($sortedSubjects[$lowestSubject], 1);
            $recommendations[] = "The subject with the lowest class average in {$class} {$section} is "
                . "{$lowestSubject} ({$lowestAvg}%). "
                . "A focused revision session and additional practice exercises are recommended for this subject "
                . "in line with the NERDC curriculum objectives.";
        }

        // Systemic challenge: >40% at risk (Nigerian threshold for class-wide intervention)
        $atRiskPct = $total > 0 ? round((count($atRisk) / $total) * 100) : 0;
        if ($atRiskPct > 40) {
            $recommendations[] = "{$atRiskPct}% of students in {$class} {$section} are at risk. "
                . "This indicates a systemic teaching or learning challenge. "
                . "A review of instructional methods, teaching aids, and curriculum delivery is strongly advised. "
                . "Consider reporting to the academic coordinator for school-wide support.";
        }

        return $recommendations;
    }

    /**
     * Generate school-level recommendations for administration (Nigerian context).
     *
     * @param array $aggregatedData Keys: total_students, school_trend_score,
     *                              subject_averages, class_at_risk_counts, class_totals,
     *                              school_pass_rate, school_average
     * @return array Array of recommendation strings
     */
    public function generateAdminRecommendations(array $aggregatedData): array
    {
        $totalStudents = (int) ($aggregatedData['total_students'] ?? 0);
        $schoolTrend = (float) ($aggregatedData['school_trend_score'] ?? 0);
        $subjectAverages = $aggregatedData['subject_averages'] ?? [];
        $subjectCounts = $aggregatedData['subject_counts'] ?? [];
        $classAtRisk = $aggregatedData['class_at_risk_counts'] ?? [];
        $classTotals = $aggregatedData['class_totals'] ?? [];
        $classAvgScores = $aggregatedData['class_avg_scores'] ?? [];
        $bestClass = $aggregatedData['best_class'] ?? '';
        $worstClass = $aggregatedData['worst_class'] ?? '';
        $schoolPassRate = (float) ($aggregatedData['school_pass_rate'] ?? 0);
        $schoolAverage = (float) ($aggregatedData['school_average'] ?? 0);
        $caAverages = $aggregatedData['ca_averages'] ?? [];

        if ($totalStudents < 5) {
            return ['Insufficient data to generate school-level recommendations.'];
        }

        $recommendations = [];

        // ── 1. School-wide average vs Nigerian pass mark ──────────────────────
        if ($schoolAverage < self::PASS_MARK) {
            $recommendations[] = sprintf(
                'The school-wide average score is %.1f%%, which is below the Nigerian pass mark of 50%%. '
                . 'A comprehensive academic improvement programme is urgently required. '
                . 'School management should review curriculum delivery, teacher effectiveness, '
                . 'and student support systems in line with NERDC and FME guidelines.',
                $schoolAverage
            );
        } else {
            $recommendations[] = sprintf(
                'The school-wide average score is %.1f%%. %s',
                $schoolAverage,
                $schoolAverage >= 70
                ? 'This is an excellent result. Continue reinforcing current teaching strategies.'
                : 'Performance is above the pass mark. Focus on moving more students into the distinction range (75%+).'
            );
        }

        // ── 2. Pass rate ──────────────────────────────────────────────────────
        if ($schoolPassRate < 50) {
            $recommendations[] = sprintf(
                'Only %.1f%% of students are passing (score ≥ 50%%). '
                . 'This is critically low. Immediate school-wide remediation, intensified continuous assessment, '
                . 'and parent engagement are required.',
                $schoolPassRate
            );
        } elseif ($schoolPassRate < 70) {
            $recommendations[] = sprintf(
                'The school pass rate is %.1f%%, below the recommended 70%% benchmark. '
                . 'Intensify continuous assessment practices, increase remediation sessions, '
                . 'and strengthen monitoring of at-risk students.',
                $schoolPassRate
            );
        } else {
            $recommendations[] = sprintf(
                'The school pass rate is %.1f%%. '
                . 'Good performance overall — focus on converting borderline students (50–59%%) into the merit range.',
                $schoolPassRate
            );
        }

        // ── 3. CA component analysis (exam vs CA breakdown) ──────────────────
        if (!empty($caAverages)) {
            $examAvg = $caAverages['exam_score'] ?? 0;
            $testAvg = $caAverages['test_score'] ?? 0;
            $classAvg = $caAverages['class_performance'] ?? 0;
            $weekAvg = $caAverages['weekly_assessment'] ?? 0;

            $examEfficiency = $examAvg / 70 * 100;
            $testEfficiency = $testAvg / 10 * 100;
            $classEfficiency = $classAvg / 15 * 100;
            $weekEfficiency = $weekAvg / 5 * 100;

            $efficiencies = [
                'Main Examination (70 marks)' => $examEfficiency,
                'CA Test (10 marks)' => $testEfficiency,
                'Class Performance (15 marks)' => $classEfficiency,
                'Weekly Assessment (5 marks)' => $weekEfficiency,
            ];
            // Sort ascending with alphabetical tie-break (deterministic)
            uksort($efficiencies, function ($a, $b) use ($efficiencies) {
                $diff = round($efficiencies[$a], 2) <=> round($efficiencies[$b], 2);
                return $diff !== 0 ? $diff : strcmp($a, $b);
            });
            $weakestComponent = array_key_first($efficiencies);
            $weakestPct = round(reset($efficiencies), 1);

            if ($weakestPct < 60) {
                $recommendations[] = sprintf(
                    'Students are weakest in %s, achieving only %.1f%% of available marks in this component. '
                    . 'School averages — Exam: %.1f/70, CA Test: %.1f/10, Class Performance: %.1f/15, Weekly: %.1f/5. '
                    . 'Targeted improvement in this component will have the greatest impact on overall scores.',
                    $weakestComponent,
                    $weakestPct,
                    round($examAvg, 1),
                    round($testAvg, 1),
                    round($classAvg, 1),
                    round($weekAvg, 1)
                );
            } else {
                $recommendations[] = sprintf(
                    'CA component breakdown — School averages: Exam %.1f/70 (%.0f%%), '
                    . 'CA Test %.1f/10 (%.0f%%), Class Performance %.1f/15 (%.0f%%), Weekly %.1f/5 (%.0f%%).',
                    round($examAvg, 1),
                    round($examEfficiency),
                    round($testAvg, 1),
                    round($testEfficiency),
                    round($classAvg, 1),
                    round($classEfficiency),
                    round($weekAvg, 1),
                    round($weekEfficiency)
                );
            }
        }

        // ── 4. Best and worst performing class ────────────────────────────────
        if ($bestClass !== '' && $worstClass !== '' && $bestClass !== $worstClass) {
            $bestAvg = round($classAvgScores[$bestClass] ?? 0, 1);
            $worstAvg = round($classAvgScores[$worstClass] ?? 0, 1);
            $recommendations[] = sprintf(
                'Best performing class: %s (average %.1f%%). '
                . 'Weakest performing class: %s (average %.1f%%). '
                . 'A gap of %.1f points exists — consider peer-learning programmes '
                . 'and resource reallocation to close this gap.',
                str_replace('|', ' ', $bestClass),
                $bestAvg,
                str_replace('|', ' ', $worstClass),
                $worstAvg,
                abs($bestAvg - $worstAvg)
            );
        }

        // ── 5. Best and worst subject (deterministic tie-breaking) ───────────
        if (!empty($subjectAverages)) {
            // Lowest first (ascending), tie-break: more students first, then alphabetical
            $sortedAsc = $this->sortSubjectAverages($subjectAverages, $subjectCounts, true);
            $lowestSubject = array_key_first($sortedAsc);
            $lowestAvg = round($sortedAsc[$lowestSubject], 1);

            // Highest first (descending), same tie-breaking
            $sortedDesc = $this->sortSubjectAverages($subjectAverages, $subjectCounts, false);
            $highestSubject = array_key_first($sortedDesc);
            $highestAvg = round($sortedDesc[$highestSubject], 1);

            $recommendations[] = sprintf(
                'Subject performance: Best subject is %s (avg %.1f%%), weakest is %s (avg %.1f%%). '
                . 'A review of teaching resources, curriculum delivery, and student engagement '
                . 'for %s is recommended. Consider subject-specific workshops for teachers.',
                $highestSubject,
                $highestAvg,
                $lowestSubject,
                $lowestAvg,
                $lowestSubject
            );
        }

        // ── 6. Highest-risk class ─────────────────────────────────────────────
        $highestRiskClass = '';
        $highestRiskPct = 0;
        foreach ($classAtRisk as $classKey => $atRiskCount) {
            $classTotal = $classTotals[$classKey] ?? 0;
            if ($classTotal > 0) {
                $pct = ($atRiskCount / $classTotal) * 100;
                if ($pct > $highestRiskPct) {
                    $highestRiskPct = $pct;
                    $highestRiskClass = $classKey;
                }
            }
        }
        if ($highestRiskClass !== '' && $highestRiskPct > 20) {
            $recommendations[] = sprintf(
                '%s has the highest concentration of at-risk students (%.0f%% of the class). '
                . 'A targeted academic intervention programme, including remedial classes and parent engagement, '
                . 'is recommended for this class in line with NERDC student support guidelines.',
                str_replace('|', ' ', $highestRiskClass),
                $highestRiskPct
            );
        }

        // ── 7. School-wide trend ──────────────────────────────────────────────
        if ($schoolTrend < -2) {
            $recommendations[] = sprintf(
                'The school-wide performance trend is declining (%.1f points this session). '
                . 'A comprehensive curriculum review, staff professional development programme, '
                . 'and enhanced student monitoring system are recommended to reverse this trend.',
                $schoolTrend
            );
        } elseif ($schoolTrend > 2) {
            $recommendations[] = sprintf(
                'The school-wide performance trend is positive (+%.1f points this session). '
                . 'Continue reinforcing current teaching strategies and recognise top-performing teachers and students.',
                $schoolTrend
            );
        }

        return $recommendations;
    }

    // -------------------------------------------------------------------------
    // Orchestration
    // -------------------------------------------------------------------------

    /**
     * Orchestrate all AI computations for the given filters.
     *
     * @param array $filters Keys: session_id, term_id, class, section, subject
     * @return array Full AI report payload
     */
    public function processAll(array $filters): array
    {
        $sessionId = $this->con->real_escape_string($filters['session_id'] ?? '');
        $termId = $this->con->real_escape_string($filters['term_id'] ?? '');
        $class = $this->con->real_escape_string($filters['class'] ?? '');
        $section = $this->con->real_escape_string($filters['section'] ?? '');

        if (empty($sessionId)) {
            throw new \InvalidArgumentException('session_id is required.');
        }

        // Build WHERE clause for the selected term/class/section
        $where = ["r.session_id = '{$sessionId}'", "r.status = 'Approved'"];
        if (!empty($termId) && $termId !== 'all') {
            $where[] = "r.term_id = '{$termId}'";
        }
        if (!empty($class) && $class !== 'all') {
            $where[] = "r.class = '{$class}'";
        }
        if (!empty($section) && $section !== 'all') {
            $where[] = "r.section = '{$section}'";
        }
        $whereClause = implode(' AND ', $where);

        // 1. Fetch per-student component scores (averaged across terms if multiple)
        $scoresSql = "
            SELECT
                r.student_id,
                s.studentName,
                s.studentno,
                s.gender,
                r.class,
                r.section,
                AVG(r.exam_score)        AS exam_score,
                AVG(r.test_score)        AS test_score,
                AVG(r.class_performance) AS class_performance,
                AVG(r.weekly_assessment) AS weekly_assessment,
                AVG(r.total_score)       AS base_score
            FROM results r
            INNER JOIN students s ON s.id = r.student_id
            WHERE {$whereClause}
            GROUP BY r.student_id, r.class, r.section
        ";
        $result = $this->con->query($scoresSql);
        $students = [];
        while ($row = $result->fetch_assoc()) {
            $students[$row['student_id']] = $row;
        }

        if (empty($students)) {
            return [
                'rankings' => [],
                'atRiskStudents' => [],
                'schoolInsights' => ['No approved results found for the selected filters.'],
                'classInsights' => [],
                'studentRecommendations' => [],
            ];
        }

        // 2. Fetch multi-term averages per student for trend computation
        $trendWhere = ["r.session_id = '{$sessionId}'", "r.status = 'Approved'"];
        if (!empty($class) && $class !== 'all') {
            $trendWhere[] = "r.class = '{$class}'";
        }
        if (!empty($section) && $section !== 'all') {
            $trendWhere[] = "r.section = '{$section}'";
        }
        $trendWhereClause = implode(' AND ', $trendWhere);

        $trendSql = "
            SELECT
                r.student_id,
                r.term_id,
                AVG(r.total_score) AS term_avg
            FROM results r
            WHERE {$trendWhereClause}
            GROUP BY r.student_id, r.term_id
            ORDER BY r.student_id, r.term_id ASC
        ";
        $trendResult = $this->con->query($trendSql);
        $trendHistory = [];
        while ($row = $trendResult->fetch_assoc()) {
            $trendHistory[$row['student_id']][] = (float) $row['term_avg'];
        }

        // 3. Fetch per-subject class averages for strength profiling
        $subjectAvgSql = "
            SELECT
                r.subject,
                r.class,
                r.section,
                AVG(r.total_score) AS class_avg
            FROM results r
            WHERE {$whereClause}
            GROUP BY r.subject, r.class, r.section
        ";
        $subjectAvgResult = $this->con->query($subjectAvgSql);
        $classSubjectAvgs = [];
        $schoolSubjectAvgs = [];
        while ($row = $subjectAvgResult->fetch_assoc()) {
            $key = $row['class'] . '|' . $row['section'];
            $classSubjectAvgs[$key][$row['subject']] = (float) $row['class_avg'];
            // Accumulate for school-wide average
            if (!isset($schoolSubjectAvgs[$row['subject']])) {
                $schoolSubjectAvgs[$row['subject']] = ['total' => 0, 'count' => 0];
            }
            $schoolSubjectAvgs[$row['subject']]['total'] += (float) $row['class_avg'];
            $schoolSubjectAvgs[$row['subject']]['count']++;
        }
        // Flatten school-wide averages
        $schoolSubjectFlat = [];
        foreach ($schoolSubjectAvgs as $subj => $data) {
            $schoolSubjectFlat[$subj] = $data['count'] > 0 ? $data['total'] / $data['count'] : 0;
        }

        // 4. Fetch per-student per-subject scores
        $studentSubjectSql = "
            SELECT
                r.student_id,
                r.subject,
                AVG(r.total_score) AS student_subject_avg
            FROM results r
            WHERE {$whereClause}
            GROUP BY r.student_id, r.subject
        ";
        $studentSubjectResult = $this->con->query($studentSubjectSql);
        $studentSubjectScores = [];
        while ($row = $studentSubjectResult->fetch_assoc()) {
            $studentSubjectScores[$row['student_id']][$row['subject']] = (float) $row['student_subject_avg'];
        }

        // 5. Compute per-student AI data
        $processedStudents = [];
        $atRiskStudents = [];
        $classGroups = [];

        foreach ($students as $studentId => $studentRow) {
            $scores = [
                'exam_score' => (float) $studentRow['exam_score'],
                'test_score' => (float) $studentRow['test_score'],
                'class_performance' => (float) $studentRow['class_performance'],
                'weekly_assessment' => (float) $studentRow['weekly_assessment'],
            ];

            $baseScore = $this->computeBaseScore($scores);
            $termAvgs = $trendHistory[$studentId] ?? [];
            $trendScore = $this->computeTrendScore($termAvgs);
            // performance_score kept for display only (trend-adjusted); all logic uses base_score
            $perfScore = $this->computePerformanceScore($baseScore, $trendScore);
            // Band assigned from base_score (actual marks)
            $band = $this->assignBand($baseScore, $studentRow['class']);

            $classKey = $studentRow['class'] . '|' . $studentRow['section'];
            $classAvgs = $classSubjectAvgs[$classKey] ?? [];
            $subjScores = $studentSubjectScores[$studentId] ?? [];
            $profile = $this->computeSubjectStrengthProfile($subjScores, $classAvgs);

            $atRiskData = $this->detectAtRisk($baseScore, $termAvgs);

            $studentData = [
                'student_id' => $studentId,
                'student_name' => $studentRow['studentName'],
                'gender' => $studentRow['gender'] ?? '',
                'studentno' => $studentRow['studentno'],
                'class' => $studentRow['class'],
                'section' => $studentRow['section'],
                'exam_score' => round($scores['exam_score'], 2),
                'test_score' => round($scores['test_score'], 2),
                'class_performance' => round($scores['class_performance'], 2),
                'weekly_assessment' => round($scores['weekly_assessment'], 2),
                'base_score' => round($baseScore, 2),
                'trend_score' => round($trendScore, 2),
                'performance_score' => round($perfScore, 2),
                'ai_band' => $band,
                'is_at_risk' => $atRiskData['isAtRisk'],
                'risk_level' => $atRiskData['riskLevel'],
                'risk_reason' => $atRiskData['reason'],
                'strength_subjects' => $profile['strengths'],
                'improvement_subjects' => $profile['improvements'],
            ];

            // Parent recommendation
            $studentData['parent_recommendation'] = $this->generateParentRecommendation($studentData);

            $processedStudents[] = $studentData;

            if ($atRiskData['isAtRisk']) {
                $atRiskStudents[] = $studentData;
            }

            // Group by class-arm for teacher recommendations
            if (!isset($classGroups[$classKey])) {
                $classGroups[$classKey] = [
                    'class' => $studentRow['class'],
                    'section' => $studentRow['section'],
                    'students' => [],
                    'subject_averages' => $classAvgs,
                ];
            }
            $classGroups[$classKey]['students'][] = $studentData;
        }

        // 6. Rank students
        $rankings = $this->rankStudents($processedStudents);

        // 7. Teacher recommendations per class-arm
        $classInsights = [];
        foreach ($classGroups as $classKey => $classData) {
            $classInsights[$classKey] = [
                'class' => $classData['class'],
                'section' => $classData['section'],
                'recommendations' => $this->generateTeacherRecommendations($classData),
            ];
        }

        // 8. Admin recommendations (school-wide) — accurate aggregation
        $classAtRiskCounts = [];
        $classTotals = [];
        $classAvgScores = []; // weighted class averages for best/worst class insight

        foreach ($classGroups as $classKey => $classData) {
            $classTotals[$classKey] = count($classData['students']);
            $classAtRiskCounts[$classKey] = count(array_filter($classData['students'], fn($s) => $s['is_at_risk']));
            // Use base_score (raw marks, no trend modifier) for honest class average
            $classScores = array_column($classData['students'], 'base_score');
            $classAvgScores[$classKey] = count($classScores) > 0
                ? array_sum($classScores) / count($classScores)
                : 0;
        }

        // School trend: weighted per-student (not average of class averages)
        $totalStudentCount = count($processedStudents);
        $schoolTrend = 0.0;
        if (!empty($trendHistory) && $totalStudentCount > 0) {
            $weightedTrendSum = 0.0;
            foreach ($processedStudents as $s) {
                $weightedTrendSum += $this->computeTrendScore($trendHistory[$s['student_id']] ?? []);
            }
            $schoolTrend = $weightedTrendSum / $totalStudentCount;
        }

        // School average: true mean of base_score across ALL students (not average of class averages)
        $allBaseScores = array_column($processedStudents, 'base_score');
        $schoolAvg = $totalStudentCount > 0 ? array_sum($allBaseScores) / $totalStudentCount : 0;

        // Pass rate: based on base_score (actual marks, not trend-adjusted)
        $passingCount = count(array_filter($allBaseScores, fn($s) => $s >= self::PASS_MARK));
        $schoolPassRate = $totalStudentCount > 0 ? ($passingCount / $totalStudentCount) * 100 : 0;

        // CA component averages school-wide (to identify weak CA areas)
        $caComponents = ['exam_score' => [], 'test_score' => [], 'class_performance' => [], 'weekly_assessment' => []];
        foreach ($processedStudents as $s) {
            $caComponents['exam_score'][] = $s['exam_score'];
            $caComponents['test_score'][] = $s['test_score'];
            $caComponents['class_performance'][] = $s['class_performance'];
            $caComponents['weekly_assessment'][] = $s['weekly_assessment'];
        }
        $caAverages = [];
        foreach ($caComponents as $component => $values) {
            $caAverages[$component] = count($values) > 0 ? array_sum($values) / count($values) : 0;
        }

        // Subject averages: weighted by actual student count (not average of class-arm averages)
        $subjectStudentTotals = [];
        foreach ($processedStudents as $s) {
            foreach ($studentSubjectScores[$s['student_id']] ?? [] as $subj => $score) {
                if (!isset($subjectStudentTotals[$subj])) {
                    $subjectStudentTotals[$subj] = ['sum' => 0.0, 'count' => 0];
                }
                $subjectStudentTotals[$subj]['sum'] += $score;
                $subjectStudentTotals[$subj]['count'] += 1;
            }
        }
        $weightedSubjectAvgs = [];
        $subjectStudentCounts = []; // for tie-breaking
        foreach ($subjectStudentTotals as $subj => $data) {
            $weightedSubjectAvgs[$subj] = $data['count'] > 0 ? $data['sum'] / $data['count'] : 0;
            $subjectStudentCounts[$subj] = $data['count'];
        }

        // Best and worst performing class-arm by average base_score
        $bestClass = '';
        $worstClass = '';
        if (!empty($classAvgScores)) {
            // Sort descending: tie-break by class total students (larger class first), then class name
            uksort($classAvgScores, function ($a, $b) use ($classAvgScores, $classTotals) {
                $diff = round($classAvgScores[$b], 2) <=> round($classAvgScores[$a], 2); // descending
                if ($diff !== 0)
                    return $diff;
                $countDiff = ($classTotals[$b] ?? 0) <=> ($classTotals[$a] ?? 0); // more students first
                return $countDiff !== 0 ? $countDiff : strcmp($a, $b);
            });
            $bestClass = array_key_first($classAvgScores);
            $worstClass = array_key_last($classAvgScores);
        }

        $aggregatedData = [
            'total_students' => $totalStudentCount,
            'school_trend_score' => round($schoolTrend, 2),
            'school_average' => round($schoolAvg, 2),
            'school_pass_rate' => round($schoolPassRate, 2),
            'subject_averages' => $weightedSubjectAvgs,
            'subject_counts' => $subjectStudentCounts,
            'class_at_risk_counts' => $classAtRiskCounts,
            'class_totals' => $classTotals,
            'class_avg_scores' => $classAvgScores,
            'best_class' => $bestClass,
            'worst_class' => $worstClass,
            'ca_averages' => $caAverages,
        ];
        $schoolInsights = $this->generateAdminRecommendations($aggregatedData);

        // 9. Build student recommendations map (keyed by student_id)
        $studentRecommendations = [];
        foreach ($processedStudents as $s) {
            $studentRecommendations[$s['student_id']] = $s['parent_recommendation'];
        }

        // 10. Persist results (non-blocking — don't fail the response if this errors)
        try {
            $this->persistResults($processedStudents, $filters);
        } catch (\Exception $e) {
            error_log('AI persistResults error: ' . $e->getMessage());
        }

        return [
            'rankings' => $rankings,
            'atRiskStudents' => $atRiskStudents,
            'schoolInsights' => $schoolInsights,
            'classInsights' => array_values($classInsights),
            'studentRecommendations' => $studentRecommendations,
        ];
    }

    /**
     * Persist AI results to ai_performance_reports using INSERT ... ON DUPLICATE KEY UPDATE.
     *
     * @param array $aiResults  Array of processed student data from processAll
     * @param array $filters    Original filter params (session_id, term_id, class, section)
     */
    public function persistResults(array $aiResults, array $filters): void
    {
        $sessionId = (int) ($filters['session_id'] ?? 0);
        $termId = (int) ($filters['term_id'] ?? 0);

        foreach ($aiResults as $s) {
            $class = $this->con->real_escape_string($s['class']);
            $section = $this->con->real_escape_string($s['section']);
            $studentId = (int) $s['student_id'];
            $examScore = (float) $s['exam_score'];
            $testScore = (float) $s['test_score'];
            $classPerf = (float) $s['class_performance'];
            $weeklyAssess = (float) $s['weekly_assessment'];
            $perfScore = (float) $s['base_score'];       // store actual marks in DB
            $trendScore = (float) $s['trend_score'];
            $band = $this->con->real_escape_string($s['ai_band']);
            $riskLevel = $s['risk_level'] ? "'" . $this->con->real_escape_string($s['risk_level']) . "'" : 'NULL';
            $strengthJson = "'" . $this->con->real_escape_string(json_encode($s['strength_subjects'])) . "'";
            $improvJson = "'" . $this->con->real_escape_string(json_encode($s['improvement_subjects'])) . "'";
            $parentRec = "'" . $this->con->real_escape_string($s['parent_recommendation']) . "'";

            $sql = "
                INSERT INTO ai_performance_reports
                    (session_id, term_id, class, section, student_id,
                     exam_score, test_score, class_performance, weekly_assessment,
                     performance_score, trend_score, ai_band, risk_level,
                     strength_subjects, improvement_subjects,
                     parent_recommendation, generated_at)
                VALUES
                    ({$sessionId}, {$termId}, '{$class}', '{$section}', {$studentId},
                     {$examScore}, {$testScore}, {$classPerf}, {$weeklyAssess},
                     {$perfScore}, {$trendScore}, '{$band}', {$riskLevel},
                     {$strengthJson}, {$improvJson},
                     {$parentRec}, NOW())
                ON DUPLICATE KEY UPDATE
                    exam_score            = VALUES(exam_score),
                    test_score            = VALUES(test_score),
                    class_performance     = VALUES(class_performance),
                    weekly_assessment     = VALUES(weekly_assessment),
                    performance_score     = VALUES(performance_score),
                    trend_score           = VALUES(trend_score),
                    ai_band               = VALUES(ai_band),
                    risk_level            = VALUES(risk_level),
                    strength_subjects     = VALUES(strength_subjects),
                    improvement_subjects  = VALUES(improvement_subjects),
                    parent_recommendation = VALUES(parent_recommendation),
                    generated_at          = NOW()
            ";

            $this->con->query($sql);
        }
    }
}
