from typing import Optional
from pydantic import BaseModel, Field, computed_field


# Auth
class UserRegister(BaseModel):
    username: str = Field(min_length=2, max_length=50)
    password: str = Field(min_length=6, max_length=100)


class UserLogin(BaseModel):
    username: str
    password: str
    remember: bool = True


class TokenResponse(BaseModel):
    access_token: str
    token_type: str = "bearer"


class UserOut(BaseModel):
    id: int
    username: str
    created_at: str

    class Config:
        from_attributes = True


class UserUpdate(BaseModel):
    username: Optional[str] = Field(None, min_length=2, max_length=50)
    current_password: Optional[str] = Field(None, min_length=6, max_length=100)
    new_password: Optional[str] = Field(None, min_length=6, max_length=100)


# Translate
class TranslateRequest(BaseModel):
    text: str = Field(min_length=1)


class TranslateResponse(BaseModel):
    source_text: str
    target_text: str
    source_lang: str
    target_lang: str
    phonetic: Optional[str] = None
    example_en: Optional[str] = None
    example_cn: Optional[str] = None
    found: bool = True


# Words
class WordCreate(BaseModel):
    source_text: str
    target_text: str
    source_lang: str
    target_lang: str
    phonetic: Optional[str] = None
    example_en: Optional[str] = None
    example_cn: Optional[str] = None


class WordUpdate(BaseModel):
    status: Optional[str] = None


class WordOut(BaseModel):
    id: int
    user_id: int
    book_id: int = 0
    book_entry_id: Optional[int] = None
    source_text: str
    target_text: str
    source_lang: str
    target_lang: str
    phonetic: Optional[str] = None
    example_en: Optional[str] = None
    example_cn: Optional[str] = None
    status: str
    correct_count: int
    wrong_count: int
    consecutive_correct_count: int
    mastery_score: int
    train_count: int = 0
    total_train_seconds: int = 0
    review_due_date: Optional[str] = None
    last_reviewed_at: Optional[str] = None
    created_at: str

    @computed_field  # type: ignore[prop-decorator]
    @property
    def entered_at(self) -> str:
        """词库进入时间（与 created_at 一致）。"""
        return self.created_at

    class Config:
        from_attributes = True


class MemoryCurvePoint(BaseModel):
    day_index: int
    date: str
    forgetting: float
    mastery: float
    risk: float


class MemoryFutureRiskPoint(BaseModel):
    day_offset: int
    date: str
    risk: float


class MemoryGraphNode(BaseModel):
    id: str
    label: str
    zh: str
    status: str
    mastery: int
    entered_at: str
    size: int


class MemoryGraphLink(BaseModel):
    source: str
    target: str
    kind: str
    strength: float


class MemoryGraph(BaseModel):
    nodes: list[MemoryGraphNode]
    links: list[MemoryGraphLink]


class MemoryWordSummary(BaseModel):
    id: int
    en: str
    zh: str
    status: str
    mastery_score: int
    correct_count: int = 0
    wrong_count: int = 0
    train_count: int = 0
    total_train_seconds: int = 0
    entered_at: str
    retention_now: float
    risk_7d: float


class WordMemoryCurvePoint(BaseModel):
    date: str
    datetime: str
    forgetting: float
    mastery: float
    risk: float
    wrong_count: int
    train_count: int
    train_seconds: int
    is_correct: Optional[bool] = None


class WordMemoryDetailResponse(BaseModel):
    word_id: int
    en: str
    zh: str
    correct_count: int
    wrong_count: int
    train_count: int
    total_train_seconds: int
    curve_points: list[WordMemoryCurvePoint]
    future_risk: list[MemoryFutureRiskPoint]


class MemoryVisualizationResponse(BaseModel):
    curve_points: list[MemoryCurvePoint]
    future_risk: list[MemoryFutureRiskPoint]
    words: list[MemoryWordSummary]
    graph: MemoryGraph


# Quiz
class QuizOption(BaseModel):
    label: str
    text: str


class QuizQuestion(BaseModel):
    word_id: int
    question_type: str
    prompt: str
    options: list[QuizOption] = []
    correct_answer: str
    phonetic: Optional[str] = None


class DailyQuizResponse(BaseModel):
    questions: list[QuizQuestion]
    total: int
    track: str = "accumulation"
    book_id: Optional[int] = None


class QuizAnswerRequest(BaseModel):
    word_id: int
    question_type: str
    user_answer: str
    correct_answer: str
    duration_seconds: Optional[int] = Field(None, ge=0, le=3600)


class QuizAnswerResponse(BaseModel):
    is_correct: bool
    correct_answer: str
    word: WordOut


class QuizStatsResponse(BaseModel):
    total_words: int
    new_count: int
    learning_count: int
    mastered_count: int
    weak_count: int
    today_quiz_count: int
    today_correct_count: int
    today_accuracy: float
    daily_target: int
    today_completed: int
    streak_days: int
    track: str = "accumulation"
    book_id: Optional[int] = None


# Word books
class WordBookOut(BaseModel):
    id: int
    slug: str
    title: str
    description: Optional[str] = None
    level: str
    word_count: int
    unit_count: int
    sort_order: int
    daily_target: int
    master_required_count: int
    weak_wrong_threshold: int
    learn_mode: str

    class Config:
        from_attributes = True


class WordBookUnitOut(BaseModel):
    unit: int
    title: Optional[str] = None
    word_count: int


class WordBookDetailOut(WordBookOut):
    units: list[WordBookUnitOut] = []


class WordBookProgressOut(BaseModel):
    book_id: int
    total: int
    introduced: int
    mastered: int
    learning: int
    locked: int


class BookPracticeSettingsOut(BaseModel):
    book_id: int
    daily_target: int
    master_required_count: int
    weak_wrong_threshold: int


class ActiveBookOut(BaseModel):
    book: Optional[WordBookOut] = None
    progress: Optional[WordBookProgressOut] = None
    settings: Optional[BookPracticeSettingsOut] = None


class SetActiveBookRequest(BaseModel):
    book_id: int = Field(ge=1)


class BookPracticeSettingsUpdate(BaseModel):
    book_id: Optional[int] = Field(None, ge=1)
    daily_target: Optional[int] = Field(None, ge=5, le=50)
    master_required_count: Optional[int] = Field(None, ge=1, le=10)
    weak_wrong_threshold: Optional[int] = Field(None, ge=1, le=10)


# Settings
class SettingsOut(BaseModel):
    daily_target: int
    master_required_count: int
    weak_wrong_threshold: int

    class Config:
        from_attributes = True


class SettingsUpdate(BaseModel):
    daily_target: Optional[int] = Field(None, ge=1, le=100)
    master_required_count: Optional[int] = Field(None, ge=1, le=20)
    weak_wrong_threshold: Optional[int] = Field(None, ge=1, le=20)


# Memory coach (Q/K/V dialogue)
class MemoryToken(BaseModel):
    role: str
    key: str
    label: str
    value: str
    revealed: bool = False


class CoachWordBrief(BaseModel):
    word_id: int
    zh: str
    phonetic: Optional[str] = None
    retention_now: float
    mastery_score: int
    status: str


class CoachSessionResponse(BaseModel):
    words: list[CoachWordBrief]
    total: int


class CoachTurnRequest(BaseModel):
    word_id: int
    stage: str = "intro"
    user_message: str = ""
    hints_used: int = Field(0, ge=0, le=20)
    duration_seconds: Optional[int] = Field(None, ge=0, le=3600)


class MemoryTransformerCurvePoint(BaseModel):
    hours_ahead: float
    recall_percent: float
    forgetting_percent: float


class MemoryTransformerAttention(BaseModel):
    index: int
    label: str
    weight: float


class MemoryTransformerPredictResponse(BaseModel):
    word_id: int
    en: str
    recall_now_percent: float
    formula_recall_percent: float
    model_recall_percent: float
    blend_weight: float
    event_count: int
    model_trained: bool
    recommended_review_days: int
    half_life_hours: Optional[float] = None
    curve: list[MemoryTransformerCurvePoint]
    attention_hint: list[MemoryTransformerAttention]


class CoachTurnResponse(BaseModel):
    assistant_messages: list[str]
    tokens: list[MemoryToken] = []
    stage: str
    expect_input: bool
    prompt_zh: str = ""
    prompt_phonetic: Optional[str] = None
    input_hint: str = "请输入对应的英文单词"
    is_correct: Optional[bool] = None
    quiz_recorded: bool = False
    word_complete: bool = False
    hints_used: int = 0
    target_en: Optional[str] = None
    target_zh: Optional[str] = None
