"""Communication router — channel routing, provider priority, failover, retry."""
from __future__ import annotations

from datetime import datetime, timezone
from typing import Any
from uuid import UUID

from sqlalchemy.ext.asyncio import AsyncSession

from app.events.publisher import get_event_publisher
from app.events.types import CommunicationEventType
from app.models.foundation import DeliveryEvent, Message, ProviderConfig, ProviderLog
from app.models.types import MessageStatus
from app.providers import get_provider
from app.providers.contracts import SendRequest
from app.repositories.foundation import (
    DeliveryEventRepository,
    MessageRepository,
    ProviderLogRepository,
    SenderNumberRepository,
)
from app.services.provider_service import ProviderService
from shared.exceptions import AppError


class CommunicationRouter:
    """Routes outbound messages through tenant providers with automatic failover."""

    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.messages = MessageRepository(session)
        self.delivery = DeliveryEventRepository(session)
        self.provider_logs = ProviderLogRepository(session)
        self.senders = SenderNumberRepository(session)
        self.providers = ProviderService(session)
        self.events = get_event_publisher()

    async def deliver(self, message: Message) -> Message:
        providers = await self.providers.repo.list_by_channel(
            message.tenant_id, message.channel, active_only=True
        )
        if not providers:
            await self._fail(message, "no_provider", "No active providers for channel")
            return message

        last_error = "all_providers_failed"
        for index, provider in enumerate(providers):
            if not self.providers.is_circuit_allowing(provider):
                continue
            if index > 0:
                self.events.publish(
                    event_type=CommunicationEventType.PROVIDER_FAILOVER,
                    aggregate_type="message",
                    aggregate_id=message.id,
                    tenant_id=message.tenant_id,
                    payload={
                        "from_provider": str(providers[index - 1].id),
                        "to_provider": str(provider.id),
                    },
                )
            success = await self._try_provider(message, provider)
            if success:
                return message
            last_error = message.error_message or last_error

        await self._fail(message, "all_providers_failed", last_error)
        return message

    async def _try_provider(self, message: Message, provider: ProviderConfig) -> bool:
        adapter = get_provider(provider.provider_kind)
        from_address = message.from_address
        if not from_address:
            sender = await self.senders.get_default(message.tenant_id, message.channel)
            if sender:
                from_address = sender.value
                message.from_address = from_address

        request = SendRequest(
            tenant_id=message.tenant_id,
            channel=message.channel,
            to_address=message.to_address,
            body=message.body,
            from_address=from_address,
            subject=message.subject,
            metadata=message.metadata_json or {},
        )
        result = await adapter.send(request, provider.credentials or {})
        log = ProviderLog(
            tenant_id=message.tenant_id,
            provider_config_id=provider.id,
            message_id=message.id,
            request_payload={
                "to": message.to_address,
                "channel": message.channel,
            },
            response_payload=result.raw_response,
            success=result.success,
            latency_ms=result.latency_ms,
            error_message=result.error_message,
        )
        await self.provider_logs.add(log)

        if result.success:
            message.status = MessageStatus.SENT.value
            message.provider_config_id = provider.id
            message.provider_message_id = result.provider_message_id
            message.sent_at = datetime.now(timezone.utc)
            message.error_code = None
            message.error_message = None
            await self.providers.record_success(provider)
            await self._timeline(
                message,
                MessageStatus.SENT.value,
                provider_config_id=provider.id,
                detail="sent via provider",
            )
            self.events.publish(
                event_type=CommunicationEventType.MESSAGE_SENT,
                aggregate_type="message",
                aggregate_id=message.id,
                tenant_id=message.tenant_id,
                payload={
                    "provider_id": str(provider.id),
                    "provider_message_id": result.provider_message_id,
                    "channel": message.channel,
                    "to_address": message.to_address,
                    "correlation_id": message.correlation_id,
                },
            )
            await self.session.flush()
            return True

        message.retry_count += 1
        message.error_code = result.error_code
        message.error_message = result.error_message
        await self.providers.record_failure(provider, result.error_message or "send_failed")
        await self._timeline(
            message,
            MessageStatus.FAILED.value,
            provider_config_id=provider.id,
            detail=result.error_message,
            payload={"error_code": result.error_code},
        )
        await self.session.flush()
        return False

    async def _fail(self, message: Message, code: str, detail: str | None) -> None:
        message.status = MessageStatus.FAILED.value
        message.failed_at = datetime.now(timezone.utc)
        message.error_code = code
        message.error_message = detail
        await self._timeline(message, MessageStatus.FAILED.value, detail=detail)
        self.events.publish(
            event_type=CommunicationEventType.MESSAGE_FAILED,
            aggregate_type="message",
            aggregate_id=message.id,
            tenant_id=message.tenant_id,
            payload={"error_code": code, "detail": detail},
        )
        await self.session.flush()

    async def _timeline(
        self,
        message: Message,
        status: str,
        *,
        provider_config_id: UUID | None = None,
        detail: str | None = None,
        payload: dict[str, Any] | None = None,
    ) -> None:
        event = DeliveryEvent(
            tenant_id=message.tenant_id,
            message_id=message.id,
            status=status,
            provider_config_id=provider_config_id,
            detail=detail,
            payload=payload,
        )
        await self.delivery.add(event)

    async def mark_delivered(
        self, tenant_id: UUID, message_id: UUID, *, detail: str | None = None
    ) -> Message:
        message = await self.messages.get(tenant_id, message_id)
        if message is None:
            raise AppError("Message not found", status_code=404, error_code="not_found")
        message.status = MessageStatus.DELIVERED.value
        message.delivered_at = datetime.now(timezone.utc)
        await self._timeline(message, MessageStatus.DELIVERED.value, detail=detail)
        self.events.publish(
            event_type=CommunicationEventType.MESSAGE_DELIVERED,
            aggregate_type="message",
            aggregate_id=message.id,
            tenant_id=tenant_id,
            payload={
                "channel": message.channel,
                "to_address": message.to_address,
                "correlation_id": message.correlation_id,
                "provider_message_id": message.provider_message_id,
            },
        )
        await self.session.commit()
        await self.session.refresh(message)
        return message

    async def cancel(self, tenant_id: UUID, message_id: UUID) -> Message:
        message = await self.messages.get(tenant_id, message_id)
        if message is None:
            raise AppError("Message not found", status_code=404, error_code="not_found")
        if message.status not in (
            MessageStatus.QUEUED.value,
            MessageStatus.SENT.value,
        ):
            raise AppError(
                f"Cannot cancel message in status {message.status}",
                status_code=422,
                error_code="invalid_status",
            )
        message.status = MessageStatus.CANCELLED.value
        await self._timeline(message, MessageStatus.CANCELLED.value)
        self.events.publish(
            event_type=CommunicationEventType.MESSAGE_CANCELLED,
            aggregate_type="message",
            aggregate_id=message.id,
            tenant_id=tenant_id,
        )
        await self.session.commit()
        await self.session.refresh(message)
        return message
