# -*- coding: utf-8 -*-
"""
Пример: хуки httpx + blinker + контекст (Account).

EventBus хранит контекст (account) — его НЕ нужно передавать вручную в каждый send().
Подписчики получают account автоматически.
"""
import asyncio
from dataclasses import dataclass
from datetime import datetime, timedelta

import httpx
from blinker import signal

from src.request_pydantic_decorator import (
    validate_response,
    request_logging,
    ResponseValidator,
    Exceptions,
    status_code_is,
)


# ============================================================================
# EventBus — один раз передал контекст, дальше send() без повторений
# ============================================================================

class EventBus:
    """
    Обёртка над blinker-сигналами с привязанным контекстом.

    Использование:
        bus = EventBus(account=acc)
        bus.emit("token_expired", response=r, error=e)
        # → подписчик получает account автоматически

    Подписка (один раз, глобально):
        EventBus.on("token_expired", handler)
        # или через декоратор:
        @EventBus.on("token_expired")
        def handler(account, response, error, **kw): ...
    """

    # Реестр сигналов — общий для всех экземпляров
    _signals: dict[str, signal] = {}

    def __init__(self, **context):
        """context — любые данные (account, session, worker_id, ...)."""
        self._context = context

    @classmethod
    def _get_signal(cls, name: str) -> signal:
        if name not in cls._signals:
            cls._signals[name] = signal(name)
        return cls._signals[name]

    def emit(self, event: str, **kwargs):
        """Отправить событие. context подмешивается автоматически."""
        sig = self._get_signal(event)
        sig.send(self, **self._context, **kwargs)

    @classmethod
    def on(cls, event: str, func=None):
        """
        Подписаться на событие. Можно как декоратор или как вызов:
            @EventBus.on("token_expired")
            def handler(sender, account, response, error, **kw): ...

            EventBus.on("auth_failed", my_handler)
        """
        sig = cls._get_signal(event)
        if func is not None:
            sig.connect(func)
            return func
        # Использование как декоратор
        def decorator(fn):
            sig.connect(fn)
            return fn
        return decorator


# ============================================================================
# Модель аккаунта
# ============================================================================

@dataclass
class Account:
    id: int
    login: str
    password: str
    token: str | None = None
    is_banned: bool = False
    ban_reason: str | None = None
    failed_attempts: int = 0
    rate_limit_until: datetime | None = None

    def refresh_token(self, new_token: str):
        self.token = new_token
        print(f"    → Токен обновлён: {new_token[:20]}...")

    def ban(self, reason: str):
        self.is_banned = True
        self.ban_reason = reason
        print(f"    → Аккаунт {self.login} забанен: {reason}")

    def increment_fails(self):
        self.failed_attempts += 1
        print(f"    → Неудачных попыток: {self.failed_attempts}")


# ============================================================================
# Валидатор
# ============================================================================

auth_validator = (
    ResponseValidator()
    .when_all(status_code_is(401), raise_as=Exceptions.Auth.TokenExpired)
    .when_all(status_code_is(429), raise_as=Exceptions.Auth.RateLimit)
    .status_code().whitelist([200, 201, 204])
)


@request_logging(save_errors_to_file=False)
@validate_response(validator=auth_validator)
def _validate_sync(response: httpx.Response):
    response.read()
    return response


@request_logging(save_errors_to_file=False)
@validate_response(validator=auth_validator)
async def _validate_async(response: httpx.Response):
    await response.aread()
    return response


# ============================================================================
# Хуки — bus.emit() без ручной передачи account
# ============================================================================

def make_sync_hook(bus: EventBus):
    def on_response(response: httpx.Response):
        try:
            _validate_sync(response)
            bus.emit("request_success", response=response)
        except Exceptions.Auth.TokenExpired as e:
            bus.emit("token_expired", response=response, error=e)
        except Exceptions.Auth.RateLimit as e:
            bus.emit("rate_limited", response=response, error=e)
        except Exceptions.Validation.StatusCode as e:
            bus.emit("unexpected_status", response=response, error=e)
            raise
        except Exception as e:
            bus.emit("unexpected_status", response=response, error=e)
            raise
    return on_response


def make_async_hook(bus: EventBus):
    async def on_response(response: httpx.Response):
        try:
            await _validate_async(response)
            bus.emit("request_success", response=response)
        except Exceptions.Auth.TokenExpired as e:
            bus.emit("token_expired", response=response, error=e)
        except Exceptions.Auth.Creds as e:
            bus.emit("auth_failed", response=response, error=e)
        except Exceptions.Auth.RateLimit as e:
            bus.emit("rate_limited", response=response, error=e)
        except Exceptions.Validation.StatusCode as e:
            bus.emit("unexpected_status", response=response, error=e)
            raise
        except Exception as e:
            bus.emit("unexpected_status", response=response, error=e)
            raise
    return on_response


# ============================================================================
# Подписчики — account приходит автоматически из контекста bus
# ============================================================================

@EventBus.on("request_success")
def handle_success(sender, account: Account, response, **kw):
    print(f"  [EVENT] {sender} Успех для {account.login}: {response.status_code} {response.url}")


@EventBus.on("token_expired")
def handle_token_expired(sender, account: Account, response, error, **kw):
    print(f"  [EVENT] Токен истёк у {account.login}")
    account.refresh_token("new_fresh_token_abc123")


@EventBus.on("auth_failed")
def handle_auth_failed(sender, account: Account, response, error, **kw):
    print(f"  [EVENT] Авторизация не прошла: {account.login}")
    account.increment_fails()
    if account.failed_attempts >= 3:
        account.ban("Слишком много неудачных попыток")


@EventBus.on("rate_limited")
def handle_rate_limit(sender, account: Account, response, error, **kw):
    print(f"  [EVENT] Rate limit для {account.login}")
    account.rate_limit_until = datetime.now() + timedelta(seconds=30)
    print(f"    → Пауза до {account.rate_limit_until}")


@EventBus.on("unexpected_status")
def handle_unexpected(sender, account: Account, response, error, **kw):
    print(f"  [EVENT] Статус {response.status_code} для {account.login}: {error.message}")


# ============================================================================
# Демо
# ============================================================================

def demo_sync():
    acc = Account(id=42, login="richard.sanchez", password="secret", token="old_token")
    bus = EventBus(account=acc)  # ← один раз передал, всё

    print(f"=== Sync: {acc.login} ===\n")
    client = httpx.Client(
        event_hooks={"response": [make_sync_hook(bus)]},
        follow_redirects=True,
        verify=False,
    )

    for code in [200, 401, 403, 403, 403, 429, 500]:
        if acc.is_banned:
            print(f"--- Аккаунт забанен, пропускаю ---\n")
            continue
        print(f"--- GET /status/{code} ---")
        try:
            r = client.get(f"https://httpbin.org/status/{code}")
            print(f"  [MAIN] Ответ: {r.status_code}\n")
        except Exception as e:
            print(f"  [MAIN] Пробросилось: {type(e).__name__}: {e}\n")

    client.close()
    print(f"\n--- Итог ---")
    print(f"  token:           {acc.token}")
    print(f"  is_banned:       {acc.is_banned}")
    print(f"  failed_attempts: {acc.failed_attempts}")
    print(f"  rate_limit:      {acc.rate_limit_until}")


async def demo_async():
    acc = Account(id=99, login="morty.smith", password="p@ss", token="tok_old")
    bus = EventBus(account=acc)  # ← один раз

    print(f"\n\n=== Async: {acc.login} ===\n")
    async with httpx.AsyncClient(
        event_hooks={"response": [make_async_hook(bus)]},
        follow_redirects=True,
        verify=False,
    ) as client:
        for code in [200, 403, 403, 403, 200]:
            if acc.is_banned:
                print(f"--- Забанен, пропускаю ---\n")
                continue
            print(f"--- GET /status/{code} ---")
            try:
                r = await client.get(f"https://httpbin.org/status/{code}")
                print(f"  [MAIN] Ответ: {r.status_code}\n")
            except Exception as e:
                print(f"  [MAIN] Пробросилось: {type(e).__name__}: {e}\n")

    print(f"\n--- Итог ---")
    print(f"  is_banned:       {acc.is_banned}")
    print(f"  failed_attempts: {acc.failed_attempts}")


if __name__ == "__main__":
    demo_sync()
    asyncio.run(demo_async())
