from __future__ import annotations

import base64
import hashlib
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 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


@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:
        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('"')

        signature_value = self._first_match(parts, self.SIGNATURE_ALIASES)
        signature = base64.b64decode(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,
        )

    @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)
        pem = key_payload.key
        if not pem:
            return False
        public_key = serialization.load_pem_public_key(pem.encode("utf-8"))
        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()
