"""Webhook endpoint management — CRUD, HMAC-signed delivery, retry logic.

Firestore paths:
- ``customers/{customer_id}/webhooks/{webhook_id}``
- ``customers/{customer_id}/webhook_deliveries/{delivery_id}``

Signing secrets stored in Secret Manager as
``webhook-secret-{customer_id}-{webhook_id}``.
"""

from __future__ import annotations

import asyncio
import hashlib
import hmac as hmac_mod
import logging
import secrets
import time
from datetime import UTC, datetime, timedelta
from typing import Any

import httpx

from backend.dashboard.context import WebhookDelivery, WebhookInfo

_log = logging.getLogger(__name__)

WEBHOOK_EVENT_TYPES: list[str] = [
    "scan.completed",
    "score.changed",
    "drift.detected",
    "report.generated",
    "license.expiring",
    "member.invited",
    "webhook.test",
]

_RETRY_DELAYS = [1, 4, 16]
_WEBHOOK_LIMIT = 3
_DELIVERY_TTL_DAYS = 30
_USER_AGENT = "ComplyForm-Webhooks/1.0"


async def _get_secret_manager_client() -> Any:
    """Lazy import Secret Manager client."""
    from google.cloud import secretmanager

    return secretmanager.SecretManagerServiceClient()


async def _store_secret(secret_name: str, payload: str) -> None:
    """Store a secret in Secret Manager."""
    client = await _get_secret_manager_client()
    parent = "projects/complyform-prod"

    # Create secret (may already exist)
    import contextlib

    with contextlib.suppress(Exception):
        client.create_secret(
            request={
                "parent": parent,
                "secret_id": secret_name,
                "secret": {"replication": {"automatic": {}}},
            }
        )

    # Add version
    client.add_secret_version(
        request={
            "parent": f"{parent}/secrets/{secret_name}",
            "payload": {"data": payload.encode()},
        }
    )


async def _read_secret(secret_name: str) -> str:
    """Read latest version of a secret from Secret Manager."""
    client = await _get_secret_manager_client()
    name = f"projects/complyform-prod/secrets/{secret_name}/versions/latest"
    response = client.access_secret_version(request={"name": name})
    return response.payload.data.decode("utf-8")


async def _delete_secret(secret_name: str) -> None:
    """Delete a secret from Secret Manager."""
    client = await _get_secret_manager_client()
    name = f"projects/complyform-prod/secrets/{secret_name}"
    try:
        client.delete_secret(request={"name": name})
    except Exception:
        _log.exception("Failed to delete secret %s", secret_name)


def _secret_name(customer_id: str, webhook_id: str) -> str:
    """Build Secret Manager secret name."""
    return f"webhook-secret-{customer_id}-{webhook_id}"


async def create_webhook(
    customer_id: str,
    url: str,
    event_types: list[str],
    actor_email: str,
) -> WebhookInfo:
    """Create a new webhook endpoint.

    1. Check limit: max 3 active webhooks per customer
    2. Generate signing secret: secrets.token_urlsafe(32)
    3. Store SHA-256 hash in Firestore webhook doc
    4. Store raw secret in Secret Manager
    5. Return WebhookInfo with raw_secret (one-time display)
    """
    from fastapi import HTTPException

    from backend.shared.firestore import get_db

    # Validate event types
    invalid = set(event_types) - set(WEBHOOK_EVENT_TYPES)
    if invalid:
        raise HTTPException(
            status_code=400,
            detail=f"Invalid event types: {sorted(invalid)}",
        )

    db = get_db()
    webhooks_ref = (
        db.collection("customers").document(customer_id).collection("webhooks")
    )

    # Check limit
    existing = [doc async for doc in webhooks_ref.stream()]
    active_count = sum(
        1
        for doc in existing
        if doc.to_dict().get("status") in ("active", "unhealthy")  # type: ignore[union-attr]
    )
    if active_count >= _WEBHOOK_LIMIT:
        raise HTTPException(
            status_code=409,
            detail=f"Webhook limit reached ({_WEBHOOK_LIMIT})",
        )

    # Generate signing secret
    raw_secret = secrets.token_urlsafe(32)
    secret_hash = hashlib.sha256(raw_secret.encode()).hexdigest()

    now = datetime.now(UTC).isoformat()
    webhook_data: dict[str, Any] = {
        "url": url,
        "signing_secret_hash": secret_hash,
        "event_types": event_types,
        "status": "active",
        "created_at": now,
        "last_delivery_at": None,
        "consecutive_failures": 0,
    }

    # Write to Firestore
    _, doc_ref = await webhooks_ref.add(webhook_data)
    webhook_id: str = doc_ref.id

    # Store secret in Secret Manager
    await _store_secret(_secret_name(customer_id, webhook_id), raw_secret)

    return WebhookInfo(
        webhook_id=webhook_id,
        url=url,
        event_types=event_types,
        status="active",
        created_at=now,
        last_delivery_at=None,
        consecutive_failures=0,
        signing_secret_last4=raw_secret[-4:],
        raw_secret=raw_secret,
    )


async def delete_webhook(
    customer_id: str,
    webhook_id: str,
) -> None:
    """Delete webhook doc + Secret Manager secret."""
    from backend.shared.firestore import get_db

    db = get_db()
    await (
        db.collection("customers")
        .document(customer_id)
        .collection("webhooks")
        .document(webhook_id)
        .delete()
    )

    await _delete_secret(_secret_name(customer_id, webhook_id))


async def update_webhook(
    customer_id: str,
    webhook_id: str,
    url: str | None = None,
    event_types: list[str] | None = None,
) -> WebhookInfo:
    """Update webhook URL and/or event types."""
    from fastapi import HTTPException

    from backend.shared.firestore import get_db

    if event_types is not None:
        invalid = set(event_types) - set(WEBHOOK_EVENT_TYPES)
        if invalid:
            raise HTTPException(
                status_code=400,
                detail=f"Invalid event types: {sorted(invalid)}",
            )

    db = get_db()
    doc_ref = (
        db.collection("customers")
        .document(customer_id)
        .collection("webhooks")
        .document(webhook_id)
    )
    doc = await doc_ref.get()
    if not doc.exists:
        raise HTTPException(status_code=404, detail="Webhook not found")

    updates: dict[str, Any] = {}
    if url is not None:
        updates["url"] = url
    if event_types is not None:
        updates["event_types"] = event_types

    if updates:
        await doc_ref.update(updates)

    data: dict[str, Any] = doc.to_dict()  # type: ignore[assignment]
    data.update(updates)

    return WebhookInfo(
        webhook_id=webhook_id,
        url=data.get("url", ""),
        event_types=data.get("event_types", []),
        status=data.get("status", "active"),
        created_at=data.get("created_at", ""),
        last_delivery_at=data.get("last_delivery_at"),
        consecutive_failures=data.get("consecutive_failures", 0),
        signing_secret_last4=data.get("signing_secret_hash", "")[-4:],
    )


async def list_webhooks(customer_id: str) -> list[WebhookInfo]:
    """List all webhooks for customer."""
    from backend.shared.firestore import get_db

    db = get_db()
    docs = [
        doc
        async for doc in db.collection("customers")
        .document(customer_id)
        .collection("webhooks")
        .stream()
    ]

    webhooks: list[WebhookInfo] = []
    for doc in docs:
        data: dict[str, Any] = doc.to_dict()  # type: ignore[assignment]
        webhooks.append(
            WebhookInfo(
                webhook_id=doc.id,
                url=data.get("url", ""),
                event_types=data.get("event_types", []),
                status=data.get("status", "active"),
                created_at=data.get("created_at", ""),
                last_delivery_at=data.get("last_delivery_at"),
                consecutive_failures=data.get("consecutive_failures", 0),
                signing_secret_last4=data.get("signing_secret_hash", "")[-4:],
            )
        )

    return webhooks


async def get_delivery_log(
    customer_id: str,
    webhook_id: str,
    limit: int = 50,
) -> list[WebhookDelivery]:
    """Last N deliveries for a webhook endpoint."""
    from backend.shared.firestore import get_db

    db = get_db()
    query = (
        db.collection("customers")
        .document(customer_id)
        .collection("webhook_deliveries")
        .where("webhook_id", "==", webhook_id)
        .order_by("attempted_at", direction="DESCENDING")
        .limit(limit)
    )
    docs = [doc async for doc in query.stream()]

    deliveries: list[WebhookDelivery] = []
    for doc in docs:
        data: dict[str, Any] = doc.to_dict()  # type: ignore[assignment]
        deliveries.append(
            WebhookDelivery(
                delivery_id=doc.id,
                webhook_id=data.get("webhook_id", ""),
                event_type=data.get("event_type", ""),
                status=data.get("status", "failed"),
                response_code=data.get("response_code"),
                attempted_at=data.get("attempted_at", ""),
            )
        )

    return deliveries


async def send_test_event(
    customer_id: str,
    webhook_id: str,
) -> WebhookDelivery:
    """Send webhook.test event to verify endpoint."""
    from backend.shared.firestore import get_db

    db = get_db()
    doc = await (
        db.collection("customers")
        .document(customer_id)
        .collection("webhooks")
        .document(webhook_id)
        .get()
    )

    from fastapi import HTTPException

    if not doc.exists:
        raise HTTPException(status_code=404, detail="Webhook not found")

    data: dict[str, Any] = doc.to_dict()  # type: ignore[assignment]
    url = data.get("url", "")

    payload = {
        "event": "webhook.test",
        "customer_id": customer_id,
        "timestamp": datetime.now(UTC).isoformat(),
        "data": {"message": "Test event from ComplyForm"},
    }

    delivery = await _deliver_single(
        customer_id=customer_id,
        webhook_id=webhook_id,
        url=url,
        event_type="webhook.test",
        payload=payload,
    )

    return delivery


async def deliver_webhook_event(
    customer_id: str,
    event_type: str,
    payload: dict[str, Any],
) -> None:
    """Fan-out: for each active webhook subscribed to event_type, deliver."""
    from backend.shared.firestore import get_db

    db = get_db()
    docs = [
        doc
        async for doc in db.collection("customers")
        .document(customer_id)
        .collection("webhooks")
        .where("status", "in", ["active", "unhealthy"])
        .stream()
    ]

    for doc in docs:
        data: dict[str, Any] = doc.to_dict()  # type: ignore[assignment]
        subscribed: list[str] = data.get("event_types", [])
        if event_type not in subscribed:
            continue

        await _deliver_single(
            customer_id=customer_id,
            webhook_id=doc.id,
            url=data.get("url", ""),
            event_type=event_type,
            payload=payload,
        )


async def _deliver_single(
    customer_id: str,
    webhook_id: str,
    url: str,
    event_type: str,
    payload: dict[str, Any],
) -> WebhookDelivery:
    """Deliver to one webhook with HMAC signing and retry."""
    import json

    from backend.shared.firestore import get_db

    # Read signing secret from Secret Manager
    try:
        raw_secret = await _read_secret(_secret_name(customer_id, webhook_id))
    except Exception:
        _log.exception(
            "Failed to read webhook secret for %s/%s", customer_id, webhook_id
        )
        return await _log_delivery(
            customer_id=customer_id,
            webhook_id=webhook_id,
            event_type=event_type,
            status="failed",
            response_code=None,
        )

    timestamp = str(int(time.time()))
    body_json = json.dumps(payload, separators=(",", ":"), sort_keys=True)
    signed_message = f"{timestamp}.{body_json}".encode()
    signature = hmac_mod.new(
        raw_secret.encode(), signed_message, hashlib.sha256
    ).hexdigest()

    headers = {
        "X-ComplyForm-Signature": f"v1={signature}",
        "X-ComplyForm-Timestamp": timestamp,
        "X-ComplyForm-Event": event_type,
        "User-Agent": _USER_AGENT,
        "Content-Type": "application/json",
    }

    db = get_db()
    webhook_ref = (
        db.collection("customers")
        .document(customer_id)
        .collection("webhooks")
        .document(webhook_id)
    )

    for attempt, delay in enumerate(_RETRY_DELAYS):
        try:
            async with httpx.AsyncClient(timeout=15.0) as client:
                resp = await client.post(url, content=body_json, headers=headers)

            if 200 <= resp.status_code < 300:
                # Success — reset failures
                await webhook_ref.update(
                    {
                        "consecutive_failures": 0,
                        "status": "active",
                        "last_delivery_at": datetime.now(UTC).isoformat(),
                    }
                )
                return await _log_delivery(
                    customer_id=customer_id,
                    webhook_id=webhook_id,
                    event_type=event_type,
                    status="success",
                    response_code=resp.status_code,
                )

            _log.warning(
                "Webhook %s returned %s (attempt %d/%d)",
                url,
                resp.status_code,
                attempt + 1,
                len(_RETRY_DELAYS),
            )

        except Exception:
            _log.exception(
                "Webhook request to %s failed (attempt %d/%d)",
                url,
                attempt + 1,
                len(_RETRY_DELAYS),
            )

        if attempt < len(_RETRY_DELAYS) - 1:
            await asyncio.sleep(delay)

    # All retries exhausted — increment failures
    webhook_doc = await webhook_ref.get()
    if webhook_doc.exists:
        wh_data: dict[str, Any] = webhook_doc.to_dict()  # type: ignore[assignment]
        failures = wh_data.get("consecutive_failures", 0) + 1
        updates: dict[str, Any] = {"consecutive_failures": failures}
        if failures >= 3:
            updates["status"] = "unhealthy"
        await webhook_ref.update(updates)

    return await _log_delivery(
        customer_id=customer_id,
        webhook_id=webhook_id,
        event_type=event_type,
        status="failed",
        response_code=None,
    )


async def _log_delivery(
    customer_id: str,
    webhook_id: str,
    event_type: str,
    status: str,
    response_code: int | None,
) -> WebhookDelivery:
    """Log a delivery attempt to webhook_deliveries subcollection."""
    from backend.shared.firestore import get_db

    now = datetime.now(UTC)
    delivery_data: dict[str, Any] = {
        "webhook_id": webhook_id,
        "event_type": event_type,
        "status": status,
        "response_code": response_code,
        "attempted_at": now.isoformat(),
        "expires_at": now + timedelta(days=_DELIVERY_TTL_DAYS),
    }

    db = get_db()
    _, doc_ref = await (
        db.collection("customers")
        .document(customer_id)
        .collection("webhook_deliveries")
        .add(delivery_data)
    )

    return WebhookDelivery(
        delivery_id=doc_ref.id,
        webhook_id=webhook_id,
        event_type=event_type,
        status=status,
        response_code=response_code,
        attempted_at=now.isoformat(),
    )
