"""Gebr. Heinemann -- global online catalogue only.

Deliberate scope limit. Heinemann's robots.txt permits the global catalogue but
disallows every per-airport storefront path (/*/fra/, /*/vie/ and the rest), and
blocks a named price-monitoring bot outright. Their intent about per-airport
price monitoring is unambiguous, so we take the global catalogue -- which is what
carries the barcodes -- and treat per-airport pricing as something to ask for,
not something to take.

This source earns its place because it is the product-identity spine: it
publishes GTIN barcodes, which turn cross-retailer matching into a join.
"""

from collections.abc import Iterator
from urllib.parse import urlencode

from app.services.collectors.base import Collector, LocationSpec, RawListing, facts_only, report_skip
from app.services.collectors.fetch import SourceBlocked, fetch_json
from app.services.collectors.robots import check_allowed
from app.services.normalize import clean_gtin, parse_abv, parse_size_ml

BASE = "https://www.heinemann-shop.com"
SEARCH_PATH = "/en/global/search/results"
PARSER_VERSION = "heinemann/2026-09-04"
ALCOHOL_QUERY = ":relevance:allCategories:cat_5000"
LOCATION_CODE = "HEINEMANN-GLOBAL"

_VOLUME_UNITS = {"LTR": 1000.0, "L": 1000.0, "MLT": 1.0, "ML": 1.0, "CLT": 10.0}

# typeOfGoods mixes real categories with packaging words ("Bottle", "Gift Box"),
# so packaging is dropped rather than shown to a shopper as a category.
_PACKAGING_WORDS = {
    "bottle", "gift box", "gift set", "gift pack", "tin",
    "carton", "case", "can", "tube", "miniature",
}


def _category(item: dict) -> str | None:
    raw = (item.get("typeOfGoods") or "").strip()
    if not raw or raw.lower() in _PACKAGING_WORDS:
        return None
    return raw


def _size_from_content_unit(item: dict) -> int | None:
    """Read a bottle size from Heinemann's structured content-unit field."""
    content = item.get("contentUnit") or {}
    unit = ((content.get("unit") or {}).get("code") or "").upper()
    quantity = content.get("quantity")
    factor = _VOLUME_UNITS.get(unit)
    if factor and isinstance(quantity, int | float) and quantity > 0:
        millilitres = round(float(quantity) * factor)
        if 10 <= millilitres <= 20000:
            return millilitres
    return None


class HeinemannGlobal(Collector):
    slug = "heinemann-global"
    retailer_slug = "heinemann"
    retailer_name = "Gebr. Heinemann"
    operator = "Gebr. Heinemann"
    homepage = "https://www.heinemann-shop.com"
    parser_version = PARSER_VERSION

    def locations(self) -> list[LocationSpec]:
        return [
            LocationSpec(
                code=LOCATION_CODE,
                iata=None,
                name="Heinemann online catalogue",
                city=None,
                country=None,
                currency="EUR",
                is_catalogue_only=True,
            )
        ]

    def _page(self, page: int, delay: float) -> tuple[list[dict], int]:
        query = urlencode({"q": ALCOHOL_QUERY, "page": page})
        payload = fetch_json(f"{BASE}{SEARCH_PATH}?{query}", delay=delay)
        if not isinstance(payload, dict):
            return [], 0
        pagination = payload.get("pagination") or {}
        return payload.get("results") or [], int(pagination.get("numberOfPages") or 0)

    def read_one(self, listing) -> RawListing | None:
        """No per-listing read: the search endpoint is disallowed and product
        pages were never part of what this host permits us."""
        raise SourceBlocked(f"{self.slug}: robots.txt disallows {SEARCH_PATH}; no read path")

    def collect(self, *, limit: int | None = None, delay: float = 1.0) -> Iterator[RawListing]:
        # Re-checked every run: this host once permitted the search path and
        # later added a Disallow covering it. A published Disallow is a refusal,
        # so the run must stop before its first request rather than after.
        delay = check_allowed(BASE, [SEARCH_PATH]).delay_for(delay)
        page, total_pages, produced = 0, 1, 0
        seen: set[str] = set()
        while page < total_pages:
            results, total_pages = self._page(page, delay if page else 0.0)
            if not results:
                return
            for item in results:
                # A product listed under two categories arrives twice in one
                # run; two same-timestamp observations make "latest" ambiguous.
                code = str(item.get("code"))
                if code in seen:
                    continue
                seen.add(code)
                price_block = item.get("price") or {}
                price = price_block.get("value")
                name = (item.get("name") or "").strip()
                if price is None or not name:
                    report_skip("no_price", source_sku=code)
                    continue
                strikethrough = (item.get("strikethroughPrice") or {}).get("value")
                stock_code = ((item.get("stock") or {}).get("stockLevelStatus") or {}).get("code")
                url = item.get("url")
                yield RawListing(
                    source_sku=str(item.get("code")),
                    name=name,
                    brand=item.get("brand") or None,
                    gtin=clean_gtin(item.get("gtin") or item.get("ean")),
                    price=float(price),
                    currency=price_block.get("currencyIso") or "EUR",
                    was_price=float(strikethrough) if strikethrough else None,
                    location_code=LOCATION_CODE,
                    size_ml=_size_from_content_unit(item) or parse_size_ml(name),
                    abv=(
                        float(item["alcoholByVolume"])
                        if item.get("alcoholByVolume")
                        else parse_abv(name)
                    ),
                    feed_categories=[c for c in (_category(item),) if c],
                    is_exclusive=bool(item.get("travelRetailExclusive")),
                    url=f"{BASE}{url}" if url else None,
                    in_stock=(stock_code == "inStock") if stock_code else None,
                    price_type="online",
                    raw=facts_only(item),
                    vertical="liquor",
                )
                produced += 1
                if limit and produced >= limit:
                    return
            page += 1
