from __future__ import annotations

import html
import time
from dataclasses import dataclass
from datetime import timedelta
from email.utils import make_msgid
from typing import Iterable

from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.db import transaction
from django.utils import timezone

from core.models import PortalConfiguration
from .models import EmailSuppression, NotificationAttempt, NotificationLog, NotificationTemplate


@dataclass(frozen=True)
class RenderedNotification:
    subject: str
    body: str
    html_body: str


@dataclass(frozen=True)
class DeliveryTarget:
    recipient: str
    original_recipient: str
    cc: tuple[str, ...]
    mode: str
    subject_prefix: str = ""


def portal_configuration() -> PortalConfiguration:
    config = PortalConfiguration.objects.order_by("pk").first()
    if config:
        return config
    return PortalConfiguration(
        company_name="Kreate4Web",
        default_notice_days=30,
        second_notice_days=15,
        final_notice_days=7,
        notification_max_attempts=3,
        notification_retry_minutes=30,
    )


def _domain_for_service(service):
    domain_details = getattr(service, "domain_details", None)
    if domain_details:
        return domain_details.domain_name
    hosting_details = getattr(service, "hosting_details", None)
    if hosting_details and hosting_details.primary_domain:
        return hosting_details.primary_domain.domain_name
    return ""


def template_context(renewal):
    service = renewal.service
    client = service.client
    config = portal_configuration()
    return {
        "cliente": client.name,
        "servico": service.name,
        "dominio": _domain_for_service(service),
        "data_renovacao": renewal.due_date.strftime("%d/%m/%Y"),
        "valor": f"{renewal.sale_amount:.2f} €",
        "metodo_pagamento": client.get_preferred_payment_method_display() if client.preferred_payment_method else "a combinar",
        "empresa": config.company_name,
        "email_empresa": config.company_email,
        "telefone_empresa": config.company_phone,
        "mbway": config.mbway_phone,
        "iban": config.iban,
    }


def _plain_to_html(value: str) -> str:
    paragraphs = []
    for block in value.split("\n\n"):
        escaped = html.escape(block.strip()).replace("\n", "<br>")
        if escaped:
            paragraphs.append(f"<p>{escaped}</p>")
    return "".join(paragraphs)


def render_template(template, renewal) -> RenderedNotification:
    context = template_context(renewal)
    body = template.body_template.format_map(context)
    html_template = template.html_body_template or _plain_to_html(template.body_template)
    return RenderedNotification(
        subject=template.subject_template.format_map(context),
        body=body,
        html_body=html_template.format_map(context),
    )


def billing_contact_emails(client) -> list[str]:
    emails: list[str] = []
    for contact in client.contacts.filter(receives_billing=True).exclude(email=""):
        normalized = contact.email.strip().lower()
        if normalized and normalized != (client.email or "").strip().lower() and normalized not in emails:
            emails.append(normalized)
    return emails


def _deduplication_key(renewal, template_type: str, recipient: str) -> str:
    return f"renewal:{renewal.pk}:{template_type}:{recipient.strip().lower()}"


@transaction.atomic
def create_notification_from_template(
    renewal,
    template_type,
    status=None,
    *,
    automatic: bool = False,
    scheduled_for=None,
):
    template = NotificationTemplate.objects.filter(notification_type=template_type, is_active=True).order_by("code").first()
    if not template:
        raise ValueError(f"Não existe um modelo ativo para {template_type}.")
    if not renewal.client.email:
        raise ValueError("O cliente não tem email principal definido.")

    config = portal_configuration()
    if status is None:
        can_auto_send = automatic and config.auto_send_notifications and template.allow_automatic_send
        status = NotificationLog.Status.SCHEDULED if can_auto_send else NotificationLog.Status.DRAFT
    if status == NotificationLog.Status.SCHEDULED and not scheduled_for:
        scheduled_for = timezone.now()
    elif status != NotificationLog.Status.SCHEDULED:
        scheduled_for = None

    rendered = render_template(template, renewal)
    recipient = renewal.client.email.strip().lower()
    cc = billing_contact_emails(renewal.client) if template.send_to_billing_contacts else []
    key = _deduplication_key(renewal, template_type, recipient)
    notification, created = NotificationLog.objects.get_or_create(
        deduplication_key=key,
        defaults={
            "client": renewal.client,
            "service": renewal.service,
            "renewal": renewal,
            "template": template,
            "channel": NotificationLog.Channel.EMAIL,
            "recipient": recipient,
            "cc_recipients": ", ".join(cc),
            "reply_to": config.reply_to_email or config.company_email,
            "subject": rendered.subject,
            "body": rendered.body,
            "html_body": rendered.html_body,
            "status": status,
            "scheduled_for": scheduled_for,
            "is_automatic": automatic,
            "max_attempts": config.notification_max_attempts or 3,
        },
    )
    return notification, created


def resolve_delivery_target(notification: NotificationLog) -> DeliveryTarget:
    config = portal_configuration()
    original = (notification.original_recipient or notification.recipient).strip().lower()
    cc = tuple(notification.cc_list())
    backend = getattr(settings, "EMAIL_BACKEND", "")

    if notification.channel != NotificationLog.Channel.EMAIL:
        return DeliveryTarget(notification.recipient, original, cc, NotificationLog.DeliveryMode.INTERNAL)

    if config.pk and config.email_test_mode:
        if not config.email_test_recipient:
            raise ValueError("O modo de teste está ativo, mas não existe destinatário de testes configurado.")
        target = config.email_test_recipient.strip().lower()
        return DeliveryTarget(
            target,
            original,
            (),
            NotificationLog.DeliveryMode.TEST,
            subject_prefix=f"[TESTE para {original}] ",
        )
    if "console" in backend:
        return DeliveryTarget(notification.recipient, original, cc, NotificationLog.DeliveryMode.CONSOLE)
    return DeliveryTarget(notification.recipient, original, cc, NotificationLog.DeliveryMode.LIVE)


def _suppressed(email: str) -> bool:
    return EmailSuppression.objects.filter(email__iexact=email, is_active=True).exists()


def _retry_time(attempt_number: int) -> timezone.datetime:
    config = portal_configuration()
    base = max(config.notification_retry_minutes or 30, 1)
    minutes = base * (2 ** max(attempt_number - 1, 0))
    return timezone.now() + timedelta(minutes=minutes)


def _create_failure_task(notification: NotificationLog) -> None:
    if notification.attempts_count < notification.max_attempts:
        return
    try:
        from operations.models import InternalTask

        InternalTask.objects.get_or_create(
            title=f"Resolver falha de email — {notification.client}",
            task_type=InternalTask.TaskType.CONTACT_CLIENT,
            client=notification.client,
            service=notification.service,
            defaults={
                "priority": InternalTask.Priority.HIGH,
                "status": InternalTask.Status.TODO,
                "due_date": timezone.localdate(),
                "description": (
                    f"A notificação '{notification.subject}' falhou após {notification.attempts_count} tentativas. "
                    f"Erro: {notification.error_message[:500]}"
                ),
            },
        )
    except Exception:
        # Uma falha de criação da tarefa nunca deve esconder o erro original de email.
        return


@transaction.atomic
def send_notification(
    notification: NotificationLog,
    *,
    user=None,
    raise_errors: bool = True,
) -> NotificationLog:
    notification = NotificationLog.objects.select_for_update().select_related(
        "client", "service", "renewal", "template"
    ).get(pk=notification.pk)

    if notification.status == NotificationLog.Status.SENT:
        return notification
    if notification.status == NotificationLog.Status.CANCELLED:
        raise ValueError("A notificação está cancelada.")

    started = time.monotonic()
    backend_name = getattr(settings, "EMAIL_BACKEND", "")
    message_id = make_msgid(domain=None)
    try:
        target = resolve_delivery_target(notification)
    except Exception as exc:
        notification.status = NotificationLog.Status.FAILED
        notification.attempts_count += 1
        notification.last_attempt_at = timezone.now()
        notification.error_message = str(exc)
        notification.next_retry_at = None
        notification.sent_by = user or notification.sent_by
        notification.save(update_fields=["status", "attempts_count", "last_attempt_at", "error_message", "next_retry_at", "sent_by", "updated_at"])
        NotificationAttempt.objects.create(
            notification=notification,
            result=NotificationAttempt.Result.FAILED,
            recipient_used=notification.recipient,
            backend=backend_name,
            duration_ms=int((time.monotonic() - started) * 1000),
            message_id=message_id,
            error_message=str(exc),
            triggered_by=user,
        )
        if raise_errors:
            raise
        return notification

    notification.original_recipient = target.original_recipient
    notification.delivery_mode = target.mode
    notification.status = NotificationLog.Status.SENDING
    notification.attempts_count += 1
    notification.last_attempt_at = timezone.now()
    notification.next_retry_at = None
    notification.sent_by = user or notification.sent_by
    notification.save(
        update_fields=[
            "original_recipient",
            "delivery_mode",
            "status",
            "attempts_count",
            "last_attempt_at",
            "next_retry_at",
            "sent_by",
            "updated_at",
        ]
    )

    if _suppressed(target.original_recipient):
        notification.status = NotificationLog.Status.SUPPRESSED
        notification.error_message = "O destinatário encontra-se na lista de bloqueios."
        notification.next_retry_at = None
        notification.save(update_fields=["status", "error_message", "next_retry_at", "updated_at"])
        NotificationAttempt.objects.create(
            notification=notification,
            result=NotificationAttempt.Result.SUPPRESSED,
            recipient_used=target.recipient,
            backend=backend_name,
            duration_ms=int((time.monotonic() - started) * 1000),
            error_message=notification.error_message,
            triggered_by=user,
        )
        return notification

    if notification.channel != NotificationLog.Channel.EMAIL:
        notification.status = NotificationLog.Status.SENT
        notification.sent_at = timezone.now()
        notification.message_id = message_id
        notification.error_message = ""
        notification.save(update_fields=["status", "sent_at", "message_id", "error_message", "updated_at"])
        NotificationAttempt.objects.create(
            notification=notification,
            result=NotificationAttempt.Result.SUCCESS,
            recipient_used=target.recipient,
            backend="internal",
            duration_ms=int((time.monotonic() - started) * 1000),
            message_id=message_id,
            triggered_by=user,
        )
        return notification

    try:
        subject = f"{target.subject_prefix}{notification.subject}"
        reply_to = [notification.reply_to] if notification.reply_to else None
        message = EmailMultiAlternatives(
            subject=subject,
            body=notification.body,
            from_email=settings.DEFAULT_FROM_EMAIL,
            to=[target.recipient],
            cc=list(target.cc),
            reply_to=reply_to,
            headers={
                "Message-ID": message_id,
                "X-Portal-Notification-ID": str(notification.public_id),
                "X-Portal-Delivery-Mode": target.mode,
            },
        )
        if notification.html_body:
            message.attach_alternative(notification.html_body, "text/html")
        sent_count = message.send(fail_silently=False)
        if sent_count != 1:
            raise RuntimeError("O backend de email não confirmou o envio da mensagem.")
    except Exception as exc:  # depende do backend externo
        notification.status = NotificationLog.Status.FAILED
        notification.error_message = str(exc)
        notification.next_retry_at = (
            _retry_time(notification.attempts_count)
            if notification.attempts_count < notification.max_attempts
            else None
        )
        notification.save(update_fields=["status", "error_message", "next_retry_at", "updated_at"])
        NotificationAttempt.objects.create(
            notification=notification,
            result=NotificationAttempt.Result.FAILED,
            recipient_used=target.recipient,
            backend=backend_name,
            duration_ms=int((time.monotonic() - started) * 1000),
            message_id=message_id,
            error_message=str(exc),
            triggered_by=user,
        )
        _create_failure_task(notification)
        if raise_errors:
            raise
        return notification

    notification.status = NotificationLog.Status.SENT
    notification.sent_at = timezone.now()
    notification.next_retry_at = None
    notification.message_id = message_id
    notification.error_message = ""
    notification.save(
        update_fields=["status", "sent_at", "next_retry_at", "message_id", "error_message", "updated_at"]
    )
    NotificationAttempt.objects.create(
        notification=notification,
        result=NotificationAttempt.Result.SUCCESS,
        recipient_used=target.recipient,
        backend=backend_name,
        duration_ms=int((time.monotonic() - started) * 1000),
        message_id=message_id,
        triggered_by=user,
    )
    _mark_renewal_notice(notification)
    return notification


def _mark_renewal_notice(notification):
    renewal = notification.renewal
    template = notification.template
    if not renewal or not template:
        return
    now = notification.sent_at or timezone.now()
    mapping = {
        NotificationTemplate.Type.RENEWAL_30: "notice_30_sent_at",
        NotificationTemplate.Type.RENEWAL_15: "notice_15_sent_at",
        NotificationTemplate.Type.RENEWAL_7: "notice_7_sent_at",
        NotificationTemplate.Type.DUE: "due_notice_sent_at",
        NotificationTemplate.Type.OVERDUE: "overdue_notice_sent_at",
    }
    field = mapping.get(template.notification_type)
    update_fields = ["updated_at"]
    if field:
        setattr(renewal, field, now)
        update_fields.append(field)
    if template.notification_type in {
        NotificationTemplate.Type.RENEWAL_30,
        NotificationTemplate.Type.RENEWAL_15,
        NotificationTemplate.Type.RENEWAL_7,
        NotificationTemplate.Type.DUE,
    } and renewal.status == renewal.Status.TO_NOTIFY:
        renewal.status = renewal.Status.NOTIFIED
        update_fields.append("status")
    renewal.save(update_fields=update_fields)


def _notice_type_for_renewal(renewal, today, config):
    days = (renewal.due_date - today).days
    if days == config.default_notice_days and not renewal.notice_30_sent_at:
        return NotificationTemplate.Type.RENEWAL_30
    if days == config.second_notice_days and not renewal.notice_15_sent_at:
        return NotificationTemplate.Type.RENEWAL_15
    if days == config.final_notice_days and not renewal.notice_7_sent_at:
        return NotificationTemplate.Type.RENEWAL_7
    if days == 0 and not renewal.due_notice_sent_at:
        return NotificationTemplate.Type.DUE
    if days < 0 and not renewal.overdue_notice_sent_at:
        return NotificationTemplate.Type.OVERDUE
    return None


def prepare_due_notifications(*, send: bool = False, automatic: bool = True) -> dict[str, int]:
    from billing.models import Renewal

    config = portal_configuration()
    result = {"created": 0, "existing": 0, "sent": 0, "failed": 0, "skipped": 0}
    if automatic and not config.auto_prepare_notifications:
        result["skipped"] += 1
        return result

    today = timezone.localdate()
    renewals = Renewal.objects.select_related("service__client").exclude(
        status__in=[Renewal.Status.RENEWED, Renewal.Status.CANCELLED, Renewal.Status.PAID]
    )
    for renewal in renewals:
        template_type = _notice_type_for_renewal(renewal, today, config)
        if not template_type:
            result["skipped"] += 1
            continue
        try:
            notification, was_created = create_notification_from_template(
                renewal,
                template_type,
                automatic=automatic,
                scheduled_for=timezone.now(),
            )
        except ValueError:
            result["skipped"] += 1
            continue
        result["created" if was_created else "existing"] += 1
        should_send = send or (
            automatic
            and config.auto_send_notifications
            and notification.template
            and notification.template.allow_automatic_send
        )
        if should_send and notification.status != NotificationLog.Status.SENT:
            delivered = send_notification(notification, raise_errors=False)
            result["sent" if delivered.status == NotificationLog.Status.SENT else "failed"] += 1
    return result


def send_scheduled_notifications(*, limit: int = 100, user=None) -> dict[str, int]:
    now = timezone.now()
    result = {"sent": 0, "failed": 0, "skipped": 0}
    queryset = NotificationLog.objects.filter(
        status=NotificationLog.Status.SCHEDULED,
        scheduled_for__lte=now,
    ).order_by("scheduled_for")[:limit]
    for notification in queryset:
        delivered = send_notification(notification, user=user, raise_errors=False)
        if delivered.status == NotificationLog.Status.SENT:
            result["sent"] += 1
        elif delivered.status in {NotificationLog.Status.FAILED, NotificationLog.Status.SUPPRESSED}:
            result["failed"] += 1
        else:
            result["skipped"] += 1
    return result


def retry_failed_notifications(*, limit: int = 100, user=None, force: bool = False) -> dict[str, int]:
    now = timezone.now()
    result = {"sent": 0, "failed": 0, "skipped": 0}
    queryset = NotificationLog.objects.filter(status=NotificationLog.Status.FAILED).order_by("next_retry_at", "created_at")
    if not force:
        queryset = queryset.filter(next_retry_at__lte=now)
    for notification in queryset[:limit]:
        if not notification.can_retry:
            result["skipped"] += 1
            continue
        delivered = send_notification(notification, user=user, raise_errors=False)
        result["sent" if delivered.status == NotificationLog.Status.SENT else "failed"] += 1
    return result


def send_test_email(recipient: str, *, user=None) -> NotificationLog:
    from clients.models import Client

    config = portal_configuration()
    client, _ = Client.objects.get_or_create(
        name="Teste de configuração de email",
        defaults={"email": recipient, "status": "active"},
    )
    notification = NotificationLog.objects.create(
        client=client,
        channel=NotificationLog.Channel.EMAIL,
        recipient=recipient,
        subject="Teste do Portal Kreate4Web",
        body=(
            "Esta mensagem confirma que a configuração de email do Portal Kreate4Web está operacional.\n\n"
            f"Empresa: {config.company_name}\nData: {timezone.localtime():%d/%m/%Y %H:%M}"
        ),
        html_body=(
            "<h2>Teste do Portal Kreate4Web</h2>"
            "<p>Esta mensagem confirma que a configuração de email está operacional.</p>"
            f"<p><strong>Empresa:</strong> {html.escape(config.company_name)}</p>"
        ),
        status=NotificationLog.Status.DRAFT,
        is_automatic=False,
        max_attempts=1,
    )
    return send_notification(notification, user=user, raise_errors=True)
