"""Outbound message orchestration — never raises into business callers as hard stop.

Business modules call this service via API; failures are tracked as message status,
so calling systems can continue their transactions independently.
"""
from __future__ import annotations

from typing import Any
from uuid import UUID

from sqlalchemy.ext.asyncio import AsyncSession

from app.models.foundation import Message
from app.models.types import MessageStatus
from app.repositories.foundation import DeliveryEventRepository, MessageRepository
from app.services.audit_service import AuditService
from app.services.contact_service import ContactService
from app.services.queue_engine import QueueEngine
from app.services.router import CommunicationRouter
from app.services.template_service import TemplateService
from app.validators import validate_message_send
from shared.exceptions import NotFoundError


class MessageService:
    def __init__(self, session: AsyncSession) -> None:
        self.session = session
        self.messages = MessageRepository(session)
        self.delivery = DeliveryEventRepository(session)
        self.templates = TemplateService(session)
        self.contacts = ContactService(session)
        self.queue = QueueEngine(session)
        self.router = CommunicationRouter(session)
        self.audit = AuditService(session)

    async def send(
        self, tenant_id: UUID, data: dict[str, Any], *, actor_id: str | None = None
    ) -> list[Message]:
        validate_message_send(data)

        correlation_id = data.get("correlation_id")
        existing_by_dest: dict[str, Message] = {}
        if correlation_id:
            for row in await self.messages.get_by_correlation(tenant_id, correlation_id):
                existing_by_dest[row.to_address] = row

        destinations: list[str] = []
        if data.get("to_address"):
            destinations.append(data["to_address"])
        if data.get("contact_source_id") or data.get("batch"):
            resolved = await self.contacts.resolve(
                tenant_id,
                source_id=data.get("contact_source_id"),
                query=data.get("contact_query"),
                channel=data["channel"],
            )
            destinations.extend(r.address for r in resolved)
        seen: set[str] = set()
        unique: list[str] = []
        for d in destinations:
            if d not in seen:
                seen.add(d)
                unique.append(d)
        if not unique:
            raise NotFoundError("No destinations resolved")

        # Full idempotent hit — all destinations already exist for this correlation
        if correlation_id and unique and all(d in existing_by_dest for d in unique):
            return [existing_by_dest[d] for d in unique]

        body, subject, template = await self.templates.resolve_body(
            tenant_id,
            template_id=data.get("template_id"),
            template_key=data.get("template_key"),
            variables=data.get("template_variables"),
            fallback_body=data.get("body"),
            fallback_subject=data.get("subject"),
        )

        batch_id = QueueEngine.new_batch_id() if len(unique) > 1 else None
        created: list[Message] = []
        for address in unique:
            if correlation_id and address in existing_by_dest:
                created.append(existing_by_dest[address])
                continue
            message = Message(
                tenant_id=tenant_id,
                channel=data["channel"],
                status=MessageStatus.QUEUED.value,
                to_address=address,
                from_address=data.get("from_address"),
                subject=subject,
                body=body,
                template_id=template.id if template else data.get("template_id"),
                template_key=template.template_key if template else data.get("template_key"),
                template_variables=data.get("template_variables"),
                priority=data.get("priority", "normal"),
                scheduled_at=data.get("scheduled_at"),
                correlation_id=correlation_id,
                source_module=data.get("source_module"),
                metadata_json=data.get("metadata_json"),
                created_by=actor_id,
                updated_by=actor_id,
            )
            await self.messages.add(message)
            await self.queue.enqueue(message, batch_id=batch_id)
            await self.audit.log(
                tenant_id=tenant_id,
                entity_type="message",
                entity_id=message.id,
                action="send",
                actor_id=actor_id,
                detail=f"queued to {address}",
            )
            created.append(message)

        await self.session.commit()
        for m in created:
            await self.session.refresh(m)

        if data.get("process_immediately", True) and not data.get("scheduled_at"):
            await self.queue.process_due(tenant_id, limit=max(20, len(created)))
            for m in created:
                await self.session.refresh(m)
        return created

    async def get(self, tenant_id: UUID, message_id: UUID) -> Message:
        entity = await self.messages.get(tenant_id, message_id)
        if entity is None:
            raise NotFoundError("Message not found")
        return entity

    async def list(
        self, tenant_id: UUID, *, status: str | None = None, offset: int = 0, limit: int = 20
    ) -> list[Message]:
        if status:
            return list(
                await self.messages.list_by_status(
                    tenant_id, status, offset=offset, limit=limit
                )
            )
        return list(await self.messages.list_by_tenant(tenant_id, offset=offset, limit=limit))

    async def timeline(self, tenant_id: UUID, message_id: UUID):
        await self.get(tenant_id, message_id)
        return list(await self.delivery.list_for_message(tenant_id, message_id))

    async def cancel(self, tenant_id: UUID, message_id: UUID) -> Message:
        return await self.router.cancel(tenant_id, message_id)

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