"""
cortex/api/routes/webhooks.py — Webhook CRUD + test-fire endpoints (M20 / #183).

Endpoints:
    GET    /api/v1/webhooks            — list webhooks for an owner
    POST   /api/v1/webhooks            — create webhook
    GET    /api/v1/webhooks/{id}       — get webhook detail
    PATCH  /api/v1/webhooks/{id}       — update webhook (url, events, active)
    DELETE /api/v1/webhooks/{id}       — delete webhook
    POST   /api/v1/webhooks/{id}/test  — send a test event
"""
from __future__ import annotations

import json
import logging
from typing import Any, Dict, List, Optional

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel

from cortex.api.deps import get_plugin_holder
from cortex.events.emitter import CortexEvent, EventType
from cortex.events.webhook import WebhookDispatcher, _row_to_webhook, validate_webhook_url as _validate_webhook_url

logger = logging.getLogger(__name__)
router = APIRouter()


# ---------------------------------------------------------------------------
# Request / Response models
# ---------------------------------------------------------------------------

class WebhookCreateRequest(BaseModel):
    url: str
    events: List[str] = []
    secret: Optional[str] = None
    name: Optional[str] = None


class WebhookUpdateRequest(BaseModel):
    url: Optional[str] = None
    events: Optional[List[str]] = None
    active: Optional[bool] = None
    name: Optional[str] = None
    secret: Optional[str] = None


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _get_storage():
    holder = get_plugin_holder()
    plugin = holder.get("plugin")
    if plugin is None or plugin.storage is None:
        raise HTTPException(status_code=503, detail="Storage unavailable")
    return plugin.storage


def _get_allow_private_urls() -> bool:
    """Read webhook_allow_private_urls from CortexConfig (#435)."""
    holder = get_plugin_holder()
    plugin = holder.get("plugin")
    if plugin is None:
        return False
    return getattr(getattr(plugin, "config", None), "webhook_allow_private_urls", False)


def _format_webhook(row: Dict[str, Any]) -> Dict[str, Any]:
    """Normalise a webhook DB row for JSON response."""
    events_raw = row.get("events", "[]") or "[]"
    if isinstance(events_raw, str):
        try:
            events = json.loads(events_raw)
        except (json.JSONDecodeError, ValueError):
            events = []
    else:
        events = list(events_raw)

    return {
        "id": row["id"],
        "owner_id": row.get("owner_id", "default"),
        "name": row.get("name", ""),
        "url": row["url"],
        "events": events,
        "active": bool(row.get("active", 1)),
        "last_triggered": row.get("last_triggered"),
        "last_status": row.get("last_status"),
        "failure_count": row.get("failure_count", 0),
        "created_at": row.get("created_at"),
        "updated_at": row.get("updated_at"),
    }


def _validate_event_types(events: List[str]) -> List[str]:
    """Validate and return normalised event type strings.

    Raises HTTPException 422 for any unknown event type.
    """
    valid = {e.value for e in EventType}
    bad = [e for e in events if e not in valid]
    if bad:
        raise HTTPException(
            status_code=422,
            detail=f"Unknown event types: {bad}. Valid values: {sorted(valid)}",
        )
    return events


# ---------------------------------------------------------------------------
# Routes (auth-gated via _make_routes pattern)
# ---------------------------------------------------------------------------

def _make_routes(verify_api_key):
    """Register webhook routes with API key authentication."""

    @router.get(
        "/api/v1/webhooks",
        summary="List webhooks",
        tags=["Webhooks"],
    )
    async def list_webhooks(
        owner_id: str = "default",
        active_only: bool = False,
        _: None = Depends(verify_api_key),
    ):
        """Return all webhook subscriptions for *owner_id*."""
        storage = _get_storage()
        try:
            rows = await storage.list_webhooks(owner_id=owner_id, active_only=active_only)
        except Exception as exc:
            logger.error("GET /api/v1/webhooks error: %s", exc, exc_info=True)
            raise HTTPException(status_code=500, detail="Internal server error")
        return {"webhooks": [_format_webhook(r) for r in rows], "count": len(rows)}

    @router.post(
        "/api/v1/webhooks",
        status_code=201,
        summary="Create webhook",
        tags=["Webhooks"],
    )
    async def create_webhook(
        body: WebhookCreateRequest,
        owner_id: str = "default",
        _: None = Depends(verify_api_key),
    ):
        """Register a new webhook subscription."""
        storage = _get_storage()
        events = _validate_event_types(body.events)

        dispatcher = WebhookDispatcher(
            storage=storage,
        )
        try:
            webhook = await dispatcher.register(
                owner_id=owner_id,
                url=body.url,
                events=[EventType(e) for e in events] if events else [],
                secret=body.secret,
                name=body.name or body.url,
            )
        except ValueError as exc:
            raise HTTPException(status_code=422, detail=str(exc))
        except Exception as exc:
            logger.error("POST /api/v1/webhooks error: %s", exc, exc_info=True)
            raise HTTPException(status_code=500, detail="Internal server error")

        row = await storage.get_webhook(webhook.id)
        return _format_webhook(row)

    @router.get(
        "/api/v1/webhooks/{webhook_id}",
        summary="Get webhook detail",
        tags=["Webhooks"],
    )
    async def get_webhook(
        webhook_id: str,
        _: None = Depends(verify_api_key),
    ):
        """Return a single webhook by ID."""
        storage = _get_storage()
        row = await storage.get_webhook(webhook_id)
        if row is None:
            raise HTTPException(status_code=404, detail=f"Webhook {webhook_id!r} not found")
        return _format_webhook(row)

    @router.patch(
        "/api/v1/webhooks/{webhook_id}",
        summary="Update webhook",
        tags=["Webhooks"],
    )
    async def update_webhook(
        webhook_id: str,
        body: WebhookUpdateRequest,
        _: None = Depends(verify_api_key),
    ):
        """Partially update a webhook (url, events, active, name, secret)."""
        storage = _get_storage()

        existing = await storage.get_webhook(webhook_id)
        if existing is None:
            raise HTTPException(status_code=404, detail=f"Webhook {webhook_id!r} not found")

        updates: Dict[str, Any] = {}
        if body.url is not None:
            # SSRF protection: validate new URL before persisting (#435)
            try:
                _validate_webhook_url(body.url, allow_private=_get_allow_private_urls())
            except ValueError as exc:
                raise HTTPException(status_code=422, detail=str(exc))
            updates["url"] = body.url
        if body.events is not None:
            updates["events"] = _validate_event_types(body.events)
        if body.active is not None:
            updates["active"] = int(body.active)
        if body.name is not None:
            updates["name"] = body.name
        if body.secret is not None:
            updates["secret"] = body.secret

        try:
            row = await storage.update_webhook(webhook_id, **updates)
        except Exception as exc:
            logger.error("PATCH /api/v1/webhooks/%s error: %s", webhook_id, exc, exc_info=True)
            raise HTTPException(status_code=500, detail="Internal server error")

        return _format_webhook(row)

    @router.delete(
        "/api/v1/webhooks/{webhook_id}",
        status_code=204,
        summary="Delete webhook",
        tags=["Webhooks"],
    )
    async def delete_webhook(
        webhook_id: str,
        _: None = Depends(verify_api_key),
    ):
        """Delete a webhook by ID."""
        storage = _get_storage()
        deleted = await storage.delete_webhook(webhook_id)
        if not deleted:
            raise HTTPException(status_code=404, detail=f"Webhook {webhook_id!r} not found")
        return None

    @router.post(
        "/api/v1/webhooks/{webhook_id}/test",
        summary="Send test event to webhook",
        tags=["Webhooks"],
    )
    async def test_webhook(
        webhook_id: str,
        owner_id: str = "default",
        _: None = Depends(verify_api_key),
    ):
        """Fire a synthetic ``cortex.test`` event to the specified webhook URL.

        Returns delivery result (status_code or error).
        """
        storage = _get_storage()

        row = await storage.get_webhook(webhook_id)
        if row is None:
            raise HTTPException(status_code=404, detail=f"Webhook {webhook_id!r} not found")

        wh = _row_to_webhook(row)

        # SSRF protection: re-validate URL at test time to catch DNS rebinding (#435)
        try:
            _validate_webhook_url(wh.url, allow_private=_get_allow_private_urls())
        except ValueError as exc:
            raise HTTPException(status_code=422, detail=str(exc))

        # Build a synthetic test event
        test_event = CortexEvent(
            event_type=EventType.SESSION_STARTED,  # use a real event type for validity
            owner_id=wh.owner_id,
            payload={"test": True, "webhook_id": webhook_id},
        )

        dispatcher = WebhookDispatcher(storage=storage)
        from cortex.events.webhook import _make_payload_bytes, _generate_signature

        payload_bytes = _make_payload_bytes(test_event)
        headers: Dict[str, str] = {
            "Content-Type": "application/json",
            "X-EvaOS-Event": "cortex.test",
            "X-EvaOS-Webhook-ID": webhook_id,
        }
        if wh.secret:
            headers["X-EvaOS-Signature"] = _generate_signature(payload_bytes, wh.secret)

        try:
            status_code = await dispatcher._http_post(wh.url, payload_bytes, headers)
            success = 200 <= status_code < 300
            await storage.log_webhook_delivery(
                webhook_id=webhook_id,
                event_type="cortex.test",
                status_code=status_code,
                success=success,
                error_msg=None,
            )
            return {
                "webhook_id": webhook_id,
                "url": wh.url,
                "status_code": status_code,
                "success": success,
            }
        except Exception as exc:
            await storage.log_webhook_delivery(
                webhook_id=webhook_id,
                event_type="cortex.test",
                status_code=None,
                success=False,
                error_msg=str(exc),
            )
            raise HTTPException(status_code=502, detail=f"Delivery failed: {exc}")
