from __future__ import annotations

import hashlib
import hmac
import json
from datetime import datetime, timezone
from typing import Any, Protocol


class PaymentWebhookVerificationError(Exception):
    def __init__(self, signature_status: str, message: str, *, status_code: int = 400) -> None:
        super().__init__(message)
        self.signature_status = signature_status
        self.message = message
        self.status_code = status_code


class PaymentProviderAdapter(Protocol):
    def get_status(self, external_payment_id: str, *, context: dict[str, Any] | None = None) -> dict[str, Any] | str:
        ...


_PROVIDER_ADAPTERS: dict[str, PaymentProviderAdapter] = {}


def register_payment_provider_adapter(provider: str, adapter: PaymentProviderAdapter) -> None:
    normalized = normalize_provider(provider)
    if normalized:
        _PROVIDER_ADAPTERS[normalized] = adapter


def get_payment_provider_adapter(provider: str | None) -> PaymentProviderAdapter | None:
    return _PROVIDER_ADAPTERS.get(normalize_provider(provider))


def normalize_provider(provider: str | None) -> str:
    return str(provider or "").strip().lower() or "manual"


def raw_payload_hash(raw_body: bytes) -> str:
    return hashlib.sha256(raw_body).hexdigest()


def safe_json_loads(raw_body: bytes) -> dict[str, Any]:
    if not raw_body:
        return {}
    parsed = json.loads(raw_body.decode("utf-8"))
    if not isinstance(parsed, dict):
        raise ValueError("Webhook payload must be a JSON object")
    return parsed


def normalize_headers(headers: Any) -> dict[str, str]:
    return {str(key).lower(): str(value) for key, value in dict(headers).items()}


def header_value(headers: dict[str, str], *names: str) -> str | None:
    for name in names:
        value = headers.get(name.lower())
        normalized = str(value or "").strip()
        if normalized:
            return normalized
    return None


def metadata_value(payload: dict[str, Any] | None, *keys: str) -> str | None:
    data = payload or {}
    metadata = data.get("metadata") if isinstance(data.get("metadata"), dict) else {}
    candidates = [data, metadata]
    payment = data.get("payment") if isinstance(data.get("payment"), dict) else {}
    candidates.append(payment)
    nested_metadata = payment.get("metadata") if isinstance(payment.get("metadata"), dict) else {}
    candidates.append(nested_metadata)
    data_object = data.get("data", {}).get("object") if isinstance(data.get("data"), dict) else {}
    if isinstance(data_object, dict):
        candidates.append(data_object)

    for source in candidates:
        for key in keys:
            value = source.get(key)
            normalized = str(value or "").strip()
            if normalized:
                return normalized
    return None


def extract_payment_provider(payload: dict[str, Any], headers: dict[str, str]) -> str:
    return normalize_provider(
        header_value(headers, "x-payment-provider", "x-provider")
        or metadata_value(payload, "payment_provider", "provider")
    )


def extract_provider_account_id(payload: dict[str, Any], headers: dict[str, str]) -> str | None:
    return header_value(
        headers,
        "x-provider-account-id",
        "x-merchant-id",
        "x-terminal-id",
        "x-integration-id",
    ) or metadata_value(
        payload,
        "provider_account_id",
        "payment_provider_account_id",
        "merchant_id",
        "terminal_id",
        "account_id",
        "integration_id",
    )


def extract_external_event_id(payload: dict[str, Any], headers: dict[str, str]) -> str | None:
    return header_value(headers, "x-webhook-event-id", "x-event-id") or metadata_value(
        payload,
        "external_event_id",
        "webhook_event_id",
        "event_id",
    )


def extract_external_payment_id(payload: dict[str, Any], headers: dict[str, str]) -> str | None:
    return header_value(headers, "x-payment-id", "x-external-payment-id") or metadata_value(
        payload,
        "external_payment_id",
        "external_payment_ref",
        "transaction_id",
        "provider_payment_id",
        "payment_id",
        "id",
    )


def extract_event_type(payload: dict[str, Any], headers: dict[str, str]) -> str:
    return (
        header_value(headers, "x-webhook-event-type", "x-event-type")
        or metadata_value(payload, "event_type", "type")
        or normalize_payment_event_type(None, metadata_value(payload, "status", "provider_status"))
    )


def normalize_payment_status(provider: str | None, provider_status: str | None) -> str:
    normalized = str(provider_status or "").strip().lower()
    normalized = normalized.replace("-", "_").replace(" ", "_")
    if normalized in {"succeeded", "success", "paid", "captured", "settled", "completed"}:
        return "success"
    if normalized in {"authorized", "authorised", "processing", "pending", "created", "new"}:
        return "pending"
    if normalized in {"failed", "declined", "error", "rejected"}:
        return "failed"
    if normalized in {"canceled", "cancelled", "voided", "expired"}:
        return "canceled"
    if normalized in {"partial", "partially_paid", "partial_paid"}:
        return "partial"
    return normalized if normalized in {"pending", "success", "failed", "canceled", "partial"} else "pending"


def normalize_payment_event_type(provider: str | None, provider_status: str | None, event_type: str | None = None) -> str:
    raw_event_type = str(event_type or "").strip().lower()
    if raw_event_type.startswith("payment."):
        if raw_event_type in {"payment.succeeded", "payment.captured", "payment.paid"}:
            return "payment.received"
        if raw_event_type in {"payment.cancelled", "payment.canceled"}:
            return "payment.canceled"
        return raw_event_type

    status = normalize_payment_status(provider, provider_status)
    if status in {"success", "partial"}:
        return "payment.received"
    if status == "failed":
        return "payment.failed"
    if status == "canceled":
        return "payment.canceled"
    return "payment.pending"


def _signature_candidates(signature_header: str) -> set[str]:
    raw = str(signature_header or "").strip()
    if not raw:
        return set()
    candidates = {raw}
    if raw.startswith("sha256="):
        candidates.add(raw.split("=", 1)[1])
    for part in raw.split(","):
        key, sep, value = part.partition("=")
        if sep and key.strip() in {"v1", "sha256"} and value.strip():
            candidates.add(value.strip())
    return candidates


def verify_payment_webhook_signature(
    *,
    raw_body: bytes,
    headers: dict[str, str],
    secret: str | None,
    replay_window_seconds: int = 600,
    now: datetime | None = None,
    required: bool = True,
) -> str:
    normalized_secret = str(secret or "").strip()
    if not required and not normalized_secret:
        return "not_required"
    if not normalized_secret:
        raise PaymentWebhookVerificationError("missing_secret", "Payment webhook secret is not configured", status_code=403)

    timestamp = header_value(headers, "x-webhook-timestamp", "x-provider-timestamp", "x-timestamp")
    if timestamp:
        try:
            event_ts = int(float(timestamp))
        except ValueError as exc:
            raise PaymentWebhookVerificationError("invalid_timestamp", "Invalid webhook timestamp") from exc
        now_ts = int((now or datetime.now(timezone.utc)).timestamp())
        if abs(now_ts - event_ts) > max(int(replay_window_seconds), 1):
            raise PaymentWebhookVerificationError("expired", "Webhook timestamp is outside the replay window")

    signature_header = header_value(headers, "x-webhook-signature", "x-signature", "stripe-signature")
    if not signature_header:
        raise PaymentWebhookVerificationError("missing", "Payment webhook signature is missing", status_code=403)

    body_to_sign = raw_body
    if timestamp:
        body_to_sign = f"{timestamp}.".encode("utf-8") + raw_body
    expected = hmac.new(normalized_secret.encode("utf-8"), body_to_sign, hashlib.sha256).hexdigest()
    for candidate in _signature_candidates(signature_header):
        if hmac.compare_digest(candidate, expected):
            return "valid"
    raise PaymentWebhookVerificationError("invalid", "Payment webhook signature is invalid", status_code=403)
