"""Policy tamper resistance via HMAC-SHA256 signing (v1) and ed25519 signing (v2).

PolicySigner  -- HMAC-SHA256 using stdlib only (zero extra dependencies).
PolicySignerV2 -- ed25519 using the 'cryptography' package (conditional import).

If 'cryptography' is not installed, _ED25519_AVAILABLE is False and
PolicySignerV2 raises RuntimeError on any operation that requires the key.
"""

from __future__ import annotations

import base64
import hashlib
import hmac
import logging
import os
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from veronica_core.security.key_providers import KeyProvider

logger = logging.getLogger(__name__)

_ENV_KEY_VAR = "VERONICA_POLICY_KEY"

# ---------------------------------------------------------------------------
# Conditional import: ed25519 via cryptography package
# ---------------------------------------------------------------------------

try:
    from cryptography.hazmat.primitives.asymmetric.ed25519 import (
        Ed25519PrivateKey,
    )
    from cryptography.hazmat.primitives.serialization import (
        Encoding,
        NoEncryption,
        PrivateFormat,
        PublicFormat,
        load_pem_private_key,
        load_pem_public_key,
    )

    _ED25519_AVAILABLE = True
except ImportError:  # pragma: no cover
    _ED25519_AVAILABLE = False


def _derive_test_key() -> bytes:
    """Return SHA256(b'veronica-dev-key') as the built-in test key.

    This key is publicly known and must never be used in production.
    """
    return hashlib.sha256(b"veronica-dev-key").digest()


def _load_key() -> bytes:
    """Return signing key from env var (hex) or the built-in test key.

    In production, set ``VERONICA_POLICY_KEY`` to a securely generated
    32-byte hex string, e.g.::

        python -c "import secrets; print(secrets.token_hex(32))"

    For secret storage, prefer AWS Secrets Manager, Azure Key Vault, or
    HashiCorp Vault over environment variables where possible.

    Raises:
        RuntimeError: If the key is not set and the security level is not DEV.
    """
    hex_key = os.environ.get(_ENV_KEY_VAR)
    if hex_key:
        if len(hex_key) > 2048:
            raise RuntimeError(
                f"{_ENV_KEY_VAR} is too long ({len(hex_key)} hex chars). "
                "Maximum is 2048 hex chars (1024 bytes)."
            )
        try:
            key = bytes.fromhex(hex_key)
        except ValueError:
            raise RuntimeError(
                f"{_ENV_KEY_VAR} is not valid hex. "
                'Provide a hex-encoded key, e.g.: python -c "import secrets; print(secrets.token_hex(32))"'
            ) from None
        if len(key) < 32:
            raise RuntimeError(
                f"{_ENV_KEY_VAR} is too short ({len(key)} bytes). "
                "A minimum of 32 bytes (64 hex chars) is required for HMAC-SHA256. "
                'Generate a key with: python -c "import secrets; print(secrets.token_hex(32))"'
            )
        return key

    from veronica_core.security.security_level import SecurityLevel, get_security_level

    if get_security_level() != SecurityLevel.DEV:
        raise RuntimeError(
            f"{_ENV_KEY_VAR} is not set. "
            "A signing key is required in non-DEV environments. "
            "Set the env var to a 32-byte hex string before deploying."
        )

    logger.warning(
        "policy_signing: %s is not set; falling back to built-in development "
        "key. This key is publicly known -- set the env var before deploying.",
        _ENV_KEY_VAR,
    )
    return _derive_test_key()


class PolicySigner:
    """Signs and verifies policy files with HMAC-SHA256.

    Args:
        key: Raw signing key bytes. If None, the key is loaded from
             ``VERONICA_POLICY_KEY`` env var (hex-encoded) or derived
             from the built-in test key.
    """

    def __init__(self, key: bytes | None = None) -> None:
        self._key: bytes = key if key is not None else _load_key()

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    @staticmethod
    def _normalize(raw: bytes) -> bytes:
        """Normalize line endings to LF for cross-platform signature stability."""
        return raw.replace(b"\r\n", b"\n")

    def sign(self, policy_path: Path, policy_bytes: bytes | None = None) -> str:
        """Return hex-encoded HMAC-SHA256 of *policy_path* content.

        Args:
            policy_path: Path to the YAML policy file to sign.
            policy_bytes: Pre-read policy bytes.  When provided the file is NOT
                          re-read from disk (TOCTOU prevention).

        Returns:
            Hex string of the HMAC-SHA256 digest.
        """
        raw = policy_bytes if policy_bytes is not None else policy_path.read_bytes()
        content = self._normalize(raw)
        mac = hmac.new(self._key, content, hashlib.sha256)
        return mac.hexdigest()

    def verify(
        self,
        policy_path: Path,
        sig_path: Path,
        policy_bytes: bytes | None = None,
    ) -> bool:
        """Compare stored signature against freshly computed HMAC.

        Args:
            policy_path: Path to the YAML policy file.
            sig_path: Path to the ``.sig`` file containing the hex digest.
            policy_bytes: Pre-read policy bytes.  When provided the file is NOT
                          re-read from disk (TOCTOU prevention, C-1 fix).

        Returns:
            True if the signature matches, False otherwise.
            Returns False if either file cannot be read.
        """
        try:
            raw = policy_bytes if policy_bytes is not None else policy_path.read_bytes()
            content = self._normalize(raw)
            stored_sig = sig_path.read_text(encoding="utf-8").strip()
        except OSError:
            return False

        expected = hmac.new(self._key, content, hashlib.sha256).hexdigest()
        return hmac.compare_digest(expected, stored_sig)

    def sign_bytes(self, data: bytes) -> str:
        """Return hex-encoded HMAC-SHA256 of arbitrary bytes.

        Used for signing audit log entries and other raw byte payloads.

        Args:
            data: Raw bytes to sign.

        Returns:
            Hex-encoded HMAC-SHA256 digest string.
        """
        mac = hmac.new(self._key, data, hashlib.sha256)
        return mac.hexdigest()

    def sign_bundle(self, bundle: "PolicyBundle") -> str:  # noqa: F821
        """Return hex-encoded HMAC-SHA256 of canonical bundle representation.

        Signs a deterministic canonical string that includes both metadata
        (policy_id, epoch, issuer) and the content hash (covering all rules).
        This prevents an attacker from altering metadata fields while keeping
        the same content hash and signature.

        Import is deferred to method level to avoid circular imports.

        Args:
            bundle: The PolicyBundle to sign.

        Returns:
            Hex-encoded HMAC-SHA256 digest of the canonical bundle representation.
        """
        # Local import to avoid circular dependency between security and policy packages.
        from veronica_core.policy.bundle import PolicyBundle  # noqa: F401

        # H1: canonical representation includes all metadata + content hash.
        # Require content_hash consistency before signing (fail-closed).
        computed_hash = bundle.content_hash()
        if bundle.metadata.content_hash and not bundle.verify_content_hash():
            raise ValueError(
                "sign_bundle: bundle.verify_content_hash() failed -- "
                "declared content_hash does not match computed hash"
            )
        # H3: Reject newlines in string fields to prevent canonical form
        # injection (attacker crafting policy_id="foo\nepoch=999" to collide).
        for field_name, field_val in [
            ("policy_id", bundle.metadata.policy_id),
            ("issuer", bundle.metadata.issuer),
            ("version", bundle.metadata.version),
        ]:
            if "\n" in str(field_val) or "\r" in str(field_val):
                raise ValueError(
                    f"sign_bundle: {field_name} contains newline characters "
                    f"(canonical form injection)"
                )
        canonical = (
            f"policy_id={bundle.metadata.policy_id}\n"
            f"epoch={bundle.metadata.epoch}\n"
            f"issuer={bundle.metadata.issuer}\n"
            f"version={bundle.metadata.version}\n"
            f"content_hash={computed_hash}\n"
        )
        mac = hmac.new(self._key, canonical.encode("utf-8"), hashlib.sha256)
        return mac.hexdigest()

    def verify_bundle(self, bundle: "PolicyBundle") -> bool:  # noqa: F821
        """Return True if the bundle signature matches the computed HMAC.

        Uses hmac.compare_digest for constant-time comparison to prevent
        timing side-channel attacks.  Fail-closed: any error returns False.

        Args:
            bundle: The PolicyBundle to verify.

        Returns:
            True if the signature is valid, False otherwise (including on error).
        """
        try:
            if not bundle.signature:
                return False
            expected = self.sign_bundle(bundle)
            return hmac.compare_digest(expected, bundle.signature)
        except Exception:
            return False


# ---------------------------------------------------------------------------
# PolicySignerV2 -- ed25519 asymmetric signing
# ---------------------------------------------------------------------------

_DEFAULT_PUBLIC_KEY_PATH = Path(__file__).parents[4] / "policies" / "public_key.pem"
_SIG_V2_SUFFIX = ".sig.v2"


class PolicySignerV2:
    """Signs and verifies policy files using ed25519 (asymmetric).

    Requires the 'cryptography' package.  If it is not installed,
    ``_ED25519_AVAILABLE`` is False and all methods raise ``RuntimeError``.

    The public key is loaded from *public_key_path* (defaults to
    ``policies/public_key.pem`` relative to the repository root).
    The private key is never stored on disk in production; it is passed
    in-memory to ``sign()``.

    Args:
        public_key_path: Path to the PEM-encoded ed25519 public key.
                         Defaults to ``policies/public_key.pem``.
    """

    def __init__(
        self,
        public_key_path: Path | None = None,
        key_provider: "KeyProvider | None" = None,
    ) -> None:
        if key_provider is not None:
            self._key_provider = key_provider
            self._public_key_path = public_key_path or _DEFAULT_PUBLIC_KEY_PATH
        else:
            from veronica_core.security.key_providers import FileKeyProvider

            self._public_key_path = public_key_path or _DEFAULT_PUBLIC_KEY_PATH
            self._key_provider = FileKeyProvider(self._public_key_path)

    # ------------------------------------------------------------------
    # Properties
    # ------------------------------------------------------------------

    @property
    def public_key_path(self) -> Path:
        """Return the path to the public key file."""
        return self._public_key_path

    @property
    def mode(self) -> str:
        """Return 'ed25519' if available, else 'unavailable'."""
        return "ed25519" if _ED25519_AVAILABLE else "unavailable"

    @staticmethod
    def is_available() -> bool:
        """Return True if the 'cryptography' package is installed."""
        return _ED25519_AVAILABLE

    # ------------------------------------------------------------------
    # Key generation (dev / CI use only)
    # ------------------------------------------------------------------

    @staticmethod
    def generate_dev_keypair() -> tuple[bytes, bytes]:
        """Generate a fresh ed25519 keypair for development / testing.

        Returns:
            (private_key_pem, public_key_pem) as bytes.

        Raises:
            RuntimeError: If 'cryptography' is not installed.
        """
        if not _ED25519_AVAILABLE:
            raise RuntimeError(
                "cryptography package is required for ed25519 signing. "
                "Install it with: pip install cryptography"
            )
        key = Ed25519PrivateKey.generate()
        priv_pem = key.private_bytes(Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())
        pub_pem = key.public_key().public_bytes(
            Encoding.PEM, PublicFormat.SubjectPublicKeyInfo
        )
        return priv_pem, pub_pem

    # ------------------------------------------------------------------
    # Sign
    # ------------------------------------------------------------------

    @staticmethod
    def _normalize(raw: bytes) -> bytes:
        """Normalize line endings to LF for cross-platform signature stability."""
        return raw.replace(b"\r\n", b"\n")

    def sign(self, policy_path: Path, private_key_pem: bytes) -> bytes:
        """Sign *policy_path* with the given private key.

        The base64-encoded signature is written to
        ``<policy_path>.sig.v2`` and also returned as raw bytes.

        Args:
            policy_path: Path to the YAML policy file.
            private_key_pem: PEM-encoded ed25519 private key bytes.

        Returns:
            Raw 64-byte ed25519 signature.

        Raises:
            RuntimeError: If 'cryptography' is not installed.
        """
        if not _ED25519_AVAILABLE:
            raise RuntimeError("cryptography package is required for ed25519 signing.")
        content = self._normalize(policy_path.read_bytes())
        private_key = load_pem_private_key(private_key_pem, password=None)
        raw_sig: bytes = private_key.sign(content)  # type: ignore[attr-defined]

        sig_path = Path(str(policy_path) + _SIG_V2_SUFFIX)
        sig_path.write_text(
            base64.b64encode(raw_sig).decode("ascii") + "\n", encoding="utf-8"
        )

        return raw_sig

    # ------------------------------------------------------------------
    # Verify
    # ------------------------------------------------------------------

    def verify(
        self,
        policy_path: Path,
        sig_path: Path,
        policy_bytes: bytes | None = None,
    ) -> bool:
        """Verify the ed25519 signature stored in *sig_path*.

        Args:
            policy_path: Path to the YAML policy file.
            sig_path: Path to the ``.sig.v2`` file (base64-encoded sig).
            policy_bytes: Pre-read policy bytes.  When provided the file is NOT
                          re-read from disk (TOCTOU prevention, C-1 fix).

        Returns:
            True if the signature is valid, False otherwise.
        """
        if not _ED25519_AVAILABLE:
            logger.warning(
                "policy_signing_v2: cryptography not available; cannot verify ed25519 sig"
            )
            return False

        try:
            raw = policy_bytes if policy_bytes is not None else policy_path.read_bytes()
            content = self._normalize(raw)
            sig_b64 = sig_path.read_text(encoding="utf-8").strip()
            raw_sig = base64.b64decode(sig_b64)
            pub_pem = self._key_provider.get_public_key_pem()
        except Exception as exc:
            # Broad catch: key_provider is an external Protocol implementation
            # that may raise any exception type (KeyError, TypeError,
            # AttributeError, ConnectionError, etc.).  All failures during
            # data loading must result in verify() returning False.
            logger.debug(
                "policy_signing_v2: failed to load verification data (%s)",
                type(exc).__name__,
            )
            return False

        try:
            public_key = load_pem_public_key(pub_pem)
            public_key.verify(raw_sig, content)  # type: ignore[attr-defined]
            return True
        except Exception as exc:
            # cryptography raises InvalidSignature on mismatch; log the type
            # but not the content to avoid leaking key material.
            logger.debug(
                "policy_signing_v2: verification failed (%s)", type(exc).__name__
            )
            return False
