<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\Paper;
use App\Models\Question;
use App\Models\QuestionBank;
use App\Models\QuizAttempt;
use App\Models\QuizAttemptQuestion;
use App\Models\User;
use App\Models\WrongQuestion;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

final class QuizService
{
    public function startPractice(User $user, QuestionBank $bank, string $mode, array $filters = []): QuizAttempt
    {
        $resumeAttempt = $this->findResumeAttempt($user, $bank, $mode, $filters);
        if ($resumeAttempt !== null) {
            return $resumeAttempt->load('items.question.options');
        }

        $query = Question::query()
            ->where('question_bank_id', $bank->id)
            ->where('is_active', true)
            ->with(['options', 'tags']);

        if (str_contains($mode, 'wrong')) {
            $query->whereHas('wrongQuestions', fn ($wrongQuery) => $wrongQuery
                ->where('user_id', $user->id)
                ->whereNull('mastered_at'));
        }

        if (! empty($filters['category_id'])) {
            $query->where('category_id', $filters['category_id']);
        }

        if (! empty($filters['type'])) {
            $types = is_array($filters['type']) ? $filters['type'] : [$filters['type']];
            $query->whereIn('type', array_values(array_filter($types)));
        }

        if (! empty($filters['tag_ids']) && is_array($filters['tag_ids'])) {
            $tagIds = array_values(array_filter(array_map('intval', $filters['tag_ids'])));
            if ($tagIds !== []) {
                $query->whereHas('tags', fn ($tagQuery) => $tagQuery->whereIn('question_tags.id', $tagIds));
            }
        }

        if (in_array($mode, ['random', 'wrong_random'], true)) {
            $limit = min(max((int) ($filters['limit'] ?? 20), 1), 100);
            $questions = $query
                ->orderBy('type')
                ->orderBy('id')
                ->get()
                ->groupBy('type')
                ->pipe(fn ($groups) => $groups->flatMap(fn ($items) => $items->shuffle()))
                ->take($limit)
                ->values();
        } else {
            $query->orderBy('id');
            if (array_key_exists('limit', $filters)) {
                $query->limit(min(max((int) $filters['limit'], 1), 100));
            }
            $questions = $query->get();
        }

        if ($questions->isEmpty()) {
            throw ValidationException::withMessages(['question_bank_id' => '没有可用题目']);
        }

        return $this->createAttempt($user, $mode, $questions->all(), [
            'question_bank_id' => $bank->id,
            'draw_rule' => $filters,
        ]);
    }

    private function findResumeAttempt(User $user, QuestionBank $bank, string $mode, array $filters): ?QuizAttempt
    {
        if (in_array($mode, ['random', 'wrong_memorize', 'wrong_practice', 'wrong_random'], true)) {
            return null;
        }

        return QuizAttempt::query()
            ->where('user_id', $user->id)
            ->where('question_bank_id', $bank->id)
            ->where('mode', $mode)
            ->where('status', 'in_progress')
            ->latest()
            ->get()
            ->first(function (QuizAttempt $attempt) use ($filters): bool {
                return $this->sameDrawRule($attempt->draw_rule ?? [], $filters);
            });
    }

    /**
     * @param  array<string, mixed>  $left
     * @param  array<string, mixed>  $right
     */
    private function sameDrawRule(array $left, array $right): bool
    {
        $normalize = function (array $rule): array {
            $rule['tag_ids'] = array_values(array_filter(array_map('intval', (array) ($rule['tag_ids'] ?? []))));
            sort($rule['tag_ids']);
            $rule['type'] = array_values(array_filter((array) ($rule['type'] ?? [])));
            sort($rule['type']);
            unset($rule['limit']);

            return $rule;
        };

        return $normalize($left) == $normalize($right);
    }

    public function startPaper(User $user, Paper $paper): QuizAttempt
    {
        $questions = $paper->questions()->with('options')->get();
        if ($questions->isEmpty()) {
            throw ValidationException::withMessages(['paper_id' => '试卷没有题目']);
        }

        if ($paper->attempt_limit !== null) {
            $usedAttempts = QuizAttempt::query()
                ->where('user_id', $user->id)
                ->where('paper_id', $paper->id)
                ->whereIn('status', ['in_progress', 'submitted'])
                ->count();

            if ($usedAttempts >= $paper->attempt_limit) {
                throw ValidationException::withMessages(['paper_id' => '已达到试卷作答次数限制']);
            }
        }

        return $this->createAttempt($user, 'paper', $questions->all(), [
            'paper_id' => $paper->id,
            'question_bank_id' => $paper->question_bank_id,
            'expires_at' => $paper->duration_minutes ? now()->addMinutes($paper->duration_minutes) : null,
        ]);
    }

    public function answer(User $user, QuizAttempt $attempt, int $questionId, array $answer, int $durationSeconds = 0): QuizAttemptQuestion
    {
        if ($attempt->user_id !== $user->id) {
            throw ValidationException::withMessages(['attempt' => '无权访问该记录']);
        }

        if ($attempt->status !== 'in_progress') {
            throw ValidationException::withMessages(['attempt' => '该记录已结束']);
        }

        if ($attempt->expires_at && $attempt->expires_at->isPast()) {
            $this->submit($user, $attempt);
            throw ValidationException::withMessages(['attempt' => '测试已超时并自动交卷']);
        }

        $item = $attempt->items()->where('question_id', $questionId)->with('question.options')->firstOrFail();
        $isCorrect = $this->judge($item->question, $answer);

        $item->update([
            'answer' => array_values($answer),
            'is_correct' => $isCorrect,
            'duration_seconds' => $durationSeconds,
            'answered_at' => now(),
        ]);

        $this->syncWrongQuestion($user, $item->question, $isCorrect);

        return $item->fresh('question.options');
    }

    public function submit(User $user, QuizAttempt $attempt): QuizAttempt
    {
        if ($attempt->user_id !== $user->id) {
            throw ValidationException::withMessages(['attempt' => '无权访问该记录']);
        }

        $items = $attempt->items()->get();
        $correct = $items->where('is_correct', true)->count();
        $score = $items->where('is_correct', true)->sum('score');

        $attempt->update([
            'status' => 'submitted',
            'submitted_at' => now(),
            'correct_count' => $correct,
            'score' => $score,
        ]);

        return $attempt->fresh('items.question.options');
    }

    /**
     * @param  array<int, Question>  $questions
     * @param  array<string, mixed>  $attributes
     */
    private function createAttempt(User $user, string $mode, array $questions, array $attributes): QuizAttempt
    {
        return DB::transaction(function () use ($user, $mode, $questions, $attributes): QuizAttempt {
            $attempt = QuizAttempt::create([
                'user_id' => $user->id,
                'paper_id' => $attributes['paper_id'] ?? null,
                'question_bank_id' => $attributes['question_bank_id'] ?? null,
                'mode' => $mode,
                'status' => 'in_progress',
                'draw_rule' => $attributes['draw_rule'] ?? null,
                'started_at' => now(),
                'expires_at' => $attributes['expires_at'] ?? null,
                'total_questions' => count($questions),
            ]);

            foreach ($questions as $sort => $question) {
                $attempt->items()->create([
                    'question_id' => $question->id,
                    'score' => $this->questionScore($question),
                    'sort' => $sort,
                ]);
            }

            return $attempt->fresh('items.question.options');
        });
    }

    private function judge(Question $question, array $answer): bool
    {
        if ($question->type === 'blank') {
            $expected = array_map('trim', $question->answers ?? []);
            $actual = array_map('trim', array_map('strval', $answer));

            return $expected === $actual;
        }

        $correct = $question->correctOptionIds();
        $actual = array_map('intval', $answer);
        sort($correct);
        sort($actual);

        return $correct === $actual;
    }

    private function syncWrongQuestion(User $user, Question $question, bool $isCorrect): void
    {
        $wrong = WrongQuestion::firstOrNew([
            'user_id' => $user->id,
            'question_id' => $question->id,
        ]);

        if ($isCorrect) {
            if ($wrong->exists) {
                $wrong->consecutive_correct_count++;
                if ($wrong->consecutive_correct_count >= 3) {
                    $wrong->mastered_at = now();
                }
                $wrong->save();
            }

            return;
        }

        $wrong->wrong_count = $wrong->exists ? $wrong->wrong_count + 1 : 1;
        $wrong->consecutive_correct_count = 0;
        $wrong->mastered_at = null;
        $wrong->last_wrong_at = now();
        $wrong->save();
    }

    private function defaultScore(string $type): float
    {
        return match ($type) {
            'multiple' => 2.0,
            'blank' => 2.0,
            default => 1.0,
        };
    }

    private function questionScore(Question $question): float
    {
        $pivotScore = $question->getAttribute('pivot')?->score;

        if ($pivotScore !== null) {
            return (float) $pivotScore;
        }

        return $this->defaultScore($question->type);
    }
}
