"""
shoot_api.py — API of the photographer's phone app (/shoot).

Auth is the school's private link (`?t=TOKEN`, sent as the `X-Shoot-Token` header): it is regenerated from the
admin when a phone is lost. The phone only ever sends counts; it never sees a photo or a family.

  GET  /api/shoot/info    school, classes, photographers, max photos per child, the live counts
  POST /api/shoot/class   {photographer, qr | class_name}   start a class (closes the previous one)
  POST /api/shoot/child   {photographer, count}              the next child had `count` photos
  POST /api/shoot/skip    {photographer, count}              `count` photos to ignore (test shots)
  POST /api/shoot/undo    {photographer}                     cancel the last child / skip of this class
  POST /api/shoot/end     {photographer}                     close the class (pause, change of room)
  GET  /api/shoot/status  live counts, problems and the last events

The design rationale (counts and a fence, never clocks) is in services/shoot_service.py.
"""
import logging
from typing import Optional

from fastapi import APIRouter, Header, HTTPException, Request
from pydantic import BaseModel, Field

from core.db import get_db
from core.rate_limit import enforce as rate_limit_enforce
from core.security import client_ip
from services import audit_service, photographer_service, school_service, shoot_service
from services.shoot_service import KIND_CHILD, KIND_CLASS, KIND_END, KIND_SKIP, KIND_UNDO

log = logging.getLogger("meeconic.shoot")
router = APIRouter()


def _school(request: Request, token: Optional[str]) -> str:
    # A phone tapping all day is fine; a token guesser is not. Generous limit, per IP.
    rate_limit_enforce(f"shoot:{client_ip(request)}", max_attempts=1500, window_seconds=300)
    school_id = shoot_service.school_for_token(token or "")
    if not school_id:
        raise HTTPException(401, detail="Invalid or revoked link.")
    school = school_service.get_school(school_id)
    if not school or not school["active"]:
        raise HTTPException(401, detail="School disabled.")
    return school_id


def _classes(school_id: str) -> list[str]:
    from admin import class_sort_key
    with get_db() as conn:
        rows = conn.execute("SELECT name FROM school_classes WHERE school_id=?", (school_id,)).fetchall()
    return sorted((r["name"] for r in rows), key=class_sort_key)


def _photographers(school_id: str) -> list[str]:
    return [p["name"] for p in photographer_service.list_photographers(school_id)] \
        or [photographer_service.IMPLICIT_PHOTOGRAPHER]


def _photographer(school_id: str, name: str) -> str:
    if name not in _photographers(school_id):
        raise HTTPException(400, detail="Unknown photographer.")
    return name


def _max_per_child(school_id: str) -> int:
    try:
        return max(1, int(school_service.get_settings(school_id).get("shoot_max_per_child") or 3))
    except (TypeError, ValueError):
        return 3


def _close_open_run(sid: str, ph: str) -> Optional[dict]:
    """Close the open run with its fence: exactly the files in the folder that belong to it right now."""
    events = shoot_service.events_for(sid, ph)
    runs = shoot_service.build_runs(events)
    run = runs[-1] if runs and not runs[-1].closed else None
    if run is None:
        return None
    processed = shoot_service.get_state(sid, ph)["processed"]
    claimed = shoot_service.claimed_names(runs, processed)
    before = set(run.before)
    own = [n for n in shoot_service.pool_names(sid, ph) if n not in claimed and n not in before]
    shoot_service.record_event(sid, ph, KIND_END, None, {"files": own})
    return {"class_name": run.class_name, "declared": run.declared, "received": len(own),
            "children": run.children, "ok": run.declared == len(own)}


def _require_open(sid: str, ph: str) -> shoot_service.Run:
    run = shoot_service.open_run(shoot_service.events_for(sid, ph))
    if run is None:
        raise HTTPException(409, detail="No class is active: scan the class QR first.")
    return run


class ShootBody(BaseModel):
    photographer: str = Field(min_length=1, max_length=64)
    qr: Optional[str] = Field(default=None, max_length=200)
    class_name: Optional[str] = Field(default=None, max_length=40)
    count: Optional[int] = Field(default=None, ge=1, le=shoot_service.MAX_SKIP)


def _live(sid: str, names: list[str]) -> dict:
    return {n: shoot_service.live_counts(sid, n) for n in names}


@router.get("/api/shoot/info")
async def shoot_info(request: Request, x_shoot_token: Optional[str] = Header(None)):
    sid = _school(request, x_shoot_token)
    settings = school_service.get_settings(sid)
    names = _photographers(sid)
    return {
        "school_id": sid,
        "school_name": settings.get("school_name") or sid,
        "enabled": settings.get("shoot_mode") == "true",
        "classes": _classes(sid),
        "photographers": names,
        "max_per_child": _max_per_child(sid),
        "live": _live(sid, names),
    }


@router.post("/api/shoot/class")
async def shoot_class(body: ShootBody, request: Request, x_shoot_token: Optional[str] = Header(None)):
    sid = _school(request, x_shoot_token)
    ph = _photographer(sid, body.photographer)
    if body.qr:
        parsed = shoot_service.parse_class_qr(body.qr)
        if not parsed:
            raise HTTPException(400, detail="This QR is not a class QR.")
        qr_school, class_name = parsed
        if qr_school != sid:
            raise HTTPException(400, detail="This class belongs to another school.")
    elif body.class_name:
        class_name = " ".join(body.class_name.split())
    else:
        raise HTTPException(400, detail="A class QR or a class name is required.")
    if class_name not in _classes(sid):
        raise HTTPException(400, detail=f"Unknown class: {class_name}")

    previous = _close_open_run(sid, ph)            # a new class always closes the one before it
    events = shoot_service.events_for(sid, ph)
    runs = shoot_service.build_runs(events)
    claimed = shoot_service.claimed_names(runs, shoot_service.get_state(sid, ph)["processed"])
    strays = [n for n in shoot_service.pool_names(sid, ph) if n not in claimed]   # taken with no class active
    ev = shoot_service.record_event(sid, ph, KIND_CLASS, class_name, {"before": strays})
    audit_service.log_event("shoot.class", user_id=ph, ip_address=client_ip(request), school_id=sid,
                            details={"class_name": class_name, "photographer": ph, "strays": len(strays)})
    return {"event": ev, "class_name": class_name, "previous": previous, "strays": len(strays)}


@router.post("/api/shoot/child")
async def shoot_child(body: ShootBody, request: Request, x_shoot_token: Optional[str] = Header(None)):
    sid = _school(request, x_shoot_token)
    ph = _photographer(sid, body.photographer)
    _require_open(sid, ph)
    limit = _max_per_child(sid)
    if body.count is None or body.count > limit:
        # More photos than the limit is how two children end up on one code: tap twice instead.
        raise HTTPException(400, detail=f"A child has at most {limit} photos: tap once per child.")
    shoot_service.record_event(sid, ph, KIND_CHILD, str(body.count))
    return {"live": shoot_service.live_counts(sid, ph)}


@router.post("/api/shoot/skip")
async def shoot_skip(body: ShootBody, request: Request, x_shoot_token: Optional[str] = Header(None)):
    sid = _school(request, x_shoot_token)
    ph = _photographer(sid, body.photographer)
    _require_open(sid, ph)
    shoot_service.record_event(sid, ph, KIND_SKIP, str(body.count or 1))
    return {"live": shoot_service.live_counts(sid, ph)}


@router.post("/api/shoot/undo")
async def shoot_undo(body: ShootBody, request: Request, x_shoot_token: Optional[str] = Header(None)):
    sid = _school(request, x_shoot_token)
    ph = _photographer(sid, body.photographer)
    run = _require_open(sid, ph)
    if not run.items:
        raise HTTPException(409, detail="Nothing to undo in this class.")
    shoot_service.record_event(sid, ph, KIND_UNDO)
    return {"live": shoot_service.live_counts(sid, ph)}


@router.post("/api/shoot/end")
async def shoot_end(body: ShootBody, request: Request, x_shoot_token: Optional[str] = Header(None)):
    sid = _school(request, x_shoot_token)
    ph = _photographer(sid, body.photographer)
    closed = _close_open_run(sid, ph)
    return {"closed": closed}


@router.get("/api/shoot/status")
async def shoot_status(request: Request, x_shoot_token: Optional[str] = Header(None)):
    sid = _school(request, x_shoot_token)
    from services.organizer_service import get_organizer_status
    org = get_organizer_status(sid)
    names = _photographers(sid)
    return {
        "events": shoot_service.recent_events(sid),
        "live": _live(sid, names),
        "pending": org.get("pending_count", 0),
        "unassigned": org.get("unassigned_count", 0),
        "blocked": {n: shoot_service.get_state(sid, n).get("blocked", [])[-5:] for n in names},
    }
