"""
Report email sender — config-driven SMTP via the Python standard library.

Connection settings come from environment variables so the mail backend
(internal relay / Microsoft 365 / Gmail / SendGrid) can be swapped without code
changes — see the build plan: terra-mechanics.com is on M365 and has a smarthost
relay that already sends the seismograph alerts as remote@terra-mechanics.com;
reuse that relay's settings here.

DRY-RUN: if SMTP isn't configured (no host/from), the message is built and
logged but NOT sent, and the call still succeeds.  This keeps report generation
working before the relay is wired up, and means a missing/incomplete mail config
can never crash the nightly pipeline.

Env vars
--------
  REPORT_SMTP_HOST        e.g. smtp.office365.com   (unset → dry-run)
  REPORT_SMTP_PORT        default 587
  REPORT_SMTP_SECURITY    starttls (default) | ssl | none
  REPORT_SMTP_USER        optional — omit for IP-authenticated relays
  REPORT_SMTP_PASSWORD    optional
  REPORT_SMTP_FROM        e.g. "TMI Monitoring <monitoring@terra-mechanics.com>"
  REPORT_SMTP_RECIPIENTS  comma-separated default recipient list
  REPORT_SMTP_TIMEOUT     seconds, default 30
"""

from __future__ import annotations

import logging
import os
import smtplib
import ssl
from dataclasses import dataclass, field
from email.message import EmailMessage
from typing import Optional

logger = logging.getLogger(__name__)

# Convenient MIME type for the Excel attachment.
XLSX_MIME = ("application", "vnd.openxmlformats-officedocument.spreadsheetml.sheet")


@dataclass
class Attachment:
    filename: str
    content: bytes
    maintype: str = "application"
    subtype: str = "octet-stream"


@dataclass
class SMTPConfig:
    host: str = ""
    port: int = 587
    security: str = "starttls"   # "starttls" | "ssl" | "none"
    user: str = ""
    password: str = ""
    sender: str = ""
    recipients: list[str] = field(default_factory=list)
    timeout: float = 30.0

    @classmethod
    def from_env(cls) -> "SMTPConfig":
        rec = os.getenv("REPORT_SMTP_RECIPIENTS", "")
        return cls(
            host=os.getenv("REPORT_SMTP_HOST", "").strip(),
            port=int(os.getenv("REPORT_SMTP_PORT", "587") or 587),
            security=os.getenv("REPORT_SMTP_SECURITY", "starttls").strip().lower(),
            user=os.getenv("REPORT_SMTP_USER", "").strip(),
            password=os.getenv("REPORT_SMTP_PASSWORD", ""),
            sender=os.getenv("REPORT_SMTP_FROM", "").strip(),
            recipients=[r.strip() for r in rec.split(",") if r.strip()],
            timeout=float(os.getenv("REPORT_SMTP_TIMEOUT", "30") or 30),
        )

    @property
    def configured(self) -> bool:
        """True only when we have enough to actually send (host + from)."""
        return bool(self.host and self.sender)


def build_message(
    cfg: SMTPConfig,
    subject: str,
    html_body: str,
    recipients: list[str],
    attachments: Optional[list[Attachment]] = None,
    text_body: Optional[str] = None,
) -> EmailMessage:
    """Assemble a multipart message: plain-text fallback + HTML + attachments."""
    msg = EmailMessage()
    msg["From"] = cfg.sender or "terra-view@localhost"
    msg["To"] = ", ".join(recipients)
    msg["Subject"] = subject
    # Plain-text part first, then the HTML alternative (clients prefer the HTML).
    msg.set_content(text_body or "This report is best viewed in an HTML email client.")
    msg.add_alternative(html_body, subtype="html")
    for att in (attachments or []):
        msg.add_attachment(
            att.content, maintype=att.maintype, subtype=att.subtype, filename=att.filename,
        )
    return msg


def send_report_email(
    subject: str,
    html_body: str,
    *,
    attachments: Optional[list[Attachment]] = None,
    recipients: Optional[list[str]] = None,
    text_body: Optional[str] = None,
    cfg: Optional[SMTPConfig] = None,
) -> dict:
    """Send (or dry-run) the report email.

    Returns a result dict: {sent, dry_run, recipients, error}.  Never raises on
    a send failure — it logs and returns error, so the orchestrator can record
    the failure without aborting the rest of the pipeline.
    """
    cfg = cfg or SMTPConfig.from_env()
    recipients = recipients if recipients is not None else cfg.recipients
    result = {"sent": False, "dry_run": False, "recipients": recipients, "error": None}

    if not recipients:
        result["error"] = "No recipients configured"
        logger.warning("Report email: no recipients set; skipping send of %r", subject)
        return result

    msg = build_message(cfg, subject, html_body, recipients, attachments, text_body)

    if not cfg.configured:
        result["dry_run"] = True
        logger.info(
            "Report email DRY-RUN (SMTP not configured): would send %r to %s with %d attachment(s)",
            subject, recipients, len(attachments or []),
        )
        return result

    # Validate the security mode: an unrecognized value (typo) must NOT silently
    # fall through to a plaintext connection while still sending credentials.
    sec = cfg.security if cfg.security in ("ssl", "starttls", "none") else "starttls"
    if sec != cfg.security:
        logger.warning("Unknown REPORT_SMTP_SECURITY=%r — falling back to 'starttls'", cfg.security)

    try:
        if sec == "ssl":
            ctx = ssl.create_default_context()
            with smtplib.SMTP_SSL(cfg.host, cfg.port, timeout=cfg.timeout, context=ctx) as s:
                if cfg.user:
                    s.login(cfg.user, cfg.password)
                s.send_message(msg)
        else:
            with smtplib.SMTP(cfg.host, cfg.port, timeout=cfg.timeout) as s:
                s.ehlo()
                if sec == "starttls":
                    s.starttls(context=ssl.create_default_context())
                    s.ehlo()
                if cfg.user:
                    if sec == "none":
                        logger.warning(
                            "Sending SMTP credentials over an UNENCRYPTED connection "
                            "(REPORT_SMTP_SECURITY=none) — set starttls/ssl if the relay supports it."
                        )
                    s.login(cfg.user, cfg.password)
                s.send_message(msg)
        result["sent"] = True
        logger.info("Report email sent: %r to %s", subject, recipients)
    except Exception as e:  # noqa: BLE001 — surface as result, never abort the pipeline
        result["error"] = str(e)
        logger.error("Report email send failed: %s", e, exc_info=True)

    return result
