<?php

namespace App\Services\Promotions\Placement;

use App\Models\StudentPromotionRecord;
use Illuminate\Support\Facades\DB;

class PlacementPoolBuilder
{
    public function __construct(private ScoreBandClassifier $bands)
    {
    }

    public function build(string $fromSchoolYear, string $toSchoolYear, ?int $toGradeLevelId = null): array
    {
        $pool = [];
        $exceptions = $this->exceptionBuckets($fromSchoolYear, $toSchoolYear);

        $returning = StudentPromotionRecord::query()
            ->from('student_promotion_records as r')
            ->join('students as s', 's.id', '=', 'r.student_id')
            ->leftJoin('student_decisions as d', function ($join) use ($fromSchoolYear) {
                $join->on('d.student_id', '=', 'r.student_id')
                    ->where('d.school_year', '=', $fromSchoolYear);
            })
            ->where('r.current_school_year', $fromSchoolYear)
            ->where('r.next_school_year', $toSchoolYear)
            ->where('r.enrollment_status', StudentPromotionRecord::ENROLLMENT_COMPLETED)
            ->where('r.promotion_status', StudentPromotionRecord::STATUS_PROMOTED_AND_ENROLLED)
            ->when($toGradeLevelId !== null, fn ($q) => $q->where('r.promoted_level_id', $toGradeLevelId))
            ->selectRaw('r.*, s.firstname, s.lastname, s.is_active, d.id as decision_id, d.decision, d.year_score')
            ->orderBy('s.lastname')
            ->orderBy('s.firstname')
            ->orderBy('s.id')
            ->get();

        foreach ($returning as $row) {
            $studentId = (int) $row->student_id;
            if ((int) ($row->is_active ?? 1) === 0) {
                $exceptions['withdrawn_or_inactive_student'][] = $studentId;
                continue;
            }
            if ($this->alreadyPlaced($studentId, $toSchoolYear)) {
                $exceptions['duplicate_target_year_placement'][] = $studentId;
                continue;
            }
            if (!$this->isPromotedDecision((string) ($row->decision ?? ''))) {
                $bucket = $row->decision === null ? 'missing_student_decision' : 'decision_conflicts_with_enrollment_request';
                $exceptions[$bucket][] = $studentId;
                continue;
            }

            $score = $row->year_score !== null ? (float) $row->year_score : ($row->final_average !== null ? (float) $row->final_average : null);
            if ($score === null) {
                $exceptions['missing_student_decision'][] = $studentId;
                continue;
            }
            try {
                $band = $this->bands->fromScore($score, true);
            } catch (\RuntimeException) {
                $exceptions['failed_retained'][] = $studentId;
                continue;
            }

            $pool[] = [
                'student_id' => $studentId,
                'student_type' => 'returning',
                'student_name' => trim((string) $row->lastname . ', ' . (string) $row->firstname),
                'source_decision_id' => $row->decision_id ? (int) $row->decision_id : null,
                'source_enrollment_id' => $row->enrollment_id ? (int) $row->enrollment_id : null,
                'final_score' => $score,
                'placement_score' => null,
                'score_band' => $band,
                'placement_band_reason' => 'returning_student_final_score',
            ];
        }

        $newStudents = DB::table('enrollments as e')
            ->join('students as s', 's.id', '=', 'e.student_id')
            ->leftJoin('classSection as cs', 'cs.class_section_id', '=', 'e.class_section_id')
            ->where('e.school_year', $toSchoolYear)
            ->where('s.is_new', 1)
            ->where('e.admission_status', 'accepted')
            ->whereIn('e.enrollment_status', ['enrolled', 'payment pending'])
            ->when($toGradeLevelId !== null, function ($q) use ($toGradeLevelId) {
                $q->where(function ($inner) use ($toGradeLevelId) {
                    $inner->whereNull('e.class_section_id')
                        ->orWhere('cs.class_id', $toGradeLevelId);
                });
            })
            ->selectRaw('e.id as enrollment_id, e.student_id, s.firstname, s.lastname, s.is_active')
            ->orderBy('s.lastname')
            ->orderBy('s.firstname')
            ->orderBy('s.id')
            ->get();

        foreach ($newStudents as $row) {
            $studentId = (int) $row->student_id;
            if ((int) ($row->is_active ?? 1) === 0) {
                $exceptions['withdrawn_or_inactive_student'][] = $studentId;
                continue;
            }
            if ($this->alreadyPlaced($studentId, $toSchoolYear)) {
                $exceptions['duplicate_target_year_placement'][] = $studentId;
                continue;
            }

            $pool[] = [
                'student_id' => $studentId,
                'student_type' => 'new',
                'student_name' => trim((string) $row->lastname . ', ' . (string) $row->firstname),
                'source_decision_id' => null,
                'source_enrollment_id' => (int) $row->enrollment_id,
                'final_score' => null,
                'placement_score' => null,
                'score_band' => 'D',
                'placement_band_reason' => 'new_student_default_band',
            ];
        }

        return [
            'pool' => $pool,
            'exceptions' => array_map(fn ($ids) => array_values(array_unique($ids)), $exceptions),
        ];
    }

    private function exceptionBuckets(string $fromSchoolYear, string $toSchoolYear): array
    {
        $buckets = [
            'passed_but_parent_enrollment_missing' => [],
            'passed_and_parent_enrollment_completed' => [],
            'failed_retained' => [],
            'missing_student_decision' => [],
            'pending_student_decision' => [],
            'decision_conflicts_with_enrollment_request' => [],
            'withdrawn_or_inactive_student' => [],
            'duplicate_target_year_placement' => [],
        ];

        $rows = DB::table('student_decisions as d')
            ->leftJoin('student_promotion_records as r', function ($join) use ($toSchoolYear) {
                $join->on('r.student_id', '=', 'd.student_id')
                    ->where('r.next_school_year', '=', $toSchoolYear);
            })
            ->where('d.school_year', $fromSchoolYear)
            ->select('d.student_id', 'd.decision', 'r.enrollment_status')
            ->get();

        foreach ($rows as $row) {
            $decision = strtolower(trim((string) $row->decision));
            $studentId = (int) $row->student_id;
            if ($this->isPromotedDecision($decision)) {
                if ($row->enrollment_status === StudentPromotionRecord::ENROLLMENT_COMPLETED) {
                    $buckets['passed_and_parent_enrollment_completed'][] = $studentId;
                } else {
                    $buckets['passed_but_parent_enrollment_missing'][] = $studentId;
                }
            } elseif (in_array($decision, ['failed', 'fail', 'retained', 'repeat', 'repeated_level'], true)) {
                $buckets['failed_retained'][] = $studentId;
            } elseif ($decision === '' || $decision === 'pending') {
                $buckets['pending_student_decision'][] = $studentId;
            }
        }

        return $buckets;
    }

    private function alreadyPlaced(int $studentId, string $schoolYear): bool
    {
        return DB::table('student_class')
            ->where('student_id', $studentId)
            ->where('school_year', $schoolYear)
            ->where('is_event_only', 0)
            ->exists();
    }

    private function isPromotedDecision(string $decision): bool
    {
        $normalized = strtolower(trim($decision));
        return in_array($normalized, ['passed', 'pass', 'promoted', 'promote', 'eligible_to_continue'], true);
    }
}
