"""Manifest signing and verification for Mira."""

from __future__ import annotations

import base64
import datetime as dt
import hashlib
import json
from typing import Any


def create_signature(
    manifest: dict[str, Any],
    private_key_pem: str,
    embed_public_key: bool = False,
) -> dict[str, Any]:
    """Create a signature dictionary for a manifest.

    Args:
        manifest: Manifest data to sign.
        private_key_pem: PEM-encoded private key string.
        embed_public_key: Whether to embed the public key in the signature.

    Returns:
        Signature dictionary.
    """
    private_key = _load_private_key(private_key_pem)
    public_key = private_key.public_key()

    manifest_bytes = canonicalize_manifest(manifest)
    signature = private_key.sign(manifest_bytes)
    public_bytes = _public_key_bytes(public_key)
    key_id = hashlib.sha256(public_bytes).hexdigest()

    signed_at = dt.datetime.now(dt.timezone.utc).replace(microsecond=0)
    signature_data: dict[str, Any] = {
        "algorithm": "ed25519",
        "key_id": key_id,
        "signature": base64.b64encode(signature).decode("ascii"),
        "manifest_hash": hashlib.sha256(manifest_bytes).hexdigest(),
        "hash_algorithm": "sha256",
        "signed_at": signed_at.isoformat().replace("+00:00", "Z"),
    }

    if embed_public_key:
        signature_data["public_key"] = public_bytes.decode("ascii")

    return signature_data


def verify_signature(
    manifest: dict[str, Any],
    signature_data: dict[str, Any],
    public_key_pem: str | None = None,
) -> None:
    """Verify a signature against a manifest.

    Args:
        manifest: Manifest data to verify.
        signature_data: Signature dictionary.
        public_key_pem: Optional PEM-encoded public key string.

    Raises:
        RuntimeError: If verification fails.
    """
    signature_b64 = signature_data.get("signature")
    if not signature_b64:
        raise RuntimeError("signature.json missing signature field")

    signature = base64.b64decode(signature_b64.encode("ascii"))
    public_key_pem = public_key_pem or signature_data.get("public_key")
    if not public_key_pem:
        raise RuntimeError("Public key required to verify signature")

    public_key = _load_public_key(public_key_pem)
    manifest_bytes = canonicalize_manifest(manifest)

    expected_hash = signature_data.get("manifest_hash")
    if expected_hash:
        actual_hash = hashlib.sha256(manifest_bytes).hexdigest()
        if actual_hash != expected_hash:
            raise RuntimeError("Manifest hash mismatch")

    try:
        public_key.verify(signature, manifest_bytes)
    except Exception as exc:  # pragma: no cover - cryptography errors
        raise RuntimeError("Signature verification failed") from exc


def canonicalize_manifest(manifest: dict[str, Any]) -> bytes:
    """Return canonical JSON bytes for a manifest."""
    return json.dumps(manifest, sort_keys=True, separators=(",", ":")).encode("utf-8")


def generate_keypair() -> tuple[str, str]:
    """Generate a new Ed25519 keypair and return PEM strings."""
    private_key = _generate_private_key()
    public_key = private_key.public_key()
    return _private_key_bytes(private_key).decode("ascii"), _public_key_bytes(
        public_key
    ).decode("ascii")


def _require_crypto():
    try:
        from cryptography.hazmat.primitives.asymmetric.ed25519 import (
            Ed25519PrivateKey,
            Ed25519PublicKey,
        )
        from cryptography.hazmat.primitives import serialization
    except ModuleNotFoundError as exc:
        raise RuntimeError(
            "Signing requires cryptography. Install with: pip install mira[crypto]"
        ) from exc
    return Ed25519PrivateKey, Ed25519PublicKey, serialization


def _generate_private_key():
    Ed25519PrivateKey, _, _ = _require_crypto()
    return Ed25519PrivateKey.generate()


def _load_private_key(pem: str):
    Ed25519PrivateKey, _, serialization = _require_crypto()
    return serialization.load_pem_private_key(
        pem.encode("ascii"), password=None
    )


def _load_public_key(pem: str):
    _, Ed25519PublicKey, serialization = _require_crypto()
    return serialization.load_pem_public_key(pem.encode("ascii"))


def _private_key_bytes(private_key) -> bytes:
    _, _, serialization = _require_crypto()
    return private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption(),
    )


def _public_key_bytes(public_key) -> bytes:
    _, _, serialization = _require_crypto()
    return public_key.public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo,
    )
