"""IndexNow: tell the search engines which pages changed, instead of waiting.

One POST to api.indexnow.org carries up to ten thousand URLs and is shared by
every participating engine (Bing, Yandex, Seznam, Naver, Yep). The key proves
we own the host: it lives in the app's environment (`INDEXNOW_KEY`) and is
served back at `/<key>.txt`; nothing else about it is secret.

What counts as changed comes from the database, not from a list a collector
keeps: a product page changed when it gained an observation, an airport page
when any of its shops did, and the sitemap whenever either happened. So the
command can run after any collection with `--since` the run's start, or on a
schedule with `--hours`, and it never depends on a run having finished cleanly.
Every submission is logged with its count and status; no URL is ever invented.
"""

import json
import logging
import urllib.error
import urllib.request
from collections.abc import Callable
from datetime import datetime

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

from app.models import Listing, Location, PriceObservation, Product
from app.services import catalog_queries
from app.services.urls import airport_path, product_path

logger = logging.getLogger(__name__)

ENDPOINT = "https://api.indexnow.org/indexnow"
#: The protocol's ceiling per request.
BATCH = 10_000
TIMEOUT = 20

Poster = Callable[[str, bytes], int]


def changed_urls(db: Session, since: datetime, base: str) -> list[str]:
    """Absolute URLs of the pages whose data changed after `since`, sitemap
    first, then airports, then products; each once, in a stable order."""
    if not base:
        raise ValueError("IndexNow needs the site origin (PUBLIC_BASE_URL)")
    urls: list[str] = []
    changed_locations = (
        select(Listing.location_id)
        .join(PriceObservation, PriceObservation.listing_id == Listing.id)
        .where(PriceObservation.observed_at > since)
        .distinct()
    )
    airports = catalog_queries.airport_locations(db)
    touched = {row for row in db.scalars(changed_locations)}
    airport_paths = sorted({
        airport_path(loc.iata, loc.city, loc.name)
        for loc in airports if loc.id in touched
    })
    products = db.execute(
        select(Product.id, Product.name)
        .join(Listing, Listing.product_id == Product.id)
        .join(PriceObservation, PriceObservation.listing_id == Listing.id)
        .join(Location, Location.id == Listing.location_id)
        .where(PriceObservation.observed_at > since, catalog_queries.publishable(db))
        .group_by(Product.id)
        .order_by(func.max(PriceObservation.observed_at).desc(), Product.id)
    ).all()
    if airport_paths or products:
        urls.append(f"{base}/sitemap.xml")
    urls.extend(base + path for path in airport_paths)
    urls.extend(base + product_path(pid, name) for pid, name in products)
    return urls


def _post(url: str, body: bytes) -> int:
    request = urllib.request.Request(
        url, data=body, method="POST",
        headers={"Content-Type": "application/json; charset=utf-8"},
    )
    try:
        with urllib.request.urlopen(request, timeout=TIMEOUT) as response:
            return response.status
    except urllib.error.HTTPError as exc:
        return exc.code


def submit(
    urls: list[str], base: str, key: str, post: Poster = _post, endpoint: str = ENDPOINT
) -> list[tuple[int, int]]:
    """POST the URLs in batches; returns (count, HTTP status) per batch.

    200 and 202 mean accepted; 400 is a malformed request, 403 a key the host
    does not serve, 422 URLs that do not belong to the host, 429 too many
    submissions. None of them is retried here: the next run resubmits whatever
    is still newer than its `since`.
    """
    if not key:
        raise ValueError("IndexNow key is not configured (INDEXNOW_KEY)")
    host = base.split("://", 1)[-1].rstrip("/")
    results: list[tuple[int, int]] = []
    for start in range(0, len(urls), BATCH):
        batch = urls[start:start + BATCH]
        payload = {
            "host": host,
            "key": key,
            "keyLocation": f"{base}/{key}.txt",
            "urlList": batch,
        }
        status = post(endpoint, json.dumps(payload).encode())
        logger.info("indexnow: %d urls -> HTTP %d", len(batch), status)
        results.append((len(batch), status))
    return results
