"""The review (Stream K4): `/api/review`, the sheet a person reads and the approval, reject and
undo they act with. Owner only (`services/access.py`, every route in the owner class); the page is
`/review`.

Sources of truth: `services/proposals.py` (every read and write here), `services/decisions/` (the
writer and undo behind it), `docs/REVIEW-PROCESS.md`. The reads write nothing: the effective
decision, drift and staleness are computed per request. The writes record who acted from the
session, never from the payload, and each is one batch a person can undo as one.
"""

from __future__ import annotations

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

from app.db import get_db
from app.models.schemas import (BrandCardIn, BrandCardOut, BrandIndexOut, CollectionRulesOut, FoldingCountOut, FoldingPageOut, GuidelinesOut, PrecedentOut, PrecedentOverturnIn, PrecedentOverturnOut, ReviewApproveIn, ReviewApproveOut, ReviewBatchDetailOut, ReviewDecisionOut, ReviewHintsOut, ReviewLineTableOut, ReviewNotesOut, ReviewProcessOut, ReviewRowDetailOut, ReviewSheetOut, ReviewSheetSummaryOut, ReviewSplitIn, ReviewSplitOut, ReviewUndoIn, ReviewUndoOut, ReviewWordListOut, RowListingsOut)
from app.services import audit_log, identity
from app.services import proposals as review
from app.services.decisions.writer import FIELDS, Refused

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


def _refused(exc: Refused) -> HTTPException:
    code = exc.code
    status = 404 if code.endswith("_NOT_FOUND") or code in ("BRAND_MISSING", "PASS_NOT_FOUND") else 422 if code == "VALUE_INVALID" else 409
    return HTTPException(status_code=status, detail={"error_code": code, "summary": exc.summary})


@router.get("/sheets", response_model=list[ReviewSheetSummaryOut])
def read_sheets(db: Session = Depends(get_db)) -> list[dict]:
    return review.sheets(db)


@router.get("/sheets/{brand_slug}", response_model=ReviewSheetOut)
def read_sheet(brand_slug: str, pass_name: str | None = Query(default=None, alias="pass", max_length=120),
               db: Session = Depends(get_db)) -> dict:
    try:
        return review.sheet(db, brand_slug, pass_name)
    except Refused as exc:
        raise _refused(exc) from exc


@router.get("/rows/{uid}", response_model=ReviewRowDetailOut)
def read_row_detail(uid: str, db: Session = Depends(get_db)) -> dict:
    """One suggestion opened up. A read: it writes nothing and decides nothing."""
    from app.services import review_detail

    data = review_detail.read(db, uid)
    if not data:
        raise HTTPException(status_code=404, detail={"error_code": "ENTITY_NOT_FOUND", "summary": f"No proposal {uid}."})
    return data


@router.get("/listings", response_model=RowListingsOut)
def read_row_listings(line: str | None = Query(None), variant: str | None = Query(None),
                      limit: int = Query(5, ge=1, le=200), offset: int = Query(0, ge=0),
                      db: Session = Depends(get_db)) -> dict:
    """The listings under one product line or one product variant, for an opened row. A read."""
    from app.services import review_detail

    return review_detail.listings_under(db, line_uid=line, variant_uid=variant, limit=limit, offset=offset)


@router.get("/collection-rules", response_model=CollectionRulesOut)
def read_collection_rules(db: Session = Depends(get_db)) -> dict:
    """The rules cleaning the data right now, read from the code that does it. A read."""
    from app.services import collection_rules

    return collection_rules.read(db)


@router.get("/guidelines", response_model=GuidelinesOut)
def read_guidelines() -> dict:
    """The guidelines a review pass follows, read from the document a pass also reads. A read."""
    from app.services import guidelines

    return guidelines.read()


@router.get("/precedents", response_model=list[PrecedentOut])
def read_precedents(db: Session = Depends(get_db)) -> list[dict]:
    """The register: every kind of judgement a person has answered, with its counts (K12.3). A read."""
    from app.services import precedents

    return precedents.describe(db)


@router.post("/precedents/{slug}/overturn", response_model=PrecedentOverturnOut)
def overturn_precedent(slug: str, payload: PrecedentOverturnIn, request: Request, db: Session = Depends(get_db)) -> dict:
    """One undoable decision on the precedent; regroups nothing; the next pass proposes against
    the decisions made under it (the plan's B6)."""
    from app.services import precedents

    who = identity.actor(request)
    try:
        result = precedents.overturn(db, slug, note=payload.note, by=who.id, force=payload.force)
    except Refused as exc:
        db.rollback()
        raise _refused(exc) from exc
    audit_log.record("review.precedent_overturn", entity_type="precedent", entity_key=slug,
                     detail={"batch": result["batch_uid"], "note": payload.note})
    return result


@router.get("/brands", response_model=BrandIndexOut)
def read_brand_index(q: str | None = Query(default=None, max_length=120), limit: int = Query(default=100, ge=1, le=500),
                     offset: int = Query(default=0, ge=0), has_brand_decision: bool = Query(default=False),
                     has_suggestions: bool = Query(default=False), db: Session = Depends(get_db)) -> dict:
    """Every brand with every count a column could show, searchable, filterable and paged. A read."""
    return review.brand_index(db, q=q, limit=limit, offset=offset,
                              has_brand_decision=has_brand_decision, has_suggestions=has_suggestions)


@router.get("/brands/{brand_slug}/card", response_model=BrandCardOut)
def read_brand_card(brand_slug: str, db: Session = Depends(get_db)) -> dict:
    """The brand's own card at the top of its review page: display name, the spellings the shops
    sent with their counts, the page's address and its status. A read."""
    from app.services import brands as brands_service

    data = brands_service.card(db, brand_slug)
    if not data:
        raise HTTPException(status_code=404, detail={"error_code": "BRAND_MISSING", "summary": f"No brand {brand_slug!r}."})
    return data


@router.post("/brands/{brand_slug}/card", response_model=BrandCardOut)
def write_brand_card(brand_slug: str, payload: BrandCardIn, request: Request, db: Session = Depends(get_db)) -> dict:
    """The two things on the card a person owns: the display name and the page status. One batch,
    one undo, each field a decision in the ledger."""
    from app.services import brands as brands_service

    who = identity.actor(request)
    try:
        result = brands_service.set_card(db, brand_slug, name=payload.name, status=payload.status, by=who.id)
    except Refused as exc:
        db.rollback()
        raise _refused(exc) from exc
    audit_log.record("review.brand_card", entity_type="brand", entity_key=brand_slug, detail={"changed": result["changed"]})
    return result


@router.get("/word-lists", response_model=list[ReviewWordListOut])
def read_word_lists(db: Session = Depends(get_db)) -> list[dict]:
    """Every word list, its words, the vertical it applies to and what it folded or proposed
    (Stream K9.6). A read: it writes nothing and offers nothing to press."""
    from app.services import proposal_rules

    return proposal_rules.word_lists(db)
@router.get("/process", response_model=ReviewProcessOut)
def read_process(db: Session = Depends(get_db)) -> dict:
    """`docs/REVIEW-PROCESS.md` as the first tab shows it, with what each pass has waiting
    (Stream K10.2). A read: it writes nothing and changes nothing."""
    from app.services import process_doc

    return {**process_doc.read(), "passes": process_doc.passes(db)}


@router.get("/folding", response_model=list[FoldingCountOut])
def read_folding_counts(db: Session = Depends(get_db)) -> list[dict]:
    """What is waiting to be joined or kept apart, per level (Stream K10.3): the first thing on
    the folding tab, so a person sees where the work is. A read."""
    from app.services import folding

    return folding.counts(db)


@router.get("/folding/{level}", response_model=FoldingPageOut)
def read_folding_rows(level: str, limit: int = Query(default=100, ge=1, le=500),
                      offset: int = Query(default=0, ge=0), db: Session = Depends(get_db)) -> dict:
    """One page of what is waiting at a level. A read: the deciding happens on the brand's sheet
    or the desk, and each row says which."""
    from app.services import folding

    try:
        return folding.rows(db, level, limit=limit, offset=offset)
    except ValueError as exc:
        raise HTTPException(status_code=422, detail={"error_code": "VALUE_INVALID", "summary": str(exc)}) from exc
@router.get("/hints/{brand_slug}", response_model=ReviewHintsOut)
def read_hints(brand_slug: str, rule: str | None = Query(default=None, max_length=60),
               limit: int = Query(default=500, ge=1, le=5000), db: Session = Depends(get_db)) -> dict:
    """What the word lists would have suggested for this brand: what the AI pass reads, and what
    rian no longer has to answer (K11.2). Nothing here is applied and nothing here is a question."""
    try:
        return review.hints(db, brand_slug, rule=rule, limit=limit)
    except Refused as exc:
        raise _refused(exc) from exc


@router.get("/sheets/{brand_slug}/line", response_model=ReviewLineTableOut)
def read_line_table(brand_slug: str, ref: str = Query(max_length=500),
                    pass_name: str | None = Query(default=None, alias="pass", max_length=120),
                    db: Session = Depends(get_db)) -> dict:
    """The product line table a pass emits: the unit of review (K11.5, K11.6)."""
    try:
        return review.line_table(db, brand_slug, ref, pass_name)
    except Refused as exc:
        raise _refused(exc) from exc


@router.get("/notes", response_model=ReviewNotesOut)
def read_notes(brand: str | None = Query(default=None, max_length=160), field: str | None = Query(default=None, max_length=80),
               key: str | None = Query(default=None, max_length=500), limit: int = Query(default=200, ge=1, le=2000),
               db: Session = Depends(get_db)) -> dict:
    """The notes a deferred or rejected question carries: what the next pass must read before it
    asks again (K11.4). Read-only."""
    try:
        return review.deferred_notes(db, brand, field=field, natural_key=key, limit=limit)
    except Refused as exc:
        raise _refused(exc) from exc


@router.get("/batches/{batch_uid}", response_model=ReviewBatchDetailOut)
def read_batch(batch_uid: str, db: Session = Depends(get_db)) -> dict:
    try:
        return review.batch_detail(db, batch_uid)
    except Refused as exc:
        raise _refused(exc) from exc


@router.get("/decisions", response_model=list[ReviewDecisionOut])
def read_decisions(entity: str = Query(pattern=r"^[a-z_]+:[0-9]{1,18}$"), limit: int = Query(default=200, ge=1, le=1000),
                   db: Session = Depends(get_db)) -> list[dict]:
    entity_type, _, entity_id = entity.partition(":")
    if entity_type not in FIELDS:
        raise HTTPException(status_code=422, detail={"error_code": "VALUE_INVALID", "summary": f"No entity type {entity_type!r}."})
    return review.decisions_for(db, entity_type, int(entity_id), limit)


@router.post("/sheets/{brand_slug}/approve", response_model=ReviewApproveOut)
def approve_sheet(brand_slug: str, payload: ReviewApproveIn, request: Request, db: Session = Depends(get_db)) -> dict:
    who = identity.actor(request)
    scope = payload.scope if payload.scope == "all" or payload.scope is None else payload.scope.model_dump(exclude_none=True)
    try:
        result = review.approve(db, brand_slug, payload.pass_name, scope=scope, by=who.id, corrections=payload.corrections,
                                reject=payload.reject, defer=payload.defer, counter=payload.counter, note=payload.note, force=payload.force)
    except Refused as exc:
        db.rollback()
        raise _refused(exc) from exc
    audit_log.record("review.approve", entity_type="brand", entity_key=brand_slug,
                     detail={"pass": payload.pass_name, "batch": result["batch_uid"], "counts": result["counts"]})
    return result


@router.post("/brands/{brand_slug}/split", response_model=ReviewSplitOut)
def split_brand(brand_slug: str, payload: ReviewSplitIn, request: Request, db: Session = Depends(get_db)) -> dict:
    """The split a person runs from a rejected fold proposal (K9.4). One click from the sheet,
    one batch, undoable like any other; a rejection never runs it by itself."""
    who = identity.actor(request)
    try:
        result = review.run_split(db, brand_slug, payload.spellings, name=payload.name, by=who.id, force=payload.force)
    except Refused as exc:
        db.rollback()
        raise _refused(exc) from exc
    audit_log.record("review.split_brand", entity_type="brand", entity_key=brand_slug,
                     detail={"to": result["brand_slug"], "moved": result["product_variants_moved"], "batch": result["batch_uid"]})
    return result


@router.post("/batches/{batch_uid}/undo", response_model=ReviewUndoOut)
def undo_batch(batch_uid: str, payload: ReviewUndoIn, request: Request, db: Session = Depends(get_db)) -> dict:
    who = identity.actor(request)
    try:
        result = review.undo_batch(db, batch_uid, who.id, payload.reason, force=payload.force)
    except Refused as exc:
        db.rollback()
        raise _refused(exc) from exc
    audit_log.record("review.undo_batch", entity_type="batch", entity_key=batch_uid,
                     detail={"undo_batch": result["undo_batch_uid"], "reason": payload.reason})
    return result


@router.post("/decisions/{decision_id}/undo", response_model=ReviewUndoOut)
def undo_decision(decision_id: int, payload: ReviewUndoIn, request: Request, db: Session = Depends(get_db)) -> dict:
    who = identity.actor(request)
    try:
        result = review.undo_decision(db, decision_id, who.id, payload.reason, force=payload.force)
    except Refused as exc:
        db.rollback()
        raise _refused(exc) from exc
    audit_log.record("review.undo_decision", entity_type="decision", entity_key=str(decision_id),
                     detail={"undo_batch": result["undo_batch_uid"], "reason": payload.reason})
    return result
