# @File: backend/services/email_sender.py
# @Desc: SMTP helper used by the public contact / get-price / support endpoints
#        and by the admin "send test email" action. Configuration precedence:
#          1. Values saved in the ``smtp_settings`` DB row (admin CMS page).
#          2. Environment variables — SMTP_HOST, SMTP_PORT, SMTP_USERNAME,
#             SMTP_PASSWORD, SMTP_FROM_EMAIL, SMTP_USE_TLS (kept for backwards
#             compatibility with deployments that pre-date the admin UI).
#
#        The loaded config is cached in-process; callers (e.g. the admin update
#        endpoint) can invalidate the cache with ``invalidate_config_cache()``.
import asyncio
import logging
import os
import smtplib
import ssl
from email.message import EmailMessage
from typing import Iterable, Optional

logger = logging.getLogger(__name__)

# Lightweight in-process cache so we don't hit the DB for every email.
_cached_config: Optional[dict] = None


def invalidate_config_cache() -> None:
    """Drop the cached SMTP config; the next send will reload it."""
    global _cached_config
    _cached_config = None


def _env(name: str, default: Optional[str] = None) -> Optional[str]:
    """Fetch an environment variable, trimming whitespace."""
    value = os.environ.get(name, default)
    if value is None:
        return None
    value = value.strip()
    return value or None


def _parse_bool(value: Optional[str], *, default: bool = True) -> bool:
    if value is None:
        return default
    return value.strip().lower() in {"1", "true", "yes", "y", "on"}


class SmtpConfigError(RuntimeError):
    """Raised when the backend is missing SMTP configuration at send time."""


def _load_from_env() -> dict:
    """Read SMTP credentials from environment variables (legacy / fallback path)."""
    host = _env("SMTP_HOST")
    port_raw = _env("SMTP_PORT")
    username = _env("SMTP_USERNAME")
    password = _env("SMTP_PASSWORD")
    from_email = _env("SMTP_FROM_EMAIL") or username
    use_tls = _parse_bool(_env("SMTP_USE_TLS"), default=True)

    missing = [
        name
        for name, value in (
            ("SMTP_HOST", host),
            ("SMTP_PORT", port_raw),
            ("SMTP_USERNAME", username),
            ("SMTP_PASSWORD", password),
            ("SMTP_FROM_EMAIL", from_email),
        )
        if not value
    ]
    if missing:
        raise SmtpConfigError(
            "SMTP is not configured. Missing: "
            + ", ".join(missing)
            + ". Configure SMTP in the admin panel or set the matching environment variables."
        )

    try:
        port = int(port_raw)  # type: ignore[arg-type]
    except (TypeError, ValueError) as exc:
        raise SmtpConfigError(f"SMTP_PORT is not a valid integer: {port_raw!r}") from exc

    return {
        "host": host,
        "port": port,
        "username": username,
        "password": password,
        "from_email": from_email,
        "from_name": _env("SMTP_FROM_NAME"),
        "reply_to": _env("SMTP_REPLY_TO"),
        "use_tls": use_tls,
    }


async def _load_from_db() -> Optional[dict]:
    """Try to read SMTP settings from the DB. Returns ``None`` if nothing usable is stored.

    Imports are local so we don't create a circular dependency at module load
    time (routers -> services -> models -> db).
    """
    try:
        from core.database import db_manager
        from models.smtp_settings import SmtpSettings
        from sqlalchemy import select
    except Exception as exc:  # pragma: no cover - defensive
        logger.debug("DB-based SMTP load unavailable: %s", exc)
        return None

    if db_manager is None or db_manager.async_session_maker is None:
        # DB not initialized yet (e.g. during tests / early startup).
        return None

    try:
        async with db_manager.async_session_maker() as session:
            result = await session.execute(
                select(SmtpSettings).order_by(SmtpSettings.id.asc()).limit(1)
            )
            row = result.scalar_one_or_none()
    except Exception as exc:
        logger.warning("Failed to load SMTP settings from DB: %s", exc)
        return None

    if row is None:
        return None

    # Require at minimum the fields we need to actually connect and send.
    if not (row.smtp_host and row.smtp_port and row.smtp_username and row.smtp_password):
        return None

    from_email = row.sender_email or row.smtp_username
    encryption = (row.smtp_encryption or "TLS").upper()

    return {
        "host": row.smtp_host.strip(),
        "port": int(row.smtp_port),
        "username": row.smtp_username.strip(),
        "password": row.smtp_password,
        "from_email": from_email,
        "from_name": (row.sender_name or None),
        "reply_to": (row.reply_to or None),
        # Maps the dropdown value back to the flag used by smtplib.
        "use_tls": encryption in {"TLS", "SSL"},
        "encryption": encryption,
    }


async def _load_smtp_config() -> dict:
    """Return the effective SMTP config. DB wins, env is the fallback."""
    global _cached_config
    if _cached_config is not None:
        return _cached_config

    db_cfg = await _load_from_db()
    if db_cfg is not None:
        _cached_config = db_cfg
        return db_cfg

    env_cfg = _load_from_env()
    _cached_config = env_cfg
    return env_cfg


def _send_sync(cfg: dict, *, to_addrs: list, subject: str, text_body: str,
               html_body: Optional[str], reply_to: Optional[str]) -> None:
    """Blocking SMTP send — runs inside a thread executor."""
    recipients = [addr for addr in to_addrs if addr]
    if not recipients:
        raise ValueError("No recipient addresses supplied.")

    # Compose the From header with an optional display name.
    if cfg.get("from_name"):
        from_header = f"{cfg['from_name']} <{cfg['from_email']}>"
    else:
        from_header = cfg["from_email"]

    msg = EmailMessage()
    msg["Subject"] = subject
    msg["From"] = from_header
    msg["To"] = ", ".join(recipients)
    effective_reply_to = reply_to or cfg.get("reply_to")
    if effective_reply_to:
        msg["Reply-To"] = effective_reply_to
    msg.set_content(text_body)
    if html_body:
        msg.add_alternative(html_body, subtype="html")

    context = ssl.create_default_context()
    encryption = (cfg.get("encryption") or ("SSL" if cfg["port"] == 465 else ("TLS" if cfg.get("use_tls") else "NONE"))).upper()

    if encryption == "SSL" or cfg["port"] == 465:
        with smtplib.SMTP_SSL(cfg["host"], cfg["port"], context=context, timeout=20) as smtp:
            smtp.login(cfg["username"], cfg["password"])
            smtp.send_message(msg, from_addr=cfg["from_email"], to_addrs=recipients)
    else:
        with smtplib.SMTP(cfg["host"], cfg["port"], timeout=20) as smtp:
            smtp.ehlo()
            if encryption == "TLS":
                smtp.starttls(context=context)
                smtp.ehlo()
            smtp.login(cfg["username"], cfg["password"])
            smtp.send_message(msg, from_addr=cfg["from_email"], to_addrs=recipients)


async def send_email(
    *,
    to_addrs: Iterable[str],
    subject: str,
    text_body: str,
    html_body: Optional[str] = None,
    reply_to: Optional[str] = None,
) -> None:
    """Async wrapper around the blocking smtplib send.

    The event loop is never blocked by the SMTP round-trip because the
    network call is executed inside the default thread executor.
    """
    cfg = await _load_smtp_config()
    loop = asyncio.get_running_loop()
    recipients_list = list(to_addrs)
    await loop.run_in_executor(
        None,
        lambda: _send_sync(
            cfg,
            to_addrs=recipients_list,
            subject=subject,
            text_body=text_body,
            html_body=html_body,
            reply_to=reply_to,
        ),
    )
    logger.info("SMTP send OK: subject=%r to=%s", subject, ", ".join(recipients_list))