"""Webhook payloads, signing, persistence, and retry delivery for SwarmPay."""
from __future__ import annotations

import hashlib
import hmac
import json
import os
import time
import urllib.request
import uuid
from dataclasses import asdict, dataclass, field


@dataclass(frozen=True)
class WebhookEvent:
    """Canonical event envelope for outbound webhook delivery."""
    event_id: str
    event_type: str
    created_at: float
    payload: dict


@dataclass(frozen=True)
class WebhookSubscription:
    """Registered outbound webhook target."""
    subscription_id: str
    target_url: str
    secret: str
    event_types: tuple[str, ...] = ()
    enabled: bool = True
    max_retries: int = 3


@dataclass(frozen=True)
class WebhookDeliveryResult:
    """Outcome of one webhook delivery attempt."""
    subscription_id: str
    event_id: str
    attempt: int
    success: bool
    status_code: int
    error: str = ""
    next_attempt_at: float = 0.0
    delivered_at: float = 0.0


def serialize_event(event: WebhookEvent) -> bytes:
    """Serialize an event payload deterministically for signing and delivery."""
    return json.dumps(asdict(event), sort_keys=True, separators=(",", ":")).encode("utf-8")


def sign_event(event: WebhookEvent, secret: str) -> str:
    """Generate an HMAC signature for a webhook event payload."""
    digest = hmac.new(secret.encode("utf-8"), serialize_event(event), hashlib.sha256).hexdigest()
    return f"sha256={digest}"


@dataclass
class WebhookDispatcher:
    """Persistent in-process event collector plus HTTP delivery with retry state."""

    handler: callable | None = None
    storage_path: str = ""
    retry_base_seconds: float = 5.0
    events: list[WebhookEvent] = field(default_factory=list)
    subscriptions: list[WebhookSubscription] = field(default_factory=list)
    deliveries: list[WebhookDeliveryResult] = field(default_factory=list)

    def __post_init__(self):
        if not self.storage_path:
            self.storage_path = os.path.abspath(
                os.path.join(os.path.dirname(__file__), "..", "webhooks.json")
            )
        self._load()

    def _load(self) -> None:
        if not os.path.exists(self.storage_path):
            return
        with open(self.storage_path, "r") as f:
            data = json.load(f)
        self.events = [WebhookEvent(**item) for item in data.get("events", [])]
        self.subscriptions = [
            WebhookSubscription(
                subscription_id=item["subscription_id"],
                target_url=item["target_url"],
                secret=item["secret"],
                event_types=tuple(item.get("event_types", [])),
                enabled=item.get("enabled", True),
                max_retries=item.get("max_retries", 3),
            )
            for item in data.get("subscriptions", [])
        ]
        self.deliveries = [WebhookDeliveryResult(**item) for item in data.get("deliveries", [])]

    def _save(self) -> None:
        os.makedirs(os.path.dirname(self.storage_path), exist_ok=True)
        with open(self.storage_path, "w") as f:
            json.dump(
                {
                    "events": [asdict(event) for event in self.events],
                    "subscriptions": [
                        {
                            **asdict(subscription),
                            "event_types": list(subscription.event_types),
                        }
                        for subscription in self.subscriptions
                    ],
                    "deliveries": [asdict(delivery) for delivery in self.deliveries],
                },
                f,
                indent=2,
                ensure_ascii=False,
            )

    def subscribe(
        self,
        target_url: str,
        secret: str,
        event_types: list[str] | tuple[str, ...] | None = None,
        enabled: bool = True,
        max_retries: int = 3,
    ) -> WebhookSubscription:
        subscription = WebhookSubscription(
            subscription_id=f"wh_{uuid.uuid4().hex[:16]}",
            target_url=target_url,
            secret=secret,
            event_types=tuple(event_types or ()),
            enabled=enabled,
            max_retries=max_retries,
        )
        self.subscriptions.append(subscription)
        self._save()
        return subscription

    def emit(self, event_type: str, payload: dict) -> WebhookEvent:
        event = WebhookEvent(
            event_id=f"evt_{uuid.uuid4().hex[:16]}",
            event_type=event_type,
            created_at=time.time(),
            payload=payload,
        )
        self.events.append(event)
        self._save()
        if self.handler is not None:
            self.handler(event)
        return event

    def deliver_event(
        self,
        event: WebhookEvent,
        sender=None,
        now: float | None = None,
    ) -> list[WebhookDeliveryResult]:
        """Deliver one event once to each matching subscription, persisting retry state."""
        results = []
        send = sender or self._default_sender
        current_time = time.time() if now is None else now
        for subscription in self.subscriptions:
            if not subscription.enabled:
                continue
            if subscription.event_types and event.event_type not in subscription.event_types:
                continue
            attempt = self._next_attempt_number(subscription.subscription_id, event.event_id)
            result = self._deliver_once(subscription, event, attempt, send, current_time)
            self.deliveries.append(result)
            results.append(result)
        if results:
            self._save()
        return results

    def retry_pending(self, sender=None, now: float | None = None) -> list[WebhookDeliveryResult]:
        """Retry deliveries that are due according to exponential backoff."""
        send = sender or self._default_sender
        current_time = time.time() if now is None else now
        results: list[WebhookDeliveryResult] = []
        for delivery in list(self.deliveries):
            if delivery.success:
                continue
            if delivery.next_attempt_at <= 0 or delivery.next_attempt_at > current_time:
                continue
            subscription = self._get_subscription(delivery.subscription_id)
            event = self._get_event(delivery.event_id)
            if subscription is None or event is None or not subscription.enabled:
                continue
            if delivery.attempt >= subscription.max_retries:
                continue
            retry_attempt = delivery.attempt + 1
            result = self._deliver_once(subscription, event, retry_attempt, send, current_time)
            self.deliveries.append(result)
            results.append(result)
        if results:
            self._save()
        return results

    def emit_and_deliver(self, event_type: str, payload: dict, sender=None) -> WebhookEvent:
        event = self.emit(event_type, payload)
        self.deliver_event(event, sender=sender)
        return event

    def _deliver_once(self, subscription, event, attempt, send, current_time) -> WebhookDeliveryResult:
        try:
            status_code = send(subscription, event)
            success = 200 <= status_code < 300
            return WebhookDeliveryResult(
                subscription_id=subscription.subscription_id,
                event_id=event.event_id,
                attempt=attempt,
                success=success,
                status_code=status_code,
                error="" if success else f"HTTP {status_code}",
                next_attempt_at=0.0 if success or attempt >= subscription.max_retries else (
                    current_time + self.retry_base_seconds * (2 ** (attempt - 1))
                ),
                delivered_at=current_time,
            )
        except Exception as exc:
            return WebhookDeliveryResult(
                subscription_id=subscription.subscription_id,
                event_id=event.event_id,
                attempt=attempt,
                success=False,
                status_code=0,
                error=str(exc),
                next_attempt_at=0.0 if attempt >= subscription.max_retries else (
                    current_time + self.retry_base_seconds * (2 ** (attempt - 1))
                ),
                delivered_at=current_time,
            )

    def _next_attempt_number(self, subscription_id: str, event_id: str) -> int:
        attempts = [
            delivery.attempt for delivery in self.deliveries
            if delivery.subscription_id == subscription_id and delivery.event_id == event_id
        ]
        return max(attempts, default=0) + 1

    def _get_subscription(self, subscription_id: str) -> WebhookSubscription | None:
        for subscription in self.subscriptions:
            if subscription.subscription_id == subscription_id:
                return subscription
        return None

    def _get_event(self, event_id: str) -> WebhookEvent | None:
        for event in self.events:
            if event.event_id == event_id:
                return event
        return None

    @staticmethod
    def _default_sender(subscription: WebhookSubscription, event: WebhookEvent) -> int:
        body = serialize_event(event)
        request = urllib.request.Request(
            subscription.target_url,
            data=body,
            headers={
                "Content-Type": "application/json",
                "X-SwarmPay-Event": event.event_type,
                "X-SwarmPay-Event-Id": event.event_id,
                "X-SwarmPay-Signature": sign_event(event, subscription.secret),
            },
            method="POST",
        )
        with urllib.request.urlopen(request, timeout=5) as response:
            return int(getattr(response, "status", 200))
