import logging
from email.mime.image import MIMEImage
from functools import lru_cache
from pathlib import Path

from django.apps import apps
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.urls import reverse

from apps.core.notifications.models import Notification

logger = logging.getLogger(__name__)


@lru_cache(maxsize=1)
def _brand_logo_bytes() -> bytes | None:
    """Read the brand logo from disk once. Returns None if it can't be found."""
    candidates = [
        Path(settings.BASE_DIR) / "static" / "img" / "brand" / "logo.png",
        Path(settings.BASE_DIR) / "staticfiles" / "img" / "brand" / "logo.png",
    ]
    for path in candidates:
        try:
            if path.is_file():
                return path.read_bytes()
        except OSError:
            continue
    return None


def _attach_brand_logo_inline(msg: EmailMultiAlternatives, cid: str = "ruforum-logo") -> None:
    """Attach the RUFORUM logo as an inline (CID) image so any HTML
    template can reference it via ``<img src="cid:ruforum-logo">`` without
    relying on remote-image loading in the recipient's mail client."""
    data = _brand_logo_bytes()
    if not data:
        return
    img = MIMEImage(data, _subtype="png")
    img.add_header("Content-ID", f"<{cid}>")
    img.add_header("Content-Disposition", "inline", filename="ruforum-logo.png")
    msg.attach(img)


def attachment_tuples_from_model_spec(spec: dict | None) -> list[tuple[str, bytes, str]]:
    """
    Load (filename, bytes, mimetype) for EmailMessage.attach from a JSON-serializable spec.

    Expected keys: app_label, model_name, pk, field_name (default pdf_file),
    optional filename, mimetype.
    """
    if not spec or not isinstance(spec, dict):
        return []
    app_label = spec.get("app_label")
    model_name = spec.get("model_name")
    pk = spec.get("pk")
    if not app_label or not model_name or pk is None:
        return []
    try:
        Model = apps.get_model(app_label, model_name)
    except LookupError:
        logger.warning("attachment_from_model: unknown model %s.%s", app_label, model_name)
        return []
    obj = Model.objects.filter(pk=pk).first()
    if not obj:
        return []
    field_name = spec.get("field_name") or "pdf_file"
    field = getattr(obj, field_name, None)
    name = getattr(field, "name", None) if field is not None else None
    if not name:
        return []
    fname = (spec.get("filename") or "").strip() or name.rsplit("/", 1)[-1] or "attachment.pdf"
    mimetype = spec.get("mimetype") or "application/octet-stream"
    with field.open("rb") as fh:
        return [(fname, fh.read(), mimetype)]


class InAppChannel:
    @staticmethod
    def deliver(notification: Notification, context: dict | None = None) -> None:
        return


class EmailChannel:
    @staticmethod
    def deliver(notification: Notification, context: dict | None = None) -> None:
        ctx = dict(context or {})
        body = ctx.get("body") or notification.message
        subject = ctx.get("subject") or f"RUFORUM IILMP — {notification.get_verb_display()}"
        portal_base = ctx.get("portal_base") or getattr(
            settings, "PUBLIC_APP_BASE_URL", "http://127.0.0.1:8000"
        ).rstrip("/")
        preferences_path = ctx.get("preferences_path") or reverse("core:notification_inbox")
        recipient_name = (
            ctx.get("recipient_name")
            or notification.recipient.get_full_name()
            or notification.recipient.email.split("@")[0]
        )
        html = render_to_string(
            "notifications/emails/generic.html",
            {
                "notification": notification,
                "body": body,
                "body_html": ctx.get("body_html") or "",
                "headline": ctx.get("headline", ""),
                "preheader": ctx.get("preheader", ""),
                "action_url": ctx.get("action_url", ""),
                "action_label": ctx.get("action_label", "Open in IILMP"),
                "recipient_name": recipient_name,
                "portal_base": portal_base,
                "preferences_path": preferences_path,
            },
        )
        plain = body if isinstance(body, str) else str(body)
        attachments = attachment_tuples_from_model_spec(ctx.get("attachment_from_model"))
        # In-memory attachments — list of (filename, bytes, mimetype). Used for
        # generated content like .ics invites that don't live in a model field.
        # Bytes are base64-encoded when serialised through the JSON context so
        # Celery can carry them; decode here when present.
        inline_attachments = ctx.get("inline_attachments") or []
        if inline_attachments:
            import base64
            for entry in inline_attachments:
                if not entry or len(entry) < 3:
                    continue
                fname, content, mimetype = entry[0], entry[1], entry[2]
                if isinstance(content, str):
                    try:
                        content = base64.b64decode(content)
                    except Exception:  # noqa: BLE001 — best-effort
                        content = content.encode("utf-8")
                attachments.append((fname, content, mimetype))
        try:
            msg = EmailMultiAlternatives(
                subject=subject,
                body=plain,
                from_email=settings.DEFAULT_FROM_EMAIL,
                to=[notification.recipient.email],
            )
            msg.attach_alternative(html, "text/html")
            _attach_brand_logo_inline(msg)
            for fname, content, mimetype in attachments:
                msg.attach(fname, content, mimetype)
            msg.send(fail_silently=False)
            notification.email_sent = True
            notification.email_error = ""
            notification.save(update_fields=["email_sent", "email_error"])
        except Exception as exc:
            logger.exception("email notification failed")
            notification.email_error = str(exc)[:500]
            notification.save(update_fields=["email_error"])
            raise


class SMSChannel:
    @staticmethod
    def deliver(notification: Notification, context: dict | None = None) -> None:
        logger.info("SMS channel stub for notification %s", notification.pk)
