import random
from datetime import datetime, timedelta, timezone
from typing import Optional

from sqlalchemy import func
from sqlalchemy.orm import Session

from models import QuizRecord, User, UserSettings, Word, WordBook
from schemas import QuizOption, QuizQuestion, QuizStatsResponse, WordOut
from services.book_service import ACCUMULATION_BOOK_ID, book_service
from services.dictionary_service import dictionary_service
from services.word_service import calc_mastery_score, utc_now_iso


def today_str() -> str:
    return datetime.now(timezone.utc).strftime("%Y-%m-%d")


def review_interval_days(consecutive: int) -> int:
    """根据连续答对次数计算下次复习间隔（天）。"""
    if consecutive <= 0:
        return 0
    if consecutive == 1:
        return 1
    if consecutive == 2:
        return 3
    if consecutive == 3:
        return 7
    if consecutive >= 5:
        return 15
    return 7


class QuizService:
    def get_settings(self, db: Session, user: User) -> UserSettings:
        settings = db.query(UserSettings).filter(UserSettings.user_id == user.id).first()
        if not settings:
            settings = UserSettings(user_id=user.id)
            db.add(settings)
            db.commit()
            db.refresh(settings)
        return settings

    def _track_words(self, db: Session, user: User, book_id: int) -> list[Word]:
        return (
            db.query(Word)
            .filter(Word.user_id == user.id, Word.book_id == book_id, Word.status != "locked")
            .all()
        )

    def select_accumulation_words(self, db: Session, user: User, limit: int) -> list[Word]:
        return self._select_adaptive_words(
            self._track_words(db, user, ACCUMULATION_BOOK_ID), limit
        )

    def select_book_words(self, db: Session, user: User, book: WordBook, limit: int) -> list[Word]:
        all_words = self._track_words(db, user, book.id)
        pool = self._select_adaptive_words(all_words, limit)
        if len(pool) < limit and book.learn_mode == "sequential":
            need = limit - len(pool)
            unlocked = book_service.unlock_more_new_words(db, user, book, need)
            if unlocked:
                all_words = self._track_words(db, user, book.id)
                pool = self._select_adaptive_words(all_words, limit)
        return pool[:limit]

    def _select_adaptive_words(self, all_words: list[Word], limit: int) -> list[Word]:
        today = today_str()
        weak = [w for w in all_words if w.status == "weak"]
        learning = [w for w in all_words if w.status == "learning"]
        new = [w for w in all_words if w.status == "new"]
        mastered_due = [
            w
            for w in all_words
            if w.status == "mastered"
            and w.review_due_date
            and w.review_due_date <= today
        ]

        pool: list[Word] = []
        for group in (weak, learning, new, mastered_due):
            random.shuffle(group)
            for w in group:
                if w not in pool:
                    pool.append(w)
                if len(pool) >= limit:
                    return pool[:limit]
        return pool[:limit]

    def select_daily_words(
        self, db: Session, user: User, limit: int, track: str = "accumulation"
    ) -> list[Word]:
        if track == "book":
            book = book_service.get_active_book(db, user)
            if not book:
                return []
            book_service.ensure_user_book_words(db, user, book)
            settings = self.get_settings(db, user)
            practice = book_service.practice_settings_dict(db, user, book)
            return self.select_book_words(db, user, book, practice["daily_target"])
        settings = self.get_settings(db, user)
        return self.select_accumulation_words(db, user, settings.daily_target)

    def build_question(self, db: Session, word: Word, all_words: list[Word]) -> QuizQuestion:
        question_type = random.choice(["en_to_zh", "zh_to_en"])

        if question_type == "en_to_zh":
            en = word.target_text if word.source_lang == "zh" else word.source_text
            zh = word.source_text if word.source_lang == "zh" else word.target_text
            prompt = en
            correct = zh
            distractor_pool = [
                (w.source_text if w.source_lang == "zh" else w.target_text)
                for w in all_words
                if w.id != word.id
            ]
        else:
            zh = word.source_text if word.source_lang == "zh" else word.target_text
            en = word.target_text if word.source_lang == "zh" else word.source_text
            prompt = zh
            correct = en
            distractor_pool = [
                (w.target_text if w.source_lang == "zh" else w.source_text)
                for w in all_words
                if w.id != word.id
            ]

        distractors = list({d for d in distractor_pool if d != correct})
        random.shuffle(distractors)
        if len(distractors) < 3:
            if question_type == "en_to_zh":
                extra = dictionary_service.random_zh_values(db, correct, 10)
            else:
                extra = dictionary_service.random_en_lemmas(db, correct, 10)
            for e in extra:
                if e not in distractors:
                    distractors.append(e)
                if len(distractors) >= 3:
                    break
        options_text = [correct] + distractors[:3]
        random.shuffle(options_text)

        labels = ["A", "B", "C", "D"]
        options = [
            QuizOption(label=labels[i], text=options_text[i])
            for i in range(min(4, len(options_text)))
        ]

        return QuizQuestion(
            word_id=word.id,
            question_type=question_type,
            prompt=prompt,
            options=options,
            correct_answer=correct,
        )

    def build_spell_question(self, word: Word) -> QuizQuestion:
        zh = word.source_text if word.source_lang == "zh" else word.target_text
        en = word.target_text if word.source_lang == "zh" else word.source_text
        return QuizQuestion(
            word_id=word.id,
            question_type="spell",
            prompt=zh,
            phonetic=word.phonetic,
            options=[],
            correct_answer=en,
        )

    def _resolve_track(self, db: Session, user: User, track: str) -> tuple[int, int]:
        if track == "book":
            book = book_service.get_active_book(db, user)
            if not book:
                from fastapi import HTTPException

                raise HTTPException(status_code=400, detail="请先在首页选择词书")
            book_service.ensure_user_book_words(db, user, book)
            practice = book_service.practice_settings_dict(db, user, book)
            return book.id, practice["daily_target"]
        settings = self.get_settings(db, user)
        return ACCUMULATION_BOOK_ID, settings.daily_target

    def get_spell_quiz(self, db: Session, user: User, track: str = "accumulation") -> dict:
        book_id, limit = self._resolve_track(db, user, track)
        if track == "book":
            book = book_service.get_book(db, book_id)
            words = self.select_book_words(db, user, book, limit)
        else:
            words = self.select_accumulation_words(db, user, limit)
        questions = [self.build_spell_question(w) for w in words]
        return {
            "questions": questions,
            "total": len(questions),
            "track": track,
            "book_id": book_id if track == "book" else None,
        }

    def get_daily_quiz(self, db: Session, user: User, track: str = "accumulation") -> dict:
        book_id, limit = self._resolve_track(db, user, track)
        if track == "book":
            book = book_service.get_book(db, book_id)
            words = self.select_book_words(db, user, book, limit)
        else:
            words = self.select_accumulation_words(db, user, limit)
        all_words = self._track_words(db, user, book_id)

        if len(all_words) < 4:
            questions = []
            if words:
                questions = [self.build_question(db, words[0], all_words)]
        else:
            questions = [self.build_question(db, w, all_words) for w in words]

        return {
            "questions": questions,
            "total": len(questions),
            "track": track,
            "book_id": book_id if track == "book" else None,
        }

    def submit_answer(
        self,
        db: Session,
        user: User,
        word_id: int,
        question_type: str,
        user_answer: str,
        correct_answer: str,
        duration_seconds: int = 0,
    ) -> dict:
        word = db.query(Word).filter(Word.id == word_id, Word.user_id == user.id).first()
        if not word:
            from fastapi import HTTPException
            raise HTTPException(status_code=404, detail="单词不存在")

        settings = self.get_settings(db, user)
        book = None
        if word.book_id > 0:
            book = db.query(WordBook).filter(WordBook.id == word.book_id).first()
        rule = book_service.book_settings(db, user, book, settings)
        if question_type in ("spell", "memory_coach"):
            is_correct = (
                user_answer.strip().lower() == correct_answer.strip().lower()
            )
        else:
            is_correct = user_answer.strip() == correct_answer.strip()
        now = utc_now_iso()
        today = today_str()
        duration_seconds = max(0, min(int(duration_seconds or 0), 3600))

        if is_correct:
            word.correct_count += 1
            word.consecutive_correct_count += 1
            if word.status == "new":
                word.status = "learning"
            if word.consecutive_correct_count >= rule["master_required_count"]:
                word.status = "mastered"
            days = review_interval_days(word.consecutive_correct_count)
            due = datetime.now(timezone.utc) + timedelta(days=days)
            word.review_due_date = due.strftime("%Y-%m-%d")
        else:
            word.wrong_count += 1
            word.consecutive_correct_count = 0
            if word.wrong_count >= rule["weak_wrong_threshold"]:
                word.status = "weak"
            elif word.status == "new":
                word.status = "learning"
            word.review_due_date = today

        word.mastery_score = calc_mastery_score(word.correct_count, word.wrong_count)
        word.last_reviewed_at = now
        word.train_count += 1
        word.total_train_seconds += duration_seconds

        record = QuizRecord(
            user_id=user.id,
            word_id=word.id,
            question_type=question_type,
            user_answer=user_answer,
            correct_answer=correct_answer,
            is_correct=1 if is_correct else 0,
            duration_seconds=duration_seconds,
            created_at=now,
        )
        db.add(record)
        db.commit()
        db.refresh(word)

        return {
            "is_correct": is_correct,
            "correct_answer": correct_answer,
            "word": WordOut.model_validate(word),
        }

    def get_stats(self, db: Session, user: User, track: str = "accumulation") -> QuizStatsResponse:
        settings = self.get_settings(db, user)
        today = today_str()
        book_id = ACCUMULATION_BOOK_ID
        daily_target = settings.daily_target
        if track == "book":
            active = book_service.get_active_book(db, user)
            if not active:
                return QuizStatsResponse(
                    total_words=0,
                    new_count=0,
                    learning_count=0,
                    mastered_count=0,
                    weak_count=0,
                    today_quiz_count=0,
                    today_correct_count=0,
                    today_accuracy=0.0,
                    daily_target=15,
                    today_completed=0,
                    streak_days=0,
                    track=track,
                )
            book_id = active.id
            daily_target = book_service.practice_settings_dict(db, user, active)["daily_target"]

        word_q = db.query(Word).filter(Word.user_id == user.id, Word.book_id == book_id)
        active_words = word_q.filter(Word.status != "locked")
        total = active_words.count()
        new_count = active_words.filter(Word.status == "new").count()
        learning_count = active_words.filter(Word.status == "learning").count()
        mastered_count = active_words.filter(Word.status == "mastered").count()
        weak_count = active_words.filter(Word.status == "weak").count()

        track_word_ids = {w.id for w in word_q.all()}
        today_records = [
            r
            for r in db.query(QuizRecord).filter(QuizRecord.user_id == user.id).all()
            if r.created_at.startswith(today) and r.word_id in track_word_ids
        ]

        today_quiz_count = len(today_records)
        today_correct = sum(1 for r in today_records if r.is_correct == 1)
        accuracy = (
            round(today_correct / today_quiz_count * 100, 1) if today_quiz_count > 0 else 0.0
        )

        streak = self._calc_streak(db, user, track_word_ids)

        return QuizStatsResponse(
            total_words=total,
            new_count=new_count,
            learning_count=learning_count,
            mastered_count=mastered_count,
            weak_count=weak_count,
            today_quiz_count=today_quiz_count,
            today_correct_count=today_correct,
            today_accuracy=accuracy,
            daily_target=daily_target,
            today_completed=today_quiz_count,
            streak_days=streak,
            track=track,
            book_id=book_id if track == "book" else None,
        )

    def _calc_streak(self, db: Session, user: User, word_ids: Optional[set[int]] = None) -> int:
        records = (
            db.query(QuizRecord)
            .filter(QuizRecord.user_id == user.id)
            .order_by(QuizRecord.created_at.desc())
            .all()
        )
        if word_ids is not None:
            records = [r for r in records if r.word_id in word_ids]
        days_with_quiz = set()
        for r in records:
            days_with_quiz.add(r.created_at[:10])

        streak = 0
        d = datetime.now(timezone.utc).date()
        while d.isoformat() in days_with_quiz:
            streak += 1
            d -= timedelta(days=1)
        return streak


quiz_service = QuizService()
