"""Orders: create (idempotent), list (cursor), read (ETag), edit (If-Match), cancel, status, live events."""
from collections.abc import AsyncIterable, AsyncIterator
from typing import Annotated

from fastapi import APIRouter, Depends, Header, Query, Request, Response
from fastapi.responses import JSONResponse
from fastapi.sse import EventSourceResponse, ServerSentEvent
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm.exc import StaleDataError

from ..db import get_session, utcnow
from ..errors import ApiError
from ..ratelimit import per_user_limit
from ..schemas import OrderIn, OrderOut, OrderPage, OrderPatch, OrderStatus, Problem, StatusChange
from ..security import Admin, CurrentUser, Principal
from ..services import (TERMINAL, change_status, check_if_match, create_order, etag_matches, etag_of,
                        list_orders, load_order, status_event)

router = APIRouter(prefix="/v1/orders", tags=["orders"], dependencies=[Depends(per_user_limit)],
                   responses={401: {"model": Problem}, 429: {"model": Problem}})
Session = Annotated[AsyncSession, Depends(get_session)]
NOT_FOUND = {404: {"model": Problem, "description": "No such order, or not yours"}}
PRECONDITIONS = {412: {"model": Problem}, 428: {"model": Problem}}
CONFLICT = {409: {"model": Problem, "description": "Not allowed in the order's current status"}}
IdempotencyKey = Annotated[str | None, Header(max_length=100, description="Unique per logical request; retries reuse it")]
IfMatch = Annotated[str | None, Header()]


async def _commit(session: AsyncSession) -> None:
    try:
        await session.commit()
    except StaleDataError:          # changed between our read and our write: the database caught it
        await session.rollback()
        raise ApiError(412, "the order was changed concurrently; GET it again and retry",
                       type_="precondition-failed") from None


def _publish(request: Request, order) -> None:
    request.state.events.publish(order.id, status_event(order))


@router.post("", status_code=201, responses={400: {"model": Problem}, 422: {"model": Problem}})
async def create(body: OrderIn, request: Request, session: Session, principal: CurrentUser,
                 idempotency_key: IdempotencyKey = None) -> OrderOut:
    if not idempotency_key:
        raise ApiError(400, "send an Idempotency-Key header so retries cannot create duplicate orders",
                       type_="idempotency-key-required")
    status, payload, replayed = await create_order(session, principal, body, idempotency_key)
    headers = {"Location": f"/v1/orders/{payload['id']}", "ETag": f'"{payload["id"]}.{payload["version"]}"'}
    if replayed:
        headers["Idempotent-Replayed"] = "true"
    return JSONResponse(payload, status_code=status, headers=headers)      # validated already: stored OrderOut


@router.get("")
async def list_(session: Session, principal: CurrentUser,
                status: Annotated[OrderStatus | None, Query()] = None,
                limit: Annotated[int, Query(ge=1, le=100)] = 20,
                cursor: Annotated[str | None, Query(max_length=200)] = None) -> OrderPage:
    orders, next_cursor = await list_orders(session, principal, status=status, limit=limit, cursor=cursor)
    return OrderPage(items=orders, next_cursor=next_cursor)


@router.get("/{order_id}", responses=NOT_FOUND | {304: {"description": "Not modified"}})
async def read(order_id: int, response: Response, session: Session, principal: CurrentUser,
               if_none_match: Annotated[str | None, Header()] = None) -> OrderOut:
    order = await load_order(session, order_id, principal)
    etag = etag_of(order)
    if if_none_match and etag_matches(if_none_match, etag):
        return Response(status_code=304, headers={"ETag": etag})
    response.headers["ETag"] = etag
    return order


@router.patch("/{order_id}", responses=NOT_FOUND | PRECONDITIONS | CONFLICT)
async def edit(order_id: int, patch: OrderPatch, response: Response, session: Session,
               principal: CurrentUser, if_match: IfMatch = None) -> OrderOut:
    order = await load_order(session, order_id, principal)
    check_if_match(if_match, order, required=True)
    if order.status != "pending":
        raise ApiError(409, "only pending orders can be edited", type_="invalid-state")
    for field, value in patch.model_dump(exclude_unset=True).items():     # only what was sent
        setattr(order, field, value)
    order.updated_at = utcnow()
    await _commit(session)
    response.headers["ETag"] = etag_of(order)
    return order


@router.post("/{order_id}/cancel", responses=NOT_FOUND | PRECONDITIONS | CONFLICT)
async def cancel(order_id: int, request: Request, response: Response, session: Session,
                 principal: CurrentUser, if_match: IfMatch = None) -> OrderOut:
    order = await load_order(session, order_id, principal)
    check_if_match(if_match, order, required=False)
    if not principal.is_admin and order.status != "pending":
        raise ApiError(409, "customers can cancel only pending orders", type_="invalid-state")
    await change_status(session, order, "cancelled")
    await _commit(session)
    _publish(request, order)
    response.headers["ETag"] = etag_of(order)
    return order


@router.post("/{order_id}/status", responses=NOT_FOUND | PRECONDITIONS | CONFLICT | {403: {"model": Problem}})
async def set_status(order_id: int, change: StatusChange, request: Request, response: Response,
                     session: Session, admin: Admin, if_match: IfMatch = None) -> OrderOut:
    order = await load_order(session, order_id, admin)
    check_if_match(if_match, order, required=True)
    await change_status(session, order, change.status)
    await _commit(session)
    _publish(request, order)
    response.headers["ETag"] = etag_of(order)
    return order


async def _subscription(order_id: int, request: Request,
                        principal: CurrentUser) -> AsyncIterator[tuple[dict, object]]:
    """Authorize and subscribe *before* the stream starts, with a short-lived session.

    The stream may stay open for minutes; holding a request-scoped session that long would
    pin a pooled database connection per viewer.
    """
    with request.state.events.subscribe(order_id) as queue:     # subscribe first: no update can slip by
        async with request.state.sessions() as session:
            order = await load_order(session, order_id, principal)
            first = status_event(order)
        yield first, queue


@router.get("/{order_id}/events", response_class=EventSourceResponse, responses=NOT_FOUND)
async def events(subscription: Annotated[tuple[dict, object], Depends(_subscription)]
                 ) -> AsyncIterable[ServerSentEvent]:
    """Server-Sent Events: the current status now, then every change, until the order is finished."""
    event, queue = subscription
    while True:
        yield ServerSentEvent(data=event, event="status", id=str(event["version"]))
        if event["status"] in TERMINAL:
            return
        event = await queue.get()
