"""
Thin HTTP client for the Baskit Ops ingestion API (/ingest/v1/*).

Handles the X-Service-Key header, the colon routes (products:batch,
runs/{id}:close), chunked batch posting, and simple retry/backoff on
network/5xx errors. Every method returns the parsed JSON body.
"""
from __future__ import annotations

import time
from typing import Any

import requests

import ops_config as cfg


class IngestError(RuntimeError):
    """Raised when the ingestion API returns a non-success response."""


class IngestClient:
    def __init__(
        self,
        base_url: str | None = None,
        service_key: str | None = None,
        timeout: float | None = None,
        verify_tls: bool | None = None,
    ) -> None:
        self.base_url = (base_url or cfg.BASE_URL).rstrip("/")
        self.timeout = cfg.TIMEOUT if timeout is None else timeout
        self.verify_tls = cfg.VERIFY_TLS if verify_tls is None else verify_tls
        key = service_key if service_key is not None else cfg.require_service_key()
        self.session = requests.Session()
        self.session.headers.update({
            "X-Service-Key": key,
            "Content-Type": "application/json",
            "Accept": "application/json",
        })

    # ------------------------------------------------------------------ core
    def _post(self, path: str, payload: dict) -> dict:
        return self._request("POST", path, json=payload)

    def _get(self, path: str) -> dict:
        return self._request("GET", path)

    def _request(self, method: str, path: str, **kwargs: Any) -> dict:
        url = f"{self.base_url}/ingest/v1/{path.lstrip('/')}"
        last_exc: Exception | None = None
        for attempt in range(1, cfg.MAX_RETRIES + 1):
            try:
                resp = self.session.request(
                    method, url, timeout=self.timeout, verify=self.verify_tls, **kwargs
                )
            except requests.RequestException as exc:
                last_exc = exc
                self._sleep(attempt)
                continue

            if resp.status_code >= 500:
                last_exc = IngestError(f"{method} {path} -> HTTP {resp.status_code}: {resp.text[:300]}")
                self._sleep(attempt)
                continue

            try:
                body = resp.json()
            except ValueError:
                raise IngestError(f"{method} {path} -> non-JSON HTTP {resp.status_code}: {resp.text[:300]}")

            if resp.status_code >= 400 or (isinstance(body, dict) and body.get("error")):
                msg = body.get("message") if isinstance(body, dict) else resp.text[:300]
                raise IngestError(f"{method} {path} -> HTTP {resp.status_code}: {msg}")
            return body

        raise IngestError(f"{method} {path} failed after {cfg.MAX_RETRIES} attempts: {last_exc}")

    @staticmethod
    def _sleep(attempt: int) -> None:
        time.sleep(cfg.RETRY_BACKOFF * attempt)

    # ------------------------------------------------------------------ api
    def open_run(self, retailer_id: int) -> int:
        body = self._post("runs", {"retailer_id": retailer_id})
        return int(body["run_id"])

    def post_products(self, run_id: int, retailer_id: int, products: list[dict]) -> dict:
        return self._post("products:batch", {
            "run_id": run_id, "retailer_id": retailer_id, "products": products,
        })

    def post_prices(self, run_id: int, retailer_id: int, prices: list[dict]) -> dict:
        return self._post("prices:batch", {
            "run_id": run_id, "retailer_id": retailer_id, "prices": prices,
        })

    def post_availability(self, run_id: int, retailer_id: int, items: list[dict]) -> dict:
        return self._post("availability:batch", {
            "run_id": run_id, "retailer_id": retailer_id, "items": items,
        })

    def close_run(self, run_id: int) -> dict:
        return self._post(f"runs/{run_id}:close", {})

    def abort_run(self, run_id: int, note: str = "Aborted by worker") -> dict:
        return self._post(f"runs/{run_id}:abort", {"note": note})

    def list_runs(self) -> dict:
        return self._get("runs")

    # ------------------------------------------------------------------ helpers
    def post_in_chunks(self, kind: str, run_id: int, retailer_id: int,
                       rows: list[dict], batch_size: int | None = None) -> int:
        """Chunk a large list into batch_size POSTs. kind: products|prices|availability."""
        sender = {
            "products": self.post_products,
            "prices": self.post_prices,
            "availability": self.post_availability,
        }[kind]
        size = batch_size or cfg.BATCH_SIZE
        sent = 0
        for i in range(0, len(rows), size):
            chunk = rows[i:i + size]
            sender(run_id, retailer_id, chunk)
            sent += len(chunk)
        return sent
