from __future__ import annotations

import base64
import hashlib
import json
from collections.abc import Callable
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from typing import Any

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import load_der_public_key
from pydantic import BaseModel, Field

from ebay_client.generated.notification.models import PublicKey


class WebhookEventEnvelope(BaseModel):
    metadata: dict[str, Any] = Field(default_factory=dict)
    notification_id: str | None = None
    publish_date: str | None = None
    topic_id: str | None = None
    data: dict[str, Any] | list[Any] | None = None

    @classmethod
    def from_payload(cls, payload: dict[str, Any] | list[Any]) -> WebhookEventEnvelope:
        if isinstance(payload, list):
            return cls(data=payload)

        metadata = dict(payload.get("metadata") or {}) if isinstance(payload.get("metadata"), dict) else {}
        for key, value in payload.items():
            if key not in {"metadata", "notificationId", "notification_id", "publishDate", "publish_date", "topicId", "topic_id", "data"}:
                metadata[key] = value

        return cls(
            metadata=metadata,
            notification_id=payload.get("notificationId") or payload.get("notification_id"),
            publish_date=payload.get("publishDate") or payload.get("publish_date"),
            topic_id=payload.get("topicId") or payload.get("topic_id"),
            data=payload.get("data"),
        )


@dataclass(slots=True)
class WebhookHttpResponse:
    status_code: int
    headers: dict[str, str]
    body: bytes = b""


@dataclass(slots=True)
class WebhookDispatchResult:
    response: WebhookHttpResponse
    event: WebhookEventEnvelope | None = None


@dataclass(slots=True)
class ParsedSignatureHeader:
    key_id: str | None
    signature: bytes | None
    algorithm: str | None
    digest: str | None
    raw_parts: dict[str, str]


class WebhookSignatureParser:
    KEY_ALIASES = ("kid", "keyid", "key_id", "public_key_id")
    SIGNATURE_ALIASES = ("sig", "signature")
    ALGORITHM_ALIASES = ("alg", "algorithm")
    DIGEST_ALIASES = ("digest",)

    def parse(self, header_value: str) -> ParsedSignatureHeader:
        parts = self._parse_parts(header_value)

        signature_value = self._first_match(parts, self.SIGNATURE_ALIASES)
        signature = self._decode_base64(signature_value) if signature_value else None
        return ParsedSignatureHeader(
            key_id=self._first_match(parts, self.KEY_ALIASES),
            signature=signature,
            algorithm=self._first_match(parts, self.ALGORITHM_ALIASES),
            digest=self._first_match(parts, self.DIGEST_ALIASES),
            raw_parts=parts,
        )

    def _parse_parts(self, header_value: str) -> dict[str, str]:
        decoded = self._decode_base64_to_text(header_value)
        if decoded:
            try:
                payload = json.loads(decoded)
            except json.JSONDecodeError:
                payload = None
            if isinstance(payload, dict):
                return {str(key).strip().lower(): str(value).strip() for key, value in payload.items() if value is not None}

        separators = header_value.replace(";", ",").split(",")
        parts: dict[str, str] = {}
        for item in separators:
            if "=" not in item:
                continue
            key, value = item.split("=", 1)
            parts[key.strip().lower()] = value.strip().strip('"')
        return parts

    @staticmethod
    def _decode_base64_to_text(value: str) -> str | None:
        decoded = WebhookSignatureParser._decode_base64(value)
        if decoded is None:
            return None
        try:
            return decoded.decode("utf-8")
        except UnicodeDecodeError:
            return None

    @staticmethod
    def _decode_base64(value: str) -> bytes | None:
        normalized = value.strip()
        if not normalized:
            return None
        padding = (-len(normalized)) % 4
        normalized = normalized + ("=" * padding)
        for decoder in (base64.b64decode, base64.urlsafe_b64decode):
            try:
                return decoder(normalized)
            except Exception:
                continue
        return None

    @staticmethod
    def _first_match(parts: dict[str, str], aliases: tuple[str, ...]) -> str | None:
        for alias in aliases:
            if alias in parts:
                return parts[alias]
        return None


class WebhookChallengeHandler:
    @staticmethod
    def build_challenge_response(*, challenge_code: str, verification_token: str, endpoint: str) -> str:
        payload = f"{challenge_code}{verification_token}{endpoint}".encode("utf-8")
        return hashlib.sha256(payload).hexdigest()

    def build_response_body(self, *, challenge_code: str, verification_token: str, endpoint: str) -> dict[str, str]:
        return {
            "challengeResponse": self.build_challenge_response(
                challenge_code=challenge_code,
                verification_token=verification_token,
                endpoint=endpoint,
            )
        }


class WebhookPublicKeyResolver:
    def __init__(self, fetcher: Callable[[str], PublicKey], *, cache_ttl_seconds: int = 3600) -> None:
        self.fetcher = fetcher
        self.cache_ttl = timedelta(seconds=cache_ttl_seconds)
        self._cache: dict[str, tuple[datetime, PublicKey]] = {}

    def resolve(self, public_key_id: str) -> PublicKey:
        cached = self._cache.get(public_key_id)
        now = datetime.now(UTC)
        if cached and cached[0] > now:
            return cached[1]
        payload = self.fetcher(public_key_id)
        self._cache[public_key_id] = (now + self.cache_ttl, payload)
        return payload


class WebhookSignatureValidator:
    def __init__(self, resolver: WebhookPublicKeyResolver, *, parser: WebhookSignatureParser | None = None) -> None:
        self.resolver = resolver
        self.parser = parser or WebhookSignatureParser()

    def validate(self, *, header_value: str, body: bytes, message_builder: Callable[[bytes], bytes] | None = None) -> bool:
        parsed = self.parser.parse(header_value)
        if not parsed.key_id or not parsed.signature:
            return False
        key_payload = self.resolver.resolve(parsed.key_id)
        key_value = key_payload.key
        if not key_value:
            return False
        public_key = self._load_public_key(key_value)
        if public_key is None:
            return False
        digest_name = (parsed.digest or key_payload.digest or "SHA256").upper()
        digest = self._hash_algorithm(digest_name)
        message = message_builder(body) if message_builder else body
        try:
            public_key.verify(parsed.signature, message, ec.ECDSA(digest))
            return True
        except InvalidSignature:
            return False

    @staticmethod
    def _hash_algorithm(name: str) -> hashes.HashAlgorithm:
        normalized = name.replace("-", "")
        if normalized == "SHA1":
            return hashes.SHA1()
        if normalized == "SHA384":
            return hashes.SHA384()
        if normalized == "SHA512":
            return hashes.SHA512()
        return hashes.SHA256()

    @staticmethod
    def _load_public_key(key_value: str):
        normalized = key_value.strip()
        if not normalized:
            return None
        if "BEGIN PUBLIC KEY" in normalized:
            return serialization.load_pem_public_key(normalized.encode("utf-8"))

        decoded = WebhookSignatureParser._decode_base64(normalized)
        if decoded is None:
            return None
        return load_der_public_key(decoded)


class WebhookRequestHandler:
    def __init__(
        self,
        *,
        signature_validator: WebhookSignatureValidator,
        challenge_handler: WebhookChallengeHandler | None = None,
        event_parser: Callable[[bytes], WebhookEventEnvelope] | None = None,
    ) -> None:
        self.signature_validator = signature_validator
        self.challenge_handler = challenge_handler or WebhookChallengeHandler()
        self.event_parser = event_parser or self.parse_event

    def handle_challenge(self, *, challenge_code: str | None, verification_token: str, endpoint: str) -> WebhookHttpResponse:
        if not challenge_code:
            return self._json_response(400, {"error": "challenge_code is required"})

        payload = self.challenge_handler.build_response_body(
            challenge_code=challenge_code,
            verification_token=verification_token,
            endpoint=endpoint,
        )
        return self._json_response(200, payload)

    def handle_notification(
        self,
        *,
        signature_header: str | None,
        body: bytes,
        message_builder: Callable[[bytes], bytes] | None = None,
    ) -> WebhookDispatchResult:
        if not signature_header or not self.signature_validator.validate(
            header_value=signature_header,
            body=body,
            message_builder=message_builder,
        ):
            return WebhookDispatchResult(
                response=WebhookHttpResponse(status_code=412, headers={}),
            )

        try:
            event = self.event_parser(body)
        except (UnicodeDecodeError, ValueError, TypeError):
            return WebhookDispatchResult(
                response=self._json_response(400, {"error": "notification body is not valid JSON"}),
            )

        return WebhookDispatchResult(
            response=WebhookHttpResponse(status_code=200, headers={}),
            event=event,
        )

    @staticmethod
    def parse_event(body: bytes) -> WebhookEventEnvelope:
        payload = json.loads(body.decode("utf-8"))
        if isinstance(payload, dict) or isinstance(payload, list):
            return WebhookEventEnvelope.from_payload(payload)
        raise ValueError("notification payload must be a JSON object or array")

    @staticmethod
    def _json_response(status_code: int, payload: dict[str, Any]) -> WebhookHttpResponse:
        return WebhookHttpResponse(
            status_code=status_code,
            headers={"Content-Type": "application/json"},
            body=json.dumps(payload, separators=(",", ":")).encode("utf-8"),
        )
