"""Incoming webhook handlers for Oclawma.

This module provides HTTP endpoints to receive webhooks and enqueue jobs.
Supports GitHub, Stripe, and generic HMAC signature verification.
"""

from __future__ import annotations

import hashlib
import hmac
import json
import logging
from dataclasses import dataclass, field
from typing import Any

from fastapi import APIRouter, FastAPI, HTTPException, Request, status
from fastapi.responses import JSONResponse

from oclawma.queue import JobPriority, JobQueue

logger = logging.getLogger(__name__)


class WebhookVerificationError(Exception):
    """Raised when webhook signature verification fails."""

    pass


class WebhookPayloadError(Exception):
    """Raised when webhook payload is invalid."""

    pass


@dataclass
class WebhookEndpoint:
    """Configuration for a webhook endpoint.

    Attributes:
        path: URL path for the webhook (e.g., "github", "stripe")
        secret: Secret key for signature verification
        job_type: Type of job to create when webhook is received
        provider: Provider type for signature verification (github, stripe, hmac)
        priority: Job priority for enqueued jobs
        payload_transform: Optional function to transform payload before enqueuing
        enabled: Whether this endpoint is active
        metadata: Additional metadata for the endpoint
    """

    path: str
    secret: str
    job_type: str = "webhook"
    provider: str = "generic"  # github, stripe, generic
    priority: JobPriority = JobPriority.NORMAL
    payload_transform: str | None = None  # JSONPath-like transformation
    enabled: bool = True
    metadata: dict[str, Any] = field(default_factory=dict)

    def __post_init__(self) -> None:
        """Validate the endpoint configuration."""
        if not self.path:
            raise ValueError("Webhook path cannot be empty")
        if not self.secret:
            raise ValueError("Webhook secret cannot be empty")
        if self.provider not in ("github", "stripe", "generic"):
            raise ValueError(f"Unsupported provider: {self.provider}")

    def to_dict(self) -> dict[str, Any]:
        """Convert endpoint to dictionary."""
        return {
            "path": self.path,
            "secret": self.secret,
            "job_type": self.job_type,
            "provider": self.provider,
            "priority": self.priority.value,
            "payload_transform": self.payload_transform,
            "enabled": self.enabled,
            "metadata": self.metadata,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> WebhookEndpoint:
        """Create endpoint from dictionary."""
        priority = data.get("priority", 0)
        if isinstance(priority, int):
            priority = _int_to_priority(priority)

        return cls(
            path=data["path"],
            secret=data["secret"],
            job_type=data.get("job_type", "webhook"),
            provider=data.get("provider", "generic"),
            priority=priority,
            payload_transform=data.get("payload_transform"),
            enabled=data.get("enabled", True),
            metadata=data.get("metadata", {}),
        )


class WebhookVerifier:
    """Handles signature verification for different webhook providers."""

    @staticmethod
    def verify_github(payload: bytes, signature: str, secret: str) -> bool:
        """Verify GitHub webhook signature.

        GitHub signatures are in the format: sha256=<hex_digest>
        """
        if not signature:
            logger.warning("Missing GitHub signature header")
            return False

        # GitHub signatures are in format "sha256=<hex_digest>"
        if signature.startswith("sha256="):
            signature = signature[7:]

        expected = hmac.new(
            secret.encode(),
            payload,
            hashlib.sha256,
        ).hexdigest()

        return hmac.compare_digest(signature, expected)

    @staticmethod
    def verify_stripe(payload: bytes, signature: str, secret: str) -> bool:
        """Verify Stripe webhook signature.

        Stripe signatures are in the format: t=<timestamp>,v1=<signature>
        """
        if not signature:
            logger.warning("Missing Stripe signature header")
            return False

        try:
            # Parse timestamp and signature from header
            parts = signature.split(",")
            timestamp = None
            signatures = []

            for part in parts:
                if part.startswith("t="):
                    timestamp = part[2:]
                elif part.startswith("v1="):
                    signatures.append(part[3:])

            if not timestamp or not signatures:
                logger.warning("Invalid Stripe signature format")
                return False

            # Create signed payload: timestamp.payload
            signed_payload = f"{timestamp.decode() if isinstance(timestamp, bytes) else timestamp}.{payload.decode()}"

            # Compute expected signature
            expected = hmac.new(
                secret.encode(),
                signed_payload.encode(),
                hashlib.sha256,
            ).hexdigest()

            # Compare against all provided signatures
            return any(hmac.compare_digest(expected, sig) for sig in signatures)

        except Exception as e:
            logger.warning(f"Stripe signature verification failed: {e}")
            return False

    @staticmethod
    def verify_hmac(payload: bytes, signature: str, secret: str, algorithm: str = "sha256") -> bool:
        """Verify generic HMAC signature.

        Args:
            payload: Raw request body
            signature: Provided signature (hex or base64)
            secret: Secret key
            algorithm: Hash algorithm to use (sha256, sha512, etc.)
        """
        if not signature:
            logger.warning("Missing HMAC signature")
            return False

        try:
            hash_func = getattr(hashlib, algorithm, hashlib.sha256)
        except AttributeError:
            logger.warning(f"Unsupported hash algorithm: {algorithm}")
            return False

        expected = hmac.new(
            secret.encode(),
            payload,
            hash_func,
        ).hexdigest()

        # Try hex comparison first
        if hmac.compare_digest(signature, expected):
            return True

        # Try base64 decoded comparison
        try:
            import base64

            decoded_sig = base64.b64decode(signature).hex()
            return hmac.compare_digest(decoded_sig, expected)
        except Exception:
            pass

        return False


class WebhookPayloadTransformer:
    """Transforms webhook payloads into job format."""

    @staticmethod
    def transform(payload: dict[str, Any], endpoint: WebhookEndpoint) -> dict[str, Any]:
        """Transform payload into job format.

        Args:
            payload: Raw webhook payload
            endpoint: Webhook endpoint configuration

        Returns:
            Transformed payload ready for job enqueueing
        """
        job_payload = {
            "webhook": {
                "path": endpoint.path,
                "provider": endpoint.provider,
                "timestamp": payload.get("timestamp") or payload.get("created_at"),
            },
            "data": payload,
        }

        # Add provider-specific metadata
        if endpoint.provider == "github":
            job_payload["webhook"]["event"] = payload.get("action")
            job_payload["webhook"]["repository"] = payload.get("repository", {}).get("full_name")
            job_payload["webhook"]["sender"] = payload.get("sender", {}).get("login")

        elif endpoint.provider == "stripe":
            job_payload["webhook"]["event"] = payload.get("type")
            job_payload["webhook"]["object_id"] = (
                payload.get("data", {}).get("object", {}).get("id")
            )

        # Apply custom metadata
        if endpoint.metadata:
            job_payload["webhook"]["metadata"] = endpoint.metadata

        return job_payload


class WebhookManager:
    """Manages webhook endpoints and handles incoming webhooks."""

    def __init__(self, job_queue: JobQueue | None = None) -> None:
        """Initialize webhook manager.

        Args:
            job_queue: Job queue for enqueueing webhook jobs
        """
        self.endpoints: dict[str, WebhookEndpoint] = {}
        self.verifier = WebhookVerifier()
        self.transformer = WebhookPayloadTransformer()
        self.job_queue = job_queue

    def register_endpoint(self, endpoint: WebhookEndpoint) -> None:
        """Register a webhook endpoint.

        Args:
            endpoint: Endpoint configuration to register

        Raises:
            ValueError: If endpoint path is already registered
        """
        if endpoint.path in self.endpoints:
            raise ValueError(f"Endpoint '{endpoint.path}' is already registered")

        self.endpoints[endpoint.path] = endpoint
        logger.info(f"Registered webhook endpoint: {endpoint.path}")

    def unregister_endpoint(self, path: str) -> bool:
        """Unregister a webhook endpoint.

        Args:
            path: Path of the endpoint to unregister

        Returns:
            True if unregistered, False if not found
        """
        if path in self.endpoints:
            del self.endpoints[path]
            logger.info(f"Unregistered webhook endpoint: {path}")
            return True
        return False

    def get_endpoint(self, path: str) -> WebhookEndpoint | None:
        """Get an endpoint by path.

        Args:
            path: Endpoint path

        Returns:
            Endpoint configuration or None if not found
        """
        return self.endpoints.get(path)

    def list_endpoints(self) -> list[WebhookEndpoint]:
        """List all registered endpoints.

        Returns:
            List of endpoint configurations
        """
        return list(self.endpoints.values())

    def verify_signature(
        self,
        endpoint: WebhookEndpoint,
        payload: bytes,
        signature: str,
    ) -> bool:
        """Verify webhook signature.

        Args:
            endpoint: Webhook endpoint configuration
            payload: Raw request body
            signature: Provided signature

        Returns:
            True if signature is valid
        """
        if endpoint.provider == "github":
            return self.verifier.verify_github(payload, signature, endpoint.secret)
        elif endpoint.provider == "stripe":
            return self.verifier.verify_stripe(payload, signature, endpoint.secret)
        else:
            return self.verifier.verify_hmac(payload, signature, endpoint.secret)

    async def handle_webhook(
        self,
        path: str,
        request: Request,
        signature_header: str | None = None,
    ) -> dict[str, Any]:
        """Handle incoming webhook request.

        Args:
            path: Webhook endpoint path
            request: FastAPI request object
            signature_header: Signature from request header

        Returns:
            Response data including job ID

        Raises:
            HTTPException: If verification fails or endpoint not found
        """
        # Find endpoint
        endpoint = self.get_endpoint(path)
        if not endpoint:
            logger.warning(f"Webhook endpoint not found: {path}")
            raise HTTPException(
                status_code=status.HTTP_404_NOT_FOUND,
                detail="Webhook endpoint not found",
            )

        if not endpoint.enabled:
            logger.warning(f"Webhook endpoint disabled: {path}")
            raise HTTPException(
                status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
                detail="Webhook endpoint is disabled",
            )

        # Read raw body
        body = await request.body()

        # Get signature from header if not provided
        if signature_header is None:
            # Try common signature headers based on provider
            header_mapping = {
                "github": "x-hub-signature-256",
                "stripe": "stripe-signature",
                "generic": "x-webhook-signature",
            }
            header_name = header_mapping.get(endpoint.provider, "x-webhook-signature")

            # Look for the header case-insensitively
            signature_header = ""
            for key, value in request.headers.items():
                if key.lower() == header_name:
                    signature_header = value
                    break

        # Verify signature
        if not self.verify_signature(endpoint, body, signature_header):
            logger.warning(f"Invalid signature for webhook: {path}")
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Invalid signature",
            )

        # Parse payload
        try:
            payload = json.loads(body)
        except json.JSONDecodeError as e:
            logger.warning(f"Invalid JSON payload for webhook {path}: {e}")
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail="Invalid JSON payload",
            ) from e

        # Transform payload to job format
        job_payload = self.transformer.transform(payload, endpoint)

        # Enqueue job if queue is configured
        if self.job_queue:
            job = self.job_queue.enqueue(
                payload=job_payload,
                priority=endpoint.priority,
                job_type=endpoint.job_type,
            )
            logger.info(f"Enqueued webhook job {job.id} from {path}")
            return {
                "status": "accepted",
                "job_id": job.id,
                "path": path,
            }
        else:
            logger.warning(f"No job queue configured for webhook: {path}")
            return {
                "status": "accepted",
                "job_id": None,
                "path": path,
                "message": "No job queue configured",
            }


def _int_to_priority(value: int) -> JobPriority:
    """Convert integer value to JobPriority enum."""
    for priority in JobPriority:
        if priority.value == value:
            return priority
    if value >= 75:
        return JobPriority.CRITICAL
    elif value >= 25:
        return JobPriority.HIGH
    elif value >= -25:
        return JobPriority.NORMAL
    else:
        return JobPriority.LOW


# =============================================================================
# FastAPI Router
# =============================================================================


def create_webhook_router(manager: WebhookManager) -> APIRouter:
    """Create FastAPI router for webhook endpoints.

    Args:
        manager: Webhook manager instance

    Returns:
        Configured APIRouter
    """
    router = APIRouter(prefix="/webhooks", tags=["webhooks"])

    @router.post("/{path:path}")
    async def webhook_handler(
        path: str,
        request: Request,
    ) -> JSONResponse:
        """Handle incoming webhook requests."""
        result = await manager.handle_webhook(path, request)
        return JSONResponse(
            content=result,
            status_code=status.HTTP_202_ACCEPTED,
        )

    @router.get("/")
    async def list_webhooks() -> JSONResponse:
        """List registered webhook endpoints."""
        endpoints = manager.list_endpoints()
        return JSONResponse(
            content={
                "endpoints": [
                    {
                        "path": e.path,
                        "provider": e.provider,
                        "job_type": e.job_type,
                        "enabled": e.enabled,
                    }
                    for e in endpoints
                ]
            }
        )

    return router


# =============================================================================
# Integration with main app
# =============================================================================


def setup_webhooks(
    app: FastAPI,
    job_queue: JobQueue | None = None,
    endpoints: list[WebhookEndpoint] | None = None,
) -> WebhookManager:
    """Set up webhooks for a FastAPI application.

    Args:
        app: FastAPI application
        job_queue: Optional job queue for enqueueing jobs
        endpoints: Optional list of endpoints to pre-register

    Returns:
        Configured WebhookManager
    """
    manager = WebhookManager(job_queue)

    if endpoints:
        for endpoint in endpoints:
            try:
                manager.register_endpoint(endpoint)
            except ValueError as e:
                logger.warning(f"Failed to register endpoint {endpoint.path}: {e}")

    # Create and include router
    router = create_webhook_router(manager)
    app.include_router(router)

    logger.info(f"Webhook endpoints registered: {len(manager.endpoints)}")
    return manager
