"""
Phase 32: Webhook System — Outbound Integrations.

Send events from Brynq to external services: Slack, Discord, custom URLs.
Supports event filtering, retry logic, and HMAC signature verification.

Features:
  - REGISTER: Add webhook endpoints for specific event types
  - FIRE: Trigger webhooks when matching events occur
  - RETRY: Failed deliveries queued for retry with exponential backoff
  - SIGNATURES: HMAC-SHA256 signing so receivers can verify authenticity
  - HISTORY: Full delivery log for debugging

Storage:
  state/broker/webhooks/
    endpoints/{endpoint_id}.json  — registered webhook endpoints
    deliveries.jsonl              — delivery attempt log
    queue/{delivery_id}.json      — pending retries
"""

import hashlib
import hmac
import json
import time
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

from brynq.server import (
    BROKER_DIR,
    _write_json_sync,
    _append_jsonl_sync,
)

# ── Paths ──────────────────────────────────────────────────────────────────

WEBHOOKS_DIR = BROKER_DIR / "webhooks"
ENDPOINTS_DIR = WEBHOOKS_DIR / "endpoints"
QUEUE_DIR = WEBHOOKS_DIR / "queue"
DELIVERIES_FILE = WEBHOOKS_DIR / "deliveries.jsonl"

for _d in (WEBHOOKS_DIR, ENDPOINTS_DIR, QUEUE_DIR):
    _d.mkdir(parents=True, exist_ok=True)

# Events that can trigger webhooks
WEBHOOK_EVENTS = [
    "task.created", "task.accepted", "task.completed", "task.failed",
    "agent.joined", "agent.left",
    "message.posted", "message.broadcast",
    "swarm.formed", "swarm.dissolved",
    "job.posted", "job.filled",
    "payment.received", "payment.sent",
    "social.post", "social.reaction",
    "alert.security", "alert.system",
]


# ── Endpoint Registration ──────────────────────────────────────────────────


def register_endpoint(
    url: str,
    events: list[str],
    name: str = "",
    secret: str = "",
    active: bool = True,
    headers: Optional[dict] = None,
) -> dict:
    """Register a webhook endpoint.

    Args:
        url: The URL to POST to when events fire.
        events: List of event types to subscribe to (or ["*"] for all).
        name: Human-readable name for this endpoint.
        secret: Shared secret for HMAC-SHA256 signing. Optional.
        active: Whether this endpoint is active.
        headers: Extra headers to include in webhook requests.

    Returns:
        The endpoint record.
    """
    if not url:
        return {"error": "URL is required"}

    # Validate events
    if events != ["*"]:
        invalid = [e for e in events if e not in WEBHOOK_EVENTS]
        if invalid:
            return {"error": f"Invalid event types: {', '.join(invalid)}"}

    endpoint_id = uuid.uuid4().hex[:12]
    endpoint = {
        "endpoint_id": endpoint_id,
        "url": url,
        "name": name or f"webhook-{endpoint_id[:6]}",
        "events": events,
        "secret": secret,
        "active": active,
        "headers": headers or {},
        "created_at": datetime.now(timezone.utc).isoformat(),
        "last_triggered": None,
        "total_deliveries": 0,
        "total_failures": 0,
    }

    _write_json_sync(ENDPOINTS_DIR / f"{endpoint_id}.json", endpoint)
    return endpoint


def get_endpoint(endpoint_id: str) -> Optional[dict]:
    """Get a webhook endpoint by ID."""
    try:
        return json.loads((ENDPOINTS_DIR / f"{endpoint_id}.json").read_text("utf-8"))
    except (FileNotFoundError, json.JSONDecodeError, OSError):
        return None


def list_endpoints(active_only: bool = False) -> list[dict]:
    """List all registered webhook endpoints."""
    endpoints = []
    for f in sorted(ENDPOINTS_DIR.glob("*.json")):
        try:
            ep = json.loads(f.read_text("utf-8"))
        except (json.JSONDecodeError, OSError):
            continue
        if active_only and not ep.get("active", True):
            continue
        endpoints.append(ep)
    return endpoints


def update_endpoint(endpoint_id: str, **kwargs) -> dict:
    """Update an endpoint's settings."""
    ep = get_endpoint(endpoint_id)
    if not ep:
        return {"error": f"Endpoint {endpoint_id} not found"}

    for key in ("url", "name", "events", "secret", "active", "headers"):
        if key in kwargs:
            ep[key] = kwargs[key]
    ep["updated_at"] = datetime.now(timezone.utc).isoformat()
    _write_json_sync(ENDPOINTS_DIR / f"{endpoint_id}.json", ep)
    return ep


def delete_endpoint(endpoint_id: str) -> bool:
    """Delete a webhook endpoint."""
    path = ENDPOINTS_DIR / f"{endpoint_id}.json"
    if path.exists():
        path.unlink(missing_ok=True)
        return True
    return False


# ── Webhook Firing ─────────────────────────────────────────────────────────


def fire_event(event_type: str, payload: dict) -> dict:
    """Fire a webhook event to all matching endpoints.

    This is the main integration point — call this whenever an event
    occurs in the platform. Matching endpoints get a POST with the payload.

    In production, the actual HTTP POST would be async. For now, we
    queue deliveries and record them.
    """
    if event_type not in WEBHOOK_EVENTS:
        return {"error": f"Unknown event type: {event_type}"}

    # Find matching endpoints
    endpoints = list_endpoints(active_only=True)
    matching = [
        ep for ep in endpoints
        if "*" in ep.get("events", []) or event_type in ep.get("events", [])
    ]

    deliveries = []
    for ep in matching:
        delivery = _create_delivery(ep, event_type, payload)
        deliveries.append(delivery)

        # Update endpoint stats
        ep["last_triggered"] = datetime.now(timezone.utc).isoformat()
        ep["total_deliveries"] = ep.get("total_deliveries", 0) + 1
        _write_json_sync(ENDPOINTS_DIR / f"{ep['endpoint_id']}.json", ep)

    return {
        "event_type": event_type,
        "endpoints_matched": len(matching),
        "deliveries_created": len(deliveries),
    }


def _create_delivery(endpoint: dict, event_type: str, payload: dict) -> dict:
    """Create a delivery record for a webhook."""
    delivery_id = uuid.uuid4().hex[:12]
    now = datetime.now(timezone.utc).isoformat()

    body = {
        "event": event_type,
        "timestamp": now,
        "payload": payload,
    }

    # Sign the payload if endpoint has a secret
    signature = ""
    if endpoint.get("secret"):
        body_str = json.dumps(body, sort_keys=True, default=str)
        signature = hmac.new(
            endpoint["secret"].encode(), body_str.encode(), hashlib.sha256
        ).hexdigest()

    delivery = {
        "delivery_id": delivery_id,
        "endpoint_id": endpoint["endpoint_id"],
        "endpoint_url": endpoint["url"],
        "event_type": event_type,
        "body": body,
        "signature": signature,
        "status": "queued",  # queued -> delivered | failed
        "attempts": 0,
        "max_attempts": 3,
        "created_at": now,
        "delivered_at": None,
        "last_error": None,
    }

    # Queue for delivery
    _write_json_sync(QUEUE_DIR / f"{delivery_id}.json", delivery)
    _append_jsonl_sync(DELIVERIES_FILE, {
        "delivery_id": delivery_id,
        "endpoint_id": endpoint["endpoint_id"],
        "event_type": event_type,
        "status": "queued",
        "timestamp": now,
    })

    return delivery


# ── Signature Verification ─────────────────────────────────────────────────


def verify_signature(body: str, signature: str, secret: str) -> bool:
    """Verify a webhook delivery signature.

    Receivers call this to verify the webhook came from Brynq.
    """
    expected = hmac.new(secret.encode(), body.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)


# ── Delivery Queue ─────────────────────────────────────────────────────────


def get_pending_deliveries() -> list[dict]:
    """Get all pending webhook deliveries."""
    pending = []
    for f in sorted(QUEUE_DIR.glob("*.json")):
        try:
            d = json.loads(f.read_text("utf-8"))
            if d.get("status") == "queued":
                pending.append(d)
        except (json.JSONDecodeError, OSError):
            continue
    return pending


def mark_delivered(delivery_id: str) -> dict:
    """Mark a delivery as successfully delivered."""
    path = QUEUE_DIR / f"{delivery_id}.json"
    try:
        d = json.loads(path.read_text("utf-8"))
        d["status"] = "delivered"
        d["delivered_at"] = datetime.now(timezone.utc).isoformat()
        d["attempts"] = d.get("attempts", 0) + 1
        _write_json_sync(path, d)
        return {"status": "delivered", "delivery_id": delivery_id}
    except (FileNotFoundError, json.JSONDecodeError, OSError):
        return {"error": f"Delivery {delivery_id} not found"}


def mark_failed(delivery_id: str, error: str) -> dict:
    """Record a delivery failure. Retries if under max attempts."""
    path = QUEUE_DIR / f"{delivery_id}.json"
    try:
        d = json.loads(path.read_text("utf-8"))
        d["attempts"] = d.get("attempts", 0) + 1
        d["last_error"] = error

        if d["attempts"] >= d.get("max_attempts", 3):
            d["status"] = "failed"
            # Update endpoint failure count
            ep = get_endpoint(d["endpoint_id"])
            if ep:
                ep["total_failures"] = ep.get("total_failures", 0) + 1
                _write_json_sync(ENDPOINTS_DIR / f"{ep['endpoint_id']}.json", ep)
        # else stays "queued" for retry

        _write_json_sync(path, d)
        return {"status": d["status"], "attempts": d["attempts"]}
    except (FileNotFoundError, json.JSONDecodeError, OSError):
        return {"error": f"Delivery {delivery_id} not found"}


def clear_completed() -> int:
    """Remove delivered/failed deliveries from the queue."""
    removed = 0
    for f in QUEUE_DIR.glob("*.json"):
        try:
            d = json.loads(f.read_text("utf-8"))
            if d.get("status") in ("delivered", "failed"):
                f.unlink(missing_ok=True)
                removed += 1
        except (json.JSONDecodeError, OSError):
            continue
    return removed


# ── History & Stats ────────────────────────────────────────────────────────


def get_delivery_history(
    endpoint_id: Optional[str] = None,
    event_type: Optional[str] = None,
    limit: int = 50,
) -> list[dict]:
    """Get webhook delivery history."""
    try:
        entries = []
        with open(DELIVERIES_FILE, "r", encoding="utf-8") as f:
            for line in f:
                line = line.strip()
                if not line:
                    continue
                try:
                    entry = json.loads(line)
                except json.JSONDecodeError:
                    continue
                if endpoint_id and entry.get("endpoint_id") != endpoint_id:
                    continue
                if event_type and entry.get("event_type") != event_type:
                    continue
                entries.append(entry)
        return entries[-limit:]
    except (FileNotFoundError, OSError):
        return []


def get_webhook_stats() -> dict:
    """Get aggregate webhook statistics."""
    endpoints = list_endpoints()
    total_deliveries = sum(ep.get("total_deliveries", 0) for ep in endpoints)
    total_failures = sum(ep.get("total_failures", 0) for ep in endpoints)
    pending = len(get_pending_deliveries())

    return {
        "total_endpoints": len(endpoints),
        "active_endpoints": len([ep for ep in endpoints if ep.get("active")]),
        "total_deliveries": total_deliveries,
        "total_failures": total_failures,
        "pending_in_queue": pending,
        "success_rate": round(
            (total_deliveries - total_failures) / max(total_deliveries, 1), 3
        ),
    }
