"""The review game endpoints — the only client-facing feature (D14).

One GET returns everything the game needs (the caller's options in their own
stable-random order plus their saved state); every tap autosaves through the
PATCH/PUT endpoints below with upsert semantics (D12). Nothing here ever
returns another person's reactions (D7).

Admins use the very same endpoints in preview: their rows are stored under
their own username and separated from the client rollup by level.
"""

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session

from app.constants import ASPECT_LABELS, OptionStatus
from app.db import get_db
from app.models.account import Account
from app.models.option import Option
from app.models.schemas import (
    AspectVoteSet,
    ClientOptionOut,
    FinalPickOut,
    FinalPickPatch,
    MyAspectVoteOut,
    MyReviewOut,
    ReviewBundle,
    ReviewPatch,
)
from app.services import options as options_service
from app.services import reviews as reviews_service
from app.services import levels
from app.services.authz import current_account, readable_project

router = APIRouter(prefix="/api/projects/{project_id}/review", tags=["review"])


def _to_client_out(option: Option) -> ClientOptionOut:
    return ClientOptionOut(
        id=option.id,
        display_label=option.display_label,
        descriptor=option.descriptor,
        reveal_source=option.reveal_source,
        # Anonymized by default (D8): the real source reaches a client only when
        # the admin flipped reveal_source on this option.
        source_name=option.source_name if option.reveal_source else "",
        source_url=option.source_url if option.reveal_source else "",
        has_desktop=bool(option.screenshot_desktop),
        has_mobile=bool(option.screenshot_mobile),
    )


def _reviewable_option(
    db: Session, project_id: int, option_id: int, account: Account
) -> Option:
    """An option the caller may react to: published, in this project. Someone who
    manages the project may also react to drafts — previewing before publish is
    exactly their job."""
    option = options_service.get_option(db, project_id, option_id)
    missing = HTTPException(
        status_code=status.HTTP_404_NOT_FOUND,
        detail={"error_code": "OPTION_NOT_FOUND", "summary": "Option not found."},
    )
    if option is None:
        raise missing
    can_manage = levels.project_can(account.username, project_id, levels.PROJECT_MANAGE)
    if option.status != OptionStatus.PUBLISHED and not can_manage:
        raise missing
    return option


@router.get("", response_model=ReviewBundle)
def review_bundle(
    project_id: int,
    account: Account = Depends(current_account),
    db: Session = Depends(get_db),
) -> ReviewBundle:
    project = readable_project(db, project_id, account)
    published = options_service.list_published(db, project.id)
    ordered = options_service.reviewer_order(published, account.username, project.id)

    pick = reviews_service.my_final_pick(db, project.id, account.username)
    published_ids = {option.id for option in published}
    return ReviewBundle(
        project_id=project.id,
        project_name=project.name,
        client_name=project.client_name,
        options=[_to_client_out(option) for option in ordered],
        my_reviews=[
            MyReviewOut(option_id=r.option_id, rating=r.rating, note=r.note)
            for r in reviews_service.my_reviews(db, project.id, account.username)
            if r.option_id in published_ids
        ],
        my_aspect_votes=[
            MyAspectVoteOut(option_id=v.option_id, aspect=v.aspect, vote=v.vote)
            for v in reviews_service.my_aspect_votes(db, project.id, account.username)
            if v.option_id in published_ids
        ],
        final_pick=FinalPickOut.model_validate(pick) if pick is not None else None,
    )


@router.patch("/options/{option_id}", response_model=MyReviewOut)
def save_review(
    project_id: int,
    option_id: int,
    payload: ReviewPatch,
    account: Account = Depends(current_account),
    db: Session = Depends(get_db),
) -> MyReviewOut:
    """Autosave a gut rating and/or note. Only fields present in the request body
    are written (a rating tap never blanks a note). Idempotent."""
    project = readable_project(db, project_id, account)
    option = _reviewable_option(db, project.id, option_id, account)
    review = reviews_service.upsert_review(
        db,
        project_id=project.id,
        option_id=option.id,
        username=account.username,
        rating=payload.rating,
        note=payload.note,
        set_rating="rating" in payload.model_fields_set,
        set_note="note" in payload.model_fields_set,
    )
    return MyReviewOut(option_id=review.option_id, rating=review.rating, note=review.note)


@router.put("/options/{option_id}/aspects/{aspect}", response_model=MyAspectVoteOut)
def save_aspect_vote(
    project_id: int,
    option_id: int,
    aspect: str,
    payload: AspectVoteSet,
    account: Account = Depends(current_account),
    db: Session = Depends(get_db),
) -> MyAspectVoteOut:
    """Thumbs up (+1), thumbs down (-1), or clear (0) on one fixed aspect."""
    project = readable_project(db, project_id, account)
    option = _reviewable_option(db, project.id, option_id, account)
    if aspect not in ASPECT_LABELS:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail={
                "error_code": "UNKNOWN_ASPECT",
                "summary": f"'{aspect}' is not one of the review aspects.",
            },
        )
    reviews_service.set_aspect_vote(
        db, option_id=option.id, username=account.username, aspect=aspect, vote=payload.vote
    )
    return MyAspectVoteOut(option_id=option.id, aspect=aspect, vote=payload.vote)


@router.put("/final-pick", response_model=FinalPickOut)
def save_final_pick(
    project_id: int,
    payload: FinalPickPatch,
    account: Account = Depends(current_account),
    db: Session = Depends(get_db),
) -> FinalPickOut:
    """The closing choice + free-text answer. `completed: true` marks their
    review finished; reopening later never clears that timestamp."""
    project = readable_project(db, project_id, account)
    if payload.option_id is not None:
        _reviewable_option(db, project.id, payload.option_id, account)
    pick = reviews_service.upsert_final_pick(
        db,
        project_id=project.id,
        username=account.username,
        option_id=payload.option_id,
        closing_note=payload.closing_note,
        set_option="option_id" in payload.model_fields_set,
        set_note="closing_note" in payload.model_fields_set,
        completed=payload.completed,
    )
    return FinalPickOut.model_validate(pick)
