"""
Outbound webhook service for emitting events and managing deliveries.
"""

import hashlib
import hmac
import json
import logging
import asyncio
from typing import Optional, List, Dict, Any, Tuple
from datetime import datetime, timedelta
from enum import Enum
import uuid
import httpx
from sqlalchemy import select, and_, or_
from sqlalchemy.ext.asyncio import AsyncSession

from fmatch.saas.model_defs.webhook_models import (
    WebhookEndpoint,
    WebhookDelivery,
    WebhookEventLog,
    WebhookStatus,
)
from fmatch.saas.database import session_context
from fmatch.saas import settings
import redis.asyncio as redis

logger = logging.getLogger(__name__)


class CircuitState(Enum):
    """Circuit breaker states."""

    CLOSED = "closed"  # Normal operation
    OPEN = "open"  # Failing, reject requests
    HALF_OPEN = "half_open"  # Testing if service recovered


class WebhookService:
    """
    Service for managing outbound webhooks with retry logic,
    circuit breaker pattern, and batching support.
    """

    def __init__(self, redis_client: Optional[redis.Redis] = None):
        self.redis_client = redis_client
        self.http_client = None
        self._batch_queues: Dict[str, List[Dict]] = {}
        self._batch_tasks: Dict[str, asyncio.Task] = {}

    async def __aenter__(self):
        """Async context manager entry."""
        self.http_client = httpx.AsyncClient(
            timeout=httpx.Timeout(
                timeout=settings.WEBHOOK_TIMEOUT or 30.0, connect=5.0
            ),
            limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
            follow_redirects=True,
        )
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        """Async context manager exit."""
        if self.http_client:
            await self.http_client.aclose()

        # Cancel any pending batch tasks
        for task in self._batch_tasks.values():
            if not task.done():
                task.cancel()

    async def emit_event(
        self,
        account_id: int,
        event_type: str,
        payload: Dict[str, Any],
        event_id: Optional[str] = None,
        session: Optional[AsyncSession] = None,
    ) -> List[str]:
        """
        Emit an event to all subscribed webhook endpoints.

        Args:
            account_id: Account ID
            event_type: Type of event (e.g., 'job.completed')
            payload: Event payload data
            event_id: Optional event identifier for idempotency
            session: Optional database session

        Returns:
            List of delivery IDs created
        """
        if not session:
            async with session_context() as session:
                return await self.emit_event(
                    account_id, event_type, payload, event_id, session
                )

        # Find all active endpoints subscribed to this event
        endpoints = await self._get_subscribed_endpoints(
            session, account_id, event_type
        )

        if not endpoints:
            logger.debug(
                f"No active endpoints for account {account_id} event {event_type}"
            )
            return []

        delivery_ids = []

        for endpoint in endpoints:
            # Check circuit breaker
            if self._is_circuit_open(endpoint):
                logger.warning(f"Circuit breaker open for endpoint {endpoint.id}")
                continue

            # Create delivery record
            delivery_id = await self._create_delivery(
                session=session,
                endpoint=endpoint,
                event_type=event_type,
                payload=payload,
                event_id=event_id,
            )

            if delivery_id:
                delivery_ids.append(str(delivery_id))

                # Handle batching if enabled
                if endpoint.batch_enabled:
                    await self._add_to_batch(endpoint, delivery_id)
                else:
                    # Queue for immediate delivery
                    await self._queue_delivery(delivery_id)

        await session.commit()
        return delivery_ids

    async def _get_subscribed_endpoints(
        self, session: AsyncSession, account_id: int, event_type: str
    ) -> List[WebhookEndpoint]:
        """Get all active endpoints subscribed to an event type."""
        query = select(WebhookEndpoint).where(
            and_(
                WebhookEndpoint.account_id == account_id,
                WebhookEndpoint.active == True,
                or_(
                    WebhookEndpoint.events.contains([event_type]),
                    WebhookEndpoint.events.contains(["*"]),  # Wildcard subscription
                ),
            )
        )

        result = await session.execute(query)
        return result.scalars().all()

    def _is_circuit_open(self, endpoint: WebhookEndpoint) -> bool:
        """Check if circuit breaker is open for an endpoint."""
        if endpoint.consecutive_failures >= endpoint.circuit_breaker_threshold:
            # Check if we should reset
            if endpoint.circuit_breaker_reset_after:
                if datetime.utcnow() > endpoint.circuit_breaker_reset_after:
                    return False  # Time to retry
            return True
        return False

    async def _create_delivery(
        self,
        session: AsyncSession,
        endpoint: WebhookEndpoint,
        event_type: str,
        payload: Dict[str, Any],
        event_id: Optional[str] = None,
    ) -> Optional[uuid.UUID]:
        """Create a webhook delivery record."""
        try:
            # Add metadata to payload
            enriched_payload = {
                "event": event_type,
                "timestamp": datetime.utcnow().isoformat(),
                "id": event_id or str(uuid.uuid4()),
                "data": payload,
            }

            delivery = WebhookDelivery(
                endpoint_id=endpoint.id,
                event_type=event_type,
                event_id=event_id,
                payload=enriched_payload,
                max_attempts=endpoint.max_retries + 1,
                status=WebhookStatus.PENDING.value,
            )

            session.add(delivery)
            await session.flush()

            return delivery.id

        except Exception as e:
            logger.error(f"Failed to create delivery: {e}")
            return None

    async def _queue_delivery(self, delivery_id: str, delay_seconds: int = 0):
        """Queue a delivery for processing."""
        if self.redis_client:
            # Add to Redis sorted set with score as timestamp
            score = (datetime.utcnow() + timedelta(seconds=delay_seconds)).timestamp()
            await self.redis_client.zadd(
                "webhook:deliveries:pending", {delivery_id: score}
            )
            logger.debug(f"Queued delivery {delivery_id} with delay {delay_seconds}s")

    async def _add_to_batch(self, endpoint: WebhookEndpoint, delivery_id: str):
        """Add delivery to batch queue."""
        batch_key = f"batch:{endpoint.id}"

        if batch_key not in self._batch_queues:
            self._batch_queues[batch_key] = []

            # Start batch timer if not running
            if (
                batch_key not in self._batch_tasks
                or self._batch_tasks[batch_key].done()
            ):
                self._batch_tasks[batch_key] = asyncio.create_task(
                    self._process_batch_after_delay(endpoint)
                )

        self._batch_queues[batch_key].append(delivery_id)

        # Process immediately if batch is full
        if len(self._batch_queues[batch_key]) >= endpoint.batch_size:
            await self._process_batch(endpoint)

    async def _process_batch_after_delay(self, endpoint: WebhookEndpoint):
        """Process batch after configured window."""
        await asyncio.sleep(endpoint.batch_window_seconds)
        await self._process_batch(endpoint)

    async def _process_batch(self, endpoint: WebhookEndpoint):
        """Process a batch of deliveries."""
        batch_key = f"batch:{endpoint.id}"

        if batch_key not in self._batch_queues or not self._batch_queues[batch_key]:
            return

        delivery_ids = self._batch_queues[batch_key][: endpoint.batch_size]
        self._batch_queues[batch_key] = self._batch_queues[batch_key][
            endpoint.batch_size :
        ]

        # Queue batch for processing
        batch_id = str(uuid.uuid4())
        for i, delivery_id in enumerate(delivery_ids):
            if self.redis_client:
                await self.redis_client.hset(
                    f"webhook:batch:{batch_id}",
                    mapping={str(i): delivery_id, "endpoint_id": str(endpoint.id)},
                )

        await self._queue_delivery(batch_id)

    async def deliver_webhook(
        self, delivery_id: str, session: Optional[AsyncSession] = None
    ) -> bool:
        """
        Deliver a single webhook.

        Args:
            delivery_id: Delivery ID
            session: Optional database session

        Returns:
            True if successful, False otherwise
        """
        if not session:
            async with session_context() as session:
                return await self.deliver_webhook(delivery_id, session)

        # Get delivery with endpoint via explicit join (no ORM relationship)
        query = (
            select(WebhookDelivery, WebhookEndpoint)
            .join(WebhookEndpoint, WebhookEndpoint.id == WebhookDelivery.endpoint_id)
            .where(WebhookDelivery.id == delivery_id)
        )

        result = await session.execute(query)
        row = result.first()

        if not row:
            logger.error(f"Delivery {delivery_id} not found")
            return False

        delivery, endpoint = row

        # Update delivery status
        delivery.status = WebhookStatus.IN_PROGRESS.value
        delivery.attempts += 1

        if not delivery.first_attempted_at:
            delivery.first_attempted_at = datetime.utcnow()
        delivery.last_attempted_at = datetime.utcnow()

        try:
            # Prepare request
            headers = self._prepare_headers(endpoint, delivery)
            # Persist headers used for audit
            try:
                delivery.headers = headers
            except Exception:
                pass

            # Send webhook
            start_time = datetime.utcnow()

            if not self.http_client:
                async with httpx.AsyncClient(
                    timeout=httpx.Timeout(timeout=endpoint.timeout_seconds)
                ) as client:
                    response = await client.post(
                        endpoint.url, json=delivery.payload, headers=headers
                    )
            else:
                response = await self.http_client.post(
                    endpoint.url, json=delivery.payload, headers=headers
                )

            # Calculate latency
            latency_ms = int((datetime.utcnow() - start_time).total_seconds() * 1000)
            delivery.latency_ms = latency_ms

            # Update delivery with response
            delivery.response_status_code = response.status_code
            delivery.response_body = (
                response.text[:5000] if response.text else None
            )  # Limit size
            delivery.response_headers = dict(response.headers)

            # Check if successful (2xx status)
            if 200 <= response.status_code < 300:
                await self._handle_success(session, delivery, endpoint)
                await session.commit()
                return True
            else:
                await self._handle_failure(
                    session,
                    delivery,
                    endpoint,
                    f"HTTP {response.status_code}: {response.text[:500]}",
                )
                await session.commit()
                return False

        except httpx.TimeoutException as e:
            await self._handle_failure(
                session, delivery, endpoint, f"Timeout: {str(e)}"
            )
            await session.commit()
            return False

        except Exception as e:
            logger.error(f"Webhook delivery failed: {e}")
            await self._handle_failure(session, delivery, endpoint, str(e))
            await session.commit()
            return False

    def _prepare_headers(
        self, endpoint: WebhookEndpoint, delivery: WebhookDelivery
    ) -> Dict[str, str]:
        """Prepare headers for webhook request."""
        headers = {
            "Content-Type": "application/json",
            "User-Agent": "FoundryMatch-Webhook/1.0",
            "X-Webhook-Id": str(delivery.id),
            "X-Webhook-Event": delivery.event_type,
            "X-Webhook-Timestamp": str(int(datetime.utcnow().timestamp())),
        }

        # Add custom headers from endpoint config
        if endpoint.headers:
            headers.update(endpoint.headers)

        # Add HMAC signature if secret is configured
        if endpoint.secret:
            signature = self._generate_signature(
                endpoint.secret, json.dumps(delivery.payload, sort_keys=True)
            )
            headers["X-Webhook-Signature"] = signature
            headers["X-Webhook-Signature-256"] = signature  # Alternative header name

        return headers

    def _generate_signature(self, secret: str, payload: str) -> str:
        """Generate HMAC-SHA256 signature."""
        signature = hmac.new(
            secret.encode("utf-8"), payload.encode("utf-8"), hashlib.sha256
        ).hexdigest()

        return f"sha256={signature}"

    async def _handle_success(
        self,
        session: AsyncSession,
        delivery: WebhookDelivery,
        endpoint: WebhookEndpoint,
    ):
        """Handle successful webhook delivery."""
        delivery.status = WebhookStatus.SUCCESS.value
        delivery.completed_at = datetime.utcnow()
        delivery.processing_time_ms = int(
            (delivery.completed_at - delivery.created_at).total_seconds() * 1000
        )

        # Update endpoint stats
        endpoint.last_success_at = datetime.utcnow()
        endpoint.consecutive_failures = 0  # Reset circuit breaker
        endpoint.total_deliveries += 1
        endpoint.successful_deliveries += 1

        # Clear circuit breaker reset time
        endpoint.circuit_breaker_reset_after = None

        # Log event
        await self._log_event(
            session=session,
            account_id=endpoint.account_id,
            event_type="webhook.delivered",
            event_category="outbound",
            endpoint_id=endpoint.id,
            delivery_id=delivery.id,
            description=f"Successfully delivered webhook to {endpoint.url}",
        )

        logger.info(f"Webhook delivered successfully: {delivery.id}")

    async def _handle_failure(
        self,
        session: AsyncSession,
        delivery: WebhookDelivery,
        endpoint: WebhookEndpoint,
        error_message: str,
    ):
        """Handle failed webhook delivery."""
        delivery.last_error = error_message
        delivery.error_count += 1

        # Check if we should retry
        if delivery.attempts < delivery.max_attempts and endpoint.retry_enabled:
            # Calculate next retry time with exponential backoff
            delay = self._calculate_retry_delay(
                delivery.attempts,
                endpoint.retry_delay_seconds,
                endpoint.retry_backoff_multiplier,
            )

            delivery.status = WebhookStatus.RETRYING.value
            delivery.next_retry_at = datetime.utcnow() + timedelta(seconds=delay)

            # Queue for retry
            await self._queue_delivery(str(delivery.id), delay)

            logger.info(f"Webhook {delivery.id} scheduled for retry in {delay}s")
        else:
            # Max retries exceeded or retries disabled
            delivery.status = WebhookStatus.DEAD_LETTER.value
            delivery.completed_at = datetime.utcnow()

            # Update endpoint stats
            endpoint.last_failure_at = datetime.utcnow()
            endpoint.last_error = error_message
            endpoint.consecutive_failures += 1
            endpoint.total_deliveries += 1
            endpoint.failed_deliveries += 1

            # Set circuit breaker reset time if threshold reached
            if endpoint.consecutive_failures >= endpoint.circuit_breaker_threshold:
                endpoint.circuit_breaker_reset_after = datetime.utcnow() + timedelta(
                    minutes=5
                )

                await self._log_event(
                    session=session,
                    account_id=endpoint.account_id,
                    event_type="webhook.circuit_breaker_opened",
                    event_category="outbound",
                    endpoint_id=endpoint.id,
                    description=f"Circuit breaker opened after {endpoint.consecutive_failures} failures",
                )

            # Log failure event
            await self._log_event(
                session=session,
                account_id=endpoint.account_id,
                event_type="webhook.failed",
                event_category="outbound",
                endpoint_id=endpoint.id,
                delivery_id=delivery.id,
                description=f"Webhook delivery failed: {error_message}",
            )

            logger.error(f"Webhook {delivery.id} failed permanently: {error_message}")

    def _calculate_retry_delay(
        self, attempt: int, initial_delay: int, multiplier: float
    ) -> int:
        """Calculate retry delay with exponential backoff."""
        # Cap at 16 seconds as specified
        max_delay = 16
        delay = min(initial_delay * (multiplier ** (attempt - 1)), max_delay)
        return int(delay)

    async def _log_event(
        self,
        session: AsyncSession,
        account_id: int,
        event_type: str,
        event_category: str,
        description: str,
        endpoint_id: Optional[uuid.UUID] = None,
        delivery_id: Optional[uuid.UUID] = None,
        metadata: Optional[Dict] = None,
    ):
        """Log webhook event for auditing."""
        try:
            event = WebhookEventLog(
                account_id=account_id,
                event_type=event_type,
                event_category=event_category,
                description=description,
                endpoint_id=endpoint_id,
                delivery_id=delivery_id,
                event_metadata=metadata or {},
                actor_type="system",
            )
            session.add(event)
            await session.flush()
        except Exception as e:
            logger.error(f"Failed to log webhook event: {e}")

    async def verify_endpoint(
        self, endpoint_id: str, session: Optional[AsyncSession] = None
    ) -> Tuple[bool, str]:
        """
        Send a test webhook to verify endpoint configuration.

        Returns:
            Tuple of (success, message)
        """
        if not session:
            async with session_context() as session:
                return await self.verify_endpoint(endpoint_id, session)

        query = select(WebhookEndpoint).where(WebhookEndpoint.id == endpoint_id)
        result = await session.execute(query)
        endpoint = result.scalar_one_or_none()

        if not endpoint:
            return False, "Endpoint not found"

        # Create test payload
        test_payload = {
            "test": True,
            "endpoint_id": str(endpoint.id),
            "timestamp": datetime.utcnow().isoformat(),
            "message": "This is a test webhook from FoundryMatch",
        }

        # Create and deliver test webhook
        delivery_id = await self._create_delivery(
            session=session,
            endpoint=endpoint,
            event_type="webhook.test",
            payload=test_payload,
        )

        await session.commit()

        if delivery_id:
            success = await self.deliver_webhook(str(delivery_id))

            if success:
                endpoint.verified = True
                await session.commit()
                return True, "Endpoint verified successfully"
            else:
                return False, "Failed to deliver test webhook"

        return False, "Failed to create test delivery"
