"""SMTP email sending helpers for user communication workflows."""

from __future__ import annotations

import logging
import mimetypes
import smtplib
import socket
import ssl
from dataclasses import dataclass
from email.message import EmailMessage
from email.utils import formataddr
from html.parser import HTMLParser
from typing import Any, Iterable

logger = logging.getLogger(__name__)


@dataclass(frozen=True)
class EmailAttachment:
    """Attachment payload passed from the API layer."""

    filename: str
    content_type: str
    data: bytes


class _HTMLToTextParser(HTMLParser):
    """Small HTML-to-text helper for plain-text fallback bodies."""

    block_tags = {"p", "div", "section", "article", "header", "footer", "li", "tr", "td", "th", "br"}

    def __init__(self) -> None:
        super().__init__()
        self.parts: list[str] = []

    def handle_starttag(self, tag: str, attrs):  # type: ignore[override]
        if tag.lower() in self.block_tags and self.parts and not self.parts[-1].endswith("\n"):
            self.parts.append("\n")

    def handle_endtag(self, tag: str) -> None:
        if tag.lower() in self.block_tags and self.parts and not self.parts[-1].endswith("\n"):
            self.parts.append("\n")

    def handle_data(self, data: str) -> None:
        if data:
            self.parts.append(data)

    def text(self) -> str:
        return "".join(self.parts)


def html_to_text(html: str) -> str:
    """Convert a small HTML body to readable plain text."""
    parser = _HTMLToTextParser()
    parser.feed(html or "")
    text = parser.text()
    lines = [line.rstrip() for line in text.splitlines()]
    return "\n".join(line for line in lines if line).strip()


def _from_address(settings: object) -> str:
    from_address = str(getattr(settings, "smtp_from_address", "") or "").strip()
    if from_address:
        return from_address
    smtp_username = str(getattr(settings, "smtp_username", "") or "").strip()
    if smtp_username:
        return smtp_username
    raise ValueError("SMTP from address is required (set SMTP_FROM_ADDRESS or SMTP_USERNAME)")


def validate_smtp_settings(settings: object) -> None:
    """Validate that the SMTP configuration is sufficient to send mail."""
    smtp_host = str(getattr(settings, "smtp_host", "") or "").strip()
    if not smtp_host:
        raise ValueError("SMTP host is required")
    _from_address(settings)


def _smtp_settings(settings: object) -> dict[str, object]:
    smtp_host = str(getattr(settings, "smtp_host", "") or "").strip()
    if not smtp_host:
        raise ValueError("SMTP host is required")
    smtp_port = int(getattr(settings, "smtp_port", 587) or 587)
    smtp_username = str(getattr(settings, "smtp_username", "") or "").strip()
    smtp_password = str(getattr(settings, "smtp_password", "") or "")
    use_tls = bool(getattr(settings, "smtp_use_tls", True))
    use_ssl = bool(getattr(settings, "smtp_use_ssl", False))
    smtp_timeout = int(getattr(settings, "smtp_timeout", 30) or 30)
    return {
        "smtp_host": smtp_host,
        "smtp_port": smtp_port,
        "smtp_username": smtp_username,
        "smtp_password": smtp_password,
        "use_tls": use_tls,
        "use_ssl": use_ssl,
        "smtp_timeout": smtp_timeout,
    }


def _smtp_mode_candidates(settings: object) -> list[dict[str, Any]]:
    base = _smtp_settings(settings)
    candidates = [dict(base, mode_label="configured")]
    smtp_host = str(base["smtp_host"]).lower()
    if "fastmail.com" in smtp_host:
        fastmail_ssl = {
            **base,
            "smtp_port": 465,
            "use_tls": False,
            "use_ssl": True,
            "mode_label": "Fastmail SSL 465",
        }
        fastmail_tls = {
            **base,
            "smtp_port": 587,
            "use_tls": True,
            "use_ssl": False,
            "mode_label": "Fastmail STARTTLS 587",
        }
        for mode in (fastmail_ssl, fastmail_tls):
            if not any(
                candidate["smtp_port"] == mode["smtp_port"]
                and candidate["use_tls"] == mode["use_tls"]
                and candidate["use_ssl"] == mode["use_ssl"]
                for candidate in candidates
            ):
                candidates.append(mode)
    return candidates


def _probe_smtp_connection(mode: dict[str, Any]) -> None:
    context = ssl.create_default_context()
    smtp_host = str(mode["smtp_host"])
    smtp_port = int(mode["smtp_port"])
    smtp_username = str(mode["smtp_username"])
    smtp_password = str(mode["smtp_password"])
    use_tls = bool(mode["use_tls"])
    use_ssl = bool(mode["use_ssl"])
    smtp_timeout = int(mode["smtp_timeout"])

    if use_ssl:
        smtp_connection = smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=smtp_timeout)
    else:
        smtp_connection = smtplib.SMTP(smtp_host, smtp_port, timeout=smtp_timeout)

    with smtp_connection as smtp:
        if use_tls and not use_ssl:
            smtp.ehlo()
            smtp.starttls(context=context)
            smtp.ehlo()
        else:
            smtp.ehlo()
        if smtp_username:
            smtp.login(smtp_username, smtp_password)
        smtp.noop()


def _smtp_sender_not_authorized(error: Exception) -> bool:
    code = getattr(error, "smtp_code", None)
    raw_error = getattr(error, "smtp_error", b"")
    if isinstance(raw_error, bytes):
        raw_error_text = raw_error.decode(errors="ignore")
    else:
        raw_error_text = str(raw_error)
    text = f"{code} {raw_error_text} {error}".lower()
    return (
        code in {551, 553}
        or "not authorised to send from this header address" in text
        or "not authorized to send from this header address" in text
    )


def _smtp_attempt_metadata(mode: dict[str, Any]) -> dict[str, Any]:
    transport = "SSL" if mode["use_ssl"] else "STARTTLS" if mode["use_tls"] else "plain SMTP"
    return {
        "label": str(mode.get("mode_label") or f"{mode['smtp_host']}:{mode['smtp_port']}"),
        "smtp_host": str(mode["smtp_host"]),
        "smtp_port": int(mode["smtp_port"]),
        "use_tls": bool(mode["use_tls"]),
        "use_ssl": bool(mode["use_ssl"]),
        "transport": transport,
        "auth_user": str(mode.get("smtp_username") or "") or "<none>",
    }


def describe_smtp_error(error: Exception) -> str:
    """Convert SMTP failures into operator-friendly messages."""
    chain: list[Exception] = []
    current: Exception | None = error
    while current is not None and current not in chain:
        chain.append(current)
        current = current.__cause__ if isinstance(current.__cause__, Exception) else None

    for item in chain:
        text = str(item).strip()
        lowered = text.lower()
        if isinstance(item, (TimeoutError, socket.timeout)) or "timed out" in lowered:
            return (
                "SMTP connection timed out while waiting for the server greeting. "
                "Check host, port, network access, and SMTP_TIMEOUT."
            )
        if isinstance(item, smtplib.SMTPAuthenticationError):
            return (
                "SMTP authentication failed. Check SMTP_USERNAME and SMTP_PASSWORD "
                "(Fastmail and similar providers usually require an app password)."
            )
        if isinstance(item, (smtplib.SMTPDataError, smtplib.SMTPResponseException)):
            smtp_code = getattr(item, "smtp_code", None)
            if (
                smtp_code in {551, 553}
                or "not authorised to send from this header address" in lowered
                or "not authorized to send from this header address" in lowered
            ):
                return (
                    "SMTP server rejected the configured From address. "
                    "Use an authorized alias for this account or change "
                    "SMTP_FROM_ADDRESS to a sender the provider allows."
                )
        if isinstance(item, smtplib.SMTPConnectError):
            return "SMTP connection was rejected by the server. Check the host and port."
        if isinstance(item, smtplib.SMTPServerDisconnected) and "timed out" in lowered:
            return (
                "SMTP connection timed out while waiting for the server greeting. "
                "Check host, port, network access, and SMTP_TIMEOUT."
            )

    return f"SMTP delivery failed: {error}"


def build_email_message(
    settings: object,
    recipients: list[str],
    subject: str,
    html_body: str,
    text_body: str,
    attachments: Iterable[EmailAttachment] = (),
    *,
    sender_address: str | None = None,
    reply_to_address: str | None = None,
) -> tuple[EmailMessage, str]:
    """Build a MIME email message with HTML and attachments."""
    from_address = sender_address or _from_address(settings)
    from_name = str(getattr(settings, "smtp_from_name", "") or "").strip() or "Manage"
    msg = EmailMessage()
    msg["Subject"] = subject
    msg["From"] = formataddr((from_name, from_address))
    msg["To"] = "Undisclosed recipients:;"
    msg["Reply-To"] = reply_to_address or from_address

    plain_text = text_body.strip() or html_to_text(html_body)
    if html_body.strip():
        msg.set_content(plain_text or " ")
        msg.add_alternative(html_body, subtype="html")
    else:
        msg.set_content(plain_text or "")

    for attachment in attachments:
        content_type = (
            attachment.content_type or mimetypes.guess_type(attachment.filename)[0] or "application/octet-stream"
        )
        maintype, subtype = content_type.split("/", 1) if "/" in content_type else ("application", "octet-stream")
        msg.add_attachment(
            attachment.data,
            maintype=maintype,
            subtype=subtype,
            filename=attachment.filename or "attachment",
        )

    return msg, from_address


def _send_email_via_mode(
    mode: dict[str, Any],
    message: EmailMessage,
    recipients: list[str],
    from_address: str,
) -> None:
    context = ssl.create_default_context()
    smtp_host = str(mode["smtp_host"])
    smtp_port = int(mode["smtp_port"])
    smtp_username = str(mode["smtp_username"])
    smtp_password = str(mode["smtp_password"])
    use_tls = bool(mode["use_tls"])
    use_ssl = bool(mode["use_ssl"])
    smtp_timeout = int(mode["smtp_timeout"])

    if use_ssl:
        smtp_connection = smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=smtp_timeout)
    else:
        smtp_connection = smtplib.SMTP(smtp_host, smtp_port, timeout=smtp_timeout)

    with smtp_connection as smtp:
        if use_tls and not use_ssl:
            smtp.ehlo()
            smtp.starttls(context=context)
            smtp.ehlo()
        else:
            smtp.ehlo()
        if smtp_username:
            smtp.login(smtp_username, smtp_password)
        smtp.send_message(message, from_addr=from_address, to_addrs=recipients)


def send_email_message(
    settings: object,
    recipients: list[str],
    subject: str,
    html_body: str,
    text_body: str = "",
    attachments: Iterable[EmailAttachment] = (),
) -> dict[str, object]:
    """Send a single outbound email to a recipient list via SMTP BCC."""
    if not recipients:
        raise ValueError("At least one recipient is required")

    attachment_list = list(attachments)
    preferred_from_address = _from_address(settings)
    smtp_username = str(getattr(settings, "smtp_username", "") or "").strip()
    message, from_address = build_email_message(
        settings,
        recipients,
        subject,
        html_body,
        text_body,
        attachment_list,
        sender_address=preferred_from_address,
        reply_to_address=preferred_from_address,
    )

    logger.info(
        "SMTP send requested subject=%s recipients=%s from_address=%s auth_user=%s attachments=%s",
        subject,
        len(recipients),
        from_address,
        smtp_username or "<none>",
        len(attachment_list),
    )

    attempts: list[dict[str, Any]] = []
    last_error = ""
    fallback_from_address = smtp_username if smtp_username and smtp_username != from_address else None
    for mode in _smtp_mode_candidates(settings):
        meta = _smtp_attempt_metadata(mode)
        logger.info(
            "SMTP send attempting label=%s host=%s port=%s transport=%s auth_user=%s from_address=%s",
            meta["label"],
            meta["smtp_host"],
            meta["smtp_port"],
            meta["transport"],
            meta["auth_user"],
            from_address,
        )
        try:
            _send_email_via_mode(mode, message, recipients, from_address)
        except Exception as exc:
            last_error = describe_smtp_error(exc)
            attempts.append(
                {
                    "label": meta["label"],
                    "smtp_host": meta["smtp_host"],
                    "smtp_port": meta["smtp_port"],
                    "use_tls": meta["use_tls"],
                    "use_ssl": meta["use_ssl"],
                    "status": "failed",
                    "error": last_error,
                }
            )
            if _smtp_sender_not_authorized(exc) and fallback_from_address:
                logger.warning(
                    "SMTP send sender rejected label=%s host=%s "
                    "port=%s transport=%s auth_user=%s "
                    "from_address=%s error=%s; retrying with smtp_username",
                    meta["label"],
                    meta["smtp_host"],
                    meta["smtp_port"],
                    meta["transport"],
                    meta["auth_user"],
                    from_address,
                    last_error,
                )
                fallback_message, fallback_from = build_email_message(
                    settings,
                    recipients,
                    subject,
                    html_body,
                    text_body,
                    attachment_list,
                    sender_address=fallback_from_address,
                    reply_to_address=from_address,
                )
                try:
                    _send_email_via_mode(mode, fallback_message, recipients, fallback_from)
                except Exception as fallback_exc:
                    last_error = describe_smtp_error(fallback_exc)
                    attempts.append(
                        {
                            "label": meta["label"],
                            "smtp_host": meta["smtp_host"],
                            "smtp_port": meta["smtp_port"],
                            "use_tls": meta["use_tls"],
                            "use_ssl": meta["use_ssl"],
                            "status": "failed",
                            "error": last_error,
                            "sender_fallback": True,
                        }
                    )
                    logger.warning(
                        "SMTP send fallback failed label=%s host=%s "
                        "port=%s transport=%s auth_user=%s "
                        "from_address=%s error=%s",
                        meta["label"],
                        meta["smtp_host"],
                        meta["smtp_port"],
                        meta["transport"],
                        meta["auth_user"],
                        fallback_from,
                        last_error,
                    )
                    continue

                attempts.append(
                    {
                        "label": meta["label"],
                        "smtp_host": meta["smtp_host"],
                        "smtp_port": meta["smtp_port"],
                        "use_tls": meta["use_tls"],
                        "use_ssl": meta["use_ssl"],
                        "status": "ok",
                        "sender_fallback": True,
                    }
                )
                logger.info(
                    "SMTP send succeeded via smtp_username label=%s host=%s "
                    "port=%s transport=%s auth_user=%s from_address=%s",
                    meta["label"],
                    meta["smtp_host"],
                    meta["smtp_port"],
                    meta["transport"],
                    meta["auth_user"],
                    fallback_from,
                )
                return {
                    "from_address": fallback_from,
                    "recipient_count": len(recipients),
                    "attachment_count": len(attachment_list),
                    "subject": subject,
                    "authenticated_as": smtp_username or None,
                    "selected_mode": {
                        "label": meta["label"],
                        "smtp_host": meta["smtp_host"],
                        "smtp_port": meta["smtp_port"],
                        "use_tls": meta["use_tls"],
                        "use_ssl": meta["use_ssl"],
                    },
                    "attempts": attempts,
                }
            logger.warning(
                "SMTP send failed label=%s host=%s port=%s transport=%s auth_user=%s from_address=%s error=%s",
                meta["label"],
                meta["smtp_host"],
                meta["smtp_port"],
                meta["transport"],
                meta["auth_user"],
                from_address,
                last_error,
            )
            continue

        attempts.append(
            {
                "label": meta["label"],
                "smtp_host": meta["smtp_host"],
                "smtp_port": meta["smtp_port"],
                "use_tls": meta["use_tls"],
                "use_ssl": meta["use_ssl"],
                "status": "ok",
            }
        )
        logger.info(
            "SMTP send succeeded label=%s host=%s port=%s transport=%s auth_user=%s from_address=%s",
            meta["label"],
            meta["smtp_host"],
            meta["smtp_port"],
            meta["transport"],
            meta["auth_user"],
            from_address,
        )
        return {
            "from_address": from_address,
            "recipient_count": len(recipients),
            "attachment_count": len(attachment_list),
            "subject": subject,
            "authenticated_as": smtp_username or None,
            "selected_mode": {
                "label": meta["label"],
                "smtp_host": meta["smtp_host"],
                "smtp_port": meta["smtp_port"],
                "use_tls": meta["use_tls"],
                "use_ssl": meta["use_ssl"],
            },
            "attempts": attempts,
        }

    raise RuntimeError(last_error or "SMTP delivery failed")
