# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
Email Management Skill

Two transport paths — whichever is configured is used automatically:

  Path A — Gmail API (preferred):
      Run /connect google auth gmail in Telegram to authorize.
      Token stored at ~/.familiar/data/google_token_gmail.json
      Advantages: OAuth-based (no app password), more reliable, labels,
      thread awareness, mark-as-read on fetch.

  Path B — IMAP/SMTP (fallback, any provider):
      EMAIL_ADDRESS: your-email@gmail.com
      EMAIL_PASSWORD: app-specific-password
      EMAIL_IMAP_SERVER: imap.gmail.com       (default)
      EMAIL_SMTP_SERVER: smtp.gmail.com       (default)

If google_token_gmail.json is present AND google-api-python-client is installed,
Path A is used. Otherwise Path B is used transparently.
"""

import base64
import email
import imaplib
import logging
import os
import re
import smtplib
from datetime import datetime, timedelta
from email.header import decode_header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from pathlib import Path

logger = logging.getLogger(__name__)

IMAP_TIMEOUT = 20
SMTP_TIMEOUT = 20


def _imap_connect(cfg: dict, timeout: int = IMAP_TIMEOUT):
    """Create an IMAP connection using the right transport for the port.

    Port 993 → IMAP4_SSL (implicit TLS, standard providers).
    Port 1143 / 143 / other → IMAP4 + STARTTLS (Proton Bridge, plain IMAP).
    """
    server = cfg["imap_server"]
    port = cfg["imap_port"]
    if port == 993:
        return imaplib.IMAP4_SSL(server, port, timeout=timeout)
    mail = imaplib.IMAP4(server, port, timeout=timeout)
    mail.starttls()
    return mail


def _data_dir() -> Path:
    try:
        from familiar.core.paths import DATA_DIR

        return DATA_DIR
    except ImportError:
        return Path.home() / ".familiar" / "data"


GMAIL_TOKEN_FILE = _data_dir() / "google_token_gmail.json"

GMAIL_SCOPES = [
    "https://www.googleapis.com/auth/gmail.readonly",
    "https://www.googleapis.com/auth/gmail.send",
    "https://www.googleapis.com/auth/gmail.modify",
]


def _imap_config(account: str | None = None) -> dict:
    from .accounts import get_account

    acct = get_account(account)
    if acct:
        return {
            "address": acct["address"],
            "password": acct["password"],
            "imap_server": acct["imap_server"],
            "smtp_server": acct["smtp_server"],
            "imap_port": acct["imap_port"],
            "smtp_port": acct["smtp_port"],
        }
    # Legacy fallback to env vars
    return {
        "address": os.environ.get("EMAIL_ADDRESS", ""),
        "password": os.environ.get("EMAIL_PASSWORD", ""),
        "imap_server": os.environ.get("EMAIL_IMAP_SERVER", "imap.gmail.com"),
        "smtp_server": os.environ.get("EMAIL_SMTP_SERVER", "smtp.gmail.com"),
        "imap_port": int(os.environ.get("EMAIL_IMAP_PORT", "993")),
        "smtp_port": int(os.environ.get("EMAIL_SMTP_PORT", "587")),
    }


# keep old name for callers in triage / meetings that import get_config
get_config = _imap_config


# ══════════════════════════════════════════════════════════════════════════════
# PATH A — Gmail API
# ══════════════════════════════════════════════════════════════════════════════


def _gmail_available() -> bool:
    if not GMAIL_TOKEN_FILE.exists():
        return False
    try:
        from googleapiclient.discovery import build  # noqa
        from google.oauth2.credentials import Credentials  # noqa

        return True
    except ImportError:
        return False


def _get_gmail_service():
    from google.auth.transport.requests import Request
    from google.oauth2.credentials import Credentials
    from googleapiclient.discovery import build

    creds = Credentials.from_authorized_user_file(str(GMAIL_TOKEN_FILE), GMAIL_SCOPES)
    if not creds.valid:
        if creds.expired and creds.refresh_token:
            try:
                creds.refresh(Request())
            except Exception as e:
                logger.warning("Gmail token refresh failed: %s — removing stale token", e)
                GMAIL_TOKEN_FILE.unlink(missing_ok=True)
                raise RuntimeError(
                    "Gmail token expired or revoked. "
                    "Run /connect google auth gmail to re-authorize."
                ) from e
            GMAIL_TOKEN_FILE.write_text(creds.to_json())
        else:
            raise RuntimeError(
                "Gmail token expired. Run /connect google auth gmail to re-authorize."
            )
    return build("gmail", "v1", credentials=creds)


def _gmail_header(payload: dict, name: str) -> str:
    for h in payload.get("headers", []):
        if h["name"].lower() == name.lower():
            return h["value"]
    return ""


def _gmail_body(msg: dict) -> str:
    payload = msg.get("payload", {})

    def _extract(p):
        mime = p.get("mimeType", "")
        if mime == "text/plain":
            data = p.get("body", {}).get("data", "")
            if data:
                return base64.urlsafe_b64decode(data + "==").decode("utf-8", errors="replace")
        for part in p.get("parts", []):
            result = _extract(part)
            if result:
                return result
        if mime == "text/html":
            import re

            data = p.get("body", {}).get("data", "")
            if data:
                html = base64.urlsafe_b64decode(data + "==").decode("utf-8", errors="replace")
                # Remove style/script blocks entirely (content + tags)
                html = re.sub(r"<style[^>]*>.*?</style>", "", html, flags=re.DOTALL | re.IGNORECASE)
                html = re.sub(r"<script[^>]*>.*?</script>", "", html, flags=re.DOTALL | re.IGNORECASE)
                # Remove HTML comments
                html = re.sub(r"<!--.*?-->", "", html, flags=re.DOTALL)
                # Strip remaining tags
                text = re.sub(r"<[^<]+?>", "", html)
                # Collapse whitespace runs (from removed CSS/JS) into single newlines
                text = re.sub(r"\n\s*\n+", "\n\n", text)
                return text.strip()
        return ""

    return _extract(payload).strip()


def _gmail_check_inbox(data: dict) -> str:
    limit = data.get("limit", 10)
    unread_only = data.get("unread_only", True)
    days_back = data.get("days_back", 7)

    svc = _get_gmail_service()
    q_parts = []
    if unread_only:
        q_parts.append("is:unread")
    if days_back:
        since = (datetime.now() - timedelta(days=days_back)).strftime("%Y/%m/%d")
        q_parts.append(f"after:{since}")

    res = svc.users().messages().list(userId="me", q=" ".join(q_parts), maxResults=limit).execute()
    msgs = res.get("messages", [])
    if not msgs:
        return "📭 No unread emails" if unread_only else "📭 No emails found"

    priority_kw = ["urgent", "deadline", "asap", "grant", "donor", "board", "important"]
    results = []
    for m in msgs:
        full = (
            svc.users()
            .messages()
            .get(userId="me", id=m["id"], format="metadata", metadataHeaders=["From", "Subject"])
            .execute()
        )
        payload = full.get("payload", {})
        subject = _gmail_header(payload, "Subject") or "(no subject)"
        sender = _gmail_header(payload, "From")
        name = sender.split("<")[0].strip().strip('"') if "<" in sender else sender.split("@")[0]
        marker = "🔴 " if any(kw in subject.lower() for kw in priority_kw) else ""
        results.append(f"{marker}From: {name}\n   Subject: {subject}")

    return (
        f"📬 {len(results)} {'unread ' if unread_only else ''}emails (Gmail API):\n\n"
        + "\n\n".join(results)
    )


def _gmail_read_email(data: dict) -> str:
    search_term = data.get("search", "")
    from_filter = data.get("from", "")
    if not search_term and not from_filter:
        return "Please provide a search term or sender."

    svc = _get_gmail_service()
    q_parts = []
    if search_term:
        q_parts.append(f"subject:{search_term}")
    if from_filter:
        q_parts.append(f"from:{from_filter}")

    res = svc.users().messages().list(userId="me", q=" ".join(q_parts), maxResults=5).execute()
    msgs = res.get("messages", [])
    if not msgs:
        return "No emails found matching your criteria."

    full = svc.users().messages().get(userId="me", id=msgs[0]["id"], format="full").execute()
    payload = full.get("payload", {})
    subject = _gmail_header(payload, "Subject")
    sender = _gmail_header(payload, "From")
    date = _gmail_header(payload, "Date")
    body = _gmail_body(full)
    if len(body) > 3000:
        body = body[:3000] + "\n... (truncated)"

    # Mark as read
    svc.users().messages().modify(
        userId="me", id=msgs[0]["id"], body={"removeLabelIds": ["UNREAD"]}
    ).execute()

    return f"📧 Email Details:\n\nFrom: {sender}\nDate: {date}\nSubject: {subject}\n\n---\n{body}\n"


def _gmail_read_email_by_id(message_id: str) -> str:
    """
    Read a specific Gmail message by its API message ID.

    Used by Phase 4 triage session context to resolve '#3' references
    without re-searching the inbox. message_id is the value stored in
    session_context['triage_emails'][n]['id'] when id_source=='gmail'.
    """
    try:
        svc = _get_gmail_service()
        full = svc.users().messages().get(userId="me", id=message_id, format="full").execute()
        payload = full.get("payload", {})
        subject = _gmail_header(payload, "Subject") or "(no subject)"
        sender = _gmail_header(payload, "From") or ""
        date = _gmail_header(payload, "Date") or ""
        body = _gmail_body(full)
        if len(body) > 3000:
            body = body[:3000] + "\n... (truncated)"
        # Mark as read
        svc.users().messages().modify(
            userId="me", id=message_id, body={"removeLabelIds": ["UNREAD"]}
        ).execute()
        return f"📧 Email Details:\n\nFrom: {sender}\nDate: {date}\nSubject: {subject}\n\n---\n{body}\n"
    except Exception as e:
        logger.error(f"Could not fetch Gmail message {message_id}: {e}")
        return "❌ Could not fetch that email. It may have been deleted or moved."


def _imap_read_email_by_id(imap_uid: str) -> str:
    """
    Read a specific email by IMAP sequence number / UID.

    Used by Phase 4 triage session context to resolve '#3' references
    when id_source=='imap'. imap_uid is the string stored in
    session_context['triage_emails'][n]['id'].
    """
    config = _imap_config()
    if not config["address"] or not config["password"]:
        return "❌ IMAP not configured."
    _pw = config["password"]
    try:
        import email as _email

        mail = _imap_connect(config, timeout=IMAP_TIMEOUT)
        mail.login(config["address"], _pw)
        _pw = None
        mail.select("INBOX")
        uid_bytes = imap_uid.encode() if isinstance(imap_uid, str) else imap_uid
        status, msg_data = mail.fetch(uid_bytes, "(RFC822)")
        mail.logout()
        if status != "OK":
            return f"❌ Could not fetch IMAP message {imap_uid}"
        raw = msg_data[0][1]
        msg = _email.message_from_bytes(raw)
        subject = msg.get("Subject", "(no subject)")
        sender = msg.get("From", "")
        date = msg.get("Date", "")
        body = ""
        if msg.is_multipart():
            for part in msg.walk():
                if part.get_content_type() == "text/plain":
                    try:
                        body = part.get_payload(decode=True).decode("utf-8", errors="replace")
                        break
                    except Exception as e:
                        logger.debug("Error suppressed: %s", e)
        else:
            try:
                body = msg.get_payload(decode=True).decode("utf-8", errors="replace")
            except Exception as e:
                logger.debug("Error suppressed: %s", e)
        if len(body) > 3000:
            body = body[:3000] + "\n... (truncated)"
        return f"📧 Email Details:\n\nFrom: {sender}\nDate: {date}\nSubject: {subject}\n\n---\n{body}\n"
    except Exception as e:
        logger.error(f"Could not fetch IMAP message {imap_uid}: {e}")
        return "❌ Could not fetch that email. Check IMAP connection and try again."


def _gmail_search_emails(data: dict) -> str:
    query = data.get("query", "")
    from_filter = data.get("from", "")
    days_back = data.get("days_back", 30)
    limit = data.get("limit", 10)

    svc = _get_gmail_service()
    q_parts = []
    if query:
        q_parts.append(query)
    if from_filter:
        q_parts.append(f"from:{from_filter}")
    if days_back:
        since = (datetime.now() - timedelta(days=days_back)).strftime("%Y/%m/%d")
        q_parts.append(f"after:{since}")

    res = svc.users().messages().list(userId="me", q=" ".join(q_parts), maxResults=limit).execute()
    msgs = res.get("messages", [])
    if not msgs:
        return "No emails found."

    results = []
    for m in msgs:
        full = (
            svc.users()
            .messages()
            .get(userId="me", id=m["id"], format="metadata", metadataHeaders=["From", "Subject"])
            .execute()
        )
        payload = full.get("payload", {})
        subject = _gmail_header(payload, "Subject") or "(no subject)"
        sender = _gmail_header(payload, "From")
        name = sender.split("<")[0].strip().strip('"') if "<" in sender else sender.split("@")[0]
        results.append(f"• {name}: {subject[:60]}")

    return f"🔍 Found {len(results)} emails:\n\n" + "\n".join(results)


def _gmail_send_email(data: dict) -> str:
    to = data.get("to", "")
    subject = data.get("subject", "")
    body = data.get("body", "")
    if not to or not body:
        return "Please provide 'to' and 'body'."
    if not data.get("_confirmed"):
        from familiar.core.confirmations import email_preview, needs_confirmation

        return needs_confirmation("send_email", data, email_preview(to, subject, body), risk="high")

    svc = _get_gmail_service()
    msg = MIMEMultipart()
    msg["To"] = to
    msg["Subject"] = subject
    msg.attach(MIMEText(body, "plain"))
    raw = base64.urlsafe_b64encode(msg.as_bytes()).decode()

    # Phase 4: thread reply into the original conversation if thread_id provided
    send_body: dict = {"raw": raw}
    _thread_id = data.get("_gmail_thread_id")
    if _thread_id:
        send_body["threadId"] = _thread_id

    svc.users().messages().send(userId="me", body=send_body).execute()
    thread_note = " (threaded reply)" if _thread_id else ""
    return f"✅ Email sent to {to} (Gmail API){thread_note}"


# ══════════════════════════════════════════════════════════════════════════════
# PATH B — IMAP/SMTP
# ══════════════════════════════════════════════════════════════════════════════


def decode_mime_header(header):
    if header is None:
        return ""
    parts = decode_header(header)
    result = []
    for part, enc in parts:
        if isinstance(part, bytes):
            result.append(part.decode(enc or "utf-8", errors="replace"))
        else:
            result.append(part)
    return "".join(result)


def get_email_body(msg):
    body = ""
    if msg.is_multipart():
        for part in msg.walk():
            ct = part.get_content_type()
            if ct == "text/plain":
                try:
                    body = part.get_payload(decode=True).decode("utf-8", errors="replace")
                    break
                except (UnicodeDecodeError, AttributeError, TypeError) as e:
                    logger.debug("Unicode decode error suppressed: %s", e)
            elif ct == "text/html" and not body:
                try:
                    import re

                    html = part.get_payload(decode=True).decode("utf-8", errors="replace")
                    body = re.sub("<[^<]+?>", "", html)
                except (UnicodeDecodeError, AttributeError, TypeError) as e:
                    logger.debug("Unicode decode error suppressed: %s", e)
    else:
        try:
            body = msg.get_payload(decode=True).decode("utf-8", errors="replace")
        except (UnicodeDecodeError, AttributeError, TypeError):
            body = str(msg.get_payload())
    return body.strip()


def _imap_check_inbox(data: dict, account: str | None = None) -> str:
    cfg = _imap_config(account)
    if not cfg["address"] or not cfg["password"]:
        return "❌ Email not configured. Set EMAIL_ADDRESS and EMAIL_PASSWORD."
    limit = min(data.get("limit", 10), 15)  # Cap at 15 to avoid flooding
    unread_only = data.get("unread_only", True)
    days_back = min(data.get("days_back", 3), 14)  # Cap at 14 days
    _pw = cfg["password"]
    try:
        mail = _imap_connect(cfg, timeout=IMAP_TIMEOUT)
        mail.login(cfg["address"], _pw)
        _pw = None
        mail.select("INBOX")
        criteria = []
        if unread_only:
            criteria.append("UNSEEN")
        if days_back:
            since = (datetime.now() - timedelta(days=days_back)).strftime("%d-%b-%Y")
            criteria.append(f'SINCE "{since}"')
        status, messages = mail.search(None, " ".join(criteria) if criteria else "ALL")
        if status != "OK":
            return "Failed to search inbox"
        ids = messages[0].split()
        total_unread = len(ids)
        if not ids:
            return "📭 No unread emails" if unread_only else "📭 No emails found"
        ids = ids[-limit:]
        ids.reverse()
        priority_kw = ["urgent", "deadline", "asap", "grant", "donor", "board", "important"]
        results = []
        for eid in ids:
            # Fetch headers only — not full RFC822 — much faster on large inboxes
            status, msg_data = mail.fetch(eid, "(BODY[HEADER.FIELDS (FROM SUBJECT DATE)])")
            if status != "OK":
                continue
            msg = email.message_from_bytes(msg_data[0][1])
            subject = decode_mime_header(msg["Subject"])
            sender = decode_mime_header(msg["From"])
            name = (
                sender.split("<")[0].strip().strip('"') if "<" in sender else sender.split("@")[0]
            )
            marker = "🔴 " if any(kw in subject.lower() for kw in priority_kw) else ""
            results.append(f"{marker}From: {name}\n   Subject: {subject}")
        mail.logout()
        acct_label = f" ({cfg['address']})" if account else ""
        header = f"📬 {len(results)} {'unread ' if unread_only else ''}emails{acct_label}"
        if total_unread > limit:
            header += f" (showing {len(results)} of {total_unread})"
        return header + ":\n\n" + "\n\n".join(results)
    except imaplib.IMAP4.error:
        return "❌ Email connection failed. Check IMAP settings."
    except Exception as e:
        logger.error(f"check_inbox failed: {e}")
        return "❌ Failed to check email."


def _imap_read_email(data: dict, account: str | None = None) -> str:
    cfg = _imap_config(account)
    if not cfg["address"] or not cfg["password"]:
        return "❌ Email not configured."
    search_term = data.get("search", "")
    from_filter = data.get("from", "")
    if not search_term and not from_filter:
        return "Please provide a search term or sender."
    _pw = cfg["password"]
    try:
        mail = _imap_connect(cfg, timeout=IMAP_TIMEOUT)
        mail.login(cfg["address"], _pw)
        _pw = None
        mail.select("INBOX")
        criteria = []
        if search_term:
            # Sanitize: strip quotes and backslashes to prevent IMAP criteria injection
            _safe_search = search_term.replace('"', "").replace("\\", "")
            criteria.append(f'SUBJECT "{_safe_search}"')
        if from_filter:
            _safe_from = from_filter.replace('"', "").replace("\\", "")
            criteria.append(f'FROM "{_safe_from}"')
        status, messages = mail.search(None, " ".join(criteria))
        if status != "OK" or not messages[0]:
            return "No emails found matching your criteria."
        eid = messages[0].split()[-1]
        status, msg_data = mail.fetch(eid, "(RFC822)")
        if status != "OK":
            return "Failed to fetch email."
        msg = email.message_from_bytes(msg_data[0][1])
        subject = decode_mime_header(msg["Subject"])
        sender = decode_mime_header(msg["From"])
        date = msg["Date"]
        body = get_email_body(msg)
        if len(body) > 3000:
            body = body[:3000] + "\n... (truncated)"
        mail.logout()
        return f"📧 Email Details:\n\nFrom: {sender}\nDate: {date}\nSubject: {subject}\n\n---\n{body}\n"
    except Exception as e:
        logger.error(f"read_email failed: {e}")
        return "❌ Email operation failed."


def _imap_search_emails(data: dict, account: str | None = None) -> str:
    cfg = _imap_config(account)
    if not cfg["address"] or not cfg["password"]:
        return "❌ Email not configured."
    query = data.get("query", "")
    from_filter = data.get("from", "")
    days_back = data.get("days_back", 30)
    limit = data.get("limit", 10)
    _pw = cfg["password"]
    try:
        mail = _imap_connect(cfg, timeout=IMAP_TIMEOUT)
        mail.login(cfg["address"], _pw)
        _pw = None
        mail.select("INBOX")
        criteria = []
        if query:
            criteria.append(f'OR SUBJECT "{query}" BODY "{query}"')
        if from_filter:
            criteria.append(f'FROM "{from_filter}"')
        if days_back:
            since = (datetime.now() - timedelta(days=days_back)).strftime("%d-%b-%Y")
            criteria.append(f'SINCE "{since}"')
        status, messages = mail.search(None, " ".join(criteria) if criteria else "ALL")
        if status != "OK" or not messages[0]:
            return "No emails found."
        ids = messages[0].split()[-limit:]
        ids.reverse()
        results = []
        for eid in ids:
            status, msg_data = mail.fetch(eid, "(BODY[HEADER.FIELDS (FROM SUBJECT DATE)])")
            if status != "OK":
                continue
            hdata = msg_data[0][1].decode("utf-8", errors="replace")
            msg = email.message_from_string(hdata)
            subject = decode_mime_header(msg["Subject"]) or "(no subject)"
            sender = decode_mime_header(msg["From"]) or "Unknown"
            name = sender.split("<")[0].strip().strip('"') if "<" in sender else sender
            results.append(f"• {name}: {subject[:60]}")
        mail.logout()
        return f"🔍 Found {len(results)} emails:\n\n" + "\n".join(results)
    except Exception as e:
        logger.error(f"search_emails failed: {e}")
        return "❌ Email search failed."


def _smtp_send_email(data: dict, account: str | None = None) -> str:
    cfg = _imap_config(account)
    if not cfg["address"] or not cfg["password"]:
        return "❌ Email not configured."
    to = data.get("to", "")
    subject = data.get("subject", "")
    body = data.get("body", "")
    if not to or not body:
        return "Please provide 'to' and 'body'."
    if not data.get("_confirmed"):
        from familiar.core.confirmations import email_preview, needs_confirmation

        return needs_confirmation("send_email", data, email_preview(to, subject, body), risk="high")
    try:
        msg = MIMEMultipart()
        msg["From"] = cfg["address"]
        msg["To"] = to
        msg["Subject"] = subject
        msg.attach(MIMEText(body, "plain"))
        _pw = cfg["password"]
        with smtplib.SMTP(cfg["smtp_server"], cfg["smtp_port"], timeout=SMTP_TIMEOUT) as server:
            server.starttls()
            server.login(cfg["address"], _pw)
            _pw = None
            server.send_message(msg)
        return f"✅ Email sent to {to}"
    except Exception as e:
        logger.error(f"send failed: {e}")
        return "❌ Failed to send email."


# ══════════════════════════════════════════════════════════════════════════════
# PUBLIC DISPATCH
# ══════════════════════════════════════════════════════════════════════════════


def _extract_summary(result: str) -> str:
    """Parse the first line of a check_inbox result to get a short count."""
    first_line = result.split("\n")[0]
    if "No unread" in first_line or "No emails" in first_line:
        return "0 unread"
    m = re.search(r"(\d+)\s+(?:unread\s+)?emails?", first_line)
    if m:
        return f"{m.group(1)} unread"
    if "connection failed" in first_line.lower():
        return "error"
    return "checked"


def _check_all_inboxes(data: dict) -> str:
    """Fan-out check_inbox across all configured accounts."""
    from familiar.core.confirmations import EMAIL_MENU_PREFIX

    from .accounts import get_all_accounts

    per_account = {}  # account_key -> result_text
    summaries = []  # [(account_key, display_label, summary_str)]
    gmail_handled = False

    # Gmail API first (if available)
    if _gmail_available():
        try:
            result = _gmail_check_inbox(data)
            per_account["gmail_api"] = result
            summaries.append(("gmail_api", "Gmail (API)", _extract_summary(result)))
            gmail_handled = True
        except Exception as e:
            logger.warning(f"Gmail API check_inbox failed: {e}")

    for acct in get_all_accounts():
        # Skip Gmail IMAP if already handled via API
        if gmail_handled and acct.get("provider") == "gmail":
            continue
        try:
            result = _imap_check_inbox(data, account=acct["id"])
            per_account[acct["id"]] = result
            label = f"{acct.get('provider', 'custom').capitalize()} ({acct['address']})"
            summaries.append((acct["id"], label, _extract_summary(result)))
        except Exception as e:
            logger.warning(f"check_inbox failed for {acct['id']}: {e}")
            err = f"❌ {acct['address']}: connection failed"
            per_account[acct["id"]] = err
            summaries.append((acct["id"], acct["address"], "error"))

    if not per_account:
        return "❌ No email accounts configured. Run /connect email to set up."

    # Single account → return plain text (unchanged behavior)
    if len(per_account) == 1:
        return next(iter(per_account.values()))

    # Multi-account → cache results and return sentinel
    ctx = data.get("_context") or {}
    session = ctx.get("session")
    if session is None:
        return "\n\n".join(per_account.values())  # fallback

    import secrets as _sec

    token = _sec.token_hex(8)
    session.session_context["email_menu"] = {
        "token": token,
        "results": per_account,
        "summaries": summaries,
    }
    session_mgr = ctx.get("session_manager")
    if session_mgr:
        session_mgr.save_session(session)

    return f"{EMAIL_MENU_PREFIX}{token}"


def check_inbox(data: dict) -> str:
    account = data.get("account")
    if account is None:
        return _check_all_inboxes(data)
    if account.lower() == "gmail" and _gmail_available():
        try:
            return _gmail_check_inbox(data)
        except Exception as e:
            logger.warning(f"Gmail API check_inbox failed, falling back: {e}")
    return _imap_check_inbox(data, account=account)


def _resolve_email_ref(data: dict):
    """
    If data contains 'index' (int or '#N' string), resolve it against
    session_context["triage_emails"] and return the matching email entry dict,
    or None if not found / context absent.
    """
    raw_idx = data.get("index") or data.get("ref") or data.get("number")
    if raw_idx is None:
        return None
    # Accept "#3", "3", 3
    if isinstance(raw_idx, str):
        raw_idx = raw_idx.lstrip("#").strip()
    try:
        idx = int(raw_idx)
    except (ValueError, TypeError):
        return None

    ctx = data.get("_context") or {}
    session = ctx.get("session")
    if session is None:
        return None

    emails = session.session_context.get("triage_emails", [])
    for em in emails:
        if em.get("index") == idx:
            return em
    return None


def _resolve_email_stale(data: dict) -> bool:
    """Return True if the triage context is older than 4 hours."""
    import datetime as _dt4e

    ctx = data.get("_context") or {}
    session = ctx.get("session")
    if session is None:
        return False
    triage_at = session.session_context.get("triage_at", "")
    if not triage_at:
        return False
    try:
        age = _dt4e.datetime.now(_dt4e.timezone.utc) - _dt4e.datetime.fromisoformat(triage_at)
        return age.total_seconds() > 14400
    except Exception:
        return False


def read_email(data: dict) -> str:
    """
    Read a specific email.

    Phase 4: Accepts index='#3' or index=3 to look up emails from the last
    triage run without re-searching the inbox.
    """
    em = _resolve_email_ref(data)
    if em is not None:
        if _resolve_email_stale(data):
            return (
                "The triage list is more than 4 hours old. "
                "Run triage_inbox to refresh, then try again."
            )
        if em.get("id_source") == "gmail":
            return _gmail_read_email_by_id(em["id"])
        elif em.get("id_source") == "imap":
            return _imap_read_email_by_id(em["id"])

    # Account routing
    account = data.get("account")
    if account and account.lower() == "gmail" and _gmail_available():
        try:
            return _gmail_read_email(data)
        except Exception as e:
            logger.warning(f"Gmail API read_email failed, falling back: {e}")
    elif account:
        return _imap_read_email(data, account=account)

    # Default: prefer Gmail API, fall back to IMAP
    if _gmail_available():
        try:
            return _gmail_read_email(data)
        except Exception as e:
            logger.warning(f"Gmail API read_email failed, falling back: {e}")
    return _imap_read_email(data)


def _search_all_accounts(data: dict) -> str:
    """Fan-out search across all configured accounts."""
    from .accounts import get_all_accounts

    results = []
    gmail_handled = False

    if _gmail_available():
        try:
            results.append(_gmail_search_emails(data))
            gmail_handled = True
        except Exception as e:
            logger.warning(f"Gmail API search failed: {e}")

    for acct in get_all_accounts():
        if gmail_handled and acct.get("provider") == "gmail":
            continue
        try:
            result = _imap_search_emails(data, account=acct["id"])
            results.append(result)
        except Exception as e:
            logger.warning(f"search failed for {acct['id']}: {e}")

    if not results:
        return "❌ No email accounts configured."
    return "\n\n".join(results)


def search_emails(data: dict) -> str:
    account = data.get("account")
    if account is None:
        return _search_all_accounts(data)
    if account.lower() == "gmail" and _gmail_available():
        try:
            return _gmail_search_emails(data)
        except Exception as e:
            logger.warning(f"Gmail API search_emails failed, falling back: {e}")
    return _imap_search_emails(data, account=account)


def draft_email(data: dict) -> str:
    to = data.get("to", "")
    subject = data.get("subject", "")
    body = data.get("body", "")
    account = data.get("account")
    if not to or not body:
        return "Please provide 'to' and 'body'."
    if account and account.lower() != "gmail":
        transport = f"SMTP ({account})"
    elif _gmail_available():
        transport = "Gmail API"
    else:
        transport = "SMTP"
    return (
        f"📝 Draft Email ({transport}):\n\nTo: {to}\nSubject: {subject}\n\n"
        f"---\n{body}\n---\n\n"
        "⚠️ This is a DRAFT. Use send_email or reply_to_email to send — you'll see a confirmation before it goes."
    )


def send_email(data: dict) -> str:
    account = data.get("account")
    if account and account.lower() != "gmail":
        return _smtp_send_email(data, account=account)
    if _gmail_available() and (account is None or account.lower() == "gmail"):
        try:
            return _gmail_send_email(data)
        except Exception as e:
            logger.warning(f"Gmail API send failed, falling back: {e}")
    return _smtp_send_email(data, account=account)


def reply_to_email(data: dict) -> str:
    """
    Draft or send a reply to an email from the last triage run.

    Resolves #N index to pre-populate To, Subject, and (for Gmail) thread_id.
    Requires body. Calls send_email after populating context.
    """
    em = _resolve_email_ref(data)
    if em is None:
        return (
            "Please specify which email to reply to (e.g. index=3 or '#3'), "
            "or use send_email with explicit to/subject."
        )
    if _resolve_email_stale(data):
        return (
            "The triage list is more than 4 hours old. Run triage_inbox to refresh before replying."
        )

    body = data.get("body", "")
    if not body:
        return "Please provide the reply body."

    # Build pre-populated send_email call
    reply_data = dict(data)
    reply_data["to"] = em.get("from_email", "")
    _subj = em.get("subject", "")
    if _subj.lower().startswith("re: "):
        _subj = _subj[4:]
    reply_data["subject"] = data.get("subject") or ("Re: " + _subj)
    reply_data["body"] = body

    # For Gmail, pass the thread_id so the reply threads correctly
    if em.get("id_source") == "gmail":
        reply_data["_gmail_thread_id"] = em.get("thread_id") or em.get("id", "")

    return send_email(reply_data)


def email_status(data: dict) -> str:
    """Report which email transport is active."""
    from .accounts import get_all_accounts

    lines = ["📧 Email Transport Status", ""]
    if _gmail_available():
        lines.append("✅ Gmail API (OAuth)")
        lines.append(f"   Token: {GMAIL_TOKEN_FILE}")
    else:
        lines.append("⬜ Gmail API: not connected (run /connect google auth gmail)")

    accounts = get_all_accounts()
    if accounts:
        lines.append("")
        lines.append(f"📋 {len(accounts)} IMAP/SMTP account(s):")
        for acct in accounts:
            provider = acct.get("provider", "custom").capitalize()
            lines.append(f"  ✅ {provider}: {acct['address']} (id: {acct['id']})")
    else:
        lines.append("⬜ IMAP/SMTP: not configured (set EMAIL_ADDRESS + EMAIL_PASSWORD)")

    if not _gmail_available() and not accounts:
        lines.append("\n❌ No email transport configured.")
        lines.append("   Run /connect google auth gmail  OR  /connect email")
    return "\n".join(lines)


# ══════════════════════════════════════════════════════════════════════════════
# TOOL DEFINITIONS
# ══════════════════════════════════════════════════════════════════════════════

TOOLS = [
    {
        "name": "check_inbox",
        "description": (
            "Check email inbox — summary of recent/unread emails. "
            "Uses Gmail API if connected, IMAP otherwise. "
            "Omit account to check ALL configured accounts. "
            "Flags urgent/grant/donor items."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "limit": {"type": "integer", "description": "Max emails to return", "default": 10},
                "unread_only": {"type": "boolean", "description": "Only unread", "default": True},
                "days_back": {"type": "integer", "description": "Days to look back", "default": 7},
                "account": {
                    "type": "string",
                    "description": "Account to check: 'gmail', 'yahoo', 'proton', etc. Omit to check all.",
                },
            },
        },
        "handler": check_inbox,
        "category": "email",
    },
    {
        "name": "read_email",
        "description": (
            "Read full content of a specific email. "
            "After a triage run, use index='#3' (or index=3) to read by triage number. "
            "Also accepts subject search or from filter. "
            "Gmail API path marks the email as read automatically."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "index": {"type": "string", "description": "Triage index e.g. '#3' or '3'"},
                "search": {"type": "string", "description": "Subject search term"},
                "from": {"type": "string", "description": "Filter by sender"},
                "account": {
                    "type": "string",
                    "description": "Account to read from: 'gmail', 'yahoo', etc. Omit for default.",
                },
            },
        },
        "handler": read_email,
        "category": "email",
    },
    {
        "name": "search_emails",
        "description": (
            "Search email inbox by keyword, sender, or date range. "
            "Use this when the user wants to find specific emails. "
            "Omit account to search all configured accounts. "
            "Returns a list of matching subjects and senders."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "Search term"},
                "from": {"type": "string", "description": "Filter by sender"},
                "days_back": {"type": "integer", "description": "Days to look back", "default": 30},
                "limit": {"type": "integer", "description": "Max results", "default": 10},
                "account": {
                    "type": "string",
                    "description": "Account to search: 'gmail', 'yahoo', etc. Omit to search all.",
                },
            },
        },
        "handler": search_emails,
        "category": "email",
    },
    {
        "name": "draft_email",
        "description": (
            "Draft an email for review before sending. "
            "Use this when the user wants to compose an email without sending it immediately. "
            "Specify account to control which provider sends it. "
            "Returns the draft text for review; use send_email to actually send."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "to": {"type": "string", "description": "Recipient email"},
                "subject": {"type": "string", "description": "Subject line"},
                "body": {"type": "string", "description": "Body text"},
                "account": {
                    "type": "string",
                    "description": "Account to send from: 'gmail', 'yahoo', etc. Omit for primary.",
                },
            },
            "required": ["to", "body"],
        },
        "handler": draft_email,
        "category": "email",
    },
    {
        "name": "send_email",
        "description": (
            "Send an email to a recipient. "
            "Use this when the user explicitly asks to send an email. "
            "Will ask for confirmation before sending. "
            "Specify account to send from a specific provider; omit for primary."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "to": {"type": "string", "description": "Recipient email"},
                "subject": {"type": "string", "description": "Subject line"},
                "body": {"type": "string", "description": "Body text"},
                "account": {
                    "type": "string",
                    "description": "Account to send from: 'gmail', 'yahoo', etc. Omit for primary.",
                },
            },
            "required": ["to", "body"],
        },
        "handler": send_email,
        "category": "email",
        "requires_confirmation": True,
    },
    {
        "name": "reply_to_email",
        "description": (
            "Reply to an email from the last triage run. "
            "Provide index='#3' (triage number) and body. "
            "Automatically fills To, Subject, and Gmail thread ID. "
            "Will ask for confirmation before sending."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "index": {"type": "string", "description": "Triage index e.g. '#3' or '3'"},
                "body": {"type": "string", "description": "Reply text"},
                "subject": {"type": "string", "description": "Override subject (optional)"},
                "account": {
                    "type": "string",
                    "description": "Account to reply from: 'gmail', 'yahoo', etc. Omit for primary.",
                },
            },
            "required": ["index", "body"],
        },
        "handler": reply_to_email,
        "category": "email",
        "requires_confirmation": True,
    },
    {
        "name": "email_status",
        "description": (
            "Show which email transport is active (Gmail API vs IMAP/SMTP). "
            "Use this to diagnose email connectivity or check configured accounts. "
            "Returns status for each configured transport path."
        ),
        "input_schema": {"type": "object", "properties": {}},
        "handler": email_status,
        "category": "email",
    },
]
