<?php

namespace App\Services;

use App\Models\SelfPaced\Course;
use App\Models\SelfPaced\Enrollment;
use App\Models\SelfPaced\ExamAttempt;
use App\Models\SelfPaced\ExamQuestion;
use App\Models\SelfPaced\ExamResult;
use App\Models\SelfPaced\SelfPacedCertificate;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

/**
 * ExamService
 *
 * Handles exam attempt lifecycle: creation, answer persistence,
 * grading, result calculation, and certificate triggering.
 *
 * Property 3: Exam score calculation correctness.
 * Property 4: Exam timer integrity.
 * Property 6: Certificate issued only on pass.
 * Property 9: Exam answer persistence (idempotence).
 */
class ExamService
{
    public function __construct(
        protected SelfPacedCertificateService $certificateService
    ) {}

    // =========================================================================
    // ATTEMPT CREATION
    // =========================================================================

    /**
     * Create a new exam attempt for an enrollment.
     *
     * Selects a random subset of active questions from the course bank,
     * sets the expiry timestamp, and persists the attempt.
     *
     * @throws \RuntimeException if no attempts remain or an active attempt exists.
     */
    public function createAttempt(Enrollment $enrollment): ExamAttempt
    {
        $course = $enrollment->course;

        // Guard: remaining attempts
        if ($enrollment->getRemainingAttempts() <= 0) {
            throw new \RuntimeException('No exam attempts remaining for this enrollment.');
        }

        // Guard: already has an in-progress attempt
        $active = $enrollment->getActiveAttempt();
        if ($active && !$active->isExpired()) {
            return $active; // Resume existing attempt
        }

        // Auto-submit any expired in-progress attempt before creating a new one
        if ($active && $active->isExpired()) {
            $this->autoSubmitExpired($active);
        }

        // Select random questions
        $selectedIds = ExamQuestion::where('course_id', $course->id)
            ->where('is_active', true)
            ->inRandomOrder()
            ->limit($course->exam_questions_per_attempt)
            ->pluck('id')
            ->toArray();

        if (empty($selectedIds)) {
            throw new \RuntimeException('No active exam questions found for this course.');
        }

        // Load questions with topic to sort and group them
        $questions = ExamQuestion::whereIn('id', $selectedIds)
            ->with('topic')
            ->get()
            ->sortBy(fn($q) => [$q->topic?->sort_order ?? PHP_INT_MAX, $q->sort_order])
            ->values();

        $questionIds = $questions->pluck('id')->toArray();

        // Build topic group metadata
        $topicGroups = [];
        $offset = 0;
        foreach ($questions->groupBy(fn($q) => $q->topic_id ?? 'general') as $key => $group) {
            $topicGroups[] = [
                'topic_id'     => $key === 'general' ? null : (int) $key,
                'topic_title'  => $key === 'general' ? 'General Questions' : ($group->first()->topic?->title ?? 'General Questions'),
                'question_ids' => $group->pluck('id')->toArray(),
                'start_index'  => $offset,
            ];
            $offset += $group->count();
        }

        $startedAt = now();
        $expiresAt = $startedAt->copy()->addMinutes($course->exam_time_limit);

        $attempt = ExamAttempt::create([
            'enrollment_id' => $enrollment->id,
            'user_id'       => $enrollment->user_id,
            'course_id'     => $course->id,
            'started_at'    => $startedAt,
            'expires_at'    => $expiresAt,
            'status'        => 'in_progress',
            'answers'       => [],
            'question_ids'  => $questionIds,
            'topic_groups'  => $topicGroups,
        ]);

        Log::info('Exam attempt created', [
            'attempt_id'    => $attempt->id,
            'enrollment_id' => $enrollment->id,
            'expires_at'    => $expiresAt->toIso8601String(),
        ]);

        return $attempt;
    }

    // =========================================================================
    // ANSWER PERSISTENCE
    // =========================================================================

    /**
     * Save (or overwrite) the answer for a single question — idempotent.
     * Also auto-submits if the attempt has expired.
     *
     * Property 9: Exam answer persistence.
     *
     * @throws \RuntimeException if the attempt is not in progress.
     */
    public function saveAnswer(ExamAttempt $attempt, int $questionId, string $selectedOption): ExamAttempt
    {
        // Server-side expiry check
        if ($attempt->isExpired() && $attempt->isInProgress()) {
            return $this->autoSubmitExpired($attempt);
        }

        if (!$attempt->isInProgress()) {
            throw new \RuntimeException('Cannot save answer: attempt is not in progress.');
        }

        // Normalise multi-answer: sort letters and join with comma e.g. "A,C"
        $parts = array_filter(array_map('trim', explode(',', strtoupper($selectedOption))));
        sort($parts);
        $normalised = implode(',', $parts);

        $attempt->saveAnswer($questionId, $normalised);

        return $attempt->fresh();
    }

    // =========================================================================
    // SUBMISSION & GRADING
    // =========================================================================

    /**
     * Submit an attempt, grade it, create ExamResult, and trigger certificate if passed.
     *
     * Property 3: score_percentage = round(correct / total * 100, 2).
     * Property 6: Certificate issued only when passed = true AND score >= threshold.
     *
     * @throws \RuntimeException if the attempt is already submitted.
     */
    public function submitAttempt(ExamAttempt $attempt, string $submissionType = 'submitted'): ExamResult
    {
        if (!$attempt->isInProgress()) {
            // If already submitted, return existing result
            if ($attempt->result) {
                return $attempt->result;
            }
            throw new \RuntimeException('Attempt is not in progress.');
        }

        return DB::transaction(function () use ($attempt, $submissionType) {
            $course = $attempt->course;

            // Mark attempt as submitted
            $attempt->update([
                'status'       => $submissionType,
                'submitted_at' => now(),
            ]);

            // Grade the attempt
            $result = $this->gradeAttempt($attempt, $course);

            Log::info('Exam attempt submitted', [
                'attempt_id'       => $attempt->id,
                'score_percentage' => $result->score_percentage,
                'passed'           => $result->passed,
                'type'             => $submissionType,
            ]);

            // Trigger certificate generation if passed
            if ($result->passed) {
                try {
                    $this->certificateService->generate(
                        result: $result,
                        generatedBy: $attempt->user_id
                    );
                } catch (\Throwable $e) {
                    Log::error('Certificate generation failed after exam pass', [
                        'attempt_id' => $attempt->id,
                        'error'      => $e->getMessage(),
                    ]);
                    // Non-fatal — result is still valid
                }
            }

            return $result;
        });
    }

    /**
     * Auto-submit an expired in-progress attempt.
     */
    public function autoSubmitExpired(ExamAttempt $attempt): ExamAttempt
    {
        if (!$attempt->isInProgress()) {
            return $attempt;
        }

        try {
            $this->submitAttempt($attempt, 'timed_out');
        } catch (\Throwable $e) {
            Log::error('Auto-submit of expired attempt failed', [
                'attempt_id' => $attempt->id,
                'error'      => $e->getMessage(),
            ]);
        }

        return $attempt->fresh();
    }

    /**
     * Check whether an attempt is expired server-side.
     * If expired and still in_progress, auto-submit it.
     */
    public function isExpired(ExamAttempt $attempt): bool
    {
        if ($attempt->isExpired() && $attempt->isInProgress()) {
            $this->autoSubmitExpired($attempt);
            return true;
        }

        return $attempt->isExpired();
    }

    // =========================================================================
    // GRADING LOGIC
    // =========================================================================

    /**
     * Grade an attempt and persist the ExamResult.
     *
     * Property 3: score_percentage = round(K / N * 100, 2) for N >= 1, 0 <= K <= N.
     */
    protected function gradeAttempt(ExamAttempt $attempt, Course $course): ExamResult
    {
        $questionIds = $attempt->question_ids ?? [];
        $answers     = $attempt->answers ?? [];
        $total       = count($questionIds);

        if ($total === 0) {
            return ExamResult::create([
                'attempt_id'       => $attempt->id,
                'enrollment_id'    => $attempt->enrollment_id,
                'user_id'          => $attempt->user_id,
                'course_id'        => $attempt->course_id,
                'total_questions'  => 0,
                'correct_answers'  => 0,
                'score_percentage' => 0.00,
                'passed'           => false,
                'passing_threshold' => $course->passing_score,
            ]);
        }

        // Load questions in one query
        $questions = ExamQuestion::whereIn('id', $questionIds)
            ->with('topic')
            ->get()
            ->keyBy('id');

        $correct = 0;
        foreach ($questionIds as $qid) {
            $question = $questions->get($qid);
            if (!$question) {
                continue;
            }

            $selected = $answers[(string) $qid] ?? null;
            if ($selected && $question->isCorrect($selected)) {
                $correct++;
            }
        }

        // Property 3: score = round(K / N * 100, 2)
        $scorePercentage = round(($correct / $total) * 100, 2);
        $passed          = $scorePercentage >= $course->passing_score;

        // Compute per-topic scores
        $topicScores = [];
        $groupedByTopic = collect($questionIds)->groupBy(function ($qid) use ($questions) {
            return $questions->get($qid)?->topic_id ?? 'general';
        });

        foreach ($groupedByTopic as $topicKey => $qids) {
            $topicCorrect = 0;
            foreach ($qids as $qid) {
                $q = $questions->get($qid);
                $selected = $answers[(string) $qid] ?? null;
                if ($q && $selected && $q->isCorrect($selected)) {
                    $topicCorrect++;
                }
            }
            $topicTotal = $qids->count();
            $firstQ = $questions->get($qids->first());
            $topicScores[] = [
                'topic_id'         => $topicKey === 'general' ? null : (int) $topicKey,
                'topic_title'      => $topicKey === 'general' ? 'General Questions' : ($firstQ?->topic?->title ?? 'General Questions'),
                'total_questions'  => $topicTotal,
                'correct_answers'  => $topicCorrect,
                'score_percentage' => $topicTotal > 0 ? round(($topicCorrect / $topicTotal) * 100, 2) : 0,
            ];
        }

        return ExamResult::create([
            'attempt_id'        => $attempt->id,
            'enrollment_id'     => $attempt->enrollment_id,
            'user_id'           => $attempt->user_id,
            'course_id'         => $attempt->course_id,
            'total_questions'   => $total,
            'correct_answers'   => $correct,
            'score_percentage'  => $scorePercentage,
            'passed'            => $passed,
            'passing_threshold' => $course->passing_score,
            'topic_scores'      => $topicScores,
        ]);
    }

    // =========================================================================
    // RESULT HELPERS
    // =========================================================================

    /**
     * Get a breakdown of incorrect answers for a submitted attempt.
     * Returns array of ['question' => ExamQuestion, 'selected' => string|null, 'correct' => string].
     */
    public function getIncorrectAnswers(ExamAttempt $attempt): array
    {
        $questionIds = $attempt->question_ids ?? [];
        $answers     = $attempt->answers ?? [];

        $questions = ExamQuestion::whereIn('id', $questionIds)->get()->keyBy('id');
        $incorrect = [];

        foreach ($questionIds as $qid) {
            $question = $questions->get($qid);
            if (!$question) {
                continue;
            }

            $selected = $answers[(string) $qid] ?? null;
            if (!$selected || !$question->isCorrect($selected)) {
                $incorrect[] = [
                    'question' => $question,
                    'selected' => $selected,
                    'correct'  => $question->correct_answer,
                ];
            }
        }

        return $incorrect;
    }

    /**
     * Calculate score percentage without persisting (used for previews/tests).
     *
     * Property 3: score_percentage = round(K / N * 100, 2).
     */
    public function calculateScore(int $totalQuestions, int $correctAnswers): float
    {
        if ($totalQuestions <= 0) {
            return 0.00;
        }

        $correctAnswers = max(0, min($correctAnswers, $totalQuestions));

        return round(($correctAnswers / $totalQuestions) * 100, 2);
    }

    // =========================================================================
    // EXAM UNLOCK AND EXHAUSTION
    // =========================================================================

    /**
     * Check if exam is unlocked for an enrollment before allowing attempt
     *
     * @param Enrollment $enrollment
     * @return void
     * @throws \RuntimeException if exam is not unlocked
     *
     * Requirements: 6.2, 6.3
     */
    public function validateExamUnlocked(Enrollment $enrollment): void
    {
        if ($enrollment->exam_unlocked_at === null) {
            throw new \RuntimeException(
                'Exam is not yet unlocked. Complete all topics first.'
            );
        }
    }

    /**
     * After exam submission, check for exhaustion and set content access expiry
     *
     * Sets exam_exhausted = true if:
     * - Attempt count >= max_exam_attempts
     * - No passing result exists
     *
     * Also sets content access expiry to 30 days.
     *
     * @param Enrollment $enrollment
     * @return void
     *
     * Requirements: 13.5, 13.10, 18.7
     */
    public function checkAndMarkExhaustionAfterSubmission(Enrollment $enrollment): void
    {
        $course = $enrollment->course;
        $maxAttempts = $course->max_exam_attempts ?? 3;

        $attemptCount = $enrollment->examAttempts()
            ->whereIn('status', ['submitted', 'timed_out'])
            ->count();

        // Check if any passing result exists
        $hasPassingResult = $enrollment->examResults()
            ->where('passed', true)
            ->exists();

        // If max attempts reached and no passing result, mark as exhausted
        if ($attemptCount >= $maxAttempts && !$hasPassingResult) {
            $enrollment->update(['exam_exhausted' => true]);
            Log::info('Exam exhausted for enrollment', ['enrollment_id' => $enrollment->id]);
        }

        // Set content access expiry
        if ($enrollment->content_access_expires_at === null) {
            $enrollment->update([
                'content_access_expires_at' => now()->addDays(30),
            ]);
        }
    }
}
