<?php

namespace App\Services;

use App\Interfaces\ScoreCalculatorInterface;
use App\Models\SemesterScoreModel;
use App\Models\ConfigurationModel;
use App\Models\FinalExamModel;
use App\Models\MidtermExamModel;
use App\Models\ParticipationModel;
use RuntimeException;

class SemesterScoreService
{
    private SemesterScoreModel $semesterScoreModel;
    protected ConfigurationModel $configModel;

    protected string $semester;
    protected string $schoolYear;

    /** Who is performing the update (nullable if unknown) */
    private ?int $actorId = null;

    /** @var array<string, ScoreCalculatorInterface> */
    private array $scoreCalculators = [];

    public function __construct(
        SemesterScoreModel $semesterScoreModel,
        ConfigurationModel $configModel,
        ScoreCalculatorInterface ...$scoreCalculators
    ) {
        $this->semesterScoreModel = $semesterScoreModel;
        $this->configModel        = $configModel;

        $this->semester   = (string) $this->configModel->getConfig('semester');
        $this->schoolYear = (string) $this->configModel->getConfig('school_year');

        // Default actor from session (can be overridden later)
        $this->actorId = (int) (session()->get('user_id') ?? 0) ?: null;

        foreach ($scoreCalculators as $calculator) {
            $this->scoreCalculators[get_class($calculator)] = $calculator;
        }
    }

    /**
     * Update scores for multiple students.
     * - If $semester/$schoolYear not provided, they default from service config.
     * - If $updatedBy is provided, it overrides the default actor for this batch.
     * - Each student can carry its own 'updated_by'; that per-student value wins.
     */
    public function updateScoresForStudents(
        array $students,
        ?string $semester = null,
        ?string $schoolYear = null
    ): void {
        // Defaults from service config if omitted
        $semester   = $semester   ?? $this->semester;
        $schoolYear = $schoolYear ?? $this->schoolYear;

        foreach ($students as $student) {
            $this->updateStudentScores($student, $semester, $schoolYear);
        }
    }

    /**
     * Update (compute + persist) the scores for a single student.
     */
    public function updateStudentScores(array $student, string $semester, string $schoolYear): void
    {
        // Validate required student data
        foreach (['student_id', 'school_id', 'class_section_id'] as $field) {
            if (!isset($student[$field])) {
                throw new RuntimeException("Missing required student field: {$field}");
            }
        }

        // Per-student actor resolution:
        // 1) student['updated_by'] if present,
        // 2) batch/service actorId if set,
        // 3) session user as last resort,
        // 4) null if none
        $updatedBy = isset($student['updated_by'])
            ? (int) $student['updated_by']
            : ($this->actorId ?? ((int) (session()->get('user_id') ?? 0) ?: null));

        // Compute all score components
        $scoreData = $this->calculateAllScores(
            (int) $student['student_id'],
            $semester,
            $schoolYear,
            (int) ($student['class_section_id'] ?? 0) ?: null
        );

        // Persist (upsert) the scores
        $this->saveStudentScores(
            (int) $student['student_id'],
            (string) $student['school_id'],
            (int) $student['class_section_id'],
            $updatedBy,      // nullable OK
            $semester,
            $schoolYear,
            $scoreData
        );
    }

    /**
     * Calculates all components needed for the semester score.
     * Returns an associative array of score columns => values (flat).
     */
    private function calculateAllScores(int $studentId, string $semester, string $schoolYear, ?int $classSectionId = null): array
    {
        $scoreData = [
            'updated_at' => utc_now(),
        ];

        // Calculate individual components via injected calculators
        foreach ($this->scoreCalculators as $calculator) {
            try {
                $scoreData = array_merge(
                    $scoreData,
                    $calculator->calculate($studentId, $semester, $schoolYear, $classSectionId)
                );
            } catch (\Throwable $e) {
                log_message(
                    'error',
                    'SemesterScoreService calculator failed for student '
                    . $studentId . ' (' . get_class($calculator) . '): ' . $e->getMessage()
                );
            }
        }

        // Participation fallback if calculator didn’t provide it
        if (!isset($scoreData['participation_score'])) {
            $participationModel = new ParticipationModel();
            $participationScore = $participationModel->getParticipationScore($studentId, $semester, $schoolYear, $classSectionId);
            $scoreData['participation_score'] = $participationScore !== null ? (float) $participationScore : null;
        }

        // Composite scores
        $scoreData['ptap_score'] = $this->calculatePTAPScore($scoreData);
        $midfinal = $this->calculateSemesterScore($scoreData, $semester, $studentId, $schoolYear, $classSectionId);

        return array_merge($scoreData, [
            'semester_score'      => $midfinal['semester_score'],
            'midterm_exam_score'  => $midfinal['midterm_score'],
            'final_exam_score'    => $midfinal['final_score'],
            'participation_score' => $scoreData['participation_score'], // explicit keep
        ]);
    }

    /**
     * PTAP weighting rules (tests are represented by quiz averages):
     * - No tests and no projects: 50% homework, 50% participation.
     * - No projects: 40% homework, 40% participation, 20% tests.
     * - No tests: 34% homework, 33% participation, 33% projects.
     * - All available: 30% homework, 30% participation, 30% projects, 10% tests.
     */
    private function calculatePTAPScore(array $scores): ?float
    {
        $avgHomework      = $scores['homework_avg']        ?? null;
        $avgQuiz          = $scores['quiz_avg']            ?? null;
        $avgProject       = $scores['project_avg']         ?? null;
        $avgParticipation = $scores['participation_score'] ?? null;

        $hasTests    = $avgQuiz !== null;
        $hasProjects = $avgProject !== null;

        if (!$hasTests && !$hasProjects) {
            if ($avgHomework === null || $avgParticipation === null) {
                return null;
            }
            return round(($avgHomework * 0.5) + ($avgParticipation * 0.5), 1);
        }

        if (!$hasProjects) {
            if ($avgHomework === null || $avgParticipation === null || $avgQuiz === null) {
                return null;
            }
            return round(($avgHomework * 0.4) + ($avgParticipation * 0.4) + ($avgQuiz * 0.2), 1);
        }

        if (!$hasTests) {
            if ($avgHomework === null || $avgParticipation === null || $avgProject === null) {
                return null;
            }
            return round(($avgHomework * 0.34) + ($avgParticipation * 0.33) + ($avgProject * 0.33), 1);
        }

        if ($avgHomework === null || $avgParticipation === null || $avgProject === null || $avgQuiz === null) {
            return null;
        }

        return round(
            ($avgHomework * 0.3) + ($avgParticipation * 0.3) + ($avgProject * 0.3) + ($avgQuiz * 0.1),
            1
        );
    }

    /**
     * Calculates semester score using:
     * - Fall:   0.2 * attendance + 0.2 * ptap + 0.6 * midterm
     * - Spring: 0.2 * attendance + 0.2 * ptap + 0.6 * final
     * Returns null when a required component is missing.
     */
    private function calculateSemesterScore(array $scores, string $semester, int $studentId, string $schoolYear, ?int $classSectionId): array
    {
        $ptap       = $scores['ptap_score'] ?? null;
        $attendance = $scores['attendance_score'] ?? null;

        $out = [
            'midterm_score'  => null,
            'final_score'    => null,
            'semester_score' => null,
        ];

        if (strcasecmp($semester, 'Fall') === 0) {
            $mid = $scores['midterm_exam_score'] ?? (new MidtermExamModel())->getMidtermExamScore($studentId, $semester, $schoolYear, $classSectionId);
            $out['midterm_score'] = $mid !== null ? round((float) $mid, 1) : null;
            if ($ptap !== null && $attendance !== null && $out['midterm_score'] !== null) {
                $out['semester_score'] = round(($attendance * 0.2) + ($ptap * 0.2) + ($out['midterm_score'] * 0.6), 1);
            }
            return $out;
        }

        if (strcasecmp($semester, 'Spring') === 0) {
            $fin = $scores['final_exam_score'] ?? (new FinalExamModel())->getFinalExamScore($studentId, $semester, $schoolYear, $classSectionId);
            $out['final_score'] = $fin !== null ? round((float) $fin, 1) : null;
            if ($ptap !== null && $attendance !== null && $out['final_score'] !== null) {
                $out['semester_score'] = round(($attendance * 0.2) + ($ptap * 0.2) + ($out['final_score'] * 0.6), 1);
            }
            return $out;
        }

        return $out;
    }

    /**
     * Persist (upsert) semester scores for a student.
     * Ensure SemesterScoreModel::$allowedFields includes ALL keys we pass here.
     */
    private function saveStudentScores(
        int $studentId,
        string $schoolId,
        int $classSectionId,
        ?int $updatedBy,
        string $semester,
        string $schoolYear,
        array $scoreData
    ): void {
        $base = [
            'student_id'       => $studentId,
            'school_id'        => $schoolId,
            'class_section_id' => $classSectionId,
            'semester'         => $semester,
            'school_year'      => $schoolYear,
            'updated_at'       => utc_now(),
        ];
        if ($updatedBy !== null) {
            $base['updated_by'] = $updatedBy;
        }

        // Merge in the computed score fields (e.g., homework_avg, quiz_avg, project_avg, attendance_score, etc.)
        $payload = array_merge($base, $scoreData);

        // Upsert using your model (make sure allowedFields has all keys in $payload)
        $this->semesterScoreModel->upsert($payload);
    }
}
