<?php

namespace App\Services\Promotions;

use App\Models\ClassSection;
use App\Models\Configuration;
use App\Models\Student;
use App\Models\StudentPromotionRecord;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

/**
 * Implements the eligibility engine described in plan sections 5–7.
 *
 * Builds (or refreshes) one `student_promotion_records` row per
 * student per next-school-year and assigns a promotion status based on
 * academic results stored in `semester_scores`.
 */
class PromotionEligibilityService
{
    public const DEFAULT_PASS_THRESHOLD = 60.0;

    public function __construct(
        private LevelProgressionService $progression,
        private PromotionStatusService $statusService,
        private PromotionAuditService $audit
    ) {
    }

    /**
     * Evaluate eligibility for a single student.
     *
     * @param int|null $userId actor id for audit
     */
    public function evaluateStudent(
        int $studentId,
        ?string $currentSchoolYear = null,
        ?int $userId = null
    ): ?StudentPromotionRecord {
        $student = Student::query()->find($studentId);
        if (!$student) {
            return null;
        }

        $current = $currentSchoolYear ?: $this->resolveCurrentSchoolYear();
        if ($current === null) {
            return null;
        }
        $next = $this->nextSchoolYear($current);
        if ($next === null) {
            return null;
        }

        return $this->evaluateOne($student, $current, $next, $userId);
    }

    /**
     * Evaluate eligibility for a class section worth of students at once.
     *
     * @return array{ evaluated:int, eligible:int, repeated:int, on_hold:int, conditional:int, graduated:int, errors:array<int,string> }
     */
    public function evaluateClassSection(
        int $classSectionId,
        ?string $currentSchoolYear = null,
        ?int $userId = null
    ): array {
        $current = $currentSchoolYear ?: $this->resolveCurrentSchoolYear();
        if ($current === null) {
            return $this->emptySummary();
        }
        $next = $this->nextSchoolYear($current);
        if ($next === null) {
            return $this->emptySummary();
        }

        $studentIds = DB::table('student_class')
            ->where('class_section_id', $classSectionId)
            ->where('school_year', $current)
            ->pluck('student_id')
            ->map(fn ($id) => (int) $id)
            ->all();

        $summary = $this->emptySummary();
        foreach ($studentIds as $studentId) {
            $student = Student::query()->find($studentId);
            if (!$student) {
                continue;
            }
            try {
                $record = $this->evaluateOne($student, $current, $next, $userId);
                if ($record) {
                    $summary['evaluated']++;
                    $key = $this->statusBucket($record->promotion_status);
                    if ($key !== null) {
                        $summary[$key]++;
                    }
                }
            } catch (\Throwable $e) {
                Log::error('Promotion eligibility evaluation failed', [
                    'student_id' => $studentId,
                    'exception' => $e,
                ]);
                $summary['errors'][] = sprintf('Student %d: %s', $studentId, $e->getMessage());
            }
        }

        return $summary;
    }

    /**
     * Evaluate eligibility for an entire school year (all students with
     * class assignments). Convenience wrapper around `evaluateOne`.
     */
    public function evaluateSchoolYear(?string $currentSchoolYear = null, ?int $userId = null): array
    {
        $current = $currentSchoolYear ?: $this->resolveCurrentSchoolYear();
        if ($current === null) {
            return $this->emptySummary();
        }
        $next = $this->nextSchoolYear($current);
        if ($next === null) {
            return $this->emptySummary();
        }

        $studentIds = DB::table('student_class')
            ->where('school_year', $current)
            ->distinct()
            ->pluck('student_id')
            ->map(fn ($id) => (int) $id)
            ->all();

        $summary = $this->emptySummary();
        foreach ($studentIds as $studentId) {
            $student = Student::query()->find($studentId);
            if (!$student) {
                continue;
            }
            try {
                $record = $this->evaluateOne($student, $current, $next, $userId);
                if ($record) {
                    $summary['evaluated']++;
                    $key = $this->statusBucket($record->promotion_status);
                    if ($key !== null) {
                        $summary[$key]++;
                    }
                }
            } catch (\Throwable $e) {
                Log::error('Promotion eligibility evaluation failed', [
                    'student_id' => $studentId,
                    'exception' => $e,
                ]);
                $summary['errors'][] = sprintf('Student %d: %s', $studentId, $e->getMessage());
            }
        }

        return $summary;
    }

    /**
     * Core per-student logic implementing section 7's decision tree.
     */
    public function evaluateOne(
        Student $student,
        string $currentSchoolYear,
        string $nextSchoolYear,
        ?int $userId = null
    ): StudentPromotionRecord {
        $studentId = (int) $student->getKey();

        $sectionId = (int) DB::table('student_class')
            ->where('student_id', $studentId)
            ->where('school_year', $currentSchoolYear)
            ->orderByDesc('updated_at')
            ->orderByDesc('id')
            ->value('class_section_id');

        $progressionInfo = $this->progression->resolveByCurrentClassSectionId($sectionId);
        if ($progressionInfo === null && $sectionId > 0) {
            // Even without a mapping we still want to record the eligibility decision.
            $progressionInfo = [
                'current_level_id' => null,
                'current_level_name' => ClassSection::getClassSectionNameBySectionId($sectionId),
                'next_level_id' => null,
                'next_level_name' => null,
                'is_terminal' => false,
            ];
        }
        $progressionInfo ??= [
            'current_level_id' => null,
            'current_level_name' => $student->registration_grade,
            'next_level_id' => null,
            'next_level_name' => null,
            'is_terminal' => false,
        ];

        $scoreInfo = $this->loadAcademicResult($studentId, $currentSchoolYear);
        $passed = $scoreInfo['passed'];
        $finalAvg = $scoreInfo['final_average'];

        return DB::transaction(function () use (
            $student,
            $studentId,
            $currentSchoolYear,
            $nextSchoolYear,
            $progressionInfo,
            $passed,
            $finalAvg,
            $userId,
            $scoreInfo
        ) {
            $existing = StudentPromotionRecord::query()
                ->where('student_id', $studentId)
                ->where('next_school_year', $nextSchoolYear)
                ->first();
            $isNew = $existing === null;

            $record = $existing ?: new StudentPromotionRecord();
            $record->fill([
                'student_id' => $studentId,
                'parent_id' => (int) ($student->parent_id ?? 0) ?: null,
                'current_school_year' => $currentSchoolYear,
                'next_school_year' => $nextSchoolYear,
                'current_level_id' => $progressionInfo['current_level_id'],
                'current_level_name' => $progressionInfo['current_level_name'],
                'promoted_level_id' => $progressionInfo['next_level_id'],
                'promoted_level_name' => $progressionInfo['next_level_name'],
                'final_average' => $finalAvg,
                'eligibility_notes' => $scoreInfo['notes'],
                'updated_by' => $userId,
            ]);
            if ($isNew) {
                $record->promotion_status = StudentPromotionRecord::STATUS_NOT_REVIEWED;
                $record->enrollment_required = true;
                $record->enrollment_status = StudentPromotionRecord::ENROLLMENT_NOT_STARTED;
            }

            $previousStatus = (string) ($record->promotion_status ?? StudentPromotionRecord::STATUS_NOT_REVIEWED);
            $record->passed_current_level = $passed;
            $newStatus = $this->resolveStatusFromEvaluation(
                $passed,
                $progressionInfo['is_terminal'] ?? false,
                $scoreInfo['has_data']
            );

            // Preserve admin overrides for terminal/holds.
            if (in_array($previousStatus, [
                StudentPromotionRecord::STATUS_WITHDRAWN,
                StudentPromotionRecord::STATUS_GRADUATED,
                StudentPromotionRecord::STATUS_PROMOTED_AND_ENROLLED,
                StudentPromotionRecord::STATUS_NOT_ENROLLED,
            ], true)) {
                $newStatus = $previousStatus;
            }

            $record->promotion_status = $newStatus;
            $record->save();

            if ($isNew) {
                $this->audit->logRecordCreated($record, $userId);
            }
            $this->audit->logEligibilityEvaluation(
                $record,
                (bool) $passed,
                $userId,
                $scoreInfo['notes']
            );
            if ($previousStatus !== $newStatus) {
                $this->audit->logStatusChange($record, $previousStatus, $newStatus, $userId, 'Auto eligibility evaluation');
            }

            return $record;
        });
    }

    /**
     * Best effort lookup of the student's final academic result for the
     * given school year. Treats missing fall + spring scores as "no
     * data" → on hold (plan section 4: On Hold for missing result).
     *
     * @return array{ passed:?bool, final_average:?float, notes:?string, has_data:bool }
     */
    private function loadAcademicResult(int $studentId, string $schoolYear): array
    {
        $decision = DB::table('student_decisions')
            ->where('student_id', $studentId)
            ->where('school_year', $schoolYear)
            ->orderByDesc('updated_at')
            ->orderByDesc('id')
            ->first();

        if (!$decision) {
            return [
                'passed' => null,
                'final_average' => null,
                'notes' => 'Missing student_decisions record for promotion eligibility',
                'has_data' => false,
            ];
        }

        $rawDecision = strtolower(trim((string) ($decision->decision ?? '')));
        $score = $decision->year_score !== null ? round((float) $decision->year_score, 2) : null;

        if (in_array($rawDecision, ['passed', 'pass', 'promoted', 'promote', 'eligible_to_continue'], true)) {
            return [
                'passed' => true,
                'final_average' => $score,
                'notes' => $score !== null
                    ? sprintf('student_decisions marked promoted with score %.2f', $score)
                    : 'student_decisions marked promoted without a year_score',
                'has_data' => true,
            ];
        }

        if (in_array($rawDecision, ['failed', 'fail', 'retained', 'repeat', 'repeated_level'], true)) {
            return [
                'passed' => false,
                'final_average' => $score,
                'notes' => 'student_decisions marked failed/retained',
                'has_data' => true,
            ];
        }

        if ($rawDecision === '' || $rawDecision === 'pending' || $rawDecision === 'manual review' || $rawDecision === 'manual_review') {
            return [
                'passed' => null,
                'final_average' => $score,
                'notes' => 'student_decisions is pending or requires manual review',
                'has_data' => false,
            ];
        }

        return [
            'passed' => null,
            'final_average' => $score,
            'notes' => 'student_decisions has unrecognized decision: ' . $rawDecision,
            'has_data' => false,
        ];
    }

    private function resolveStatusFromEvaluation(?bool $passed, bool $terminal, bool $hasData): string
    {
        if (!$hasData) {
            return StudentPromotionRecord::STATUS_ON_HOLD;
        }
        if ($passed === false) {
            return StudentPromotionRecord::STATUS_REPEATED;
        }
        if ($terminal) {
            return StudentPromotionRecord::STATUS_GRADUATED;
        }
        return StudentPromotionRecord::STATUS_AWAITING_PARENT;
    }

    private function statusBucket(string $status): ?string
    {
        return match ($status) {
            StudentPromotionRecord::STATUS_AWAITING_PARENT,
            StudentPromotionRecord::STATUS_ELIGIBLE => 'eligible',
            StudentPromotionRecord::STATUS_REPEATED => 'repeated',
            StudentPromotionRecord::STATUS_ON_HOLD => 'on_hold',
            StudentPromotionRecord::STATUS_CONDITIONAL => 'conditional',
            StudentPromotionRecord::STATUS_GRADUATED => 'graduated',
            default => null,
        };
    }

    private function emptySummary(): array
    {
        return [
            'evaluated' => 0,
            'eligible' => 0,
            'repeated' => 0,
            'on_hold' => 0,
            'conditional' => 0,
            'graduated' => 0,
            'errors' => [],
        ];
    }

    private function resolveCurrentSchoolYear(): ?string
    {
        $year = (string) (Configuration::getConfigValueByKey('school_year') ?? '');
        return $year !== '' ? $year : null;
    }

    private function nextSchoolYear(string $current): ?string
    {
        if (!preg_match('/^(\d{4})-(\d{4})$/', $current, $m)) {
            return null;
        }
        return ((int) $m[1] + 1) . '-' . ((int) $m[2] + 1);
    }

    private function passThreshold(): float
    {
        $configured = Configuration::getConfigValueByKey('promotion_pass_threshold');
        if ($configured !== null && is_numeric($configured)) {
            return (float) $configured;
        }
        return self::DEFAULT_PASS_THRESHOLD;
    }
}
