"""The collector and catalogue review area: /collectors, and its merge session.

Sources of truth: app/services/collector_view.py (the reads), app/services/merge_session.py
(the queue and the decisions). Owner-only (the same `sources.manage` permission as the kill
switch), computed on every request, so nothing here can show a number the database no
longer agrees with. The reads write nothing; the three merge writes record who decided
from the session, never from the payload.
"""

from fastapi import APIRouter, Depends, HTTPException, Query, Request
from pydantic import BaseModel
from sqlalchemy.orm import Session

from app.db import get_db
from app.services import audit_log, identity
from app.services import collector_view as view
from app.services import merge_session

router = APIRouter(prefix="/api/collectors", tags=["collectors"])


@router.get("")
def read_collectors(db: Session = Depends(get_db)) -> dict:
    return view.collectors(db)


@router.get("/catalogue")
def read_catalogue(db: Session = Depends(get_db)) -> dict:
    return view.catalogue(db)


@router.get("/runs")
def read_runs(
    collector: str | None = None,
    limit: int = Query(default=60, ge=1, le=400),
    db: Session = Depends(get_db),
) -> dict:
    return view.runs(db, collector=collector, limit=limit)


@router.get("/problems")
def read_problems(db: Session = Depends(get_db)) -> dict:
    return view.problems(db)


@router.get("/products")
def read_products(
    q: str | None = None,
    collector: str | None = None,
    airport: str | None = None,
    category: str | None = None,
    vertical: str | None = None,
    brand: str | None = None,
    size_ml: float | None = Query(default=None, ge=0),
    missing: str | None = Query(default=None, pattern="^(image|size|gtin|category|brand)$"),
    min_airports: int | None = Query(default=None, ge=1, le=25),
    group: str | None = Query(default=None, pattern="^(brand|category|vertical|size)$"),
    sort: str = Query(default="name", pattern="^(name|brand|category|size|airports|newest)$"),
    descending: bool = False,
    page: int = Query(default=1, ge=1),
    per_page: int = Query(default=50, ge=1, le=200),
    db: Session = Depends(get_db),
) -> dict:
    filters = dict(q=q, collector=collector, airport=airport, category=category,
                   vertical=vertical, missing=missing, min_airports=min_airports,
                   brand=brand, size_ml=size_ml)
    if group:
        return view.grouped(db, group=group, **filters)
    return view.products(db, sort=sort, descending=descending, page=page, per_page=per_page, **filters)


@router.get("/listings")
def read_listings(
    q: str | None = None,
    collector: str | None = None,
    airport: str | None = None,
    brand: str | None = None,
    differs: str | None = Query(default=None, pattern="^(any|brand|name|size)$"),
    page: int = Query(default=1, ge=1),
    per_page: int = Query(default=50, ge=1, le=200),
    db: Session = Depends(get_db),
) -> dict:
    return view.listings(db, q=q, collector=collector, airport=airport, brand=brand,
                         differs=differs, page=page, per_page=per_page)


# --------------------------------------------------------------------------- the merge session

class MergeConfirmIn(BaseModel):
    """The preferred name (one of the pair's spellings, or any), which side to keep when
    the name does not settle it, and a note (the barcode override records why)."""

    preferred_name: str | None = None
    keep: str | None = None
    note: str | None = None


class MergeRejectIn(BaseModel):
    note: str | None = None


def _refused(exc: merge_session.Refused) -> HTTPException:
    status = 404 if exc.code == "PAIR_NOT_FOUND" else 422 if exc.code == "NOTE_REQUIRED" else 409
    return HTTPException(status_code=status, detail={"error_code": exc.code, "summary": exc.summary})


@router.get("/merge")
def read_merge_queue(
    level: str = Query(default="brand", pattern="^(brand|line|product)$"),
    page: int = Query(default=1, ge=1),
    per_page: int = Query(default=20, ge=1, le=100),
    db: Session = Depends(get_db),
) -> dict:
    return merge_session.queue(db, level=level, page=page, per_page=per_page)


@router.post("/merge/{candidate_id}/confirm")
def confirm_merge(candidate_id: int, payload: MergeConfirmIn, request: Request, db: Session = Depends(get_db)) -> dict:
    who = identity.actor(request)
    try:
        result = merge_session.confirm(
            db, candidate_id, decided_by=who.id, preferred_name=payload.preferred_name,
            keep=payload.keep if payload.keep in ("left", "right") else None, note=payload.note,
        )
    except merge_session.Refused as exc:
        raise _refused(exc) from exc
    audit_log.record("merge.confirm", entity_type=result["level"], entity_key=str(candidate_id),
                     detail={"applied": result["applied"], "preferred_name": payload.preferred_name})
    result["progress"] = merge_session.progress(db)
    return result


@router.post("/merge/{candidate_id}/reject")
def reject_merge(candidate_id: int, payload: MergeRejectIn, request: Request, db: Session = Depends(get_db)) -> dict:
    who = identity.actor(request)
    try:
        result = merge_session.reject(db, candidate_id, decided_by=who.id, note=payload.note)
    except merge_session.Refused as exc:
        raise _refused(exc) from exc
    audit_log.record("merge.reject", entity_type=result["level"], entity_key=str(candidate_id),
                     detail={"note": payload.note})
    result["progress"] = merge_session.progress(db)
    return result


@router.post("/merge/suggest")
def refresh_suggestions(request: Request, db: Session = Depends(get_db)) -> dict:
    identity.actor(request)
    counts = merge_session.refresh(db)
    return {"suggested": counts, "progress": merge_session.progress(db)}
