"""DID:web resolution for obtaining public keys."""

from __future__ import annotations

import base64
import time
from typing import TYPE_CHECKING, Any
from urllib.parse import unquote

import httpx
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

from mailio_receipt.errors import (
    DIDNetworkError,
    DIDNotFoundError,
    DIDResolutionError,
)

if TYPE_CHECKING:
    from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey
    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey


def did_to_https_url(did: str) -> str:
    """
    Convert a DID:web identifier to its HTTPS URL.

    DID:web URL Resolution Algorithm:
    - did:web:example.com → https://example.com/.well-known/did.json
    - did:web:example.com:user:alice → https://example.com/user/alice/did.json
    - did:web:example.com%3A8443 → https://example.com:8443/.well-known/did.json

    Args:
        did: DID:web identifier

    Returns:
        HTTPS URL to fetch the DID document

    Raises:
        DIDResolutionError: If DID format is invalid
    """
    if not did.startswith("did:web:"):
        raise DIDResolutionError(f"Invalid DID:web format: {did}")

    # Remove the did:web: prefix
    identifier = did[8:]

    # Split by colons BEFORE decoding to get path components
    # Encoded colons (%3A) in the domain represent actual colons (like ports)
    parts = identifier.split(":")

    # First part is the domain (URL decode to get actual domain:port)
    domain = unquote(parts[0])

    # Remaining parts are path segments (URL decode each)
    if len(parts) == 1:
        # No path, use .well-known
        path = "/.well-known/did.json"
    else:
        # Build path from remaining parts
        path_parts = [unquote(p) for p in parts[1:]]
        path = "/" + "/".join(path_parts) + "/did.json"

    return f"https://{domain}{path}"


class DIDWebResolver:
    """
    Resolver for DID:web identifiers.

    Fetches DID documents over HTTPS and extracts verification keys.
    Supports caching with configurable TTL.
    """

    def __init__(
        self,
        *,
        cache_ttl: int = 300,
        http_client: httpx.AsyncClient | None = None,
    ) -> None:
        """
        Create a DID:web resolver.

        Args:
            cache_ttl: Cache TTL in seconds (default: 5 minutes)
            http_client: Optional custom HTTP client
        """
        self._cache_ttl = cache_ttl
        self._http_client = http_client
        self._owns_client = http_client is None
        self._cache: dict[str, tuple[dict[str, Any], float]] = {}

    async def _get_client(self) -> httpx.AsyncClient:
        """Get or create HTTP client."""
        if self._http_client is None:
            self._http_client = httpx.AsyncClient(timeout=30.0)
        return self._http_client

    async def close(self) -> None:
        """Close the HTTP client if we own it."""
        if self._owns_client and self._http_client is not None:
            await self._http_client.aclose()
            self._http_client = None

    def _get_cached(self, did: str) -> dict[str, Any] | None:
        """Get cached DID document if still valid."""
        if did in self._cache:
            doc, timestamp = self._cache[did]
            if time.time() - timestamp < self._cache_ttl:
                return doc
            else:
                del self._cache[did]
        return None

    def _set_cached(self, did: str, doc: dict[str, Any]) -> None:
        """Cache a DID document."""
        self._cache[did] = (doc, time.time())

    async def resolve(self, did: str) -> dict[str, Any]:
        """
        Resolve a DID to its document.

        Args:
            did: DID:web identifier

        Returns:
            DID document as dictionary

        Raises:
            DIDNotFoundError: If DID document not found (404)
            DIDNetworkError: If network error occurs (retryable)
            DIDResolutionError: For other resolution failures
        """
        # Check cache first
        cached = self._get_cached(did)
        if cached is not None:
            return cached

        # Convert DID to URL
        url = did_to_https_url(did)

        # Fetch DID document
        try:
            client = await self._get_client()
            response = await client.get(url)

            if response.status_code == 404:
                raise DIDNotFoundError(f"DID document not found: {did}")

            if response.status_code != 200:
                raise DIDResolutionError(
                    f"Failed to fetch DID document: HTTP {response.status_code}"
                )

            doc = response.json()

            # Cache the result
            self._set_cached(did, doc)

            return doc

        except httpx.ConnectError as e:
            raise DIDNetworkError(f"Network error resolving {did}: {e}") from e
        except httpx.TimeoutException as e:
            raise DIDNetworkError(f"Timeout resolving {did}: {e}") from e
        except httpx.HTTPError as e:
            raise DIDNetworkError(f"HTTP error resolving {did}: {e}") from e

    async def get_verification_key(
        self,
        did: str,
        key_id: str | None = None,
    ) -> EllipticCurvePublicKey | Ed25519PublicKey:
        """
        Get a verification public key from a DID document.

        Args:
            did: DID:web identifier
            key_id: Optional key ID to select specific key

        Returns:
            EC P-256 or Ed25519 public key for signature verification

        Raises:
            DIDResolutionError: If key cannot be extracted
        """
        doc = await self.resolve(did)

        # Get verification methods
        verification_methods = doc.get("verificationMethod", [])
        if not verification_methods:
            raise DIDResolutionError(f"No verification methods in DID document: {did}")

        # Find the right key
        method = None
        if key_id:
            # Match by key ID
            for vm in verification_methods:
                vm_id = vm.get("id", "")
                # Key ID might be full or fragment
                if vm_id == key_id or vm_id.endswith(f"#{key_id}") or f"#{key_id}" in vm_id:
                    method = vm
                    break
            if method is None:
                raise DIDResolutionError(f"Key ID not found: {key_id}")
        else:
            # Use first verification method
            method = verification_methods[0]

        # Extract the public key
        return self._extract_public_key(method)

    def _extract_public_key(
        self, method: dict[str, Any]
    ) -> EllipticCurvePublicKey | Ed25519PublicKey:
        """
        Extract public key from verification method.

        Supports:
        - publicKeyJwk (JWK format)
        - publicKeyMultibase (Multibase encoded)
        """
        if "publicKeyJwk" in method:
            return self._jwk_to_public_key(method["publicKeyJwk"])
        elif "publicKeyMultibase" in method:
            return self._multibase_to_public_key(method["publicKeyMultibase"])
        else:
            raise DIDResolutionError(
                f"Unsupported key format in verification method: {method.get('id')}"
            )

    def _jwk_to_public_key(self, jwk: dict[str, Any]) -> EllipticCurvePublicKey | Ed25519PublicKey:
        """Convert JWK to EC or Ed25519 public key."""
        kty = jwk.get("kty")
        if kty == "OKP":
            crv = jwk.get("crv")
            if crv != "Ed25519":
                raise DIDResolutionError(f"Unsupported curve: {crv}")
            x_b64 = jwk["x"]
            padding = "=" * (-len(x_b64) % 4)
            x_bytes = base64.urlsafe_b64decode(x_b64 + padding)
            if len(x_bytes) != 32:
                raise DIDResolutionError("Invalid Ed25519 public key length")
            return Ed25519PublicKey.from_public_bytes(x_bytes)

        if kty != "EC":
            raise DIDResolutionError(f"Unsupported key type: {kty}")

        crv = jwk.get("crv")
        if crv != "P-256":
            raise DIDResolutionError(f"Unsupported curve: {crv}")

        # Decode x and y coordinates
        x_bytes = base64.urlsafe_b64decode(jwk["x"] + "==")
        y_bytes = base64.urlsafe_b64decode(jwk["y"] + "==")

        # Build uncompressed point format: 0x04 || x || y
        point_bytes = b"\x04" + x_bytes + y_bytes

        # Create public key from point
        return ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256R1(), point_bytes)

    def _multibase_to_public_key(
        self, multibase: str
    ) -> EllipticCurvePublicKey | Ed25519PublicKey:
        """Convert multibase-encoded key to EC or Ed25519 public key."""
        # Multibase prefix 'z' means base58btc
        if not multibase.startswith("z"):
            raise DIDResolutionError(f"Unsupported multibase encoding: {multibase[0]}")

        # This is a simplified implementation
        # Full implementation would need proper multibase/multicodec decoding
        raise DIDResolutionError("Multibase key format not fully implemented")
