from __future__ import annotations

import base64
import binascii
import hashlib
import logging
import os
from datetime import datetime, timezone
from typing import Any, Dict, Iterable, Mapping, Optional, Tuple

from nacl.exceptions import BadSignatureError
from nacl.signing import SigningKey

logger = logging.getLogger(__name__)

from api.keys.store__keys import SigningKeyStore, TenantSigningKey, get_default_signing_store
from receipts.immutability import deep_freeze, ensure_frozen, immutable_receipts_enabled
from receipts.models import BaseReceipt, Ed25519Signature, canonical_body_bytes
from receipts.canonicalize import to_jcs_bytes

SIGNATURE_ALGORITHM = "ed25519"
_SIGNING_MODE_ENV = "LOOM_SIGNING_MODE"


def _signing_mode() -> str:
    token = os.getenv(_SIGNING_MODE_ENV, "prod").strip().lower()
    return token or "prod"


def _test_signing_enabled() -> bool:
    return _signing_mode() == "test"


class SigningUnavailableError(Exception):
    """
    Raised when receipt signing is required but unavailable.

    This error indicates that canonical Ed25519 signing cannot proceed,
    typically due to missing signing keys or infrastructure failure.
    Receipts MUST NOT fall back to hash-only mode.
    """

    def __init__(self, reason: str, tenant: Optional[str] = None):
        self.reason = reason
        self.tenant = tenant
        super().__init__(f"Signing unavailable for tenant '{tenant}': {reason}")


def _resolve_store(store: Optional[SigningKeyStore]) -> SigningKeyStore:
    return store or get_default_signing_store()


def _resolve_signing_record(
    store: SigningKeyStore,
    tenant: str,
    *,
    create_if_missing: bool,
) -> Optional[TenantSigningKey]:
    if create_if_missing:
        return store.ensure_key(tenant)
    return store.get(tenant)


def sign_receipt(
    receipt: BaseReceipt,
    *,
    store: Optional[SigningKeyStore] = None,
    tenant: Optional[str] = None,
    create_if_missing: bool = False,
    enforce: bool = True,
) -> Ed25519Signature:
    """
    Produce a detached Ed25519 signature for ``receipt``.

    Args:
        receipt: Receipt to sign.
        store: Optional signing key store to use.
        tenant: Tenant identifier; defaults to ``receipt.tenant``.
        create_if_missing: When True, generate a new signing key if one is absent.
        enforce: When True (default), raise SigningUnavailableError on failure.
                 When False, return None for backward compatibility (deprecated).

    Returns:
        Ed25519 signature envelope.

    Raises:
        SigningUnavailableError: When signing is unavailable and enforce=True.
    """
    target_tenant = tenant or receipt.tenant
    if not target_tenant:
        if enforce:
            raise SigningUnavailableError("No tenant identifier on receipt", tenant=target_tenant)
        return None

    store = _resolve_store(store)
    record = _resolve_signing_record(store, target_tenant, create_if_missing=create_if_missing)
    if record is None and _test_signing_enabled() and isinstance(store, SigningKeyStore):
        record = store.ensure_key(target_tenant)
    if record is None:
        if enforce:
            raise SigningUnavailableError("No signing key found for tenant", tenant=target_tenant)
        return None
    signing_key = record.signing_key()
    if signing_key is None:
        if enforce:
            raise SigningUnavailableError("Signing key is None", tenant=target_tenant)
        return None

    if immutable_receipts_enabled():
        ensure_frozen(receipt, context="receipt")

    payload = canonical_body_bytes(receipt, strip_signature_metadata=True)
    signature = signing_key.sign(payload).signature
    encoded = base64.b64encode(signature).decode("ascii")

    envelope = Ed25519Signature(
        kid=record.kid,
        signature_b64=encoded,
        signed_at=datetime.now(timezone.utc),
    )

    context = getattr(receipt, "context", None)
    if context:
        legacy_payload = {
            "alg": SIGNATURE_ALGORITHM.upper(),
            "kid": envelope.kid,
            "sig": envelope.signature_b64,
            "signed_at": envelope.signed_at.isoformat().replace("+00:00", "Z"),
        }
        if immutable_receipts_enabled():
            context_map = dict(context)
            legacy_block = dict(context_map.get("legacy") or {})
            legacy_block["signature"] = legacy_payload
            context_map["legacy"] = legacy_block
            object.__setattr__(receipt, "context", deep_freeze(context_map))
            ensure_frozen(receipt, context="receipt")
        elif isinstance(context, dict):
            legacy = context.setdefault("legacy", {})
            if isinstance(legacy, dict):
                legacy["signature"] = legacy_payload

    return envelope


def _iter_signatures(receipt: BaseReceipt) -> Iterable[Ed25519Signature]:
    signatures = getattr(receipt, "signatures", None)
    if signatures:
        for signature in signatures:
            if isinstance(signature, Ed25519Signature):
                yield signature
            elif isinstance(signature, Mapping):
                yield Ed25519Signature.model_validate(signature)
    legacy_block = getattr(receipt, "signature", None)
    if isinstance(legacy_block, Ed25519Signature):
        yield legacy_block
    elif isinstance(legacy_block, Mapping):
        yield Ed25519Signature.model_validate(legacy_block)


def verify_signature(
    receipt: BaseReceipt,
    *,
    store: Optional[SigningKeyStore] = None,
) -> bool:
    """
    Verify the signature block on a receipt.

    Args:
        receipt: Receipt possessing a signature block.
        store: Optional signing key store supplying verification keys.

    Returns:
        ``True`` when a signature is present and valid, otherwise ``False``.
    """
    store = _resolve_store(store)
    for envelope in _iter_signatures(receipt):
        if envelope.algorithm != SIGNATURE_ALGORITHM:
            logger.warning(
                "Signature algorithm mismatch: expected=%s, got=%s, kid=%s",
                SIGNATURE_ALGORITHM,
                envelope.algorithm,
                envelope.kid,
            )
            continue  # CVE-2 FIX: Continue checking other signatures instead of returning False
        record = store.get_by_kid(envelope.kid)
        if record is None:
            logger.debug("No signing key found for kid=%s", envelope.kid)
            continue
        verify_key = record.verify_key()
        if verify_key is None:
            logger.warning("Verification key is None for kid=%s", envelope.kid)
            continue
        payload = canonical_body_bytes(receipt, strip_signature_metadata=True)
        try:
            verify_key.verify(payload, base64.b64decode(envelope.signature_b64))
            logger.info("Signature verified successfully for kid=%s", envelope.kid)
            return True
        except BadSignatureError:
            logger.warning("Bad signature for kid=%s", envelope.kid)
            return False
        except (ValueError, binascii.Error) as e:
            # CVE-3 FIX: Only catch specific exceptions related to signature verification
            logger.warning("Signature verification error for kid=%s: %s", envelope.kid, str(e))
            return False
        # All other exceptions will propagate - they indicate system errors, not invalid signatures
    return False


# =============================================================================
# CVE-1 FIX: Comprehensive Cryptographic Verification
# =============================================================================

HASH_EXCLUDE_FIELDS = frozenset({"receipt_hash", "signature", "signatures", "verifier_signatures"})


def _strip_signature_metadata_from_payload(payload: Dict[str, Any]) -> None:
    """Remove signature metadata from payload dict in-place (for hash computation)."""
    for key in list(payload.keys()):
        if key in HASH_EXCLUDE_FIELDS:
            payload.pop(key, None)

    # Also strip from nested context.legacy if present
    context = payload.get("context")
    if isinstance(context, dict):
        legacy = context.get("legacy")
        if isinstance(legacy, dict):
            legacy.pop("signature", None)


def _canonicalize_payload(payload: Dict[str, Any]) -> Dict[str, Any]:
    """
    Canonicalize payload for hashing (deep copy with type normalization).

    This matches the canonicalization in receipts.models._canonicalize().
    """
    from datetime import datetime

    def _canon(obj: Any) -> Any:
        if isinstance(obj, datetime):
            # Normalize datetime to ISO 8601 string
            return obj.isoformat().replace("+00:00", "Z")
        elif isinstance(obj, dict):
            return {k: _canon(v) for k, v in obj.items()}
        elif isinstance(obj, (list, tuple)):
            return [_canon(item) for item in obj]
        elif isinstance(obj, (int, float, str, bool, type(None))):
            return obj
        else:
            # Handle enums and other types
            return str(obj)

    return _canon(payload)


def _extract_signatures_from_payload(payload: Dict[str, Any]) -> Iterable[Dict[str, Any]]:
    """Extract signature envelopes from a raw payload dict."""
    # Check modern 'signatures' field (list)
    signatures = payload.get("signatures")
    if signatures and isinstance(signatures, list):
        for sig in signatures:
            if isinstance(sig, dict):
                yield sig

    # Check legacy 'signature' field (single object)
    signature = payload.get("signature")
    if signature and isinstance(signature, dict):
        yield signature

    # Check context.legacy.signature
    context = payload.get("context")
    if isinstance(context, dict):
        legacy = context.get("legacy")
        if isinstance(legacy, dict):
            legacy_sig = legacy.get("signature")
            if isinstance(legacy_sig, dict):
                yield legacy_sig


def verify_receipt_crypto(
    payload: Dict[str, Any],
    *,
    store: Optional[SigningKeyStore] = None,
) -> Tuple[bool, str, Dict[str, Any]]:
    """
    Perform complete cryptographic verification of a receipt payload.

    This is the API-layer verification that runs BEFORE Guardian/OPA policy checks.
    It validates:
      1. Receipt structure (has required fields)
      2. JCS canonical digest integrity
      3. Ed25519 signature authenticity

    Args:
        payload: Receipt payload (dict from JSON)
        store: Optional signing key store

    Returns:
        Tuple of (is_valid, reason, details)
        - is_valid: True if all crypto checks pass
        - reason: Human-readable reason for failure (or "valid")
        - details: Dict with check results for auditing

    Example:
        >>> valid, reason, details = verify_receipt_crypto(receipt_payload)
        >>> if not valid:
        ...     return {"error": "invalid_signature", "reason": reason}, 403
    """
    store = _resolve_store(store)
    details: Dict[str, Any] = {
        "structure_valid": False,
        "hash_valid": False,
        "signature_valid": False,
        "signatures_checked": 0,
        "signatures_found": 0,
    }

    # Step 1: Validate basic structure
    if not isinstance(payload, dict):
        logger.error("verify_receipt_crypto: payload is not a dict, type=%s", type(payload))
        return False, "payload_not_dict", details

    if not payload.get("receipt_id"):
        logger.warning("verify_receipt_crypto: missing receipt_id")
        return False, "missing_receipt_id", details

    details["structure_valid"] = True
    details["receipt_id"] = payload.get("receipt_id")

    # Step 2: Extract and verify JCS digest
    claimed_hash = payload.get("receipt_hash")
    if claimed_hash:
        # Compute actual hash from canonical bytes
        try:
            # Create a copy for hashing (exclude signature fields)
            hash_payload = dict(payload)
            _strip_signature_metadata_from_payload(hash_payload)

            # Canonicalize and compute hash
            canonical_data = _canonicalize_payload(hash_payload)
            canonical_bytes = to_jcs_bytes(canonical_data)
            computed_hash = hashlib.sha256(canonical_bytes).hexdigest()

            if computed_hash != claimed_hash:
                logger.warning(
                    "verify_receipt_crypto: hash mismatch for receipt_id=%s, claimed=%s, computed=%s",
                    payload.get("receipt_id"),
                    claimed_hash[:16],
                    computed_hash[:16],
                )
                details["claimed_hash"] = claimed_hash[:16] + "..."
                details["computed_hash"] = computed_hash[:16] + "..."
                return False, "hash_mismatch", details

            details["hash_valid"] = True
            details["receipt_hash"] = claimed_hash[:16] + "..."
            logger.debug("verify_receipt_crypto: hash verified for receipt_id=%s", payload.get("receipt_id"))

        except Exception as e:
            logger.error("verify_receipt_crypto: hash computation failed: %s", str(e), exc_info=True)
            details["hash_error"] = str(e)
            return False, f"hash_computation_failed: {str(e)}", details
    else:
        # No hash present - this is acceptable for some receipt types
        logger.debug("verify_receipt_crypto: no receipt_hash present, skipping hash verification")
        details["hash_valid"] = None  # Not applicable

    # Step 3: Extract and verify signatures
    signature_envelopes = list(_extract_signatures_from_payload(payload))
    details["signatures_found"] = len(signature_envelopes)

    if not signature_envelopes:
        logger.warning("verify_receipt_crypto: no signatures found in receipt_id=%s", payload.get("receipt_id"))
        return False, "no_signatures_found", details

    # Compute canonical payload for signature verification (once)
    try:
        verify_payload = dict(payload)
        _strip_signature_metadata_from_payload(verify_payload)
        canonical_data = _canonicalize_payload(verify_payload)
        canonical_bytes = to_jcs_bytes(canonical_data)
    except Exception as e:
        logger.error("verify_receipt_crypto: failed to canonicalize payload: %s", str(e), exc_info=True)
        details["canonicalization_error"] = str(e)
        return False, f"canonicalization_failed: {str(e)}", details

    # Verify at least one signature
    verified_signatures = []
    failed_signatures = []

    for sig_envelope in signature_envelopes:
        details["signatures_checked"] += 1

        try:
            # Validate signature envelope structure
            algorithm = sig_envelope.get("algorithm", "ed25519")
            kid = sig_envelope.get("kid")
            signature_b64 = sig_envelope.get("signature_b64")

            if not kid:
                logger.debug("verify_receipt_crypto: signature missing kid")
                failed_signatures.append({"reason": "missing_kid"})
                continue

            if not signature_b64:
                logger.debug("verify_receipt_crypto: signature missing signature_b64")
                failed_signatures.append({"kid": kid, "reason": "missing_signature_b64"})
                continue

            if algorithm != SIGNATURE_ALGORITHM:
                logger.warning(
                    "verify_receipt_crypto: signature algorithm mismatch: expected=%s, got=%s, kid=%s",
                    SIGNATURE_ALGORITHM,
                    algorithm,
                    kid,
                )
                failed_signatures.append({"kid": kid, "reason": "algorithm_mismatch", "algorithm": algorithm})
                continue

            # Retrieve verification key
            record = store.get_by_kid(kid)
            if record is None:
                logger.debug("verify_receipt_crypto: no signing key found for kid=%s", kid)
                failed_signatures.append({"kid": kid, "reason": "key_not_found"})
                continue

            verify_key = record.verify_key()
            if verify_key is None:
                logger.warning("verify_receipt_crypto: verification key is None for kid=%s", kid)
                failed_signatures.append({"kid": kid, "reason": "verify_key_none"})
                continue

            # Verify the signature
            try:
                signature_bytes = base64.b64decode(signature_b64, validate=True)
                verify_key.verify(canonical_bytes, signature_bytes)

                logger.info("verify_receipt_crypto: signature verified successfully for kid=%s", kid)
                verified_signatures.append({"kid": kid, "algorithm": algorithm})

            except BadSignatureError:
                logger.warning("verify_receipt_crypto: bad signature for kid=%s", kid)
                failed_signatures.append({"kid": kid, "reason": "bad_signature"})

            except (ValueError, binascii.Error) as e:
                logger.warning("verify_receipt_crypto: signature decode error for kid=%s: %s", kid, str(e))
                failed_signatures.append({"kid": kid, "reason": "decode_error", "error": str(e)})

        except Exception as e:
            logger.error("verify_receipt_crypto: unexpected error verifying signature: %s", str(e), exc_info=True)
            failed_signatures.append({"reason": "unexpected_error", "error": str(e)})

    # At least one signature must be valid
    details["verified_signatures"] = verified_signatures
    details["failed_signatures"] = failed_signatures

    if verified_signatures:
        details["signature_valid"] = True
        logger.info(
            "verify_receipt_crypto: receipt_id=%s verified successfully (%d/%d signatures valid)",
            payload.get("receipt_id"),
            len(verified_signatures),
            len(signature_envelopes),
        )
        return True, "valid", details
    else:
        logger.warning(
            "verify_receipt_crypto: receipt_id=%s failed verification (0/%d signatures valid)",
            payload.get("receipt_id"),
            len(signature_envelopes),
        )
        return False, "all_signatures_invalid", details
