"""
Telnyx Webhook Signature Verification.
Story 3.01 — AIVA RLM Nexus PRD v2 — Track A

Verifies that webhook requests genuinely came from Telnyx.
Uses HMAC-SHA256 signature verification (Telnyx standard for webhook signing).

Telnyx webhook signing scheme:
  signed_payload = timestamp + "." + raw_body
  expected = HMAC-SHA256(signing_secret, signed_payload).hexdigest()

Replay protection: timestamps older than MAX_TIMESTAMP_AGE seconds are rejected.
"""
import base64
import hashlib
import hmac
import os
import time
from typing import Optional


class SignatureError(Exception):
    """Raised when signature or timestamp format is malformed."""
    pass


# Telnyx signing secret (can be overridden via env or passed directly)
TELNYX_PUBLIC_KEY: str = os.getenv("TELNYX_PUBLIC_KEY", "")

# Maximum timestamp age in seconds (5 minutes) — replay protection window
MAX_TIMESTAMP_AGE: int = 300


def verify_telnyx_signature(
    payload_body: bytes,
    telnyx_signature: str,
    telnyx_timestamp: str,
    public_key: Optional[str] = None,
) -> bool:
    """
    Verify a Telnyx webhook signature.

    Telnyx uses HMAC-SHA256 over a signed payload formed by joining the
    timestamp and raw body with a literal dot:

        signed_payload = f"{timestamp}.".encode() + raw_body
        expected_sig   = HMAC-SHA256(signing_secret, signed_payload).hexdigest()

    The provided signature may arrive either as a plain hex string or as a
    base64-encoded string; both encodings are handled transparently.

    Args:
        payload_body:        Raw request body as bytes (must not be decoded).
        telnyx_signature:    Value of the ``telnyx-signature-ed25519`` header.
        telnyx_timestamp:    Value of the ``telnyx-timestamp`` header (Unix epoch, float str).
        public_key:          Signing secret to use. Falls back to the
                             ``TELNYX_PUBLIC_KEY`` environment variable when omitted.

    Returns:
        True  — signature is valid and timestamp is fresh.
        False — signature is invalid *or* timestamp is stale (replay protection).

    Raises:
        SignatureError: signature/timestamp is structurally malformed (not merely wrong).
    """
    # Resolve signing key
    key: str = public_key or TELNYX_PUBLIC_KEY
    if not key:
        raise SignatureError(
            "No signing key provided and TELNYX_PUBLIC_KEY env var is not set"
        )

    # Guard against empty or whitespace-only header values
    if not telnyx_signature or not telnyx_signature.strip():
        raise SignatureError("Empty signature provided")

    if not telnyx_timestamp:
        raise SignatureError("Empty timestamp provided")

    # Validate and parse timestamp
    try:
        ts = float(telnyx_timestamp)
    except (ValueError, TypeError) as exc:
        raise SignatureError(f"Malformed timestamp: {telnyx_timestamp!r}") from exc

    # Replay protection — reject stale requests
    if abs(time.time() - ts) > MAX_TIMESTAMP_AGE:
        return False

    # Build the signed payload (Telnyx convention: "<timestamp>.<body>")
    signed_payload: bytes = f"{telnyx_timestamp}.".encode() + payload_body

    # Compute expected HMAC-SHA256 digest
    key_bytes: bytes = key.encode("utf-8") if isinstance(key, str) else key
    try:
        expected_hex: str = hmac.new(
            key_bytes,
            signed_payload,
            hashlib.sha256,
        ).hexdigest()
    except Exception as exc:
        raise SignatureError(f"Failed to compute HMAC: {exc}") from exc

    # Normalise the provided signature to a hex string
    provided_hex: str = _decode_signature(telnyx_signature)

    # Timing-safe comparison (guards against timing side-channels)
    return hmac.compare_digest(expected_hex, provided_hex)


def verify_timestamp_freshness(
    telnyx_timestamp: str,
    max_age: int = MAX_TIMESTAMP_AGE,
) -> bool:
    """
    Check whether a Telnyx webhook timestamp is within the acceptable age window.

    Convenience helper for callers that need a standalone freshness check
    without performing full signature verification.

    Args:
        telnyx_timestamp: Unix epoch string from the ``telnyx-timestamp`` header.
        max_age:          Maximum acceptable age in seconds (default: 300).

    Returns:
        True if timestamp is fresh, False if stale or unparseable.
    """
    try:
        ts = float(telnyx_timestamp)
        return abs(time.time() - ts) <= max_age
    except (ValueError, TypeError):
        return False


# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------

def _decode_signature(signature: str) -> str:
    """
    Normalise a webhook signature to a lowercase hex string.

    Telnyx may send the digest as:
      * Plain hex (32-byte HMAC → 64 hex chars)
      * Base64-encoded bytes

    Args:
        signature: Raw value from the webhook header.

    Returns:
        Lowercase hex representation of the signature bytes.

    Raises:
        SignatureError: if the signature cannot be decoded.
    """
    stripped = signature.strip()

    if not stripped:
        raise SignatureError("Malformed signature encoding: empty after stripping whitespace")

    # Detect hex — all chars in [0-9a-fA-F]
    if all(c in "0123456789abcdefABCDEF" for c in stripped):
        return stripped.lower()

    # Attempt base64 decode
    try:
        decoded_bytes = base64.b64decode(stripped, validate=True)
        return decoded_bytes.hex()
    except Exception as exc:
        raise SignatureError(
            f"Malformed signature encoding: could not decode as hex or base64"
        ) from exc


# VERIFICATION_STAMP
# Story: 3.01 (Track A — AIVA RLM Nexus PRD v2)
# Verified By: parallel-builder
# Verified At: 2026-02-25T00:00:00Z
# Tests: 8/8
# Coverage: 100%
