"""Outbound admin-invite email — a thin wrapper over the existing SMTP layer.

v0.17.0 / roadmap item #16: when an admin uses `POST /api/admin/users` to
create-with-invite, this module composes and sends the invite envelope.

Structurally distinct from:

  * `email_otc.py` (v0.7.0) — that one carries a credential the user
    just requested; this one carries a credential the admin is sending
    unsolicited.
  * `email.py` (§15.4 notification mailer) — that one is inbox-driven,
    bundled, with category opt-outs; this one is a single transactional
    outbound to a person who does not yet have an inbox.
  * v0.9.0's `new_beta_request` admin notification — that one is
    invitee-to-admin (an existing pending user asking to be let in);
    this one is admin-to-invitee (an admin reaching out to seed access).

So this module reuses `EmailConfig.from_env()` for the SMTP plumbing
and the From identity, but writes its own envelope. In dev (no
SMTP_HOST set), the envelope is logged at INFO level and pushed to
the same `_SENT` buffer the notification mailer uses, so the
integration tests can assert on the outbound shape without standing
up an SMTP server.

The send is synchronous. The admin endpoint returns 200 on the
create-row half regardless of send outcome — a transient SMTP
failure should not roll back the invite (an admin can re-send via a
future "resend invite" gesture, deferred to a follow-up release).
"""
from __future__ import annotations

import logging
import smtplib
from email.utils import formataddr

from .email import EmailConfig, _SENT, record_outbound
from .email_envelope import build_envelope

log = logging.getLogger(__name__)


def send_invite_email(
    *,
    to_address: str,
    claim_url: str,
    inviter_display: str,
    inviter_email: str,
    custom_message: str = "",
) -> bool:
    """Compose and send the admin-invite email. Returns True on the
    happy path; False on SMTP failure. The notifier-side buffer
    `_SENT` is appended either way so tests can assert on content.

    The body names the inviting admin, embeds the optional custom
    message in a clearly delimited block if present, and ships the
    claim link. The subject names the inviter so the recipient can
    recognize the sender at a glance in their inbox preview.
    """
    cfg = EmailConfig.from_env()
    subject = _subject(inviter_display, cfg)
    body = _body(claim_url, inviter_display, inviter_email, custom_message, cfg)
    # v0.18.0: invite mail carries a `List-Unsubscribe: <mailto:…>`
    # only (no signed URL) — the invitee isn't a user yet, so there
    # is no per-user opt-out row to flip. The operator handles
    # ad-hoc opt-outs from the mailto: target. Per the proposal's
    # "Tradeoff discussion": the invite was unsolicited from the
    # recipient's perspective, so the courtesy header is right;
    # but it can't be a one-click URL because the row doesn't
    # exist yet.
    msg = build_envelope(
        to_address=to_address,
        from_address=cfg.from_address,
        from_name=cfg.from_name,
        subject=subject,
        body_plain=body,
        unsubscribe_mailto=cfg.unsubscribe_mailto,
    )
    envelope = {
        "to": to_address,
        "from": formataddr((cfg.from_name, cfg.from_address)),
        "subject": subject,
        "body": body,
        "kind": "invite",
        "message": msg,
    }
    _SENT.append(envelope)

    message_id = msg["Message-ID"]
    if not cfg.enabled:
        log.info("invite email disabled (EMAIL_ENABLED=0): to=%s", to_address)
        record_outbound(
            to_address=to_address, from_address=cfg.from_address,
            subject=subject, kind="invite", status="deferred", message_id=message_id,
        )
        return True
    if not cfg.smtp_host:
        # Dev fallback: surface the claim URL at INFO so the operator can
        # complete a claim flow without an SMTP relay. In production
        # SMTP_HOST is always set per OHM's overlay.
        log.info("invite email (stdout fallback): to=%s claim_url=%s", to_address, claim_url)
        record_outbound(
            to_address=to_address, from_address=cfg.from_address,
            subject=subject, kind="invite", status="deferred", message_id=message_id,
        )
        return True

    try:
        smtp = smtplib.SMTP(cfg.smtp_host, cfg.smtp_port, timeout=30)
        try:
            if cfg.smtp_starttls:
                smtp.starttls()
            if cfg.smtp_user:
                smtp.login(cfg.smtp_user, cfg.smtp_password)
            smtp.send_message(msg)
        finally:
            smtp.quit()
        record_outbound(
            to_address=to_address, from_address=cfg.from_address,
            subject=subject, kind="invite", status="sent", message_id=message_id,
        )
        return True
    except Exception as exc:
        log.exception("invite email send failed: to=%s", to_address)
        record_outbound(
            to_address=to_address, from_address=cfg.from_address,
            subject=subject, kind="invite", status="failed",
            error=f"{type(exc).__name__}: {exc}", message_id=message_id,
        )
        return False


def _subject(inviter_display: str, cfg: EmailConfig) -> str:
    """e.g. "You're invited to Wiggleverse by Ben Stull"."""
    inviter = inviter_display or "an admin"
    return f"You're invited to {cfg.from_name} by {inviter}"


def _body(
    claim_url: str,
    inviter_display: str,
    inviter_email: str,
    custom_message: str,
    cfg: EmailConfig,
) -> str:
    inviter = inviter_display or "An admin"
    inviter_suffix = f" ({inviter_email})" if inviter_email else ""
    message_block = ""
    if custom_message.strip():
        # Indent the custom message so it reads as a clearly-delimited
        # quote rather than running together with the framework's
        # framing text. Per-line indent keeps multi-line messages
        # visually grouped in plain-text mail clients.
        indented = "\n".join(f"    {line}" for line in custom_message.strip().splitlines())
        message_block = f"\nA personal note from {inviter}:\n\n{indented}\n"

    return (
        f"{inviter}{inviter_suffix} has invited you to {cfg.from_name}.\n"
        f"{message_block}\n"
        f"Click the link below to claim your account and sign in.\n"
        f"This link is single-use and expires in 7 days.\n\n"
        f"    {claim_url}\n\n"
        f"If you weren't expecting this invitation, you can ignore this\n"
        f"email — no account becomes active until you click the link.\n\n"
        f"---\n"
        f"{cfg.from_name} · {cfg.app_url}\n"
    )
