"""
PrivacyCreds SDK
================

Drop-in privacy-preserving identity verification for games and applications.

Game developer usage::

    from privacy_creds import GameVerifier

    verifier = GameVerifier(issuer_pubkey_pem=open("issuer_public.pem").read())
    challenge = verifier.create_challenge()
    # ... send challenge to player, receive proof back ...
    result = verifier.verify(proof)
    if result:
        grant_access()

Player-side usage::

    from privacy_creds import PlayerWallet

    wallet = PlayerWallet(user_id="u001")
    wallet.setup(credential)
    proof = wallet.prove(challenge)
"""

from __future__ import annotations

import base64
import logging
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

from .device import generate_bound_keypair, load_bound_private_key, get_device_id
from .exceptions import (
    PrivacyCredsError,
    SignatureError,
    VerificationError,
    WalletError,
)
from .storage import FileStorage
from .verifier import verify_credential_signature, verify_challenge_signature

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Verification result
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class VerificationResult:
    """Outcome of a player verification attempt."""
    allowed: bool
    reason: str

    def __bool__(self) -> bool:
        return self.allowed

    def __repr__(self) -> str:
        status = "ALLOWED" if self.allowed else "BLOCKED"
        return f"VerificationResult({status}: {self.reason})"


# ---------------------------------------------------------------------------
# Player side
# ---------------------------------------------------------------------------

class PlayerWallet:
    """
    Player-side credential wallet.

    Stores the signed credential and a device-bound private key.
    Generates proofs in response to game challenges.
    """

    def __init__(
        self,
        user_id: str,
        storage_dir: Path | str | None = None,
    ):
        if not user_id or not user_id.isalnum():
            raise WalletError(f"user_id must be alphanumeric, got: {user_id!r}")
        self.user_id = user_id
        self.storage = FileStorage(storage_dir)

    @property
    def is_setup(self) -> bool:
        """True if this wallet has a credential and device-bound key."""
        return (
            self.storage.exists(f"wallet_{self.user_id}.json")
            and self.storage.exists(f"device_key_{self.user_id}.json")
        )

    def setup(self, credential_data: dict[str, Any]) -> None:
        """
        Initialise the wallet with a signed credential from the issuer.

        Generates a device-bound keypair and attaches the public key
        to the credential. Call this once during onboarding.

        Args:
            credential_data: The signed credential dict from the issuer,
                with keys ``"credential"`` and ``"signature"``.
        """
        if "credential" not in credential_data or "signature" not in credential_data:
            raise WalletError("credential_data must contain 'credential' and 'signature' keys")

        # Generate device-bound keypair
        public_key_pem = generate_bound_keypair(self.user_id, self.storage)

        # Attach public key to wallet
        wallet_data = {
            "credential": credential_data["credential"],
            "signature": credential_data["signature"],
            "user_public_key": public_key_pem,
        }
        self.storage.write_json(f"wallet_{self.user_id}.json", wallet_data, secret=True)
        logger.info("Wallet setup complete for %s on device %s", self.user_id, get_device_id())

    def prove(self, challenge: str) -> dict[str, Any]:
        """
        Respond to a game's verification challenge.

        Returns a proof dict containing:
        - The original challenge
        - A signature proving ownership of the device-bound key
        - The credential and its issuer signature
        - The user's public key

        The game learns NOTHING about the player's identity beyond
        the boolean claims in the credential.

        Args:
            challenge: The single-use challenge string from the game.

        Returns:
            A proof dict ready to send to the verifier.

        Raises:
            WalletError: If the wallet is not set up.
            DeviceBindingError: If run on a different device.
        """
        if not self.is_setup:
            raise WalletError(f"Wallet not set up for user {self.user_id}")

        if not challenge:
            raise WalletError("Challenge must be a non-empty string")

        wallet_data = self.storage.read_json(f"wallet_{self.user_id}.json")

        # Sign challenge with device-bound key
        private_key = load_bound_private_key(self.user_id, self.storage)
        signature = private_key.sign(challenge.encode())

        return {
            "challenge": challenge,
            "challenge_signature": base64.b64encode(signature).decode(),
            "credential": wallet_data["credential"],
            "credential_signature": wallet_data["signature"],
            "user_public_key": wallet_data["user_public_key"],
        }


# ---------------------------------------------------------------------------
# Game / verifier side
# ---------------------------------------------------------------------------

class GameVerifier:
    """
    Game-side proof verifier.

    Initialise once per game backend. Call ``create_challenge()`` to generate
    a single-use challenge for a player, then ``verify()`` with their proof.

    Usage::

        verifier = GameVerifier(issuer_pubkey_pem=pem_bytes)
        challenge = verifier.create_challenge()
        # send challenge to player...
        result = verifier.verify(proof, required_age=18)
        if result:
            grant_access()
    """

    def __init__(self, issuer_pubkey_pem: str | bytes):
        if isinstance(issuer_pubkey_pem, str):
            issuer_pubkey_pem = issuer_pubkey_pem.encode()
        self._issuer_public_key: Ed25519PublicKey = serialization.load_pem_public_key(
            issuer_pubkey_pem
        )

    def create_challenge(self) -> str:
        """Generate a cryptographically random single-use challenge."""
        return base64.b64encode(os.urandom(32)).decode()

    def verify(
        self,
        proof: dict[str, Any],
        required_age: int = 18,
    ) -> VerificationResult:
        """
        Verify a player's proof in one call.

        Checks:
        1. Credential was signed by the trusted issuer.
        2. Challenge was signed by the credential's bound key (device check).
        3. The requested age claim is satisfied.

        Args:
            proof: The proof dict returned by ``PlayerWallet.prove()``.
            required_age: Minimum age threshold (13, 16, 18, 21, or 25).

        Returns:
            VerificationResult with ``allowed`` and ``reason``.
        """
        required_fields = [
            "challenge",
            "challenge_signature",
            "credential",
            "credential_signature",
            "user_public_key",
        ]
        for field_name in required_fields:
            if field_name not in proof:
                return VerificationResult(False, f"Missing field: {field_name}")

        # 1. Verify issuer signature on credential
        try:
            verify_credential_signature(
                proof["credential"],
                proof["credential_signature"],
                self._issuer_public_key,
            )
        except SignatureError:
            return VerificationResult(False, "Credential not from trusted issuer")
        except Exception:
            return VerificationResult(False, "Credential verification failed")

        # 2. Verify player owns this credential (device binding)
        try:
            verify_challenge_signature(
                proof["challenge"],
                proof["challenge_signature"],
                proof["user_public_key"],
            )
        except SignatureError:
            return VerificationResult(False, "Player does not own this credential")
        except Exception:
            return VerificationResult(False, "Challenge verification failed")

        # 3. Check age claim
        claims = proof.get("credential", {}).get("claims", {})
        age_field = f"age_over_{required_age}"
        if age_field not in claims:
            return VerificationResult(False, f"Unsupported age threshold: {required_age}")

        if not claims[age_field]:
            return VerificationResult(False, "Age requirement not met")

        return VerificationResult(True, "Verified")
