from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi.responses import RedirectResponse
from sqlalchemy import func, select
from sqlalchemy.orm import Session

from app.db import get_db
from app.models import Award, Listing, Location, PriceObservation, Product, Retailer
from app.models.hubs import AirportDetail, AirportSummary, BrandDetail, BrandSummary, DatasetFacts
from app.models.schemas import ProductDetail, ProductPage, ProductSuggestion, ProductSummary, StatsOut
from app.services import catalog_queries

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


@router.get("/products", response_model=ProductPage)
def list_products(
    q: str | None = Query(default=None, max_length=120),
    category: str | None = Query(default=None, max_length=80),
    family: str | None = Query(
        default=None, max_length=40,
        description="A category family (taxonomy's vertical: liquor, beauty); every shown category in it",
    ),
    multi_only: bool = False,
    awarded_only: bool = False,
    exclusives_only: bool = False,
    with_image_only: bool = False,
    at: list[str] = Query(
        default=[], max_length=12,
        description="Airport codes the shopper will pass through",
    ),
    sort: str = Query(default="featured", pattern="^(featured|compared|price|name)$"),
    limit: int = Query(default=24, ge=1, le=100),
    offset: int = Query(default=0, ge=0),
    db: Session = Depends(get_db),
) -> ProductPage:
    total, items = catalog_queries.list_products(
        db,
        query=q,
        category=category,
        family=family,
        multi_only=multi_only,
        awarded_only=awarded_only,
        exclusives_only=exclusives_only,
        with_image_only=with_image_only,
        at_codes=[c for c in at if c] or None,
        sort=sort,
        limit=limit,
        offset=offset,
    )
    return ProductPage(total=total, limit=limit, offset=offset, items=items)


@router.get("/search/suggest", response_model=list[ProductSuggestion])
def search_suggest(
    q: str = Query(min_length=1, max_length=120),
    limit: int = Query(default=6, ge=1, le=10),
    db: Session = Depends(get_db),
) -> list[ProductSuggestion]:
    """Products for the header search as a shopper types. Airports and brands are
    matched in the browser from the lists it already holds (/api/airports,
    /api/brands); only products are too many to send."""
    return catalog_queries.suggest_products(db, q, limit)


@router.get("/products/featured-savings", response_model=list[ProductSummary])
def featured_savings(
    at: list[str] = Query(
        default=[], max_length=12,
        description="Airport codes the shopper will pass through",
    ),
    total: int = Query(default=8, ge=1, le=12),
    db: Session = Depends(get_db),
) -> list[ProductSummary]:
    """The home page's lead cards, selected by the agreed featuring rules."""
    return catalog_queries.featured_savings(
        db, at_codes=[c for c in at if c] or None, total=total
    )


def _merged_redirect(request: Request, product_id: int, resolved: int) -> RedirectResponse | None:
    """A merged product's API answers live under the survivor's id, by 301, so a
    cached link to the old id keeps working and clients learn the new one."""
    if resolved == product_id:
        return None
    target = request.url.path.replace(f"/products/{product_id}", f"/products/{resolved}", 1)
    if request.url.query:
        target += f"?{request.url.query}"
    return RedirectResponse(target, status_code=301)


@router.get("/products/{product_id}", response_model=ProductDetail)
def get_product(
    product_id: int,
    request: Request,
    at: list[str] = Query(
        default=[], max_length=12,
        description="Airport codes the shopper will pass through (orders the medals)",
    ),
    db: Session = Depends(get_db),
):
    resolved = catalog_queries.resolve_product_id(db, product_id)
    if redirect := _merged_redirect(request, product_id, resolved):
        return redirect
    product = catalog_queries.get_product(db, resolved, at_codes=[c for c in at if c] or None)
    if product is None:
        raise HTTPException(status_code=404, detail={"error_code": "PRODUCT_NOT_FOUND"})
    return product


@router.get("/products/{product_id}/similar", response_model=list[ProductSummary])
def similar(
    product_id: int,
    request: Request,
    limit: int = Query(default=8, ge=1, le=24),
    at: list[str] = Query(
        default=[], max_length=12,
        description="Airport codes the shopper will pass through",
    ),
    db: Session = Depends(get_db),
):
    resolved = catalog_queries.resolve_product_id(db, product_id)
    if redirect := _merged_redirect(request, product_id, resolved):
        return redirect
    return catalog_queries.similar_products(
        db, resolved, limit=limit, at_codes=[c for c in at if c] or None
    )


@router.get("/airports", response_model=list[AirportSummary])
def list_airports(db: Session = Depends(get_db)) -> list[AirportSummary]:
    """Every airport that has a page: visible storefronts we hold stock for."""
    return catalog_queries.list_airports(db)


@router.get("/airports/{iata}", response_model=AirportDetail)
def get_airport(
    iata: str,
    category: str | None = Query(default=None, max_length=80),
    sort: str = Query(default="featured", pattern="^(featured|compared|price|name)$"),
    limit: int = Query(default=24, ge=1, le=100),
    offset: int = Query(default=0, ge=0),
    db: Session = Depends(get_db),
) -> AirportDetail:
    """The airport page's data: the same object the server renders the page from."""
    if len(iata) != 3 or not iata.isalpha():
        raise HTTPException(status_code=404, detail={"error_code": "AIRPORT_NOT_FOUND"})
    detail = catalog_queries.airport_detail(
        db, iata, category=category, sort=sort, limit=limit, offset=offset
    )
    if detail is None:
        raise HTTPException(status_code=404, detail={"error_code": "AIRPORT_NOT_FOUND"})
    return detail


@router.get("/brands", response_model=list[BrandSummary])
def list_brands(db: Session = Depends(get_db)) -> list[BrandSummary]:
    """Every brand with a page: canonical rows with enough published products."""
    return catalog_queries.list_brands(db)


@router.get("/brands/{slug}", response_model=BrandDetail)
def get_brand(
    slug: str,
    category: str | None = Query(default=None, max_length=80),
    sort: str = Query(default="featured", pattern="^(featured|compared|price|name)$"),
    limit: int = Query(default=24, ge=1, le=100),
    offset: int = Query(default=0, ge=0),
    db: Session = Depends(get_db),
) -> BrandDetail:
    """The brand page's data: the same object the server renders the page from.
    An alias slug answers with its house (the page route 301s; the API just answers)."""
    detail = catalog_queries.brand_detail(
        db, slug.lower(), category=category, sort=sort, limit=limit, offset=offset
    )
    if detail is None:
        raise HTTPException(status_code=404, detail={"error_code": "BRAND_NOT_FOUND"})
    return detail


@router.get("/dataset", response_model=DatasetFacts)
def dataset(db: Session = Depends(get_db)) -> DatasetFacts:
    """The public data page's figures: the same object the server renders it from."""
    return catalog_queries.dataset_facts(db)


@router.get("/stats", response_model=StatsOut)
def stats(db: Session = Depends(get_db)) -> StatsOut:
    # Every figure is scoped to the shops the site shows (visible, not blocked),
    # so the home page's numbers always describe the site the visitor is looking at.
    multi = (
        select(Listing.product_id)
        .join(Location, Location.id == Listing.location_id)
        .where(Location.is_catalogue_only.is_(False), catalog_queries.publishable(db))
        .group_by(Listing.product_id)
        .having(func.count(func.distinct(Listing.location_id)) > 1)
        .subquery()
    )
    visible_listing = (
        select(Listing.id, Listing.product_id, Listing.location_id)
        .join(Location, Location.id == Listing.location_id)
        .where(catalog_queries.publishable(db))
        .subquery()
    )
    return StatsOut(
        products=db.scalar(
            select(func.count(func.distinct(visible_listing.c.product_id)))
        ) or 0,
        products_multi_location=db.scalar(select(func.count()).select_from(multi)) or 0,
        # Only shops we actually hold stock for. A configured-but-empty store
        # (one whose site we cannot read yet) is coverage we do not have, and
        # counting it would overstate the catalogue on the home page.
        locations=db.scalar(
            select(func.count(func.distinct(visible_listing.c.location_id)))
        ) or 0,
        retailers=db.scalar(select(func.count(Retailer.id))) or 0,
        observations=db.scalar(
            select(func.count(PriceObservation.id)).where(
                PriceObservation.listing_id.in_(select(visible_listing.c.id))
            )
        ) or 0,
        awards=db.scalar(
            select(func.count(Award.id)).where(
                Award.product_id.in_(select(visible_listing.c.product_id))
            )
        ) or 0,
        last_collected_at=db.scalar(
            select(func.max(PriceObservation.observed_at)).where(
                PriceObservation.listing_id.in_(select(visible_listing.c.id))
            )
        ),
        categories=catalog_queries.category_counts(db),
    )
