"""Async email client using aiosmtplib."""

from __future__ import annotations

import html
import logging
import re
import ssl
from dataclasses import dataclass
from email.headerregistry import Address
from email.message import EmailMessage
from typing import Any, Final, Literal

try:  # Optional dependency — fail at first send rather than at import.
    import aiosmtplib
    from aiosmtplib import SMTPException
except ImportError:  # pragma: no cover
    aiosmtplib = None  # type: ignore[assignment]

    class SMTPException(Exception):  # type: ignore[no-redef]
        pass

_LOGGER = logging.getLogger(__name__)

_DEFAULT_TIMEOUT_S: Final = 30.0
_TlsMode = Literal["auto", "implicit", "starttls", "none"]
# RFC 5322 lite: catches the obvious-bad addresses ("foo bar", "no-at",
# embedded CRLF) without pretending to fully validate addresses.
_EMAIL_RE: Final = re.compile(r"^[^\s@\r\n,;<>]+@[^\s@\r\n,;<>]+\.[^\s@\r\n,;<>]+$")


@dataclass
class SmtpConfig:
    """SMTP connection settings."""

    host: str
    port: int = 587
    username: str = ""
    password: str = ""
    from_address: str = ""
    from_name: str = "Notify Bridge"
    use_tls: bool = True
    # Explicit TLS mode. ``auto`` (back-compat) infers from ``use_tls`` and
    # ``port``: 465 → implicit; 587 with use_tls=False → starttls; 25 → none.
    tls_mode: _TlsMode = "auto"
    timeout_s: float = _DEFAULT_TIMEOUT_S


def _strip_header(value: str) -> str:
    """Reject CRLF and bare CR/LF in header-bound strings.

    SMTP header injection turns user-controlled subject/name strings into
    arbitrary headers (``\\r\\nBcc: attacker@x``). The Python stdlib
    accepts CRLF when followed by SP/HT (header folding), so explicit
    sanitization is required even though :class:`EmailMessage` does some
    validation of its own.
    """
    return re.sub(r"[\r\n]+", " ", str(value or "")).strip()


def _validate_email(addr: str) -> str:
    addr = _strip_header(addr)
    if not addr:
        raise ValueError("email address is empty")
    if not _EMAIL_RE.match(addr):
        raise ValueError("email address is invalid")
    return addr


def _resolve_tls(cfg: SmtpConfig) -> tuple[bool, bool]:
    """Resolve ``(use_tls, start_tls)`` flags from the config.

    ``tls_mode`` overrides ``use_tls``/port heuristics when provided.
    """
    mode = cfg.tls_mode
    if mode == "implicit":
        return True, False
    if mode == "starttls":
        return False, True
    if mode == "none":
        return False, False
    # auto — preserve the historical "use_tls bool + port heuristic" behavior
    # but make the path explicit.
    if cfg.use_tls:
        return True, False
    return False, cfg.port != 25


def _to_html(text: str) -> str:
    """Convert plain text to a minimal HTML body, escaped for safety."""
    return "<html><body><pre>" + html.escape(text) + "</pre></body></html>"


class EmailClient:
    """Sends email notifications via SMTP."""

    def __init__(self, smtp_config: SmtpConfig) -> None:
        self._config = smtp_config

    @staticmethod
    def _ssl_context() -> ssl.SSLContext:
        # Explicit context so the TLS posture is auditable; aiosmtplib
        # defaults look correct today but past regressions (and downstream
        # repackaging) make implicit reliance fragile.
        return ssl.create_default_context()

    async def verify_connection(self) -> dict[str, Any]:
        """Test SMTP connection and authentication without sending an email."""
        if aiosmtplib is None:
            return {"success": False, "error": "aiosmtplib not installed"}

        cfg = self._config
        if not cfg.host:
            return {"success": False, "error": "SMTP host not configured"}

        use_tls, start_tls = _resolve_tls(cfg)
        try:
            smtp = aiosmtplib.SMTP(
                hostname=cfg.host,
                port=cfg.port,
                use_tls=use_tls,
                start_tls=start_tls,
                tls_context=self._ssl_context(),
                timeout=cfg.timeout_s,
                validate_certs=True,
            )
            await smtp.connect()
            if cfg.username and cfg.password:
                await smtp.login(cfg.username, cfg.password)
            await smtp.quit()
            return {"success": True}
        except (SMTPException, OSError) as e:
            _LOGGER.warning("SMTP verification failed for %s:%d: %s", cfg.host, cfg.port, e)
            return {"success": False, "error": str(e)}

    async def send(
        self,
        to_email: str,
        subject: str,
        body_text: str,
        body_html: str | None = None,
        to_name: str = "",
    ) -> dict[str, Any]:
        """Send an email.

        Returns ``{"success": True}`` or ``{"success": False, "error": "..."}``.

        ``body_html`` is treated as already-safe markup. Pass ``None`` to
        derive a safe HTML alternative from ``body_text`` automatically.
        """
        if aiosmtplib is None:
            return {"success": False, "error": "aiosmtplib not installed. Run: pip install aiosmtplib"}

        cfg = self._config
        if not cfg.host or not cfg.from_address:
            return {"success": False, "error": "SMTP not configured (missing host or from_address)"}

        try:
            to_addr = _validate_email(to_email)
            from_addr = _validate_email(cfg.from_address)
        except ValueError as exc:
            return {"success": False, "error": f"Invalid email address: {exc}"}

        # EmailMessage with structured Address objects rejects CRLF and
        # framework-folds long headers safely. We still strip first because
        # EmailMessage's display-name slot is a pure string.
        msg = EmailMessage()
        from_display = _strip_header(cfg.from_name) or ""
        to_display = _strip_header(to_name) or ""
        try:
            from_user, _, from_domain = from_addr.partition("@")
            to_user, _, to_domain = to_addr.partition("@")
            msg["From"] = Address(from_display, from_user, from_domain) if from_display else from_addr
            msg["To"] = Address(to_display, to_user, to_domain) if to_display else to_addr
        except ValueError as exc:
            return {"success": False, "error": f"Invalid email address: {exc}"}
        msg["Subject"] = _strip_header(subject)

        msg.set_content(body_text or "", subtype="plain", charset="utf-8")
        # If the caller provided HTML explicitly, honor it; otherwise build a
        # safe escaped version so a stray "<" in the rendered template can't
        # break the markup.
        msg.add_alternative(
            body_html if body_html is not None else _to_html(body_text or ""),
            subtype="html",
            charset="utf-8",
        )

        use_tls, start_tls = _resolve_tls(cfg)
        try:
            await aiosmtplib.send(
                msg,
                hostname=cfg.host,
                port=cfg.port,
                username=cfg.username or None,
                password=cfg.password or None,
                use_tls=use_tls,
                start_tls=start_tls,
                tls_context=self._ssl_context(),
                timeout=cfg.timeout_s,
                validate_certs=True,
            )
            _LOGGER.info("Email sent to %s", to_addr)
            return {"success": True}
        except (SMTPException, OSError) as e:
            _LOGGER.error("Failed to send email to %s: %s", to_addr, e)
            return {"success": False, "error": str(e)}
