from __future__ import annotations

from typing import Protocol

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

from blackout_notifier.models import Outage, OutageDataError

SAAPA_URL = "https://uiapi2.saapa.ir/api/ebills/PlannedBlackoutsReport"
EITAAYAR_BASE_URL = "https://eitaayar.ir/api"


class ExternalServiceError(RuntimeError):
    """Raised when an external API fails or returns unsafe data."""


class Notifier(Protocol):
    def send(self, chat_id: str, text: str) -> None: ...


class SaapaClient:
    def __init__(
        self,
        token: str,
        *,
        timeout: float,
        max_events_per_bill: int,
        session: requests.Session | None = None,
    ) -> None:
        self._token = token
        self._timeout = timeout
        self._max_events_per_bill = max_events_per_bill
        self._session = session or _retrying_session({"POST"})

    def fetch(self, bill_id: str, from_date: str, to_date: str) -> list[Outage]:
        try:
            response = self._session.post(
                SAAPA_URL,
                headers={
                    "Accept": "application/json",
                    "Authorization": f"Bearer {self._token}",
                },
                json={"bill_id": bill_id, "from_date": from_date, "to_date": to_date},
                timeout=self._timeout,
            )
        except requests.RequestException as error:
            raise ExternalServiceError(
                f"SAAPA request failed for bill {bill_id}: {error}"
            ) from error

        if not response.ok:
            raise ExternalServiceError(
                f"SAAPA returned HTTP {response.status_code} for bill {bill_id}"
            )
        try:
            body = response.json()
        except requests.JSONDecodeError as error:
            raise ExternalServiceError(f"SAAPA returned invalid JSON for bill {bill_id}") from error
        if not isinstance(body, dict) or not isinstance(body.get("data"), list):
            raise ExternalServiceError(f"SAAPA response schema is invalid for bill {bill_id}")

        raw_events = body["data"]
        if len(raw_events) > self._max_events_per_bill:
            raise ExternalServiceError(
                f"SAAPA returned {len(raw_events)} events for bill {bill_id}; "
                f"limit is {self._max_events_per_bill}"
            )

        events: list[Outage] = []
        seen_numbers: set[str] = set()
        try:
            for raw_event in raw_events:
                event = Outage.from_api(raw_event)
                if event.outage_number in seen_numbers:
                    raise OutageDataError(
                        f"duplicate outage_number {event.outage_number} in one snapshot"
                    )
                seen_numbers.add(event.outage_number)
                events.append(event)
        except OutageDataError as error:
            raise ExternalServiceError(f"Invalid outage for bill {bill_id}: {error}") from error
        return events


class EitaaClient:
    def __init__(
        self,
        token: str,
        *,
        timeout: float,
        session: requests.Session | None = None,
    ) -> None:
        self._url = f"{EITAAYAR_BASE_URL}/{token}/sendMessage"
        self._timeout = timeout
        self._session = session or _retrying_session({"GET"})

    def send(self, chat_id: str, text: str) -> None:
        try:
            response = self._session.get(
                self._url,
                params={"chat_id": chat_id, "text": text},
                timeout=self._timeout,
            )
        except requests.RequestException as error:
            raise ExternalServiceError(
                f"Eitaa request failed for chat {chat_id}: {error}"
            ) from error
        if not response.ok:
            raise ExternalServiceError(
                f"Eitaa returned HTTP {response.status_code} for chat {chat_id}"
            )
        try:
            body = response.json()
        except requests.JSONDecodeError as error:
            raise ExternalServiceError(f"Eitaa returned invalid JSON for chat {chat_id}") from error
        if not isinstance(body, dict) or body.get("ok") is not True:
            description = body.get("description") if isinstance(body, dict) else None
            raise ExternalServiceError(
                f"Eitaa rejected the message for chat {chat_id}: {description or 'unknown error'}"
            )


def _retrying_session(methods: set[str]) -> requests.Session:
    retry = Retry(
        total=3,
        connect=3,
        read=3,
        status=3,
        backoff_factor=0.5,
        status_forcelist=(429, 500, 502, 503, 504),
        allowed_methods=frozenset(methods),
        raise_on_status=False,
    )
    session = requests.Session()
    session.mount("https://", HTTPAdapter(max_retries=retry))
    return session
