"""Playtomic tenants extractor — venue listings via unauthenticated API.

Paginates through the global tenant list (sorted by UUID) using the `page`
parameter.  Deduplicates on tenant_id and writes a gzipped JSONL file to the
landing zone (one tenant object per line).

API notes (discovered 2026-02):
  - bbox params (min_latitude etc.) are silently ignored by the API
  - `offset` param is ignored; `page` param works correctly
  - `size=100` is the maximum effective page size
  - ~14K venues globally as of Feb 2026

Parallel mode: when proxy tiers are configured, fires BATCH_SIZE pages
concurrently. Each page gets its own fresh session + proxy from the tiered
cycler. On failure the cycler escalates through free → datacenter →
residential tiers. Without proxies, falls back to single-threaded with
THROTTLE_SECONDS between pages.

Rate: 1 req / 2 s per IP (see docs/data-sources-inventory.md §1.2).

Landing: {LANDING_DIR}/playtomic/{year}/{month}/tenants.jsonl.gz
"""

import os
import sqlite3
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import UTC, datetime
from pathlib import Path

import niquests

from ._shared import HTTP_TIMEOUT_SECONDS, run_extractor, setup_logging, ua_for_proxy
from .proxy import load_proxy_tiers, make_tiered_cycler
from .utils import landing_path, write_jsonl_atomic

logger = setup_logging("padelnomics.extract.playtomic_tenants")

EXTRACTOR_NAME = "playtomic_tenants"
PLAYTOMIC_TENANTS_URL = "https://api.playtomic.io/v1/tenants"

THROTTLE_SECONDS = 2
PAGE_SIZE = 100
MAX_PAGES = 500  # safety bound — ~50K venues max, well above current ~14K
BATCH_SIZE = 20  # concurrent pages per batch (fixed, independent of proxy count)
CIRCUIT_BREAKER_THRESHOLD = int(os.environ.get("CIRCUIT_BREAKER_THRESHOLD") or "10")
MAX_PAGE_ATTEMPTS = 5  # max retries per individual page before giving up


def _fetch_one_page(proxy_url: str | None, page: int) -> tuple[int, list[dict]]:
    """Fetch a single page using a fresh session with the given proxy.

    Returns (page, tenants_list). Raises on HTTP error.
    """
    s = niquests.Session()
    s.headers["User-Agent"] = ua_for_proxy(proxy_url)
    if proxy_url:
        s.proxies = {"http": proxy_url, "https": proxy_url}
    params = {"sport_ids": "PADEL", "size": PAGE_SIZE, "page": page}
    resp = s.get(PLAYTOMIC_TENANTS_URL, params=params, timeout=HTTP_TIMEOUT_SECONDS)
    resp.raise_for_status()
    tenants = resp.json()
    assert isinstance(tenants, list), f"Expected list from Playtomic API, got {type(tenants)}"
    return (page, tenants)


def _fetch_page_via_cycler(cycler: dict, page: int) -> tuple[int, list[dict]]:
    """Fetch a single page, retrying across proxy tiers via the circuit breaker.

    On each attempt, pulls the next proxy from the active tier. Records
    success/failure so the circuit breaker can escalate tiers. Raises
    RuntimeError if all tiers are exhausted or MAX_PAGE_ATTEMPTS is exceeded.
    """
    last_exc: Exception | None = None
    for attempt in range(MAX_PAGE_ATTEMPTS):
        proxy_url = cycler["next_proxy"]()
        if proxy_url is None:  # all tiers exhausted
            raise RuntimeError(f"All proxy tiers exhausted fetching page {page}")
        try:
            result = _fetch_one_page(proxy_url, page)
            cycler["record_success"](proxy_url)
            return result
        except Exception as exc:
            last_exc = exc
            logger.warning(
                "Page %d attempt %d/%d failed (proxy=%s): %s",
                page,
                attempt + 1,
                MAX_PAGE_ATTEMPTS,
                proxy_url,
                exc,
            )
            cycler["record_failure"](proxy_url)
            if cycler["is_exhausted"]():
                raise RuntimeError(f"All proxy tiers exhausted fetching page {page}") from exc
    raise RuntimeError(f"Page {page} failed after {MAX_PAGE_ATTEMPTS} attempts") from last_exc


def _fetch_pages_parallel(pages: list[int], cycler: dict) -> list[tuple[int, list[dict]]]:
    """Fetch multiple pages concurrently using the tiered cycler.

    Returns [(page_num, tenants_list), ...]. Raises if any page exhausts all tiers.
    """
    with ThreadPoolExecutor(max_workers=len(pages)) as pool:
        futures = [pool.submit(_fetch_page_via_cycler, cycler, p) for p in pages]
        return [f.result() for f in as_completed(futures)]


def extract(
    landing_dir: Path,
    year_month: str,  # noqa: ARG001 — unused; tenants uses daily partition instead
    conn: sqlite3.Connection,
    session: niquests.Session,
) -> dict:
    """Fetch all Playtomic venues via global pagination. Returns run metrics.

    Partitioned by day (e.g. 2026/03/01) so each daily run produces a
    fresh file. _load_tenant_ids() in playtomic_availability globs across all
    partitions and picks the most recent one.
    """
    today = datetime.now(UTC)
    year, month, day = today.strftime("%Y"), today.strftime("%m"), today.strftime("%d")
    dest_dir = landing_path(landing_dir, "playtomic", year, month, day)
    dest = dest_dir / "tenants.jsonl.gz"
    if dest.exists():
        logger.info("Already have tenants for %s/%s/%s — skipping", year, month, day)
        return {"files_written": 0, "files_skipped": 1, "bytes_written": 0}

    tiers = load_proxy_tiers()
    cycler = make_tiered_cycler(tiers, CIRCUIT_BREAKER_THRESHOLD) if tiers else None
    batch_size = BATCH_SIZE if cycler else 1

    if cycler:
        logger.info(
            "Parallel mode: %d pages/batch, %d tier(s), threshold=%d",
            batch_size,
            cycler["tier_count"](),
            CIRCUIT_BREAKER_THRESHOLD,
        )
    else:
        logger.info("Serial mode: 1 page at a time (no proxies)")

    all_tenants: list[dict] = []
    seen_ids: set[str] = set()
    page = 0
    done = False

    while not done and page < MAX_PAGES:
        if cycler and cycler["is_exhausted"]():
            logger.error(
                "All proxy tiers exhausted — stopping at page %d (%d venues collected)",
                page,
                len(all_tenants),
            )
            break

        batch_end = min(page + batch_size, MAX_PAGES)
        pages_to_fetch = list(range(page, batch_end))

        if cycler and len(pages_to_fetch) > 1:
            logger.info(
                "Fetching pages %d-%d in parallel (%d workers, total so far: %d)",
                page,
                batch_end - 1,
                len(pages_to_fetch),
                len(all_tenants),
            )
            try:
                results = _fetch_pages_parallel(pages_to_fetch, cycler)
            except RuntimeError:
                logger.error(
                    "Proxy tiers exhausted mid-batch — writing partial results (%d venues)",
                    len(all_tenants),
                )
                break
        else:
            # Serial: reuse the shared session, throttle between pages
            page_num = pages_to_fetch[0]
            logger.info("GET page=%d (total so far: %d)", page_num, len(all_tenants))
            params = {"sport_ids": "PADEL", "size": PAGE_SIZE, "page": page_num}
            resp = session.get(PLAYTOMIC_TENANTS_URL, params=params, timeout=HTTP_TIMEOUT_SECONDS)
            resp.raise_for_status()
            tenants = resp.json()
            assert isinstance(tenants, list), (
                f"Expected list from Playtomic API, got {type(tenants)}"
            )
            results = [(page_num, tenants)]

        # Process pages in order so done-detection on < PAGE_SIZE is deterministic
        for p, tenants in sorted(results):
            new_count = 0
            for tenant in tenants:
                tid = tenant.get("tenant_id") or tenant.get("id")
                if tid and tid not in seen_ids:
                    seen_ids.add(tid)
                    all_tenants.append(tenant)
                    new_count += 1

            logger.info(
                "page=%d got=%d new=%d total=%d",
                p,
                len(tenants),
                new_count,
                len(all_tenants),
            )

            # Last page — fewer than PAGE_SIZE results means we've exhausted the list
            if len(tenants) < PAGE_SIZE:
                done = True
                break

        page = batch_end
        if not cycler:
            time.sleep(THROTTLE_SECONDS)

    # Write each tenant as a JSONL line, then compress atomically
    bytes_written = write_jsonl_atomic(dest, all_tenants)
    logger.info("%d unique venues -> %s", len(all_tenants), dest)

    return {
        "files_written": 1,
        "files_skipped": 0,
        "bytes_written": bytes_written,
        "cursor_value": f"{year}/{month}/{day}",
    }


def main() -> None:
    run_extractor(EXTRACTOR_NAME, extract)


if __name__ == "__main__":
    main()
