# tools/email_tools.py — Read inbox (IMAP) and open email draft (mailto).
# Inbox: configure in Settings → Access → Email (or [email] in mugi_config.toml + Keychain or MUGI_IMAP_PASSWORD).
# Compose draft opens the default mail client with a new message; no credentials needed.
from __future__ import annotations

import email
import imaplib
import os
import ssl
import subprocess
from email.header import decode_header
from urllib.parse import quote

from langchain_core.tools import tool

from tools._logging import _debug_log, _emit_input_status

_SSL_CTX = ssl.create_default_context()
try:
    import certifi
    _SSL_CTX.load_verify_locations(certifi.where())
except ImportError:
    pass
_IMAP_TIMEOUT = 25
_IMAP_TEST_TIMEOUT = 15
_MAX_SNIPPET = 120

_IMAP_LOCALHOST = frozenset({"127.0.0.1", "::1", "localhost", "localhost."})

# Protonmail Bridge uses port 1143 (STARTTLS) and 1993 (implicit SSL).
_STARTTLS_PORTS = frozenset({143, 1143})


def _format_imap_error(exc: Exception) -> str:
    """Normalize imaplib errors so UI does not show Python bytes repr."""
    parts: list[str] = []
    for arg in getattr(exc, "args", ()) or ():
        if isinstance(arg, bytes):
            parts.append(arg.decode("utf-8", errors="replace"))
        else:
            parts.append(str(arg))
    msg = " ".join(p.strip() for p in parts if p).strip()
    return msg or str(exc)


def _imap_ssl_context(host: str) -> ssl.SSLContext:
    """Use default (verifying) context for remote hosts; permissive for localhost (Bridge, etc.)."""
    h = (host or "").strip().lower()
    if h in _IMAP_LOCALHOST:
        ctx = ssl.create_default_context()
        ctx.check_hostname = False
        ctx.verify_mode = ssl.CERT_NONE
        return ctx
    return _SSL_CTX


def _imap_connect(host: str, port: int, timeout: int) -> imaplib.IMAP4:
    """Open an IMAP connection using implicit SSL or STARTTLS depending on port."""
    ctx = _imap_ssl_context(host)
    if port in _STARTTLS_PORTS:
        conn = imaplib.IMAP4(host, port=port, timeout=timeout)
        conn.starttls(ssl_context=ctx)
    else:
        conn = imaplib.IMAP4_SSL(host, port=port, ssl_context=ctx, timeout=timeout)
    return conn


def test_imap_connection(host: str, port: int, user: str, password: str) -> tuple[bool, str]:
    """Try IMAP connect, login, logout. Returns (True, "") on success, (False, error_message) on failure."""
    host = (host or "").strip()
    user = (user or "").strip()
    password = (password or "").strip()
    if not host or not user:
        return False, "IMAP server and email/user are required."
    if not password:
        return False, "Password is required."
    try:
        conn = _imap_connect(host, port, _IMAP_TEST_TIMEOUT)
        conn.login(user, password)
        conn.logout()
        return True, ""
    except imaplib.IMAP4.error as e:
        return False, _format_imap_error(e)
    except OSError as e:
        return False, str(e)
    except Exception as e:
        return False, str(e)


def _get_email_accounts() -> tuple[list[dict], str]:
    from core.config import get_email_accounts, get_default_email_account
    accounts = get_email_accounts()
    default_account = get_default_email_account()
    default_id = str(default_account.get("id") or "").strip()
    return accounts, default_id


def _decode_mime_header(header: str) -> str:
    if not header:
        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(str(part))
    return " ".join(result).strip()


def _get_imap_password(account_id: str = "") -> str:
    """Password from account keychain entry first, then legacy/env fallback."""
    try:
        from persistence import keychain_get_imap_password, keychain_get_imap_password_for_account
        p = ""
        if account_id:
            p = (keychain_get_imap_password_for_account(account_id) or "").strip()
        if not p:
            p = (keychain_get_imap_password() or "").strip()
        if p:
            return p
    except Exception:
        pass
    return (os.environ.get("MUGI_IMAP_PASSWORD") or "").strip()


def _pick_email_account(account_id: str = "", account_label: str = "") -> tuple[dict, str]:
    accounts, default_id = _get_email_accounts()
    enabled_accounts = [a for a in accounts if bool(a.get("enabled", True))]
    account_id = (account_id or "").strip()
    account_label = (account_label or "").strip().lower()
    selected = None
    if account_id:
        selected = next((a for a in accounts if str(a.get("id") or "") == account_id), None)
        if selected is None:
            return {}, f'No email account found for account_id "{account_id}".'
    elif account_label:
        selected = next(
            (
                a
                for a in accounts
                if account_label in str(a.get("label") or "").lower()
                or account_label in str(a.get("imap_user") or "").lower()
            ),
            None,
        )
        if selected is None:
            return {}, f'No email account found for account_label "{account_label}".'
    elif default_id:
        selected = next((a for a in accounts if str(a.get("id") or "") == default_id), None)
    if selected is None and enabled_accounts:
        selected = enabled_accounts[0]
    if selected is None and accounts:
        selected = accounts[0]
    if selected is None:
        return {}, (
            "Email (IMAP) not configured. Add at least one account in Settings → Access → Email."
        )
    if not bool(selected.get("enabled", True)):
        return {}, f'Email account "{selected.get("label") or selected.get("id")}" is disabled.'
    return selected, ""


def _sanitize_imap_search_term(value: str) -> str:
    """Strip characters that would break an IMAP SEARCH quoted literal.

    IMAP's quoted syntax disallows unescaped ``"`` and ``\\``; CR/LF would
    terminate the command. We drop those rather than attempt to escape so
    callers can pass raw user input without worrying about the protocol.
    """
    if not value:
        return ""
    cleaned = value.replace("\r", " ").replace("\n", " ").replace("\\", " ").replace('"', " ").strip()
    # Keep the length sane — IMAP servers impose line-length limits.
    return cleaned[:128]


def _fetch_inbox(
    limit: int,
    unread_only: bool,
    from_contains: str = "",
    subject_contains: str = "",
    account_id: str = "",
    account_label: str = "",
) -> tuple[bool, str]:
    cfg, select_error = _pick_email_account(account_id=account_id, account_label=account_label)
    if select_error:
        return False, select_error
    host = (cfg.get("imap_host") or "").strip()
    user = (cfg.get("imap_user") or "").strip()
    port = int(cfg.get("imap_port") or 993)
    selected_id = str(cfg.get("id") or "")
    selected_label = (cfg.get("label") or user or selected_id).strip()
    password = _get_imap_password(selected_id)

    if not host or not user:
        return False, (
            f'Email account "{selected_label}" is missing IMAP host or username. '
            "Update it in Settings → Access → Email."
        )
    if not password:
        return False, (
            f'Password is not set for account "{selected_label}". '
            "Enter it in Settings → Access → Email and save."
        )

    from_q = _sanitize_imap_search_term(from_contains)
    subject_q = _sanitize_imap_search_term(subject_contains)

    try:
        conn = _imap_connect(host, port, _IMAP_TIMEOUT)
        conn.login(user, password)
        conn.select("INBOX", readonly=True)

        search_terms: list[str] = []
        if unread_only:
            search_terms.append("UNSEEN")
        if from_q:
            search_terms.extend(["FROM", f'"{from_q}"'])
        if subject_q:
            search_terms.extend(["SUBJECT", f'"{subject_q}"'])
        if not search_terms:
            search_terms.append("ALL")
        typ, data = conn.search(None, *search_terms)

        if typ != "OK" or not data or not data[0]:
            conn.logout()
            if from_q or subject_q:
                bits: list[str] = []
                if from_q:
                    bits.append(f'from "{from_q}"')
                if subject_q:
                    bits.append(f'with subject containing "{subject_q}"')
                return True, "No matching messages found (" + ", ".join(bits) + ")."
            return True, f'No messages in "{selected_label}" inbox.' if unread_only else f'"{selected_label}" inbox is empty.'

        ids = data[0].split()
        # Newest first
        ids = ids[-limit:] if len(ids) >= limit else ids
        ids.reverse()

        lines = []
        for msg_id in ids:
            typ, msg_data = conn.fetch(msg_id, "(RFC822)")
            if typ != "OK" or not msg_data:
                continue
            raw = msg_data[0]
            if isinstance(raw, tuple) and len(raw) >= 2:
                raw = raw[1]
            try:
                msg = email.message_from_bytes(raw)
            except Exception:
                continue
            subject = _decode_mime_header(msg.get("Subject", ""))
            from_ = _decode_mime_header(msg.get("From", ""))
            date_str = msg.get("Date", "")
            body_snippet = ""
            if msg.is_multipart():
                for part in msg.walk():
                    if part.get_content_maintype() == "text" and part.get_content_subtype() == "plain":
                        try:
                            payload = part.get_payload(decode=True)
                            if payload:
                                body_snippet = payload.decode("utf-8", errors="replace").strip()[:_MAX_SNIPPET]
                        except Exception:
                            pass
                        break
            else:
                try:
                    payload = msg.get_payload(decode=True)
                    if payload:
                        body_snippet = payload.decode("utf-8", errors="replace").strip()[:_MAX_SNIPPET]
                except Exception:
                    pass
            if body_snippet:
                body_snippet = body_snippet.replace("\n", " ")
            line = f"- **From:** {from_}\n  **Subject:** {subject}\n  **Date:** {date_str}"
            if body_snippet:
                line += f"\n  **Snippet:** {body_snippet}..."
            lines.append(line)

        conn.logout()
        if not lines:
            return True, "No messages in inbox." if unread_only else "Inbox is empty."
        return True, f'Inbox "{selected_label}" (most recent first):\n\n' + "\n\n".join(lines)

    except imaplib.IMAP4.error as e:
        msg = _format_imap_error(e)
        _debug_log(f"[email] IMAP error: {msg}", "DEBUG")
        return False, f"IMAP error: {msg}. Check imap_host, imap_user, and that MUGI_IMAP_PASSWORD is correct (e.g. app password for Gmail)."
    except OSError as e:
        return False, f"Connection error: {e}. Check imap_host and imap_port (e.g. 993 for SSL)."
    except Exception as e:
        _debug_log(f"[email] inbox error: {e}", "DEBUG")
        return False, f"Inbox error: {e}"


@tool
def list_inbox(
    limit: int = 20,
    unread_only: bool = True,
    from_contains: str = "",
    subject_contains: str = "",
    account_id: str = "",
    account_label: str = "",
) -> str:
    """List recent emails from the user's inbox (IMAP).

    Use when the user asks "check my email", "any new emails?", "emails from Julie", or "show receipts from this week".

    Args:
        limit: Max rows (1–50). Default 20.
        unread_only: When True (default), show only unseen messages. Pass False to include already-read mail.
        from_contains: Optional IMAP ``FROM`` substring (e.g. "julie" matches "Julie Smith <julie@…>").
        subject_contains: Optional IMAP ``SUBJECT`` substring (e.g. "receipt").
        account_id: Optional account id from Settings → Access → Email.
        account_label: Optional account label/email substring for account lookup.

    Configure accounts in Settings → Access → Email.
    Returns subject, from, date, and a short snippet for each message.
    """
    _emit_input_status("Checking inbox…")
    ok, msg = _fetch_inbox(
        limit=min(max(1, limit), 50),
        unread_only=unread_only,
        from_contains=from_contains or "",
        subject_contains=subject_contains or "",
        account_id=account_id or "",
        account_label=account_label or "",
    )
    return msg


@tool
def compose_email_draft(to: str, subject: str = "", body: str = "") -> str:
    """Open the user's default mail client with a new message (to, subject, body) so they can review and send. Use when the user says 'draft an email to X', 'compose a message to Y about Z', or 'write an email to...'. The message opens as a draft; the user sends it manually. No email credentials or send permissions required."""
    _emit_input_status("Opening mail draft…")
    to = (to or "").strip()
    if not to:
        return "Provide at least one recipient (to)."
    subject = (subject or "").strip()
    body = (body or "").strip()
    # mailto: can have multiple recipients separated by comma
    to_enc = quote(to)
    parts = [f"mailto:{to_enc}"]
    if subject:
        parts.append(f"subject={quote(subject)}")
    if body:
        parts.append(f"body={quote(body)}")
    url = "?".join(parts) if len(parts) > 1 else parts[0]
    try:
        if os.name == "posix" and os.uname().sysname == "Darwin":
            subprocess.run(["open", url], check=False, timeout=5)
        else:
            import webbrowser
            webbrowser.open(url)
    except Exception as e:
        return f"Could not open mail client: {e}. Try opening your mail app and composing manually."
    return "Opened your default mail client with a new message. Review and send when ready."
