"""study_cards API — 암기카드 검수 (공부 암기노트 Phase 1 검수 UI).

needs_review=true 카드를 '출처 문제별 그룹'으로 보고 채택(approve)/수정(edit)/폐기(delete).
별 라우터(prefix=/api/study-cards)라 /api/study-questions/{id} 와 경로 충돌 없음.
정적 경로(/needs-review/count, /approve-batch)는 /{card_id} 보다 먼저 정의.

결정(2026-06-07):
  - 수정(cue/fact/cloze 편집) 시 dedup_hash 재계산 + needs_review=false(사용자 확정본). flagged 클리어.
  - 전체 일괄승인 버튼 없음 — approve-batch 는 source_question_id 단위(그 문제의 카드만).
"""

from __future__ import annotations

from datetime import datetime, timezone
from typing import Annotated

from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy import and_, func, or_, select, update
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession

from core.auth import get_current_user
from core.database import get_session
from models.study_memo_card import StudyMemoCard, StudyMemoCardEvidence, record_card_view
from models.study_memo_card_progress import StudyMemoCardProgress, rate_card
from models.study_question import StudyQuestion
from models.user import User
from services.study.card_normalize import compute_dedup_hash

router = APIRouter()


class CardEvidence(BaseModel):
    source_type: str
    source_id: int | None = None
    snippet: str | None = None


class CardItem(BaseModel):
    id: int
    source_kind: str = "question"
    format: str
    cue: str
    fact: str
    cloze_text: str | None = None
    needs_review: bool
    flagged_by: str | None = None
    evidence: list[CardEvidence] = []
    # 복습(SR) 큐에서만 채움 — 정답('암') 시 다음 복습일 미리보기 라벨 계산용
    # (stage별 동적: +3/7/14일·졸업). deck/검수 응답에선 None.
    review_stage: int | None = None


class CardQuestionGroup(BaseModel):
    source_question_id: int | None = None
    question_text: str | None = None
    correct_choice: int | None = None
    cards: list[CardItem] = []


class CardUpdate(BaseModel):
    needs_review: bool | None = None
    cue: str | None = None
    fact: str | None = None
    cloze_text: str | None = None


class ApproveBatch(BaseModel):
    source_question_id: int


class RateBody(BaseModel):
    outcome: str  # 암/애매/모름 또는 correct/unsure/wrong


class RateResult(BaseModel):
    card_id: int
    outcome: str
    review_stage: int | None = None
    due_at: datetime | None = None


# 자기평가 read-time 매핑 (신규 enum 0 — last_outcome 어휘는 기존 4종 재사용)
_RATE_MAP = {
    "암": "correct", "애매": "unsure", "모름": "wrong",
    "correct": "correct", "unsure": "unsure", "wrong": "wrong",
}


async def _build_card_items(
    session: AsyncSession,
    cards: list[StudyMemoCard],
    stages: dict[int, int | None] | None = None,
) -> list[CardItem]:
    """카드 목록 → CardItem(evidence 동반). due/deck 학습 flow 공용.

    stages: card_id → review_stage (복습 큐에서만 전달, 동적 라벨 미리보기용).
    """
    if not cards:
        return []
    stages = stages or {}
    ids = [c.id for c in cards]
    ev_rows = (
        await session.execute(
            select(StudyMemoCardEvidence).where(StudyMemoCardEvidence.card_id.in_(ids))
        )
    ).scalars().all()
    ev_by: dict[int, list[CardEvidence]] = {}
    for e in ev_rows:
        ev_by.setdefault(e.card_id, []).append(
            CardEvidence(source_type=e.source_type, source_id=e.source_id, snippet=e.snippet)
        )
    return [
        CardItem(
            id=c.id, source_kind=c.source_kind, format=c.format, cue=c.cue, fact=c.fact,
            cloze_text=c.cloze_text, needs_review=c.needs_review, flagged_by=c.flagged_by,
            evidence=ev_by.get(c.id, []), review_stage=stages.get(c.id),
        )
        for c in cards
    ]


def _verify_card(card: StudyMemoCard | None, user: User) -> StudyMemoCard:
    if card is None or card.user_id != user.id or card.deleted_at is not None:
        raise HTTPException(status_code=404, detail="카드를 찾을 수 없습니다")
    return card


@router.get("/needs-review/count")
async def count_needs_review_cards(
    user: Annotated[User, Depends(get_current_user)],
    session: Annotated[AsyncSession, Depends(get_session)],
):
    """검수 대기 카드 수 (배지용)."""
    n = (
        await session.execute(
            select(func.count())
            .select_from(StudyMemoCard)
            .where(
                StudyMemoCard.user_id == user.id,
                StudyMemoCard.deleted_at.is_(None),
                StudyMemoCard.needs_review,
            )
        )
    ).scalar_one()
    return {"count": n}


@router.get("", response_model=list[CardQuestionGroup])
async def list_cards(
    user: Annotated[User, Depends(get_current_user)],
    session: Annotated[AsyncSession, Depends(get_session)],
    needs_review: Annotated[bool, Query()] = True,
    format: Annotated[str | None, Query()] = None,
    limit: Annotated[int, Query(ge=1, le=2000)] = 600,
):
    """카드 목록 — 출처 문제별 그룹. 기본 needs_review=true 검수 큐."""
    conds = [StudyMemoCard.user_id == user.id, StudyMemoCard.deleted_at.is_(None)]
    if needs_review:
        conds.append(StudyMemoCard.needs_review)
    if format in ("qa", "cloze"):
        conds.append(StudyMemoCard.format == format)

    rows = (
        await session.execute(
            select(StudyMemoCard)
            .where(*conds)
            .order_by(StudyMemoCard.source_question_id.asc().nulls_last(), StudyMemoCard.id.asc())
            .limit(limit)
        )
    ).scalars().all()
    if not rows:
        return []

    # evidence 일괄 조회
    card_ids = [c.id for c in rows]
    ev_rows = (
        await session.execute(
            select(StudyMemoCardEvidence).where(StudyMemoCardEvidence.card_id.in_(card_ids))
        )
    ).scalars().all()
    ev_by_card: dict[int, list[CardEvidence]] = {}
    for e in ev_rows:
        ev_by_card.setdefault(e.card_id, []).append(
            CardEvidence(source_type=e.source_type, source_id=e.source_id, snippet=e.snippet)
        )

    # 출처 문제 메타 일괄 조회
    qids = sorted({c.source_question_id for c in rows if c.source_question_id is not None})
    q_meta: dict[int, tuple[str, int]] = {}
    if qids:
        q_rows = (
            await session.execute(
                select(StudyQuestion.id, StudyQuestion.question_text, StudyQuestion.correct_choice)
                .where(StudyQuestion.id.in_(qids))
            )
        ).all()
        q_meta = {r.id: (r.question_text, r.correct_choice) for r in q_rows}

    # 그룹핑 (출제순서=rows 순서 유지). question 카드는 출처 문제별,
    # manual(직접 추가) 카드는 extra.material 별로 묶는다.
    groups: dict[str, CardQuestionGroup] = {}
    order: list[str] = []
    for c in rows:
        if c.source_question_id is not None:
            gkey = f"q:{c.source_question_id}"
        else:
            material = c.extra.get("material") if isinstance(c.extra, dict) else None
            gkey = f"m:{material or '직접 추가'}"
        if gkey not in groups:
            if c.source_question_id is not None:
                qt, cc = q_meta.get(c.source_question_id, (None, None))
                groups[gkey] = CardQuestionGroup(
                    source_question_id=c.source_question_id, question_text=qt, correct_choice=cc, cards=[]
                )
            else:
                material = c.extra.get("material") if isinstance(c.extra, dict) else None
                groups[gkey] = CardQuestionGroup(
                    source_question_id=None,
                    question_text=(f"[자료] {material}" if material else "직접 추가 카드"),
                    correct_choice=None, cards=[],
                )
            order.append(gkey)
        groups[gkey].cards.append(
            CardItem(
                id=c.id, source_kind=c.source_kind, format=c.format, cue=c.cue, fact=c.fact,
                cloze_text=c.cloze_text, needs_review=c.needs_review, flagged_by=c.flagged_by,
                evidence=ev_by_card.get(c.id, []),
            )
        )
    return [groups[k] for k in order]


@router.post("/approve-batch")
async def approve_batch(
    body: ApproveBatch,
    user: Annotated[User, Depends(get_current_user)],
    session: Annotated[AsyncSession, Depends(get_session)],
):
    """한 출처 문제의 검수 대기 카드를 일괄 승인(needs_review=false). 전체 일괄승인은 없음."""
    result = await session.execute(
        update(StudyMemoCard)
        .where(
            StudyMemoCard.user_id == user.id,
            StudyMemoCard.source_question_id == body.source_question_id,
            StudyMemoCard.deleted_at.is_(None),
            StudyMemoCard.needs_review,
        )
        .values(needs_review=False, flagged_by=None, flagged_at=None)
    )
    await session.commit()
    return {"approved": result.rowcount or 0}


# ─── 복습(SR) 트랙 ───

@router.get("/due", response_model=list[CardItem])
async def due_cards(
    user: Annotated[User, Depends(get_current_user)],
    session: Annotated[AsyncSession, Depends(get_session)],
    limit: Annotated[int, Query(ge=1, le=200)] = 30,
):
    """오늘 복습할 카드 (검수 통과만). 두 부류:
      - 신규 승인 카드(progress 없음=첫 회상 전) — SR 큐 진입 경로(첫 회상). '암'이면 due 안
        박고 종료('큐 폭발 방지'), 애매/모름이면 평가 즉시 due(내일)로 입고.
      - 예정 due 카드(due_at<=now, stage<4).
    progress 는 user+card UNIQUE 라 outer join 으로 최대 1행. 예정 due 먼저, 신규(due NULL) 뒤로."""
    now = datetime.now(timezone.utc)
    P = StudyMemoCardProgress
    rows = (
        await session.execute(
            select(StudyMemoCard, P.review_stage)
            .outerjoin(P, and_(P.card_id == StudyMemoCard.id, P.user_id == user.id))
            .where(
                StudyMemoCard.user_id == user.id,
                StudyMemoCard.deleted_at.is_(None),
                StudyMemoCard.needs_review.is_(False),
                or_(
                    P.id.is_(None),  # 신규(첫 회상 전) — progress 미생성
                    and_(
                        P.due_at.is_not(None),
                        P.due_at <= now,
                        or_(P.review_stage.is_(None), P.review_stage < 4),
                    ),
                ),
            )
            .order_by(P.due_at.asc().nulls_last(), StudyMemoCard.id.asc())
            .limit(limit)
        )
    ).all()
    cards = [r[0] for r in rows]
    stages = {r[0].id: r[1] for r in rows}
    return await _build_card_items(session, cards, stages)


@router.post("/{card_id}/rate", response_model=RateResult)
async def rate(
    card_id: int,
    body: RateBody,
    user: Annotated[User, Depends(get_current_user)],
    session: Annotated[AsyncSession, Depends(get_session)],
):
    """카드 자기평가(암/애매/모름) → SR 즉시 자동 입고."""
    card = await session.get(StudyMemoCard, card_id)
    card = _verify_card(card, user)
    if card.needs_review:
        raise HTTPException(status_code=400, detail="검수 안 된 카드는 복습(SR) 대상이 아닙니다")
    outcome = _RATE_MAP.get((body.outcome or "").strip())
    if outcome is None:
        raise HTTPException(status_code=422, detail=f"invalid outcome: {body.outcome!r}")
    progress = await rate_card(session, card=card, outcome=outcome, now=datetime.now(timezone.utc))
    await session.commit()
    return RateResult(
        card_id=card.id, outcome=outcome, review_stage=progress.review_stage, due_at=progress.due_at
    )


# ─── 그냥 공부(cram) 트랙 — 봤다 기록, SR 무관 ───

@router.get("/deck", response_model=list[CardItem])
async def deck(
    user: Annotated[User, Depends(get_current_user)],
    session: Annotated[AsyncSession, Depends(get_session)],
    material: Annotated[str | None, Query()] = None,
    format: Annotated[str | None, Query()] = None,
    limit: Annotated[int, Query(ge=1, le=100)] = 20,
):
    """'그냥 공부'(cram) 덱 — 검수 통과 카드를 덜 본 순서로. material/format 필터. SR 무관."""
    conds = [
        StudyMemoCard.user_id == user.id,
        StudyMemoCard.deleted_at.is_(None),
        StudyMemoCard.needs_review.is_(False),
    ]
    if format in ("qa", "cloze"):
        conds.append(StudyMemoCard.format == format)
    if material:
        conds.append(StudyMemoCard.extra["material"].astext == material)
    rows = (
        await session.execute(
            select(StudyMemoCard)
            .where(*conds)
            .order_by(StudyMemoCard.last_viewed_at.asc().nulls_first(), StudyMemoCard.id.asc())
            .limit(limit)
        )
    ).scalars().all()
    return await _build_card_items(session, list(rows))


@router.post("/{card_id}/view", status_code=204)
async def view_card(
    card_id: int,
    user: Annotated[User, Depends(get_current_user)],
    session: Annotated[AsyncSession, Depends(get_session)],
):
    """'그냥 공부' 봤다 기록 (view_count++, SR 무관)."""
    ok = await record_card_view(session, user_id=user.id, card_id=card_id)
    await session.commit()
    if not ok:
        raise HTTPException(status_code=404, detail="카드를 찾을 수 없습니다")


@router.patch("/{card_id}", response_model=CardItem)
async def update_card(
    card_id: int,
    body: CardUpdate,
    user: Annotated[User, Depends(get_current_user)],
    session: Annotated[AsyncSession, Depends(get_session)],
):
    """승인(needs_review=false) 또는 수정(cue/fact/cloze). 내용 수정 시 dedup_hash 재계산 + 검수완료."""
    card = await session.get(StudyMemoCard, card_id)
    card = _verify_card(card, user)
    fields_set = body.model_fields_set

    content_changed = False
    for fname in {"cue", "fact", "cloze_text"} & fields_set:
        setattr(card, fname, getattr(body, fname))
        content_changed = True

    if content_changed:
        # 정답 토큰(fact) 기준 dedup_hash 재계산 + 사용자 확정본 → 검수 완료.
        card.dedup_hash = compute_dedup_hash(card.source_question_id, card.format, card.fact)
        card.needs_review = False
        card.flagged_by = None
        card.flagged_at = None
    elif "needs_review" in fields_set:
        card.needs_review = bool(body.needs_review)
        if card.needs_review:
            card.flagged_by = "user"
            card.flagged_at = datetime.now(timezone.utc)
        else:
            card.flagged_by = None
            card.flagged_at = None

    try:
        await session.commit()
    except IntegrityError:
        await session.rollback()
        raise HTTPException(status_code=409, detail="같은 정답의 중복 카드가 이미 있습니다")

    return CardItem(
        id=card.id, source_kind=card.source_kind, format=card.format, cue=card.cue, fact=card.fact,
        cloze_text=card.cloze_text, needs_review=card.needs_review, flagged_by=card.flagged_by, evidence=[],
    )


@router.delete("/{card_id}", status_code=204)
async def delete_card(
    card_id: int,
    user: Annotated[User, Depends(get_current_user)],
    session: Annotated[AsyncSession, Depends(get_session)],
):
    """저품질 카드 soft-delete. partial unique(WHERE deleted_at IS NULL)가 자연 정합."""
    card = await session.get(StudyMemoCard, card_id)
    card = _verify_card(card, user)
    card.deleted_at = datetime.now(timezone.utc)
    await session.commit()
