"""Webhook delivery utility for notifying external systems of document events.

Provides functions to dispatch webhook payloads with HMAC-SHA256 signatures
and to query active webhook configurations from the database.

Supported events:
- ``document.uploaded``  – a new document has been ingested
- ``document.processed`` – a document finished processing successfully
- ``document.failed``    – document processing failed
- ``user.signup``        – a new user account was created
- ``user.plan_changed``  – a user's subscription plan changed
- ``user.payment_issue`` – a payment issue was reported for a user
"""

import hashlib
import hmac
import json
import logging
import time
from typing import Any

import requests

from app.database import SessionLocal
from app.models import WebhookConfig

logger = logging.getLogger(__name__)

#: Events recognised by the webhook subsystem.
VALID_EVENTS: frozenset[str] = frozenset(
    {
        "document.uploaded",
        "document.processed",
        "document.failed",
        "user.signup",
        "user.plan_changed",
        "user.payment_issue",
    }
)

#: Timeout (seconds) for outgoing webhook HTTP requests.
WEBHOOK_TIMEOUT = 10


def compute_signature(payload_bytes: bytes, secret: str) -> str:
    """Compute an HMAC-SHA256 hex-digest for *payload_bytes* using *secret*.

    Args:
        payload_bytes: The raw JSON body to sign.
        secret: The shared secret string.

    Returns:
        ``sha256=<hex-digest>`` signature string.
    """
    mac = hmac.new(secret.encode("utf-8"), payload_bytes, hashlib.sha256)
    return f"sha256={mac.hexdigest()}"


def deliver_webhook(url: str, payload: dict[str, Any], secret: str | None = None) -> bool:
    """Send a single webhook POST request.

    Args:
        url: Target URL.
        payload: JSON-serialisable dictionary.
        secret: If provided, an ``X-Webhook-Signature`` header is included.

    Returns:
        ``True`` when the remote server responds with a 2xx status.
    """
    body = json.dumps(payload, default=str, sort_keys=True)
    body_bytes = body.encode("utf-8")

    headers: dict[str, str] = {"Content-Type": "application/json"}
    if secret:
        headers["X-Webhook-Signature"] = compute_signature(body_bytes, secret)

    try:
        resp = requests.post(url, data=body_bytes, headers=headers, timeout=WEBHOOK_TIMEOUT)
        if resp.ok:
            logger.info("Webhook delivered to %s (status %d)", url, resp.status_code)
            return True
        logger.warning("Webhook to %s returned status %d", url, resp.status_code)
        return False
    except requests.RequestException as exc:
        logger.error("Webhook delivery to %s failed: %s", url, exc)
        return False


def build_payload(event: str, data: dict[str, Any]) -> dict[str, Any]:
    """Build a standardised webhook payload envelope.

    Args:
        event: The event name (e.g. ``document.uploaded``).
        data: Event-specific data.

    Returns:
        Dictionary with ``event``, ``timestamp``, and ``data`` keys.
    """
    return {
        "event": event,
        "timestamp": time.time(),
        "data": data,
    }


def get_active_webhooks_for_event(event: str) -> list[dict[str, Any]]:
    """Return all active webhook configs subscribed to *event*.

    Queries the database directly so this helper can be called from both the
    API layer and Celery tasks.

    Args:
        event: The event name to filter on.

    Returns:
        A list of dicts with ``id``, ``url``, ``secret``, and ``events`` keys.
    """
    db = SessionLocal()
    try:
        configs = db.query(WebhookConfig).filter(WebhookConfig.is_active.is_(True)).all()
        result: list[dict[str, Any]] = []
        for cfg in configs:
            try:
                subscribed = json.loads(cfg.events)
            except (json.JSONDecodeError, TypeError):
                subscribed = []
            if event in subscribed:
                result.append(
                    {
                        "id": cfg.id,
                        "url": cfg.url,
                        "secret": cfg.secret,
                        "events": subscribed,
                    }
                )
        return result
    finally:
        db.close()


def dispatch_webhook_event(event: str, data: dict[str, Any]) -> None:
    """Fan-out a webhook event to all matching active configurations.

    This is the main entry-point used by application code to trigger webhooks.
    It delegates to :func:`deliver_webhook_task` (Celery) for each matching
    webhook so delivery happens asynchronously with automatic retries.

    Also dispatches to automation hooks (Zapier / Make.com) if enabled.

    Args:
        event: Event name (must be in :data:`VALID_EVENTS`).
        data: Event-specific payload data.
    """
    if event not in VALID_EVENTS:
        logger.warning("Ignoring unknown webhook event: %s", event)
        return

    webhooks = get_active_webhooks_for_event(event)
    if not webhooks:
        logger.debug("No active webhooks for event %s", event)
    else:
        payload = build_payload(event, data)

        # Import here to avoid circular dependency with celery_app
        from app.tasks.webhook_tasks import deliver_webhook_task

        for wh in webhooks:
            try:
                deliver_webhook_task.delay(wh["url"], payload, wh["secret"])
                logger.debug("Queued webhook delivery to %s for event %s", wh["url"], event)
            except Exception as exc:
                logger.error("Failed to queue webhook to %s: %s", wh["url"], exc)

    # Also fan-out to Zapier / Make.com automation hooks
    try:
        from app.utils.automation_hooks import dispatch_automation_hooks

        dispatch_automation_hooks(event, data)
    except Exception as exc:
        logger.error("Failed to dispatch automation hooks for event %s: %s", event, exc)
