"""
Device binding — ties private keys to the physical hardware they were created on.

The private key is encrypted with an AES-256-GCM key derived from a device
fingerprint. If the encrypted key file is copied to another machine,
decryption fails because the fingerprint differs.

On supported hardware (Pi Zero, phones), this can be extended to use
TPM chips or OS secure enclaves for true hardware-backed key storage.
"""

from __future__ import annotations

import base64
import hashlib
import logging
import os
import platform
import uuid

from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

from .exceptions import DeviceBindingError
from .storage import FileStorage

logger = logging.getLogger(__name__)


def get_device_fingerprint() -> str:
    """
    Generate a stable SHA-256 fingerprint from hardware identifiers.

    Combines hostname, architecture, processor, and MAC address.
    On Raspberry Pi, also reads the unique CPU serial from /proc/cpuinfo.
    """
    identifiers = [
        platform.node(),
        platform.machine(),
        platform.processor(),
        str(uuid.getnode()),
    ]

    # On Pi / Linux, add CPU serial for stronger binding
    try:
        with open("/proc/cpuinfo", "r") as f:
            for line in f:
                if line.startswith("Serial"):
                    identifiers.append(line.split(":")[1].strip())
                    break
    except FileNotFoundError:
        pass

    combined = "|".join(identifiers)
    return hashlib.sha256(combined.encode()).hexdigest()


def get_device_id() -> str:
    """Short 16-character device identifier for display."""
    return get_device_fingerprint()[:16]


def _derive_aes_key(fingerprint: str) -> bytes:
    """Derive a 32-byte AES key from the device fingerprint."""
    return hashlib.sha256(fingerprint.encode()).digest()


def generate_bound_keypair(
    user_id: str,
    storage: FileStorage,
) -> str:
    """
    Generate an Ed25519 keypair and encrypt the private key with
    a device-derived AES key.

    Returns the PEM-encoded public key as a string.
    """
    private_key = Ed25519PrivateKey.generate()
    public_key = private_key.public_key()

    # Serialize private key to raw 32-byte seed
    priv_raw = private_key.private_bytes(
        serialization.Encoding.Raw,
        serialization.PrivateFormat.Raw,
        serialization.NoEncryption(),
    )

    # Encrypt with device-bound AES key
    fingerprint = get_device_fingerprint()
    aes_key = _derive_aes_key(fingerprint)
    nonce = os.urandom(12)
    aesgcm = AESGCM(aes_key)
    encrypted_priv = aesgcm.encrypt(nonce, priv_raw, None)

    # Store encrypted key
    bound_key_data = {
        "device_id": get_device_id(),
        "nonce": base64.b64encode(nonce).decode(),
        "encrypted_key": base64.b64encode(encrypted_priv).decode(),
    }
    storage.write_json(f"device_key_{user_id}.json", bound_key_data, secret=True)

    # Store public key (not secret)
    pub_pem = public_key.public_bytes(
        serialization.Encoding.PEM,
        serialization.PublicFormat.SubjectPublicKeyInfo,
    )
    storage.write_bytes(f"user_{user_id}_public.pem", pub_pem, secret=False)

    logger.info("Keypair generated and bound to device %s for user %s", get_device_id(), user_id)
    return pub_pem.decode()


def load_bound_private_key(
    user_id: str,
    storage: FileStorage,
) -> Ed25519PrivateKey:
    """
    Load and decrypt the device-bound private key.

    Raises DeviceBindingError if run on a different device.
    """
    bound_key_data = storage.read_json(f"device_key_{user_id}.json")

    current_id = get_device_id()
    stored_id = bound_key_data.get("device_id", "")
    if stored_id != current_id:
        raise DeviceBindingError(
            f"Key is bound to device {stored_id}, current device is {current_id}"
        )

    fingerprint = get_device_fingerprint()
    aes_key = _derive_aes_key(fingerprint)
    nonce = base64.b64decode(bound_key_data["nonce"])
    encrypted_priv = base64.b64decode(bound_key_data["encrypted_key"])

    aesgcm = AESGCM(aes_key)
    try:
        priv_raw = aesgcm.decrypt(nonce, encrypted_priv, None)
    except Exception as exc:
        raise DeviceBindingError("Decryption failed — device mismatch or tampered key") from exc

    return Ed25519PrivateKey.from_private_bytes(priv_raw)
