"""The board API: one read (the whole board, projected for the requesting
VIEWER — M6, plan §4.3), two writes (patch ball, serve ball), and the
mention-autocomplete roster. Thin routers — policy, the status<->review
coupling, and the projection live in services/. Sync def (react.md: sync
for DB routes; FastAPI thread-pools them). Auth: the default-deny
middleware gates every route here.

READS resolve the viewer via services.viewer (session player, or the View
As / dev-header target); WRITES act as the real session player
(auth.current_player) — reads project, writes act."""
from __future__ import annotations

import uuid

from fastapi import APIRouter, Depends, Request
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session

from ..auth import current_player
from ..db import get_db
from ..schemas import BallCreate, BallOut, BallPatch, BoardOut, MentionablePlayerOut
from ..services import board as board_service
from ..services import projection as projection_service
from ..services import viewer as viewer_service

router = APIRouter()


def _error_response(err: board_service.BoardError) -> JSONResponse:
    return JSONResponse(
        status_code=err.status_code,
        content={
            "error_code": err.error_code,
            "summary": err.summary,
            "detail": err.detail,
        },
    )


@router.get("/api/matches/{slug}/board", response_model=BoardOut)
def get_board(slug: str, request: Request, db: Session = Depends(get_db)):
    viewer = viewer_service.resolve_viewer(request, db)
    try:
        return board_service.board_payload(db, slug, viewer)
    except board_service.BoardError as err:
        return _error_response(err)


@router.get(
    "/api/matches/{slug}/mentionable", response_model=list[MentionablePlayerOut]
)
def mentionable(slug: str, request: Request, db: Session = Depends(get_db)):
    """The composer's @mention autocomplete surface: exactly the viewer's
    visible players (§4.3 — the autocomplete only offers players the
    author can see; services.notifications enforces the same set at
    write time as the backstop)."""
    viewer = viewer_service.resolve_viewer(request, db)
    try:
        match = board_service.get_match_by_slug(db, slug)
    except board_service.BoardError as err:
        return _error_response(err)
    lens = projection_service.build_lens(db, match.id, viewer)
    return projection_service.visible_players_payload(db, lens)


@router.patch("/api/balls/{ball_id}", response_model=BallOut)
def patch_ball(
    ball_id: uuid.UUID, patch: BallPatch, request: Request, db: Session = Depends(get_db)
):
    # Only explicitly-sent fields apply (and the coupling automation fills
    # the counterpart field only when it was NOT explicitly sent). The actor
    # comes from the SESSION identity — touches are attributed server-side.
    data = patch.model_dump(exclude_unset=True)
    actor = current_player(request)
    try:
        ball, extras = board_service.patch_ball(db, ball_id, data, actor)
    except board_service.BoardError as err:
        return _error_response(err)
    out = BallOut.model_validate(ball)
    out.ace = bool(extras.get("ace"))
    return out


@router.post("/api/matches/{slug}/balls", response_model=BallOut, status_code=201)
def create_ball(
    slug: str, payload: BallCreate, request: Request, db: Session = Depends(get_db)
):
    # Author is stamped from the SESSION identity (ref §2.7 port note: the
    # client never says who it is). None is allowed — a signed-in BW user
    # without a Player row yet can still serve; authorship just stays empty.
    author = current_player(request)
    try:
        return board_service.create_ball(db, slug, payload.model_dump(), author)
    except board_service.BoardError as err:
        return _error_response(err)
