"""HTTP-обёртка над TGStat API: ретраи, маскирование токена, классификация ошибок."""
from __future__ import annotations

import re
import time
from typing import Any

import requests

API_BASE = "https://api.tgstat.ru"
TOKEN_RE = re.compile(r"token=[^&\s]+")
DEFAULT_TIMEOUT = 30
NETWORK_RETRIES = 3
NETWORK_BACKOFFS = [5, 15, 30]
RATE_LIMIT_RETRIES = 4
RATE_LIMIT_BACKOFFS = [2, 4, 8]  # экспоненциальный бэкофф на HTTP 429

class TGStatError(Exception):
    pass


class LimitReachedError(TGStatError):
    """Месячный лимит запросов исчерпан."""


class RateLimitError(TGStatError):
    """Per-second rate limit."""


class NotFoundError(TGStatError):
    """Канал/пост не найден или удалён."""


def mask_token(s: str) -> str:
    return TOKEN_RE.sub("token=***", s)


class TGStatClient:
    def __init__(self, token: str, *, delay: float = 0.3) -> None:
        self._token = token
        self._delay = delay

    def _request(self, method: str, params: dict[str, Any]) -> Any:
        params = {**params, "token": self._token}
        url = f"{API_BASE}/{method}"

        last_exc: Exception | None = None
        network_attempt = 0
        rate_limit_attempt = 0
        while True:
            try:
                r = requests.get(url, params=params, timeout=DEFAULT_TIMEOUT)
            except requests.RequestException as exc:
                last_exc = exc
                if network_attempt < NETWORK_RETRIES - 1:
                    time.sleep(NETWORK_BACKOFFS[network_attempt])
                    network_attempt += 1
                    continue
                raise TGStatError(f"network error on /{method}: {exc}") from exc

            if r.status_code == 429:
                if rate_limit_attempt < RATE_LIMIT_RETRIES - 1:
                    time.sleep(RATE_LIMIT_BACKOFFS[rate_limit_attempt])
                    rate_limit_attempt += 1
                    continue
                raise RateLimitError("HTTP 429 rate limit hit")

            try:
                data = r.json()
            except ValueError as exc:
                raise TGStatError(f"non-JSON response from /{method}: {r.text[:200]}") from exc

            if data.get("status") == "ok":
                if "response" not in data:
                    raise TGStatError(f"unexpected shape from /{method}: {data!r}")
                time.sleep(self._delay)
                return data["response"]

            error = (data.get("error") or data.get("message") or "").lower()
            if "limit" in error and "reached" in error:
                raise LimitReachedError(error)
            if "not found" in error or "deleted" in error:
                raise NotFoundError(error)
            if re.search(r"\brate\b", error):
                raise RateLimitError(error)

            safe_url = mask_token(r.url)
            raise TGStatError(f"TGStat /{method} returned error: {data!r} (url={safe_url})")

    def channels_get(self, channel_id: str) -> dict:
        return self._request("channels/get", {"channelId": channel_id})

    def channels_posts(
        self,
        channel_id: str,
        *,
        limit: int = 50,
        offset: int = 0,
        start_time: int | None = None,
        end_time: int | None = None,
        hide_deleted: int = 1,
        extended: int = 1,
    ) -> dict:
        params: dict[str, Any] = {
            "channelId": channel_id,
            "limit": limit,
            "offset": offset,
            "hideDeleted": hide_deleted,
            "extended": extended,
        }
        if start_time is not None:
            params["startTime"] = start_time
        if end_time is not None:
            params["endTime"] = end_time
        return self._request("channels/posts", params)

    def posts_stat(self, post_id: str) -> dict:
        return self._request("posts/stat", {"postId": post_id})

    def channels_mentions(
        self,
        channel_id: str,
        *,
        start_time: int | None = None,
        end_time: int | None = None,
        limit: int = 20,
        extended: int = 1,
    ) -> dict:
        params: dict[str, Any] = {
            "channelId": channel_id,
            "limit": limit,
            "extended": extended,
        }
        if start_time is not None:
            params["startTime"] = start_time
        if end_time is not None:
            params["endTime"] = end_time
        return self._request("channels/mentions", params)

    def channels_subscribers(
        self,
        channel_id: str,
        *,
        start_date: int | None = None,
        end_date: int | None = None,
        group: str = "day",
    ) -> Any:
        params: dict[str, Any] = {"channelId": channel_id, "group": group}
        if start_date is not None:
            params["startDate"] = start_date
        if end_date is not None:
            params["endDate"] = end_date
        return self._request("channels/subscribers", params)
