"""
StandShare webhook signature verifier — Python 3 reference implementation

Signature algorithm:
  Header:      X-StandShare-Signature: t=<unix_seconds>,v1=<hmac_sha256_hex>
  Signed data: "{t}.{raw_body}" (the exact raw body string, UTF-8)
  Key:         your signing secret (the full "whsec_..." string, UTF-8 bytes)
  Digest:      HMAC-SHA256, lowercase hex
  Tolerance:   |now − t| must be <= 300 seconds (5 minutes)
  Comparison:  timing-safe (hmac.compare_digest on hex strings)

Usage:
  python3 verify_webhook.py   # runs the built-in self-test

Requirements: Python 3.6+ (standard library only — no pip install needed)

Test vectors (from packages/api/src/fixtures/webhook-signature-fixture.ts in
GraditiPro/StandShare — the single source of truth):
  secret:    "whsec_testonly_AAAA..."
  timestamp: 1749877200
  body:      '{"type":"event.created","data":{"id":"evt_001"}}'
  expected:  "bffa13b5964bd097b5cca399ef2386585906f5af213ddcba2a2f1a9b41399680"
  header:    "t=1749877200,v1=bffa13b5964bd097b5cca399ef2386585906f5af213ddcba2a2f1a9b41399680"
"""

from __future__ import annotations

import hashlib
import hmac
import time
from dataclasses import dataclass
from typing import Optional

# ── Constants ────────────────────────────────────────────────────────────────

TOLERANCE_SECONDS = 300  # 5 minutes

# ── Types ────────────────────────────────────────────────────────────────────

@dataclass(frozen=True)
class VerifyResult:
    ok: bool
    reason: Optional[str] = None  # "missing_header" | "malformed_header" | "timestamp_expired" | "invalid_signature"


VERIFY_OK = VerifyResult(ok=True)

# ── Core ─────────────────────────────────────────────────────────────────────

def verify_webhook_signature(
    secret: str,
    raw_body: str,
    signature_header: Optional[str],
    *,
    _now: Optional[int] = None,  # overridable for tests
) -> VerifyResult:
    """
    Verify the X-StandShare-Signature header on an incoming webhook delivery.

    Args:
        secret:           Your webhook signing secret (the full "whsec_..." value).
        raw_body:         The request body exactly as received. Do NOT parse or
                          re-serialize it — use the raw bytes decoded as UTF-8.
        signature_header: Value of the X-StandShare-Signature header (str or None).

    Returns:
        VerifyResult(ok=True) on success, or VerifyResult(ok=False, reason=...).
    """
    # 1. Missing header
    if not signature_header or not signature_header.strip():
        return VerifyResult(ok=False, reason="missing_header")

    # 2. Parse t= and v1= from the header
    fields: dict[str, str] = {}
    for part in signature_header.split(","):
        if "=" not in part:
            continue
        k, _, v = part.partition("=")
        k = k.strip()
        v = v.strip()
        if k:
            fields[k] = v

    t_str = fields.get("t")
    v1 = fields.get("v1")
    if not t_str or not v1:
        return VerifyResult(ok=False, reason="malformed_header")

    try:
        timestamp = int(t_str)
    except ValueError:
        return VerifyResult(ok=False, reason="malformed_header")

    if len(v1) != 64 or not all(c in "0123456789abcdef" for c in v1):
        return VerifyResult(ok=False, reason="malformed_header")

    # 3. Timestamp tolerance check
    now = _now if _now is not None else int(time.time())
    if abs(now - timestamp) > TOLERANCE_SECONDS:
        return VerifyResult(ok=False, reason="timestamp_expired")

    # 4. Recompute HMAC-SHA256 over "{timestamp}.{raw_body}"
    signed_data = f"{timestamp}.{raw_body}".encode("utf-8")
    expected = hmac.new(
        secret.encode("utf-8"),
        signed_data,
        hashlib.sha256,
    ).hexdigest()

    # 5. Timing-safe comparison
    if not hmac.compare_digest(expected, v1):
        return VerifyResult(ok=False, reason="invalid_signature")

    return VERIFY_OK


# ── Self-test ────────────────────────────────────────────────────────────────
# Run:  python3 verify_webhook.py

_PINNED = dict(
    secret="whsec_testonly_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
    timestamp=1749877200,
    body='{"type":"event.created","data":{"id":"evt_001"}}',
    expected_sig="bffa13b5964bd097b5cca399ef2386585906f5af213ddcba2a2f1a9b41399680",
    header="t=1749877200,v1=bffa13b5964bd097b5cca399ef2386585906f5af213ddcba2a2f1a9b41399680",
)

if __name__ == "__main__":
    passed = 0
    failed = 0

    def _assert(label: str, result: VerifyResult, expect_ok: bool, expect_reason: Optional[str] = None) -> None:
        global passed, failed
        ok = result.ok == expect_ok and (expect_reason is None or result.reason == expect_reason)
        if ok:
            print(f"  PASS  {label}")
            passed += 1
        else:
            print(f"  FAIL  {label} — got {result}")
            failed += 1

    print("\nStandShare webhook verifier — self-test (Python)")

    # Happy path — pinned fixture vector
    _assert(
        "pinned fixture vector passes",
        verify_webhook_signature(
            _PINNED["secret"],
            _PINNED["body"],
            _PINNED["header"],
            _now=_PINNED["timestamp"],
        ),
        True,
    )

    # Missing header
    _assert(
        "missing header → missing_header",
        verify_webhook_signature(_PINNED["secret"], _PINNED["body"], None),
        False,
        "missing_header",
    )

    # Malformed header (no v1=)
    _assert(
        "header without v1= → malformed_header",
        verify_webhook_signature(
            _PINNED["secret"],
            _PINNED["body"],
            f"t={_PINNED['timestamp']}",
        ),
        False,
        "malformed_header",
    )

    # Expired timestamp (using real clock — pinned timestamp is in the past)
    _assert(
        "expired timestamp → timestamp_expired",
        verify_webhook_signature(
            _PINNED["secret"],
            _PINNED["body"],
            _PINNED["header"],
        ),
        False,
        "timestamp_expired",
    )

    # Tampered body
    _assert(
        "tampered body → invalid_signature",
        verify_webhook_signature(
            _PINNED["secret"],
            _PINNED["body"] + " tampered",
            _PINNED["header"],
            _now=_PINNED["timestamp"],
        ),
        False,
        "invalid_signature",
    )

    # Wrong secret
    _assert(
        "wrong secret → invalid_signature",
        verify_webhook_signature(
            "whsec_wrong",
            _PINNED["body"],
            _PINNED["header"],
            _now=_PINNED["timestamp"],
        ),
        False,
        "invalid_signature",
    )

    # Wrong v1 value (correct length but wrong bits)
    bad_header = _PINNED["header"].replace(_PINNED["expected_sig"], "a" * 64)
    _assert(
        "wrong v1 hex → invalid_signature",
        verify_webhook_signature(
            _PINNED["secret"],
            _PINNED["body"],
            bad_header,
            _now=_PINNED["timestamp"],
        ),
        False,
        "invalid_signature",
    )

    total = passed + failed
    print(f"\n{total} tests — {passed} passed, {failed} failed")
    if failed:
        raise SystemExit(1)
