"""
SendService – logica di business per l'invio PEC (Fase 4).

Responsabilità:
  1. Valida permessi (check_can_send) sulla casella selezionata
  2. Crea il record Message (direction=outbound, state=queued)
  3. Crea il record SendJob (status=pending)
  4. Enqueue il job arq 'send_pec' tramite il pool Redis/arq
  5. Ritorna SendJobResponse

Il worker (arq) gestisce la connessione SMTP, il retry e la scrittura
del raw EML su MinIO.
"""

import asyncio
import uuid
from datetime import datetime, timezone

from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.exceptions import ForbiddenError, NotFoundError
from app.models.mailbox import Mailbox
from app.models.message import Message, SendJob
from app.models.user import User
from app.schemas.send import SendJobResponse, SendPecRequest
from app.services.permission_service import PermissionService

# ─── Pool arq (singleton lazy) ────────────────────────────────────────────────
# Usato per fare enqueue dei job send_pec senza avviare un worker nel backend.

_arq_pool = None
_arq_pool_lock = asyncio.Lock()


async def _get_arq_pool():
    """Restituisce il pool arq condiviso, creandolo se necessario."""
    global _arq_pool
    if _arq_pool is not None:
        return _arq_pool

    async with _arq_pool_lock:
        if _arq_pool is None:
            from arq import create_pool
            from arq.connections import RedisSettings

            from app.config import get_settings
            settings = get_settings()

            import urllib.parse
            parsed = urllib.parse.urlparse(settings.redis_url)
            rs = RedisSettings(
                host=parsed.hostname or "localhost",
                port=parsed.port or 6379,
                database=int(parsed.path.lstrip("/") or "0"),
                password=parsed.password or None,
            )
            _arq_pool = await create_pool(rs)

    return _arq_pool


async def close_arq_pool() -> None:
    """Chiude il pool arq alla shutdown dell'applicazione."""
    global _arq_pool
    if _arq_pool is not None:
        await _arq_pool.aclose()
        _arq_pool = None


# ─── SendService ──────────────────────────────────────────────────────────────

class SendService:
    def __init__(self, db: AsyncSession) -> None:
        self.db = db

    # ── Crea e accoda un invio ────────────────────────────────────────────────

    async def create_send_job(
        self,
        current_user: User,
        data: SendPecRequest,
    ) -> SendJob:
        """
        Crea Message + SendJob e accoda il job di invio.

        Args:
            current_user: utente autenticato che richiede l'invio
            data:         dati della PEC da inviare

        Returns:
            SendJob appena creato

        Raises:
            NotFoundError:    casella non trovata o non appartenente al tenant
            ForbiddenError:   utente senza can_send sulla casella
        """
        # ── Verifica casella ──────────────────────────────────────────────────
        mailbox = await self.db.get(Mailbox, data.mailbox_id)
        if (
            not mailbox
            or mailbox.tenant_id != current_user.tenant_id
            or mailbox.status == "deleted"
        ):
            raise NotFoundError("casella PEC mittente")

        if mailbox.status != "active":
            from app.core.exceptions import ForbiddenError
            raise ForbiddenError(
                f"La casella è in stato '{mailbox.status}' e non può inviare"
            )

        # ── Verifica permesso can_send ────────────────────────────────────────
        if not current_user.is_admin:
            perm_svc = PermissionService(self.db)
            if not await perm_svc.check_can_send(current_user, data.mailbox_id):
                raise ForbiddenError(
                    "Non hai il permesso di inviare da questa casella"
                )

        # ── Crea il messaggio outbound ────────────────────────────────────────
        now = datetime.now(tz=timezone.utc)
        message = Message(
            tenant_id=current_user.tenant_id,
            mailbox_id=data.mailbox_id,
            direction="outbound",
            pec_type="posta_certificata",
            state="queued",
            subject=data.subject,
            from_address=mailbox.email_address,
            to_addresses=[str(a) for a in data.to_addresses],
            cc_addresses=[str(a) for a in data.cc_addresses] if data.cc_addresses else [],
            body_text=data.body_text or "",
            body_html=data.body_html,
            has_attachments=False,  # allegati in Fase 5
            sent_at=None,
            received_at=None,
        )

        # Collegamento a messaggio originale (per risposta/threading)
        if data.reply_to_message_id:
            parent = await self.db.get(Message, data.reply_to_message_id)
            if parent and parent.tenant_id == current_user.tenant_id:
                message.parent_message_id = data.reply_to_message_id

        self.db.add(message)
        await self.db.flush()

        # ── Crea il SendJob ───────────────────────────────────────────────────
        job = SendJob(
            tenant_id=current_user.tenant_id,
            mailbox_id=data.mailbox_id,
            message_id=message.id,
            status="pending",
            attempt_count=0,
            max_attempts=5,
            created_by=current_user.id,
            queued_at=now,
        )
        self.db.add(job)
        await self.db.flush()

        # ── Enqueue job arq ───────────────────────────────────────────────────
        try:
            arq_pool = await _get_arq_pool()
            await arq_pool.enqueue_job("send_pec", str(job.id))
        except Exception as e:
            from app.core.logging import get_logger
            logger = get_logger(__name__)
            logger.warning(
                f"[send_service] Impossibile enqueue send_pec job {job.id}: {e}. "
                "Il job resterà in stato 'pending' per pickup manuale."
            )
            # Non alziamo eccezione: il job è nel DB e verrà processato
            # dal cron di polling se disponibile

        return job

    # ── Lista job di invio ────────────────────────────────────────────────────

    async def list_send_jobs(
        self,
        tenant_id: uuid.UUID,
        page: int = 1,
        page_size: int = 50,
        mailbox_id: uuid.UUID | None = None,
        status_filter: str | None = None,
    ) -> tuple[list[SendJob], int]:
        """Lista i job di invio del tenant con filtri opzionali."""
        base_q = select(SendJob).where(SendJob.tenant_id == tenant_id)

        if mailbox_id:
            base_q = base_q.where(SendJob.mailbox_id == mailbox_id)
        if status_filter:
            base_q = base_q.where(SendJob.status == status_filter)

        count_q = select(func.count()).select_from(base_q.subquery())
        total = (await self.db.execute(count_q)).scalar_one()

        items_q = (
            base_q.order_by(SendJob.queued_at.desc())
            .offset((page - 1) * page_size)
            .limit(page_size)
        )
        result = await self.db.execute(items_q)
        items = list(result.scalars().all())
        return items, total

    # ── Get singolo job ───────────────────────────────────────────────────────

    async def get_send_job(
        self,
        job_id: uuid.UUID,
        tenant_id: uuid.UUID,
    ) -> SendJob:
        """Carica un singolo SendJob verificando l'appartenenza al tenant."""
        job = await self.db.get(SendJob, job_id)
        if not job or job.tenant_id != tenant_id:
            raise NotFoundError("job di invio")
        return job

    # ── Annulla job (solo se pending) ─────────────────────────────────────────

    async def cancel_send_job(
        self,
        job_id: uuid.UUID,
        tenant_id: uuid.UUID,
    ) -> SendJob:
        """
        Annulla un job di invio se è ancora in stato 'pending'.
        Non cancella il messaggio associato.
        """
        job = await self.get_send_job(job_id, tenant_id)

        if job.status not in ("pending", "retrying"):
            raise ForbiddenError(
                f"Impossibile annullare: il job è in stato '{job.status}'"
            )

        job.status = "failed"
        job.last_error = "Annullato dall'utente"

        # Aggiorna anche il messaggio
        if job.message_id:
            msg = await self.db.get(Message, job.message_id)
            if msg and msg.state in ("queued", "draft"):
                msg.state = "failed"

        await self.db.flush()
        return job
