"""WebAuthn passkey service."""
from __future__ import annotations
from typing import Any, Optional, Protocol
from urllib.parse import urlparse
from uuid import UUID

from webauthn import (
    generate_authentication_options,
    generate_registration_options,
    verify_authentication_response,
    verify_registration_response,
)
from webauthn.helpers import base64url_to_bytes, bytes_to_base64url, generate_challenge, options_to_json_dict
from webauthn.helpers.structs import (
    AuthenticatorAttachment,
    AuthenticatorSelectionCriteria,
    ResidentKeyRequirement,
    UserVerificationRequirement,
)

from src.config import get_settings
from src.domain.auth.mfa_repository import WebAuthnCredentialRepository

settings = get_settings()


def _get_origin(app_base_url: Optional[str] = None) -> str:
    """Origin for WebAuthn (must be HTTPS in prod, localhost ok for dev)."""
    base = (app_base_url or settings.app_url).rstrip("/")
    return base


def _get_rp_id(app_base_url: Optional[str] = None) -> str:
    """Relying party ID - use host from app_base_url or config."""
    if settings.webauthn_rp_id and not app_base_url:
        return settings.webauthn_rp_id
    base = app_base_url or settings.app_url
    parsed = urlparse(base)
    return parsed.hostname or "localhost"


class ChallengeStore(Protocol):
    """Store WebAuthn challenges for verification."""

    async def set(self, key: str, value: str, ttl_seconds: int = 300) -> None: ...
    async def get(self, key: str) -> Optional[str]: ...
    async def delete(self, key: str) -> None: ...


class WebAuthnService:
    """WebAuthn registration and authentication."""

    def __init__(
        self,
        credential_repo: WebAuthnCredentialRepository,
        challenge_store: Optional[ChallengeStore] = None,
    ):
        self._repo = credential_repo
        self._store = challenge_store

    async def get_registration_options(
        self, user_id: UUID, user_name: str, app_base_url: Optional[str] = None
    ) -> dict:
        """Generate registration options for the client. Returns JSON-serializable dict."""
        user_handle = user_id.bytes
        # py_webauthn 2.x: ``generate_challenge()`` takes no args (default length is fine for WebAuthn).
        challenge = generate_challenge()

        options = generate_registration_options(
            rp_id=_get_rp_id(app_base_url),
            rp_name=settings.webauthn_rp_name,
            user_id=user_handle,
            user_name=user_name,
            user_display_name=user_name,
            authenticator_selection=AuthenticatorSelectionCriteria(
                authenticator_attachment=AuthenticatorAttachment.PLATFORM,
                resident_key=ResidentKeyRequirement.PREFERRED,
                user_verification=UserVerificationRequirement.PREFERRED,
            ),
            challenge=challenge,
        )
        result = options_to_json_dict(options)
        if self._store:
            challenge_b64 = bytes_to_base64url(challenge)
            await self._store.set(f"webauthn_reg:{user_id}", challenge_b64)
        return result

    async def verify_registration(
        self,
        user_id: UUID,
        credential: dict[str, Any],
        expected_challenge: Optional[str] = None,
        app_base_url: Optional[str] = None,
    ) -> bool:
        """Verify registration response and store credential. Gets challenge from store if not passed."""
        challenge_b64 = expected_challenge
        if not challenge_b64 and self._store:
            challenge_b64 = await self._store.get(f"webauthn_reg:{user_id}")
            if challenge_b64:
                await self._store.delete(f"webauthn_reg:{user_id}")
        if not challenge_b64:
            return False
        try:
            verification = verify_registration_response(
                credential=credential,
                expected_challenge=base64url_to_bytes(challenge_b64),
                expected_rp_id=_get_rp_id(app_base_url),
                expected_origin=_get_origin(app_base_url),
                require_user_verification=False,
            )
        except Exception:
            return False

        credential_id_b64 = bytes_to_base64url(verification.credential_id)
        public_key_b64 = bytes_to_base64url(verification.credential_public_key)

        transports = None
        if isinstance(credential.get("response"), dict):
            transports = credential["response"].get("transports")

        await self._repo.create(
            user_id=user_id,
            credential_id=credential_id_b64,
            public_key=public_key_b64,
            sign_count=verification.sign_count,
            device_type=verification.credential_device_type.value if verification.credential_device_type else None,
            backed_up=verification.credential_backed_up,
            transports=transports,
        )
        return True

    async def get_authentication_options(
        self,
        user_id: Optional[UUID] = None,
        user_credentials: Optional[list[dict]] = None,
    ) -> dict:
        """Generate authentication options. Store challenge for later verify. Returns dict with 'options' and optionally 'state_id' for usernameless."""
        from webauthn.helpers.structs import PublicKeyCredentialDescriptor

        allow_credentials = None
        if user_credentials:
            allow_credentials = [
                PublicKeyCredentialDescriptor(id=base64url_to_bytes(c["id"]))
                for c in user_credentials
            ]

        options = generate_authentication_options(
            rp_id=_get_rp_id(),
            allow_credentials=allow_credentials,
            user_verification=UserVerificationRequirement.PREFERRED,
        )
        result = options_to_json_dict(options)
        challenge_b64 = bytes_to_base64url(options.challenge)

        if self._store:
            if user_id:
                await self._store.set(f"webauthn_auth:{user_id}", challenge_b64)
                return result
            from src.domain.auth.challenge_store import create_state_id
            state_id = create_state_id()
            await self._store.set(f"webauthn_auth:{state_id}", challenge_b64)
            return {"options": result, "state_id": state_id}
        return result

    async def verify_authentication(
        self,
        credential: dict[str, Any],
        user_id: Optional[UUID] = None,
        state_id: Optional[str] = None,
        expected_challenge: Optional[str] = None,
        app_base_url: Optional[str] = None,
    ) -> Optional[UUID]:
        """
        Verify authentication response. Returns user_id if valid.
        Gets challenge from store: by user_id, state_id, or expected_challenge direct.
        """
        challenge_b64 = expected_challenge
        if not challenge_b64 and self._store:
            if user_id:
                challenge_b64 = await self._store.get(f"webauthn_auth:{user_id}")
                if challenge_b64:
                    await self._store.delete(f"webauthn_auth:{user_id}")
            elif state_id:
                challenge_b64 = await self._store.get(f"webauthn_auth:{state_id}")
                if challenge_b64:
                    await self._store.delete(f"webauthn_auth:{state_id}")
        if not challenge_b64:
            return None

        credential_id = credential.get("id")
        if not credential_id:
            return None

        stored = await self._repo.get_by_credential_id(credential_id)
        if not stored:
            return None

        try:
            verification = verify_authentication_response(
                credential=credential,
                expected_challenge=base64url_to_bytes(challenge_b64),
                expected_rp_id=_get_rp_id(app_base_url),
                expected_origin=_get_origin(app_base_url),
                credential_public_key=base64url_to_bytes(stored.public_key),
                credential_current_sign_count=stored.sign_count,
                require_user_verification=False,
            )
        except Exception:
            return None

        await self._repo.update_sign_count(credential_id, verification.new_sign_count)
        return stored.user_id
