"""发邮件(host-side,DESIGN §8.5 投递第 2/3 层)。

- `send_email_smtp(...)`:纯函数,读 SMTP_* env 发信。SendEmailTool 与定时任务的
  确定性兜底投递(core/scheduler.py notify)共用它。
- `smtp_configured()`:agent_builder 据此决定挂不挂 tool(沿用"有 key 才注册"范式,
  §3.4);没配 SMTP 的部署里 agent 看不到一个永远报错的工具。
- `SendEmailTool`:agent 在(定时或交互)run 里调,附件路径强制落 user_root 内防越界。

密钥只在 host 进程读,绝不进沙箱 / run_python。env:
  SMTP_HOST  SMTP_PORT(默 465)  SMTP_USER  SMTP_PASSWORD
  SMTP_FROM(默 SMTP_USER)  SMTP_FROM_NAME(发件人显示名,默"总院科研辅助智能体")
  SMTP_TLS(ssl|starttls|none;默按端口:465→ssl 否则 starttls)
"""
from __future__ import annotations

import os
import smtplib
from email.message import EmailMessage
from email.utils import formataddr
from pathlib import Path
from typing import Iterable, Optional

from .base import FileOutOfBounds, Tool

_MAX_ATTACH_BYTES = 20 * 1024 * 1024   # 单封附件总上限,防把大产物塞爆 SMTP
_MAX_RECIPIENTS = 10


def smtp_configured() -> bool:
    """最小可发信集合是否齐全。"""
    return bool(
        os.getenv("SMTP_HOST", "").strip()
        and os.getenv("SMTP_USER", "").strip()
        and os.getenv("SMTP_PASSWORD", "").strip()
    )


def _tls_mode(port: int) -> str:
    mode = os.getenv("SMTP_TLS", "").strip().lower()
    if mode in ("ssl", "starttls", "none"):
        return mode
    return "ssl" if port == 465 else "starttls"


def send_email_smtp(
    to: Iterable[str] | str,
    subject: str,
    body: str,
    attachments: Optional[Iterable[Path]] = None,
    *,
    timeout: float = 30.0,
) -> None:
    """同步发一封纯文本邮件(可带附件)。失败抛异常,由调用方决定如何处理。

    host-side 调用(SendEmailTool 在 to_thread 的 run 线程里;scheduler 在
    run_in_executor 里)—— smtplib 是阻塞 IO,不要在 asyncio loop 直接 await。
    """
    if not smtp_configured():
        raise RuntimeError("SMTP 未配置(需 SMTP_HOST/SMTP_USER/SMTP_PASSWORD)")

    host = os.getenv("SMTP_HOST", "").strip()
    port = int(os.getenv("SMTP_PORT", "465").strip() or "465")
    user = os.getenv("SMTP_USER", "").strip()
    password = os.getenv("SMTP_PASSWORD", "").strip()
    sender = os.getenv("SMTP_FROM", "").strip() or user
    from_name = os.getenv("SMTP_FROM_NAME", "").strip() or "总院科研辅助智能体"

    if isinstance(to, str):
        to_list = [to]
    else:
        to_list = list(to)
    to_list = [a.strip() for a in to_list if a and a.strip()]
    if not to_list:
        raise ValueError("收件人为空")
    if len(to_list) > _MAX_RECIPIENTS:
        raise ValueError(f"收件人过多(上限 {_MAX_RECIPIENTS})")

    msg = EmailMessage()
    msg["From"] = formataddr((from_name, sender))
    msg["To"] = ", ".join(to_list)
    msg["Subject"] = subject or "(无主题)"
    msg.set_content(body or "")

    total = 0
    for p in attachments or []:
        p = Path(p)
        if not p.is_file():
            continue
        data = p.read_bytes()
        total += len(data)
        if total > _MAX_ATTACH_BYTES:
            raise ValueError(f"附件总大小超过 {_MAX_ATTACH_BYTES // (1024*1024)}MB")
        msg.add_attachment(
            data, maintype="application", subtype="octet-stream", filename=p.name
        )

    tls = _tls_mode(port)
    if tls == "ssl":
        with smtplib.SMTP_SSL(host, port, timeout=timeout) as smtp:
            smtp.login(user, password)
            smtp.send_message(msg)
    else:
        with smtplib.SMTP(host, port, timeout=timeout) as smtp:
            if tls == "starttls":
                smtp.starttls()
            smtp.login(user, password)
            smtp.send_message(msg)


class SendEmailTool(Tool):
    name = "send_email"
    description = (
        "Send an email (plain text, optional file attachments) via the server's configured "
        "SMTP account. Use this when the user asks to email a result/report to someone, or when "
        "a scheduled task's instruction says to email its output. Attachments are paths inside "
        "the working directory (e.g. 'report.docx' or '<wd>/figures/x.png'). Returns a short "
        "confirmation or an [Error] line."
    )
    parameters = {
        "type": "object",
        "properties": {
            "to": {
                "type": "array",
                "items": {"type": "string"},
                "description": "Recipient email address(es).",
            },
            "subject": {"type": "string", "description": "Email subject line."},
            "body": {"type": "string", "description": "Plain-text email body."},
            "attachments": {
                "type": "array",
                "items": {"type": "string"},
                "description": "Optional file paths (relative to working dir) to attach.",
            },
        },
        "required": ["to", "subject", "body"],
    }

    def execute(
        self,
        to: list[str] | str,
        subject: str,
        body: str,
        attachments: Optional[list[str]] = None,
    ) -> str:
        if isinstance(to, str):
            to = [to]
        recipients = [a.strip() for a in (to or []) if isinstance(a, str) and a.strip()]
        if not recipients:
            return "[Error] to 不能为空"

        resolved: list[Path] = []
        for raw in attachments or []:
            if not isinstance(raw, str) or not raw.strip():
                continue
            # host-side 解析:容器 /workspace 路径翻回宿主 + 强制落 user_root 内(防越界)
            try:
                p = self._resolve_user_file(raw.strip())
            except FileOutOfBounds:
                return f"[Error] 附件路径越界(必须在工作目录内): {raw}"
            if not p.is_file():
                return f"[Error] 附件不存在: {raw}"
            resolved.append(p)

        try:
            send_email_smtp(recipients, subject, body, resolved)
        except Exception as e:
            return f"[Error] 发送失败: {type(e).__name__}: {e}"
        n = f"(含 {len(resolved)} 个附件)" if resolved else ""
        return f"[ok] 已发送给 {', '.join(recipients)} {n}".strip()
