"""Webhook dispatch service — outbound HTTP notifications.

Supported event types:
- test.validated          — fired when a test reaches "validated" state
- test.rejected           — fired when a test reaches "rejected" state
- campaign.completed      — fired when a campaign is completed
- campaign.started        — fired when a campaign is activated
- mitre.synced            — fired after MITRE ATT&CK sync completes
- technique.status_changed — fired when a technique's status changes
- webhook.test            — manual test ping from the admin UI
- alert.fired             — fired when an operational alert is triggered
"""

import hashlib
import hmac
import logging
import uuid
from datetime import datetime

import requests

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

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------


def dispatch_webhook_targeted(webhook_id: str, event_type: str, payload: dict) -> None:
    """Send to a single specific WebhookConfig by ID (used by alert rules).

    Opens its own DB session.  All exceptions are caught.
    """
    db = SessionLocal()
    try:
        wh = db.query(WebhookConfig).filter(
            WebhookConfig.id == webhook_id,
            WebhookConfig.is_active == True,  # noqa: E712
        ).first()
        if wh:
            _send_webhook(db, wh, event_type, payload)
    except Exception:
        logger.exception(
            "dispatch_webhook_targeted: error for webhook_id=%s event=%s",
            webhook_id, event_type,
        )
    finally:
        db.close()


def dispatch_webhook(event_type: str, payload: dict) -> None:
    """Send an outbound webhook to all active subscribers for *event_type*.

    Opens its own DB session so this can be called outside request context
    (e.g. from background jobs).  All exceptions are caught; webhook failures
    never crash the caller.
    """
    db = SessionLocal()
    try:
        webhooks = (
            db.query(WebhookConfig)
            .filter(WebhookConfig.is_active == True)  # noqa: E712
            .all()
        )
        for wh in webhooks:
            # Filter by subscribed events — empty list means "all events"
            subscribed: list = wh.events or []
            if subscribed and event_type not in subscribed:
                continue
            _send_webhook(db, wh, event_type, payload)
    except Exception:
        logger.exception("dispatch_webhook: unexpected error for event_type=%s", event_type)
    finally:
        db.close()


_EVENT_COLORS: dict[str, str] = {
    "test.validated":         "28a745",  # green
    "test.rejected":          "dc3545",  # red
    "campaign.completed":     "17a2b8",  # teal
    "campaign.started":       "007bff",  # blue
    "mitre.synced":           "6f42c1",  # purple
    "technique.status_changed":"fd7e14", # orange
    "alert.fired":            "dc3545",  # red
    "webhook.test":           "6c757d",  # gray
}


def _build_teams_card(event_type: str, payload: dict, timestamp: str) -> dict:
    """Format payload as a Microsoft Teams Incoming Webhook MessageCard."""
    color = _EVENT_COLORS.get(event_type, "0078D4")

    # Build facts from payload key/value pairs (simple types only)
    facts = [{"name": "Event", "value": event_type},
             {"name": "Time",  "value": timestamp}]
    for k, v in (payload or {}).items():
        if isinstance(v, (str, int, float, bool)) and v is not None:
            facts.append({"name": str(k).replace("_", " ").title(), "value": str(v)})

    return {
        "@type": "MessageCard",
        "@context": "http://schema.org/extensions",
        "themeColor": color,
        "summary": f"Aegis · {event_type}",
        "sections": [{
            "activityTitle": f"**{event_type}**",
            "activitySubtitle": "Aegis Platform Notification",
            "activityImage": "https://raw.githubusercontent.com/microsoft/fluentui-system-icons/main/assets/Shield/SVG/ic_fluent_shield_24_filled.svg",
            "facts": facts,
            "markdown": True,
        }],
    }


def _build_slack_body(event_type: str, payload: dict, timestamp: str) -> dict:
    """Format payload as a Slack Incoming Webhook message."""
    color = "#" + _EVENT_COLORS.get(event_type, "0078D4")
    fields = [{"title": "Event", "value": event_type, "short": True},
              {"title": "Time",  "value": timestamp,  "short": True}]
    for k, v in (payload or {}).items():
        if isinstance(v, (str, int, float, bool)) and v is not None:
            fields.append({"title": str(k).replace("_", " ").title(),
                           "value": str(v), "short": True})
    return {
        "attachments": [{
            "color": color,
            "title": f"Aegis · {event_type}",
            "fields": fields,
            "footer": "Aegis Platform",
            "ts": int(datetime.utcnow().timestamp()),
        }]
    }


def _send_webhook(db, wh: WebhookConfig, event_type: str, payload: dict) -> None:
    """Send a single webhook POST and update its metadata.

    Auto-detects the target platform from the URL and formats accordingly:
    - webhook.office.com / teams.microsoft.com  →  Teams MessageCard
    - hooks.slack.com                           →  Slack attachments
    - everything else                           →  generic Aegis JSON
    """
    import json
    timestamp = datetime.utcnow().isoformat() + "Z"
    url_lower = wh.url.lower()

    if "webhook.office.com" in url_lower or "teams.microsoft.com" in url_lower:
        body = _build_teams_card(event_type, payload, timestamp)
        headers = {"Content-Type": "application/json"}
    elif "hooks.slack.com" in url_lower:
        body = _build_slack_body(event_type, payload, timestamp)
        headers = {"Content-Type": "application/json"}
    else:
        body = {
            "event": event_type,
            "data": payload,
            "timestamp": timestamp,
        }
        headers = {"Content-Type": "application/json"}
        if wh.secret:
            raw = json.dumps(body, separators=(",", ":"), sort_keys=True).encode()
            sig = hmac.new(wh.secret.encode(), raw, hashlib.sha256).hexdigest()
            headers["X-Aegis-Signature"] = f"sha256={sig}"

    try:
        resp = requests.post(wh.url, json=body, headers=headers, timeout=10)
        resp.raise_for_status()
        wh.last_triggered_at = datetime.utcnow()
        wh.failure_count = 0
        db.commit()
        logger.info("Webhook '%s' (%s) dispatched OK for event=%s", wh.name, wh.url, event_type)
    except Exception as exc:
        wh.failure_count = (wh.failure_count or 0) + 1
        wh.last_triggered_at = datetime.utcnow()
        try:
            db.commit()
        except Exception:
            db.rollback()
        logger.warning(
            "Webhook '%s' (%s) failed for event=%s: %s (failure_count=%d)",
            wh.name, wh.url, event_type, exc, wh.failure_count,
        )


# ---------------------------------------------------------------------------
# CRUD
# ---------------------------------------------------------------------------


def list_webhooks(db, *, offset: int = 0, limit: int = 50) -> list[WebhookConfig]:
    """Return paginated webhook configs."""
    return (
        db.query(WebhookConfig)
        .order_by(WebhookConfig.created_at.desc())
        .offset(offset)
        .limit(limit)
        .all()
    )


def get_webhook_or_raise(db, webhook_id: uuid.UUID) -> WebhookConfig:
    """Fetch a webhook by ID or raise 404."""
    from app.domain.errors import EntityNotFoundError
    wh = db.query(WebhookConfig).filter(WebhookConfig.id == webhook_id).first()
    if wh is None:
        raise EntityNotFoundError("WebhookConfig", str(webhook_id))
    return wh


def create_webhook(db, created_by: uuid.UUID, payload) -> WebhookConfig:
    """Create and persist a new WebhookConfig."""
    wh = WebhookConfig(
        name=payload.name,
        url=payload.url,
        secret=payload.secret,
        events=payload.events,
        is_active=payload.is_active,
        created_by=created_by,
    )
    db.add(wh)
    db.flush()
    return wh


def update_webhook(db, webhook_id: uuid.UUID, payload) -> WebhookConfig:
    """Apply a partial update to an existing WebhookConfig."""
    wh = get_webhook_or_raise(db, webhook_id)
    update_data = payload.model_dump(exclude_unset=True)
    for field, value in update_data.items():
        setattr(wh, field, value)
    db.flush()
    return wh


def delete_webhook(db, webhook_id: uuid.UUID) -> None:
    """Hard-delete a WebhookConfig."""
    wh = get_webhook_or_raise(db, webhook_id)
    db.delete(wh)
    db.flush()
