"""What we cover, what we do not, and how good each source's data is.

Being straight about the gaps is a feature: several of the world's busiest
airports publish no prices at all, and a shopper (or a client) is better served
by seeing that plainly than by wondering why a search came back empty.
"""

from sqlalchemy import distinct, func, select
from sqlalchemy.orm import Session

from app.models import Listing, Location, PriceObservation, Product, Retailer
from app.models.schemas import CoverageLocation, UncoveredAirport
from app.services import airport_guides
from app.services.catalog_queries import publishable
from app.services.urls import airport_path

# Airports we cannot collect, and why. Kept here rather than in the page so the
# reason travels with the data and stays reviewable.
UNCOVERED: list[UncoveredAirport] = [
    UncoveredAirport(
        name="Doha Hamad", iata="DOH", operator="Qatar Duty Free",
        reason="No public product catalogue at all; the site's checkout sells raffle tickets.",
    ),
    UncoveredAirport(
        name="Amsterdam Schiphol", iata="AMS", operator="Schiphol / See Buy Fly",
        reason="Schiphol's own site carries a product catalogue, but its bot protection turns automated readers away, and we treat a locked door as a no. One for a conversation, not a crawl.",
    ),
    UncoveredAirport(
        name="Istanbul", iata="IST", operator="Unifree / Heinemann",
        reason="Publishes how large its catalogue is, but withholds the products.",
    ),
    UncoveredAirport(
        name="Seoul Incheon", iata="ICN", operator="Lotte / Shilla",
        reason="Lotte's crawling rules ask everyone but the big search engines to stay out, and we respect that. Shilla's store is open to crawlers and shows prices, so a collector there is under assessment.",
    ),
    UncoveredAirport(
        name="Singapore Changi (iShopChangi)", iata="SIN", operator="Changi Airport Group",
        reason="Welcomes crawlers (permissive robots, full product sitemaps with 30,000+ items including wine, spirits and beauty), but every page is built in the browser, so there is nothing in the page itself to read. A rendering step or a partnership would open one of the world's biggest airports.",
    ),
    UncoveredAirport(
        name="Duty Free Americas (a second operator at JFK, Miami and other US airports)",
        iata="MIA", operator="Duty Free Americas",
        reason="Their robots.txt asks crawlers not to browse any part of the site, so we do not. The Avolta shops at the same airports are open to us, which is how JFK is covered at all.",
    ),
    UncoveredAirport(
        name="Heinemann: its catalogue, Sydney, Keflavik and the border shop", iata="FRA",
        operator="Gebr. Heinemann",
        reason="Every site on Heinemann's platform publishes a crawling rule that covers its product listings, so we do not read them. This is the clearest partnership ask in the project: their catalogue carries the barcodes that make cross-shop matching work.",
    ),
]

# Airports we could add without new code — the collector already handles their
# platform and their robots.txt permits it. Kept separate from UNCOVERED because
# "not configured yet" is a very different thing from "cannot be collected".
AVAILABLE_NOT_YET_ADDED = (
    "Avolta runs one platform across 168 airports and we collect a handful of them. "
    "The rest are a configuration list rather than new work."
)



def coverage(db: Session) -> list[CoverageLocation]:
    # Hidden locations are absent here too: the coverage page describes the
    # site the visitor is on, and every card links to the airport's page, which
    # answers 404 for a shop verify has blocked. The fuller collected-but-not-
    # shown story belongs to the client review page, not this one.
    rows = db.execute(
        select(
            Location,
            Retailer.name,
            func.count(distinct(Listing.product_id)).label("products"),
            func.count(distinct(Product.id))
            .filter(Product.gtin.isnot(None))
            .label("with_barcode"),
            func.max(PriceObservation.observed_at).label("last_collected_at"),
        )
        .join(Retailer, Retailer.id == Location.retailer_id)
        .outerjoin(Listing, Listing.location_id == Location.id)
        .outerjoin(Product, Product.id == Listing.product_id)
        .outerjoin(PriceObservation, PriceObservation.listing_id == Listing.id)
        .where(publishable(db))
        .group_by(Location.id, Retailer.name)
        .order_by(func.count(distinct(Listing.product_id)).desc())
    ).all()

    return [
        CoverageLocation(
            code=location.code,
            name=location.name,
            iata=location.iata,
            path=(
                airport_path(location.iata, location.city, location.name)
                if location.iata and not location.is_catalogue_only else None
            ),
            city=location.city,
            country=location.country,
            currency=location.currency,
            retailer_name=retailer_name,
            is_catalogue_only=location.is_catalogue_only,
            products=products or 0,
            with_barcode=with_barcode or 0,
            last_collected_at=last_collected_at,
            has_guide=airport_guides.guide_for(location.iata) is not None,
        )
        for location, retailer_name, products, with_barcode, last_collected_at in rows
    ]
