"""
ReportService – calcola KPI, serie storiche e produce export CSV/PDF.

Non richiede migrazioni: lavora sulle tabelle messages e mailboxes esistenti.
"""

import csv
import io
import uuid
from datetime import date, datetime, timedelta, timezone
from typing import AsyncGenerator, Optional

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

from app.core.logging import get_logger
from app.models.mailbox import Mailbox
from app.models.message import Message
from app.schemas.reports import (
    DailyStat,
    KpiSummary,
    MailboxStat,
    OutboundStateStat,
    ReportSummaryResponse,
)

logger = get_logger(__name__)


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

    # ─── KPI principali ──────────────────────────────────────────────────────

    async def get_summary(
        self,
        tenant_id: uuid.UUID,
        period_days: int = 7,
        visible_mailbox_ids: Optional[list[uuid.UUID]] = None,
    ) -> ReportSummaryResponse:
        """
        Restituisce il riepilogo completo per la dashboard.

        visible_mailbox_ids: se None l'utente e admin e vede tutto il tenant,
        altrimenti filtra sulle caselle accessibili.
        """
        now = datetime.now(timezone.utc)
        today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
        d7_start = now - timedelta(days=7)
        d30_start = now - timedelta(days=30)

        # ── Filtro base tenant + caselle visibili ────────────────────────────
        def _base(q):
            q = q.where(Message.tenant_id == tenant_id)
            if visible_mailbox_ids is not None:
                if not visible_mailbox_ids:
                    # Nessuna casella visibile: ritorna subito valori zero
                    return None
                q = q.where(Message.mailbox_id.in_(visible_mailbox_ids))
            return q

        async def _count(q) -> int:
            q = _base(q)
            if q is None:
                return 0
            r = await self.db.execute(q)
            return r.scalar_one() or 0

        # PEC ricevute
        received_today = await _count(
            select(func.count(Message.id)).where(
                Message.direction == "inbound",
                Message.received_at >= today_start,
            )
        )
        received_7d = await _count(
            select(func.count(Message.id)).where(
                Message.direction == "inbound",
                Message.received_at >= d7_start,
            )
        )
        received_30d = await _count(
            select(func.count(Message.id)).where(
                Message.direction == "inbound",
                Message.received_at >= d30_start,
            )
        )

        # PEC inviate
        sent_today = await _count(
            select(func.count(Message.id)).where(
                Message.direction == "outbound",
                Message.parent_message_id.is_(None),
                Message.sent_at >= today_start,
            )
        )
        sent_7d = await _count(
            select(func.count(Message.id)).where(
                Message.direction == "outbound",
                Message.parent_message_id.is_(None),
                Message.sent_at >= d7_start,
            )
        )
        sent_30d = await _count(
            select(func.count(Message.id)).where(
                Message.direction == "outbound",
                Message.parent_message_id.is_(None),
                Message.sent_at >= d30_start,
            )
        )

        # Anomalie (outbound con state=anomaly, senza genitore)
        anomalie = await _count(
            select(func.count(Message.id)).where(
                Message.direction == "outbound",
                Message.parent_message_id.is_(None),
                Message.state == "anomaly",
            )
        )

        # Tasso consegna: delivered / (delivered + anomaly + failed)
        delivered = await _count(
            select(func.count(Message.id)).where(
                Message.direction == "outbound",
                Message.parent_message_id.is_(None),
                Message.state == "delivered",
            )
        )
        failed = await _count(
            select(func.count(Message.id)).where(
                Message.direction == "outbound",
                Message.parent_message_id.is_(None),
                Message.state.in_(["anomaly", "failed"]),
            )
        )
        total_terminal = delivered + failed
        tasso_consegna = round((delivered / total_terminal * 100), 1) if total_terminal > 0 else 0.0

        # Non letti
        non_letti = await _count(
            select(func.count(Message.id)).where(
                Message.direction == "inbound",
                Message.is_read == False,  # noqa: E712
                Message.is_trashed == False,  # noqa: E712
            )
        )

        # Totale messaggi
        totale = await _count(select(func.count(Message.id)))

        # Caselle in errore (NON filtrato per visible_mailbox_ids – e una info admin)
        caselle_errore_r = await self.db.execute(
            select(func.count(Mailbox.id)).where(
                Mailbox.tenant_id == tenant_id,
                Mailbox.status == "error",
            )
        )
        caselle_errore = caselle_errore_r.scalar_one() or 0

        kpi = KpiSummary(
            received_today=received_today,
            sent_today=sent_today,
            received_7d=received_7d,
            sent_7d=sent_7d,
            received_30d=received_30d,
            sent_30d=sent_30d,
            anomalie_attive=anomalie,
            tasso_consegna=tasso_consegna,
            caselle_in_errore=caselle_errore,
            messaggi_non_letti=non_letti,
            totale_messaggi=totale,
        )

        # ── Serie storica giornaliera ─────────────────────────────────────────
        daily_stats = await self._get_daily_stats(tenant_id, period_days, visible_mailbox_ids)

        # ── Distribuzione stati outbound ──────────────────────────────────────
        outbound_states = await self._get_outbound_states(tenant_id, visible_mailbox_ids)

        # ── Statistiche per casella ───────────────────────────────────────────
        mailbox_stats = await self._get_mailbox_stats(tenant_id, visible_mailbox_ids)

        return ReportSummaryResponse(
            generated_at=now,
            period_days=period_days,
            kpi=kpi,
            daily_stats=daily_stats,
            outbound_states=outbound_states,
            mailbox_stats=mailbox_stats,
        )

    # ─── Serie storica ────────────────────────────────────────────────────────

    async def _get_daily_stats(
        self,
        tenant_id: uuid.UUID,
        days: int,
        visible_mailbox_ids: Optional[list[uuid.UUID]],
    ) -> list[DailyStat]:
        """Conta PEC ricevute e inviate per ciascuno degli ultimi `days` giorni."""
        since = datetime.now(timezone.utc) - timedelta(days=days)

        def _apply_filters(q):
            q = q.where(Message.tenant_id == tenant_id)
            if visible_mailbox_ids is not None:
                if not visible_mailbox_ids:
                    return None
                q = q.where(Message.mailbox_id.in_(visible_mailbox_ids))
            return q

        # Aggregazione ricevute per giorno
        q_recv = (
            select(
                func.date_trunc("day", Message.received_at).label("day"),
                func.count(Message.id).label("cnt"),
            )
            .where(
                Message.direction == "inbound",
                Message.received_at >= since,
            )
            .group_by(text("day"))
            .order_by(text("day"))
        )
        q_recv = _apply_filters(q_recv)

        # Aggregazione inviate per giorno
        q_sent = (
            select(
                func.date_trunc("day", Message.sent_at).label("day"),
                func.count(Message.id).label("cnt"),
            )
            .where(
                Message.direction == "outbound",
                Message.parent_message_id.is_(None),
                Message.sent_at >= since,
            )
            .group_by(text("day"))
            .order_by(text("day"))
        )
        q_sent = _apply_filters(q_sent)

        recv_map: dict[date, int] = {}
        sent_map: dict[date, int] = {}

        if q_recv is not None:
            r = await self.db.execute(q_recv)
            for row in r.all():
                if row.day:
                    d = row.day.date() if hasattr(row.day, "date") else row.day
                    recv_map[d] = row.cnt

        if q_sent is not None:
            r = await self.db.execute(q_sent)
            for row in r.all():
                if row.day:
                    d = row.day.date() if hasattr(row.day, "date") else row.day
                    sent_map[d] = row.cnt

        # Costruisce la serie completa (tutti i giorni, anche quelli a zero)
        result: list[DailyStat] = []
        for i in range(days, -1, -1):
            d = (datetime.now(timezone.utc) - timedelta(days=i)).date()
            result.append(DailyStat(
                day=d,
                received=recv_map.get(d, 0),
                sent=sent_map.get(d, 0),
            ))
        return result

    # ─── Distribuzione stati outbound ────────────────────────────────────────

    async def _get_outbound_states(
        self,
        tenant_id: uuid.UUID,
        visible_mailbox_ids: Optional[list[uuid.UUID]],
    ) -> list[OutboundStateStat]:
        q = (
            select(Message.state, func.count(Message.id).label("cnt"))
            .where(
                Message.tenant_id == tenant_id,
                Message.direction == "outbound",
                Message.parent_message_id.is_(None),
            )
            .group_by(Message.state)
        )
        if visible_mailbox_ids is not None:
            if not visible_mailbox_ids:
                return []
            q = q.where(Message.mailbox_id.in_(visible_mailbox_ids))

        r = await self.db.execute(q)
        return [OutboundStateStat(state=row.state, count=row.cnt) for row in r.all()]

    # ─── Statistiche per casella ──────────────────────────────────────────────

    async def _get_mailbox_stats(
        self,
        tenant_id: uuid.UUID,
        visible_mailbox_ids: Optional[list[uuid.UUID]],
    ) -> list[MailboxStat]:
        # Carica le caselle
        mb_q = select(Mailbox).where(Mailbox.tenant_id == tenant_id)
        if visible_mailbox_ids is not None:
            if not visible_mailbox_ids:
                return []
            mb_q = mb_q.where(Mailbox.id.in_(visible_mailbox_ids))
        mb_result = await self.db.execute(mb_q)
        mailboxes = mb_result.scalars().all()

        if not mailboxes:
            return []

        mailbox_ids = [m.id for m in mailboxes]
        mailbox_map = {m.id: m for m in mailboxes}

        # Aggregazione messaggi per casella e direction
        agg_q = (
            select(
                Message.mailbox_id,
                Message.direction,
                Message.state,
                Message.is_read,
                func.count(Message.id).label("cnt"),
            )
            .where(
                Message.tenant_id == tenant_id,
                Message.mailbox_id.in_(mailbox_ids),
                Message.parent_message_id.is_(None),
            )
            .group_by(Message.mailbox_id, Message.direction, Message.state, Message.is_read)
        )
        agg_result = await self.db.execute(agg_q)

        # Accumula per casella
        stats: dict[uuid.UUID, MailboxStat] = {}
        for mb in mailboxes:
            stats[mb.id] = MailboxStat(
                mailbox_id=mb.id,
                email_address=mb.email_address,
                display_name=mb.display_name,
                status=mb.status,
                last_sync_at=mb.last_sync_at,
            )

        for row in agg_result.all():
            s = stats.get(row.mailbox_id)
            if not s:
                continue
            if row.direction == "inbound":
                s.received_total += row.cnt
                if not row.is_read:
                    s.non_letti += row.cnt
            elif row.direction == "outbound":
                s.sent_total += row.cnt
                if row.state == "anomaly":
                    s.anomalie += row.cnt

        # Ordina per volume decrescente
        return sorted(
            stats.values(),
            key=lambda x: x.received_total + x.sent_total,
            reverse=True,
        )

    # ─── Export CSV ───────────────────────────────────────────────────────────

    async def export_csv(
        self,
        tenant_id: uuid.UUID,
        date_from: Optional[datetime],
        date_to: Optional[datetime],
        mailbox_id: Optional[uuid.UUID],
        visible_mailbox_ids: Optional[list[uuid.UUID]],
    ) -> bytes:
        """Genera un CSV con tutti i messaggi del periodo."""
        q = (
            select(
                Message.id,
                Message.direction,
                Message.state,
                Message.pec_type,
                Message.subject,
                Message.from_address,
                Message.received_at,
                Message.sent_at,
                Message.size_bytes,
                Message.is_read,
                Message.has_attachments,
                Mailbox.email_address.label("mailbox_email"),
            )
            .join(Mailbox, Message.mailbox_id == Mailbox.id)
            .where(
                Message.tenant_id == tenant_id,
                Message.parent_message_id.is_(None),
            )
            .order_by(Message.received_at.desc().nullslast(), Message.created_at.desc())
        )

        if visible_mailbox_ids is not None:
            if not visible_mailbox_ids:
                buf = io.StringIO()
                writer = csv.writer(buf)
                writer.writerow(CSV_HEADERS)
                return buf.getvalue().encode("utf-8-sig")
            q = q.where(Message.mailbox_id.in_(visible_mailbox_ids))

        if mailbox_id:
            q = q.where(Message.mailbox_id == mailbox_id)

        if date_from:
            q = q.where(
                (Message.received_at >= date_from) | (Message.sent_at >= date_from)
            )
        if date_to:
            q = q.where(
                (Message.received_at <= date_to) | (Message.sent_at <= date_to)
            )

        result = await self.db.execute(q)
        rows = result.all()

        buf = io.StringIO()
        writer = csv.writer(buf)
        writer.writerow(CSV_HEADERS)

        for r in rows:
            ts = r.received_at or r.sent_at
            writer.writerow([
                str(r.id),
                r.mailbox_email or "",
                r.direction or "",
                r.state or "",
                r.pec_type or "",
                r.subject or "",
                r.from_address or "",
                ts.strftime("%Y-%m-%d %H:%M:%S") if ts else "",
                r.size_bytes or "",
                "Si" if r.is_read else "No",
                "Si" if r.has_attachments else "No",
            ])

        return buf.getvalue().encode("utf-8-sig")

    # ─── Export PDF ───────────────────────────────────────────────────────────

    async def export_pdf(
        self,
        tenant_id: uuid.UUID,
        date_from: Optional[datetime],
        date_to: Optional[datetime],
        visible_mailbox_ids: Optional[list[uuid.UUID]],
    ) -> bytes:
        """
        Genera un PDF di riepilogo con KPI e tabella caselle.
        Usa reportlab (puro Python, nessuna dipendenza di sistema).
        """
        try:
            from reportlab.lib import colors
            from reportlab.lib.pagesizes import A4
            from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
            from reportlab.lib.units import cm
            from reportlab.platypus import (
                Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle,
            )
        except ImportError:
            raise RuntimeError(
                "reportlab non installato. Aggiungere 'reportlab>=4.2.0' "
                "alle dipendenze del backend."
            )

        summary = await self.get_summary(tenant_id, 30, visible_mailbox_ids)
        now_str = summary.generated_at.strftime("%d/%m/%Y %H:%M")

        buf = io.BytesIO()
        doc = SimpleDocTemplate(
            buf,
            pagesize=A4,
            leftMargin=2 * cm,
            rightMargin=2 * cm,
            topMargin=2 * cm,
            bottomMargin=2 * cm,
        )

        styles = getSampleStyleSheet()
        title_style = ParagraphStyle(
            "Title", parent=styles["Title"], fontSize=18, spaceAfter=6,
        )
        subtitle_style = ParagraphStyle(
            "Subtitle", parent=styles["Normal"], fontSize=10, textColor=colors.grey, spaceAfter=20,
        )
        heading_style = ParagraphStyle(
            "Heading2", parent=styles["Heading2"], fontSize=13, spaceBefore=14, spaceAfter=6,
        )

        story = []

        # Intestazione
        story.append(Paragraph("PEChub – Report Attivita PEC", title_style))
        date_range = ""
        if date_from:
            date_range += f"Dal {date_from.strftime('%d/%m/%Y')} "
        if date_to:
            date_range += f"Al {date_to.strftime('%d/%m/%Y')} "
        story.append(Paragraph(
            f"Generato il {now_str}  {date_range}",
            subtitle_style,
        ))

        # Sezione KPI
        story.append(Paragraph("Indicatori Chiave (ultimi 30 giorni)", heading_style))
        kpi = summary.kpi
        kpi_data = [
            ["Indicatore", "Valore"],
            ["PEC ricevute oggi", str(kpi.received_today)],
            ["PEC inviate oggi", str(kpi.sent_today)],
            ["PEC ricevute (7 gg)", str(kpi.received_7d)],
            ["PEC inviate (7 gg)", str(kpi.sent_7d)],
            ["PEC ricevute (30 gg)", str(kpi.received_30d)],
            ["PEC inviate (30 gg)", str(kpi.sent_30d)],
            ["Anomalie attive", str(kpi.anomalie_attive)],
            ["Tasso di consegna", f"{kpi.tasso_consegna}%"],
            ["Caselle in errore", str(kpi.caselle_in_errore)],
            ["Messaggi non letti", str(kpi.messaggi_non_letti)],
        ]
        kpi_table = Table(kpi_data, colWidths=[10 * cm, 5 * cm])
        kpi_table.setStyle(TableStyle([
            ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1e40af")),
            ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
            ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
            ("FONTSIZE", (0, 0), (-1, -1), 10),
            ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#f0f4ff")]),
            ("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
            ("ALIGN", (1, 0), (1, -1), "RIGHT"),
            ("LEFTPADDING", (0, 0), (-1, -1), 8),
            ("RIGHTPADDING", (0, 0), (-1, -1), 8),
            ("TOPPADDING", (0, 0), (-1, -1), 5),
            ("BOTTOMPADDING", (0, 0), (-1, -1), 5),
        ]))
        story.append(kpi_table)
        story.append(Spacer(1, 0.5 * cm))

        # Sezione caselle
        if summary.mailbox_stats:
            story.append(Paragraph("Dettaglio per Casella", heading_style))
            mb_header = ["Casella", "Stato", "Ricevute", "Inviate", "Anomalie", "Non letti"]
            mb_data = [mb_header]
            for ms in summary.mailbox_stats:
                mb_data.append([
                    ms.email_address,
                    ms.status,
                    str(ms.received_total),
                    str(ms.sent_total),
                    str(ms.anomalie),
                    str(ms.non_letti),
                ])
            mb_table = Table(
                mb_data,
                colWidths=[6.5 * cm, 2.2 * cm, 2.2 * cm, 2.2 * cm, 2.2 * cm, 2.2 * cm],
            )
            mb_table.setStyle(TableStyle([
                ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1e40af")),
                ("TEXTCOLOR", (0, 0), (-1, 0), colors.white),
                ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
                ("FONTSIZE", (0, 0), (-1, -1), 9),
                ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#f0f4ff")]),
                ("GRID", (0, 0), (-1, -1), 0.5, colors.lightgrey),
                ("ALIGN", (1, 0), (-1, -1), "RIGHT"),
                ("LEFTPADDING", (0, 0), (-1, -1), 6),
                ("RIGHTPADDING", (0, 0), (-1, -1), 6),
                ("TOPPADDING", (0, 0), (-1, -1), 4),
                ("BOTTOMPADDING", (0, 0), (-1, -1), 4),
            ]))
            story.append(mb_table)

        doc.build(story)
        return buf.getvalue()


# ─── Costanti ────────────────────────────────────────────────────────────────

CSV_HEADERS = [
    "ID",
    "Casella",
    "Direzione",
    "Stato",
    "Tipo PEC",
    "Oggetto",
    "Mittente",
    "Data/Ora",
    "Dimensione (byte)",
    "Letto",
    "Allegati",
]
