<?php

namespace App\Services\Administrator\Trophy;

use App\Models\Configuration;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

/**
 * Ports the legacy CodeIgniter trophy projection + winner reports to
 * Laravel JSON payloads for the SPA/admin API.
 */
class TrophyReportService
{
    public function projection(?string $schoolYear = null, ?float $percentile = null): array
    {
        $selectedYear = $this->resolveSchoolYear($schoolYear);
        $selectedPercentile = $this->sanitizePercentile($percentile);
        $sections = $this->sectionMap($this->studentRows($selectedYear));

        $classResults = [];

        foreach ($sections as $sectionId => $section) {
            $students = $this->sortByScore($section['students'], 'fall_score');
            $scores = array_column($students, 'fall_score');
            $calc = $this->calculateThreshold($scores, $selectedPercentile);
            $threshold = $calc['threshold'];

            $students = array_map(function (array $student) use ($threshold): array {
                $student['projected_trophy'] = $threshold !== null
                    && $student['fall_score'] !== null
                    && $student['fall_score'] >= $threshold;

                return $student;
            }, $students);

            $scoredCount = count(array_filter(
                array_column($students, 'fall_score'),
                static fn ($score): bool => $score !== null
            ));

            [$boys, $girls] = $this->splitByBinaryGender($students);
            $trophyBoys = array_values(array_filter($boys, static fn (array $student): bool => ! empty($student['projected_trophy'])));
            $trophyGirls = array_values(array_filter($girls, static fn (array $student): bool => ! empty($student['projected_trophy'])));
            $studentCount = count($students);

            $classResults[] = [
                'section_id' => $sectionId,
                'section_name' => $section['name'],
                'students' => $students,
                'threshold' => $threshold,
                'trophy_count' => $calc['winners'],
                'student_count' => $studentCount,
                'scored_count' => $scoredCount,
                'method' => $calc['method'],
                'boys' => count($boys),
                'girls' => count($girls),
                'trophy_boys' => count($trophyBoys),
                'trophy_girls' => count($trophyGirls),
                'pct_boys' => $studentCount > 0 ? round(count($boys) / $studentCount * 100) : 0,
                'pct_girls' => $studentCount > 0 ? round(count($girls) / $studentCount * 100) : 0,
                'pct_trophy_boys' => count($boys) > 0 ? round(count($trophyBoys) / count($boys) * 100) : 0,
                'pct_trophy_girls' => count($girls) > 0 ? round(count($trophyGirls) / count($girls) * 100) : 0,
                'pct_trophy_total' => $studentCount > 0 ? round($calc['winners'] / $studentCount * 100) : 0,
            ];
        }

        return [
            'selected_year' => $selectedYear,
            'selected_percentile' => $selectedPercentile,
            'years' => $this->schoolYears(),
            'class_results' => $classResults,
            'summary' => $this->projectionSummary($classResults),
        ];
    }

    public function winners(?string $schoolYear = null, ?float $percentile = null): array
    {
        $selectedYear = $this->resolveSchoolYear($schoolYear);
        $selectedPercentile = $this->sanitizePercentile($percentile);
        $sections = $this->sectionMap($this->studentRows($selectedYear));

        $classResults = [];

        foreach ($sections as $sectionId => $section) {
            $students = $this->sortByScore($section['students'], 'fall_score');
            $calc = $this->calculateThreshold(array_column($students, 'fall_score'), $selectedPercentile);
            $threshold = $calc['threshold'];

            $winners = array_values(array_filter($students, static function (array $student) use ($threshold): bool {
                return $threshold !== null
                    && $student['fall_score'] !== null
                    && $student['fall_score'] >= $threshold;
            }));

            if ($winners === []) {
                continue;
            }

            [$boys, $girls] = $this->splitByBinaryGender($students);
            [$winnerBoys, $winnerGirls] = $this->splitByBinaryGender($winners);
            $studentCount = count($students);

            $classResults[] = [
                'section_id' => $sectionId,
                'section_name' => $section['name'],
                'threshold' => $threshold,
                'winners' => $winners,
                'student_count' => $studentCount,
                'boys' => count($boys),
                'girls' => count($girls),
                'trophy_boys' => count($winnerBoys),
                'trophy_girls' => count($winnerGirls),
                'pct_boys' => $studentCount > 0 ? round(count($boys) / $studentCount * 100) : 0,
                'pct_girls' => $studentCount > 0 ? round(count($girls) / $studentCount * 100) : 0,
                'pct_trophy_boys' => count($boys) > 0 ? round(count($winnerBoys) / count($boys) * 100) : 0,
                'pct_trophy_girls' => count($girls) > 0 ? round(count($winnerGirls) / count($girls) * 100) : 0,
                'pct_trophy_total' => $studentCount > 0 ? round(count($winners) / $studentCount * 100) : 0,
            ];
        }

        return [
            'selected_year' => $selectedYear,
            'selected_percentile' => $selectedPercentile,
            'years' => $this->schoolYears(),
            'class_results' => $classResults,
            'summary' => $this->winnersSummary($classResults),
        ];
    }

    public function final(?string $schoolYear = null, ?float $percentile = null): array
    {
        $selectedYear = $this->resolveSchoolYear($schoolYear);
        $selectedPercentile = $this->sanitizePercentile($percentile);
        $sections = $this->sectionMap($this->studentRows($selectedYear));

        $classResults = [];
        $allStudents = [];
        $passStudents = [];
        $winnerStudents = [];
        $winnerStickers = [];

        foreach ($sections as $sectionId => $section) {
            $students = $section['students'];
            $fallCalc = $this->calculateThreshold(array_column($students, 'fall_score'), $selectedPercentile);
            $yearCalc = $this->calculateThreshold(array_column($students, 'year_score'), $selectedPercentile);
            $fallThreshold = $fallCalc['threshold'];
            $yearThreshold = $yearCalc['threshold'];

            $annotated = array_map(function (array $student) use ($fallThreshold, $yearThreshold): array {
                $predicted = $fallThreshold !== null
                    && $student['fall_score'] !== null
                    && $student['fall_score'] >= $fallThreshold;
                $actual = $yearThreshold !== null
                    && $student['year_score'] !== null
                    && $student['year_score'] >= $yearThreshold;
                $status = match (true) {
                    $predicted && $actual => 'confirmed',
                    ! $predicted && $actual => 'surprise',
                    $predicted && ! $actual => 'missed',
                    default => 'none',
                };

                return $student + compact('predicted', 'actual', 'status');
            }, $students);

            $annotated = $this->sortByScore($annotated, 'year_score');

            foreach ($annotated as $student) {
                $allStudents[] = $student;

                if ($student['year_score'] !== null && $student['year_score'] >= 60.0) {
                    $passStudents[] = $student;
                }

                if (! empty($student['actual'])) {
                    $winnerStudents[] = $student;
                    $winnerStickers[] = [
                        'name' => $student['name'],
                        'section' => $section['name'],
                    ];
                }
            }

            $confirmed = count(array_filter($annotated, static fn (array $student): bool => $student['status'] === 'confirmed'));
            $surprises = count(array_filter($annotated, static fn (array $student): bool => $student['status'] === 'surprise'));
            $missed = count(array_filter($annotated, static fn (array $student): bool => $student['status'] === 'missed'));
            $predictedCount = count(array_filter($annotated, static fn (array $student): bool => ! empty($student['predicted'])));
            $actualCount = count(array_filter($annotated, static fn (array $student): bool => ! empty($student['actual'])));
            $studentCount = count($annotated);

            $classResults[] = [
                'section_id' => $sectionId,
                'section_name' => $section['name'],
                'students' => $annotated,
                'student_count' => $studentCount,
                'fall_threshold' => $fallThreshold,
                'year_threshold' => $yearThreshold,
                'predicted_count' => $predictedCount,
                'actual_count' => $actualCount,
                'confirmed' => $confirmed,
                'surprises' => $surprises,
                'missed' => $missed,
                'accuracy' => $predictedCount > 0 ? round($confirmed / $predictedCount * 100) : ($actualCount === 0 ? 100 : 0),
            ];
        }

        return [
            'selected_year' => $selectedYear,
            'selected_percentile' => $selectedPercentile,
            'years' => $this->schoolYears(),
            'class_results' => $classResults,
            'summary' => $this->finalSummary($classResults),
            'winner_gender_summary' => $this->genderBreakdown($winnerStudents),
            'all_gender_summary' => $this->genderBreakdown($allStudents),
            'pass_gender_summary' => $this->genderBreakdown($passStudents),
            'winner_stickers' => array_values(array_filter($winnerStickers, static fn (array $sticker): bool => trim((string) ($sticker['name'] ?? '')) !== '')),
        ];
    }

    /**
     * @param  array<int, float|int|string|null>  $scores
     * @return array{threshold: float|null, winners: int, method: string}
     */
    public function calculateThreshold(array $scores, float $percentile = 75.0): array
    {
        $scores = array_values(array_filter(
            $scores,
            static fn ($value): bool => is_numeric($value) && $value !== null
        ));
        $scores = array_map('floatval', $scores);
        sort($scores);

        $count = count($scores);

        if ($count === 0) {
            return ['threshold' => null, 'winners' => 0, 'method' => 'empty'];
        }

        $minWinners = 3;
        $maxWinners = max($minWinners, (int) floor($count * (1 - $percentile / 100)));

        $threshold = $this->empiricalPercentile($scores, $percentile);
        $winners = $this->countAtOrAbove($scores, $threshold);

        if ($winners < $minWinners) {
            $target = min($minWinners, $count);
            $descending = array_reverse($scores);
            $threshold = $descending[$target - 1];
            $winners = $this->countAtOrAbove($scores, $threshold);

            return ['threshold' => $threshold, 'winners' => $winners, 'method' => 'min3_reduced'];
        }

        if ($winners <= $maxWinners) {
            return ['threshold' => $threshold, 'winners' => $winners, 'method' => 'empirical_percentile'];
        }

        $result = $this->capByRank($scores, $maxWinners);

        if ($result['winners'] < $minWinners) {
            $target = min($minWinners, $count);
            $descending = array_reverse($scores);
            $threshold = $descending[$target - 1];
            $winners = $this->countAtOrAbove($scores, $threshold);

            return ['threshold' => $threshold, 'winners' => $winners, 'method' => 'min3_after_cap'];
        }

        return $result;
    }

    private function resolveSchoolYear(?string $schoolYear): string
    {
        $selected = trim((string) $schoolYear);

        if ($selected !== '') {
            return $selected;
        }

        return (string) (Configuration::getConfig('school_year') ?? '');
    }

    private function sanitizePercentile(?float $percentile): float
    {
        $value = $percentile ?? 75.0;

        return max(1.0, min(99.0, $value));
    }

    /**
     * @return list<string>
     */
    private function schoolYears(): array
    {
        return DB::table('student_class')
            ->select('school_year')
            ->distinct()
            ->orderBy('school_year', 'DESC')
            ->pluck('school_year')
            ->filter(static fn ($value): bool => $value !== null && trim((string) $value) !== '')
            ->map(static fn ($value): string => (string) $value)
            ->values()
            ->all();
    }

    /**
     * @return \Illuminate\Support\Collection<int, array<string, mixed>>
     */
    private function studentRows(string $schoolYear): Collection
    {
        $fallScores = DB::table('semester_scores')
            ->select(
                'student_id',
                'class_section_id',
                DB::raw('MAX(semester_score) AS fall_score')
            )
            ->where('school_year', $schoolYear)
            ->whereRaw('LOWER(semester) = ?', ['fall'])
            ->groupBy('student_id', 'class_section_id');

        $springScores = DB::table('semester_scores')
            ->select(
                'student_id',
                'class_section_id',
                DB::raw('MAX(semester_score) AS spring_score')
            )
            ->where('school_year', $schoolYear)
            ->whereRaw('LOWER(semester) = ?', ['spring'])
            ->groupBy('student_id', 'class_section_id');

        return DB::table('student_class as sc')
            ->select(
                'sc.student_id',
                'sc.class_section_id',
                'cs.class_section_name',
                's.firstname',
                's.lastname',
                's.school_id',
                's.gender',
                DB::raw('MAX(sf.fall_score) AS fall_score'),
                DB::raw('MAX(sp.spring_score) AS spring_score')
            )
            ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'sc.class_section_id')
            ->leftJoin('students as s', 's.id', '=', 'sc.student_id')
            ->leftJoinSub($fallScores, 'sf', function ($join): void {
                $join->on('sf.student_id', '=', 'sc.student_id')
                    ->on('sf.class_section_id', '=', 'sc.class_section_id');
            })
            ->leftJoinSub($springScores, 'sp', function ($join): void {
                $join->on('sp.student_id', '=', 'sc.student_id')
                    ->on('sp.class_section_id', '=', 'sc.class_section_id');
            })
            ->where('sc.school_year', $schoolYear)
            ->groupBy(
                'sc.student_id',
                'sc.class_section_id',
                'cs.class_section_name',
                's.firstname',
                's.lastname',
                's.school_id',
                's.gender'
            )
            ->orderBy('cs.class_section_name', 'ASC')
            ->orderBy('s.firstname', 'ASC')
            ->orderBy('s.lastname', 'ASC')
            ->get()
            ->map(function (object $row): array {
                $student = (array) $row;
                $fall = is_numeric($student['fall_score'] ?? null) ? round((float) $student['fall_score'], 1) : null;
                $spring = is_numeric($student['spring_score'] ?? null) ? round((float) $student['spring_score'], 1) : null;
                $year = ($fall !== null && $spring !== null)
                    ? round(($fall + $spring) / 2, 1)
                    : ($fall ?? $spring);
                $firstname = trim((string) ($student['firstname'] ?? ''));
                $lastname = trim((string) ($student['lastname'] ?? ''));
                $name = trim($firstname.' '.$lastname);

                return [
                    'student_id' => (int) ($student['student_id'] ?? 0),
                    'class_section_id' => (int) ($student['class_section_id'] ?? 0),
                    'section_name' => (string) ($student['class_section_name'] ?? ''),
                    'name' => $name !== '' ? $name : 'Student #'.($student['student_id'] ?? ''),
                    'firstname' => $firstname !== '' ? $firstname : null,
                    'lastname' => $lastname !== '' ? $lastname : null,
                    'school_id' => $student['school_id'] ?? null,
                    'gender' => (string) ($student['gender'] ?? ''),
                    'fall_score' => $fall,
                    'spring_score' => $spring,
                    'year_score' => $year,
                ];
            });
    }

    /**
     * @param  \Illuminate\Support\Collection<int, array<string, mixed>>  $rows
     * @return array<int, array{name: string, students: list<array<string, mixed>>}>
     */
    private function sectionMap(Collection $rows): array
    {
        $sections = [];

        foreach ($rows as $row) {
            $sectionId = (int) ($row['class_section_id'] ?? 0);
            $name = trim((string) ($row['section_name'] ?? '')) ?: 'Section '.$sectionId;

            if (! isset($sections[$sectionId])) {
                $sections[$sectionId] = [
                    'name' => $name,
                    'students' => [],
                ];
            }

            unset($row['section_name']);
            $sections[$sectionId]['students'][] = $row;
        }

        return $sections;
    }

    /**
     * @param  list<array<string, mixed>>  $students
     * @return list<array<string, mixed>>
     */
    private function sortByScore(array $students, string $scoreKey): array
    {
        usort($students, static function (array $left, array $right) use ($scoreKey): int {
            $leftScore = $left[$scoreKey] ?? null;
            $rightScore = $right[$scoreKey] ?? null;

            if ($leftScore === null && $rightScore === null) {
                return strcmp((string) ($left['name'] ?? ''), (string) ($right['name'] ?? ''));
            }

            if ($leftScore === null) {
                return 1;
            }

            if ($rightScore === null) {
                return -1;
            }

            return $rightScore <=> $leftScore ?: strcmp((string) ($left['name'] ?? ''), (string) ($right['name'] ?? ''));
        });

        return $students;
    }

    /**
     * @param  list<array<string, mixed>>  $students
     * @return array{0: list<array<string, mixed>>, 1: list<array<string, mixed>>}
     */
    private function splitByBinaryGender(array $students): array
    {
        $boys = array_values(array_filter($students, fn (array $student): bool => $this->genderKey($student['gender'] ?? '') === 'boys'));
        $girls = array_values(array_filter($students, fn (array $student): bool => $this->genderKey($student['gender'] ?? '') === 'girls'));

        return [$boys, $girls];
    }

    /**
     * @param  list<array<string, mixed>>  $classResults
     * @return array<string, int|float>
     */
    private function projectionSummary(array $classResults): array
    {
        $totalStudents = array_sum(array_column($classResults, 'student_count'));
        $totalScored = array_sum(array_column($classResults, 'scored_count'));
        $totalTrophies = array_sum(array_column($classResults, 'trophy_count'));
        $totalBoys = array_sum(array_column($classResults, 'boys'));
        $totalGirls = array_sum(array_column($classResults, 'girls'));
        $totalTrophyBoys = array_sum(array_column($classResults, 'trophy_boys'));
        $totalTrophyGirls = array_sum(array_column($classResults, 'trophy_girls'));

        return [
            'classes' => count($classResults),
            'students' => $totalStudents,
            'scored' => $totalScored,
            'trophies' => $totalTrophies,
            'boys' => $totalBoys,
            'girls' => $totalGirls,
            'trophy_boys' => $totalTrophyBoys,
            'trophy_girls' => $totalTrophyGirls,
            'pct_scored' => $totalStudents > 0 ? round($totalScored / $totalStudents * 100) : 0,
            'pct_boys' => $totalStudents > 0 ? round($totalBoys / $totalStudents * 100) : 0,
            'pct_girls' => $totalStudents > 0 ? round($totalGirls / $totalStudents * 100) : 0,
            'pct_trophies' => $totalStudents > 0 ? round($totalTrophies / $totalStudents * 100) : 0,
            'pct_trophy_boys' => $totalBoys > 0 ? round($totalTrophyBoys / $totalBoys * 100) : 0,
            'pct_trophy_girls' => $totalGirls > 0 ? round($totalTrophyGirls / $totalGirls * 100) : 0,
        ];
    }

    /**
     * @param  list<array<string, mixed>>  $classResults
     * @return array<string, int|float>
     */
    private function winnersSummary(array $classResults): array
    {
        $totalWinners = array_sum(array_map(static fn (array $classResult): int => count($classResult['winners'] ?? []), $classResults));
        $totalStudents = array_sum(array_column($classResults, 'student_count'));
        $totalBoys = array_sum(array_column($classResults, 'boys'));
        $totalGirls = array_sum(array_column($classResults, 'girls'));
        $totalTrophyBoys = array_sum(array_column($classResults, 'trophy_boys'));
        $totalTrophyGirls = array_sum(array_column($classResults, 'trophy_girls'));

        return [
            'classes' => count($classResults),
            'students' => $totalStudents,
            'winners' => $totalWinners,
            'boys' => $totalBoys,
            'girls' => $totalGirls,
            'trophy_boys' => $totalTrophyBoys,
            'trophy_girls' => $totalTrophyGirls,
            'pct_winners' => $totalStudents > 0 ? round($totalWinners / $totalStudents * 100) : 0,
            'pct_boys' => $totalStudents > 0 ? round($totalBoys / $totalStudents * 100) : 0,
            'pct_girls' => $totalStudents > 0 ? round($totalGirls / $totalStudents * 100) : 0,
            'pct_trophy_boys' => $totalBoys > 0 ? round($totalTrophyBoys / $totalBoys * 100) : 0,
            'pct_trophy_girls' => $totalGirls > 0 ? round($totalTrophyGirls / $totalGirls * 100) : 0,
        ];
    }

    /**
     * @param  list<array<string, mixed>>  $classResults
     * @return array<string, int|float>
     */
    private function finalSummary(array $classResults): array
    {
        $totalStudents = array_sum(array_column($classResults, 'student_count'));
        $totalPredicted = array_sum(array_column($classResults, 'predicted_count'));
        $totalActual = array_sum(array_column($classResults, 'actual_count'));
        $totalConfirmed = array_sum(array_column($classResults, 'confirmed'));
        $totalSurprises = array_sum(array_column($classResults, 'surprises'));
        $totalMissed = array_sum(array_column($classResults, 'missed'));
        $overallAccuracy = $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted * 100) : ($totalActual === 0 ? 100 : 0);

        return [
            'classes' => count($classResults),
            'students' => $totalStudents,
            'predicted' => $totalPredicted,
            'actual' => $totalActual,
            'confirmed' => $totalConfirmed,
            'surprises' => $totalSurprises,
            'missed' => $totalMissed,
            'accuracy' => $overallAccuracy,
            'pct_predicted' => $totalStudents > 0 ? round($totalPredicted / $totalStudents * 100) : 0,
            'pct_actual' => $totalStudents > 0 ? round($totalActual / $totalStudents * 100) : 0,
            'pct_confirmed_of_predicted' => $totalPredicted > 0 ? round($totalConfirmed / $totalPredicted * 100) : 0,
        ];
    }

    /**
     * @param  list<array<string, mixed>>  $students
     * @return array<string, int|float>
     */
    private function genderBreakdown(array $students): array
    {
        $stats = ['boys' => 0, 'girls' => 0, 'other' => 0];

        foreach ($students as $student) {
            $stats[$this->genderKey($student['gender'] ?? '')]++;
        }

        $total = array_sum($stats);

        return [
            'total' => $total,
            'boys' => $stats['boys'],
            'girls' => $stats['girls'],
            'other' => $stats['other'],
            'pct_boys' => $total > 0 ? round(($stats['boys'] / $total) * 100, 1) : 0.0,
            'pct_girls' => $total > 0 ? round(($stats['girls'] / $total) * 100, 1) : 0.0,
            'pct_other' => $total > 0 ? round(($stats['other'] / $total) * 100, 1) : 0.0,
        ];
    }

    private function genderKey(?string $gender): string
    {
        $value = strtolower(trim((string) $gender));

        return match (true) {
            in_array($value, ['male', 'm', 'boy', 'boys'], true) => 'boys',
            in_array($value, ['female', 'f', 'girl', 'girls'], true) => 'girls',
            default => 'other',
        };
    }

    /**
     * @param  list<float>  $sortedScores
     * @return array{threshold: float, winners: int, method: string}
     */
    private function capByRank(array $sortedScores, int $max): array
    {
        $descending = array_reverse($sortedScores);
        $threshold = $descending[$max - 1];
        $winners = $this->countAtOrAbove($sortedScores, $threshold);

        if ($winners <= $max) {
            return ['threshold' => $threshold, 'winners' => $winners, 'method' => 'capped_25pct'];
        }

        $uniqueCandidates = array_values(array_unique(array_filter(
            $sortedScores,
            static fn (float $score): bool => $score > $threshold
        )));
        sort($uniqueCandidates);

        foreach ($uniqueCandidates as $candidate) {
            $winnerCount = $this->countAtOrAbove($sortedScores, $candidate);

            if ($winnerCount <= $max) {
                return ['threshold' => $candidate, 'winners' => $winnerCount, 'method' => 'capped_25pct'];
            }
        }

        return ['threshold' => $sortedScores[0], 'winners' => count($sortedScores), 'method' => 'all_equal'];
    }

    /**
     * @param  list<float>  $sortedScores
     */
    private function empiricalPercentile(array $sortedScores, float $percentile): float
    {
        $count = count($sortedScores);

        if ($count === 0) {
            return 0.0;
        }

        $index = ($percentile / 100.0) * ($count - 1);
        $lower = (int) floor($index);
        $upper = (int) ceil($index);

        if ($lower === $upper) {
            return $sortedScores[$lower];
        }

        return $sortedScores[$lower] + ($index - $lower) * ($sortedScores[$upper] - $sortedScores[$lower]);
    }

    /**
     * @param  list<float>  $scores
     */
    private function countAtOrAbove(array $scores, float $threshold): int
    {
        return count(array_filter($scores, static fn (float $score): bool => $score >= $threshold));
    }
}
