"""Async queue engine — scheduling, priority, retry, DLQ, batch, rate limiting."""
from __future__ import annotations

from datetime import datetime, timedelta, timezone
from uuid import UUID, uuid4

from sqlalchemy.ext.asyncio import AsyncSession

from app.core.config import settings
from app.events.publisher import get_event_publisher
from app.events.types import CommunicationEventType
from app.models.foundation import Message, QueueItem
from app.models.types import MessageStatus, QueueItemStatus
from app.repositories.foundation import (
    MessageRepository,
    ProviderConfigRepository,
    QueueItemRepository,
)
from app.services.router import CommunicationRouter


class RateLimiter:
    """Simple in-process sliding window rate limiter (per process)."""

    def __init__(self) -> None:
        self._hits: dict[str, list[datetime]] = {}
        self.deferrals: int = 0

    def allow(self, key: str, limit_per_minute: int | None) -> bool:
        if not limit_per_minute:
            return True
        now = datetime.now(timezone.utc)
        window_start = now - timedelta(minutes=1)
        hits = [t for t in self._hits.get(key, []) if t >= window_start]
        if len(hits) >= limit_per_minute:
            self._hits[key] = hits
            self.deferrals += 1
            return False
        hits.append(now)
        self._hits[key] = hits
        return True

    def reset_stats(self) -> None:
        self.deferrals = 0
        self._hits.clear()


_rate_limiter = RateLimiter()


def get_rate_limiter() -> RateLimiter:
    return _rate_limiter


class QueueEngine:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.queue = QueueItemRepository(session)
        self.messages = MessageRepository(session)
        self.providers = ProviderConfigRepository(session)
        self.router = CommunicationRouter(session)
        self.events = get_event_publisher()
        self.rate_limiter = _rate_limiter

    async def enqueue(
        self,
        message: Message,
        *,
        max_attempts: int | None = None,
        batch_id: str | None = None,
    ) -> QueueItem:
        available_at = message.scheduled_at or datetime.now(timezone.utc)
        status = (
            QueueItemStatus.SCHEDULED.value
            if message.scheduled_at and message.scheduled_at > datetime.now(timezone.utc)
            else QueueItemStatus.PENDING.value
        )
        item = QueueItem(
            tenant_id=message.tenant_id,
            message_id=message.id,
            status=status,
            priority=message.priority,
            max_attempts=max_attempts or settings.default_max_retries,
            available_at=available_at,
            batch_id=batch_id,
        )
        await self.queue.enqueue(item)
        message.status = MessageStatus.QUEUED.value
        self.events.publish(
            event_type=CommunicationEventType.MESSAGE_QUEUED,
            aggregate_type="message",
            aggregate_id=message.id,
            tenant_id=message.tenant_id,
            payload={
                "queue_item_id": str(item.id),
                "channel": message.channel,
                "to_address": message.to_address,
                "correlation_id": message.correlation_id,
                "priority": message.priority,
            },
        )
        await self.session.flush()
        return item

    async def _channel_rate_limit(
        self, tenant_id: UUID, channel: str
    ) -> tuple[bool, int | None]:
        """Return (allowed, strictest_limit) using active provider rate limits."""
        providers = await self.providers.list_by_channel(
            tenant_id, channel, active_only=True
        )
        if not providers:
            return True, None
        # Use the strictest (lowest) positive rate limit among eligible providers
        limits = [p.rate_limit_per_minute for p in providers if p.rate_limit_per_minute]
        if not limits:
            return True, None
        limit = min(limits)
        key = f"{tenant_id}:{channel}:send"
        return self.rate_limiter.allow(key, limit), limit

    async def process_due(self, tenant_id: UUID | None = None, *, limit: int = 20) -> int:
        items = await self.queue.claim_next(tenant_id, limit=limit, increment_attempt=False)
        processed = 0
        for item in items:
            message = await self.messages.get(item.tenant_id, item.message_id)
            if message is None:
                item.status = QueueItemStatus.FAILED.value
                item.last_error = "message_missing"
                continue
            if message.status == MessageStatus.CANCELLED.value:
                item.status = QueueItemStatus.CANCELLED.value
                continue

            allowed, _limit = await self._channel_rate_limit(
                item.tenant_id, message.channel
            )
            if not allowed:
                # Defer without burning attempt
                item.status = QueueItemStatus.PENDING.value
                item.locked_at = None
                item.available_at = datetime.now(timezone.utc) + timedelta(seconds=5)
                item.last_error = "rate_limited"
                continue

            item.attempt += 1
            await self.session.flush()
            await self.router.deliver(message)
            if message.status in (
                MessageStatus.SENT.value,
                MessageStatus.DELIVERED.value,
            ):
                item.status = QueueItemStatus.COMPLETED.value
                item.completed_at = datetime.now(timezone.utc)
            elif item.attempt >= item.max_attempts:
                item.status = QueueItemStatus.DEAD_LETTER.value
                item.is_dead_letter = True
                item.last_error = message.error_message
                self.events.publish(
                    event_type=CommunicationEventType.QUEUE_DEAD_LETTER,
                    aggregate_type="queue_item",
                    aggregate_id=item.id,
                    tenant_id=item.tenant_id,
                    payload={
                        "message_id": str(message.id),
                        "channel": message.channel,
                        "correlation_id": message.correlation_id,
                        "attempts": item.attempt,
                        "error": message.error_message,
                    },
                )
            else:
                delay = 0 if settings.environment == "test" else min(300, 2**item.attempt)
                item.status = QueueItemStatus.PENDING.value
                item.available_at = datetime.now(timezone.utc) + timedelta(seconds=delay)
                item.last_error = message.error_message
                message.status = MessageStatus.QUEUED.value
            processed += 1
        await self.session.commit()
        return processed

    async def list_dead_letters(self, tenant_id: UUID) -> list[QueueItem]:
        return list(await self.queue.list_dead_letters(tenant_id))

    async def stats(self, tenant_id: UUID) -> dict[str, int]:
        return await self.queue.count_by_status(tenant_id)

    @staticmethod
    def new_batch_id() -> str:
        return f"batch-{uuid4()}"
