"""
Async webhook worker for processing webhook deliveries.
"""

import asyncio
import logging
import signal
import time
from typing import Optional, Set
from datetime import datetime, timedelta
import redis.asyncio as redis

from sqlalchemy import select, and_, update

from fmatch.saas.database import session_context
from fmatch.saas.model_defs.webhook_models import WebhookDelivery, WebhookStatus
from fmatch.saas.services.webhook_service import WebhookService
from fmatch.saas import settings

logger = logging.getLogger(__name__)


class WebhookWorker:
    """
    Async worker that polls Redis for pending webhook deliveries
    and processes them concurrently.
    """

    def __init__(
        self,
        redis_url: Optional[str] = None,
        max_concurrent: int = 10,
        poll_interval: float = 1.0,
        batch_size: int = 100,
    ):
        self.redis_url = redis_url or settings.REDIS_URL
        self.max_concurrent = max_concurrent
        self.poll_interval = poll_interval
        self.batch_size = batch_size

        self.redis_client: Optional[redis.Redis] = None
        self.webhook_service: Optional[WebhookService] = None
        self.running = False
        self.shutdown_event = asyncio.Event()
        self.active_deliveries: Set[str] = set()

        # Metrics
        self.metrics = {
            "processed": 0,
            "successful": 0,
            "failed": 0,
            "retried": 0,
            "dlq": 0,
            "total_latency_ms": 0,
            "last_process_time": None,
            "start_time": None,
        }

    async def start(self):
        """Start the webhook worker."""
        logger.info("Starting webhook worker...")

        # Setup signal handlers
        signal.signal(signal.SIGINT, self._handle_shutdown)
        signal.signal(signal.SIGTERM, self._handle_shutdown)

        # Initialize connections
        await self._initialize()

        self.running = True
        self.metrics["start_time"] = datetime.utcnow()

        # Start background tasks
        tasks = [
            asyncio.create_task(self._process_deliveries()),
            asyncio.create_task(self._process_retries()),
            asyncio.create_task(self._cleanup_stuck_deliveries()),
            asyncio.create_task(self._report_metrics()),
        ]

        try:
            # Wait for shutdown signal
            await self.shutdown_event.wait()
        finally:
            self.running = False

            # Cancel all tasks
            for task in tasks:
                task.cancel()

            # Wait for tasks to complete
            await asyncio.gather(*tasks, return_exceptions=True)

            # Cleanup
            await self._cleanup()

            logger.info("Webhook worker stopped")

    def _handle_shutdown(self, signum, frame):
        """Handle shutdown signal."""
        logger.info(f"Received signal {signum}, initiating shutdown...")
        self.shutdown_event.set()

    async def _initialize(self):
        """Initialize connections and services."""
        # Connect to Redis
        self.redis_client = await redis.from_url(
            self.redis_url, encoding="utf-8", decode_responses=True
        )

        # Initialize webhook service
        self.webhook_service = WebhookService(redis_client=self.redis_client)
        await self.webhook_service.__aenter__()

        logger.info("Worker initialized successfully")

    async def _cleanup(self):
        """Clean up connections."""
        if self.webhook_service:
            await self.webhook_service.__aexit__(None, None, None)

        if self.redis_client:
            await self.redis_client.close()

    async def _process_deliveries(self):
        """Main loop for processing pending deliveries."""
        logger.info("Started delivery processor")

        while self.running:
            try:
                # Get pending deliveries from Redis
                now = datetime.utcnow().timestamp()

                # Get deliveries that are due (score <= now)
                delivery_ids = await self.redis_client.zrangebyscore(
                    "webhook:deliveries:pending",
                    min=0,
                    max=now,
                    start=0,
                    num=self.batch_size,
                )

                if delivery_ids:
                    # Remove from queue
                    removed = await self.redis_client.zrem(
                        "webhook:deliveries:pending", *delivery_ids
                    )

                    logger.debug(f"Processing {removed} deliveries")

                    # Process concurrently with semaphore
                    semaphore = asyncio.Semaphore(self.max_concurrent)
                    tasks = []

                    for delivery_id in delivery_ids:
                        if delivery_id not in self.active_deliveries:
                            task = asyncio.create_task(
                                self._process_single_delivery(delivery_id, semaphore)
                            )
                            tasks.append(task)

                    if tasks:
                        await asyncio.gather(*tasks, return_exceptions=True)

                # Sleep before next poll
                await asyncio.sleep(self.poll_interval)

            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error(f"Error in delivery processor: {e}")
                await asyncio.sleep(self.poll_interval)

    async def _process_single_delivery(
        self, delivery_id: str, semaphore: asyncio.Semaphore
    ):
        """Process a single webhook delivery."""
        async with semaphore:
            # Track active delivery
            self.active_deliveries.add(delivery_id)

            try:
                start_time = time.time()

                # Check if this is a batch
                if await self.redis_client.exists(f"webhook:batch:{delivery_id}"):
                    await self._process_batch_delivery(delivery_id)
                else:
                    # Process single delivery
                    success = await self.webhook_service.deliver_webhook(delivery_id)

                    # Update metrics
                    self.metrics["processed"] += 1
                    if success:
                        self.metrics["successful"] += 1
                    else:
                        await self._check_delivery_status(delivery_id)

                # Track latency
                latency_ms = int((time.time() - start_time) * 1000)
                self.metrics["total_latency_ms"] += latency_ms
                self.metrics["last_process_time"] = datetime.utcnow()

            except Exception as e:
                logger.error(f"Failed to process delivery {delivery_id}: {e}")
                self.metrics["failed"] += 1

                # Move to DLQ
                await self._move_to_dlq(delivery_id, str(e))

            finally:
                # Remove from active set
                self.active_deliveries.discard(delivery_id)

    async def _process_batch_delivery(self, batch_id: str):
        """Process a batch of webhook deliveries."""
        try:
            # Get all delivery IDs in the batch
            batch_data = await self.redis_client.hgetall(f"webhook:batch:{batch_id}")

            if not batch_data:
                logger.warning(f"Empty batch {batch_id}")
                return

            # Extract delivery IDs (skip endpoint_id key)
            delivery_ids = [v for k, v in batch_data.items() if k != "endpoint_id"]

            # Process each delivery
            for delivery_id in delivery_ids:
                success = await self.webhook_service.deliver_webhook(delivery_id)

                self.metrics["processed"] += 1
                if success:
                    self.metrics["successful"] += 1
                else:
                    await self._check_delivery_status(delivery_id)

            # Clean up batch data
            await self.redis_client.delete(f"webhook:batch:{batch_id}")

        except Exception as e:
            logger.error(f"Failed to process batch {batch_id}: {e}")

    async def _check_delivery_status(self, delivery_id: str):
        """Check delivery status and update metrics."""
        async with session_context() as session:
            query = select(WebhookDelivery).where(WebhookDelivery.id == delivery_id)
            result = await session.execute(query)
            delivery = result.scalar_one_or_none()

            if delivery:
                if delivery.status == WebhookStatus.RETRYING.value:
                    self.metrics["retried"] += 1
                elif delivery.status == WebhookStatus.DEAD_LETTER.value:
                    self.metrics["dlq"] += 1
                else:
                    self.metrics["failed"] += 1

    async def _process_retries(self):
        """Process deliveries scheduled for retry."""
        logger.info("Started retry processor")

        while self.running:
            try:
                async with session_context() as session:
                    # Find deliveries due for retry
                    now = datetime.utcnow()

                    query = (
                        select(WebhookDelivery)
                        .where(
                            and_(
                                WebhookDelivery.status == WebhookStatus.RETRYING.value,
                                WebhookDelivery.next_retry_at <= now,
                            )
                        )
                        .limit(self.batch_size)
                    )

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

                    for delivery in deliveries:
                        # Re-queue for processing
                        await self.redis_client.zadd(
                            "webhook:deliveries:pending",
                            {str(delivery.id): now.timestamp()},
                        )

                        logger.debug(f"Re-queued delivery {delivery.id} for retry")

                # Sleep for 10 seconds between checks
                await asyncio.sleep(10)

            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error(f"Error in retry processor: {e}")
                await asyncio.sleep(10)

    async def _cleanup_stuck_deliveries(self):
        """Clean up deliveries stuck in IN_PROGRESS state."""
        logger.info("Started stuck delivery cleanup")

        while self.running:
            try:
                async with session_context() as session:
                    # Find deliveries stuck in IN_PROGRESS for > 5 minutes
                    threshold = datetime.utcnow() - timedelta(minutes=5)

                    query = (
                        select(WebhookDelivery)
                        .where(
                            and_(
                                WebhookDelivery.status
                                == WebhookStatus.IN_PROGRESS.value,
                                WebhookDelivery.last_attempted_at < threshold,
                            )
                        )
                        .limit(100)
                    )

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

                    if stuck_deliveries:
                        logger.warning(
                            f"Found {len(stuck_deliveries)} stuck deliveries"
                        )

                        for delivery in stuck_deliveries:
                            # Reset to PENDING if under max attempts
                            if delivery.attempts < delivery.max_attempts:
                                delivery.status = WebhookStatus.PENDING.value

                                # Re-queue
                                await self.redis_client.zadd(
                                    "webhook:deliveries:pending",
                                    {str(delivery.id): datetime.utcnow().timestamp()},
                                )
                            else:
                                # Move to dead letter
                                delivery.status = WebhookStatus.DEAD_LETTER.value
                                delivery.last_error = "Stuck in processing state"
                                self.metrics["dlq"] += 1

                        await session.commit()

                # Run cleanup every minute
                await asyncio.sleep(60)

            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error(f"Error in stuck delivery cleanup: {e}")
                await asyncio.sleep(60)

    async def _move_to_dlq(self, delivery_id: str, error: str):
        """Move a delivery to the dead letter queue."""
        try:
            # Add to DLQ in Redis
            await self.redis_client.zadd(
                "webhook:deliveries:dlq", {delivery_id: datetime.utcnow().timestamp()}
            )

            # Store error details
            await self.redis_client.hset(
                f"webhook:dlq:{delivery_id}",
                mapping={
                    "error": error,
                    "timestamp": datetime.utcnow().isoformat(),
                    "attempts": "max_exceeded",
                },
            )

            # Update database
            async with session_context() as session:
                await session.execute(
                    update(WebhookDelivery)
                    .where(WebhookDelivery.id == delivery_id)
                    .values(
                        status=WebhookStatus.DEAD_LETTER.value,
                        last_error=error,
                        completed_at=datetime.utcnow(),
                    )
                )
                await session.commit()

            self.metrics["dlq"] += 1
            logger.warning(f"Moved delivery {delivery_id} to DLQ: {error}")

        except Exception as e:
            logger.error(f"Failed to move {delivery_id} to DLQ: {e}")

    async def _report_metrics(self):
        """Periodically report worker metrics."""
        logger.info("Started metrics reporter")

        while self.running:
            try:
                # Wait 30 seconds between reports
                await asyncio.sleep(30)

                if self.metrics["processed"] > 0:
                    # Calculate rates
                    uptime = (
                        datetime.utcnow() - self.metrics["start_time"]
                    ).total_seconds()
                    success_rate = (
                        self.metrics["successful"] / self.metrics["processed"]
                    ) * 100
                    avg_latency = (
                        self.metrics["total_latency_ms"] / self.metrics["processed"]
                    )

                    # Get queue depth
                    queue_depth = await self.redis_client.zcard(
                        "webhook:deliveries:pending"
                    )
                    dlq_depth = await self.redis_client.zcard("webhook:deliveries:dlq")

                    # Log metrics
                    logger.info(
                        f"Webhook Worker Metrics - "
                        f"Processed: {self.metrics['processed']}, "
                        f"Success Rate: {success_rate:.1f}%, "
                        f"Avg Latency: {avg_latency:.0f}ms, "
                        f"Queue Depth: {queue_depth}, "
                        f"DLQ: {dlq_depth}, "
                        f"Active: {len(self.active_deliveries)}, "
                        f"Uptime: {uptime:.0f}s"
                    )

                    # Store metrics in Redis for monitoring
                    await self.redis_client.hset(
                        "webhook:worker:metrics",
                        mapping={
                            "processed": self.metrics["processed"],
                            "successful": self.metrics["successful"],
                            "failed": self.metrics["failed"],
                            "retried": self.metrics["retried"],
                            "dlq": self.metrics["dlq"],
                            "success_rate": success_rate,
                            "avg_latency_ms": avg_latency,
                            "queue_depth": queue_depth,
                            "dlq_depth": dlq_depth,
                            "active_deliveries": len(self.active_deliveries),
                            "uptime_seconds": uptime,
                            "last_update": datetime.utcnow().isoformat(),
                        },
                    )

            except asyncio.CancelledError:
                break
            except Exception as e:
                logger.error(f"Error reporting metrics: {e}")

    async def get_metrics(self) -> dict:
        """Get current worker metrics."""
        if self.redis_client:
            metrics = await self.redis_client.hgetall("webhook:worker:metrics")
            return metrics
        return self.metrics


async def run_worker():
    """Run the webhook worker."""
    worker = WebhookWorker(
        max_concurrent=settings.WEBHOOK_WORKER_CONCURRENCY or 10,
        poll_interval=1.0,
        batch_size=settings.WEBHOOK_BATCH_SIZE or 100,
    )

    await worker.start()


if __name__ == "__main__":
    # Setup logging
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    )

    # Run worker
    asyncio.run(run_worker())
