# 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 Triage Skill - Themis, Titan of Order

Themis was the Titan of divine law, order, and custom. She weighs each
matter with her scales and assigns it to its proper place.

Sort incoming emails into organizational categories.
Learns sender categorizations over time.

Categories:
- Campaign
- Events
- Operations
- Staff
- Board
- Coalitions
- Donors
- Media
- Other
"""

import email
import imaplib
import json
import logging
import os
import re
from datetime import datetime, timedelta
from email.header import decode_header
from pathlib import Path

logger = logging.getLogger(__name__)


IMAP_TIMEOUT = 20  # seconds — prevent hung IMAP connections


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


# Category configuration - customize for your org
CATEGORIES = [
    "Campaign",
    "Trans Pride",
    "GJL Operations",
    "Staff",
    "Board",
    "Coalitions",
    "Donors",
    "Grants",
    "Media",
    "Volunteers",
    "Other",
]

# Keyword hints for auto-categorization (lowercase)
CATEGORY_KEYWORDS = {
    "Campaign": [
        "campaign",
        "action alert",
        "advocacy",
        "petition",
        "lobby",
        "legislative",
        "bill",
        "vote",
        "call to action",
        "mobilize",
        "rally",
        "protest",
        "testimony",
        "policy",
        "ordinance",
        "rights",
        "discrimination",
        "equality",
        "justice",
    ],
    "Trans Pride": [
        "trans pride",
        "pride event",
        "pride parade",
        "pride festival",
        "pride volunteer",
        "pride planning",
        "march",
        "celebration",
        "pride seattle",
        "pride committee",
    ],
    "GJL Operations": [
        "operations",
        "admin",
        "office",
        "facilities",
        "supplies",
        "invoice",
        "vendor",
        "contract",
        "lease",
        "utilities",
        "meeting room",
        "calendar",
        "schedule",
    ],
    "Staff": [
        "staff meeting",
        "team",
        "pto",
        "time off",
        "schedule",
        "1:1",
        "one on one",
        "hr",
        "payroll",
        "benefits",
        "all staff",
        "staff retreat",
        "performance",
    ],
    "Board": [
        "board meeting",
        "board member",
        "governance",
        "bylaws",
        "board agenda",
        "board minutes",
        "fiduciary",
        "treasurer",
        "board vote",
        "executive committee",
        "board packet",
    ],
    "Coalitions": [
        "coalition",
        "partner",
        "alliance",
        "collaborative",
        "joint",
        "allied",
        "solidarity",
        "collective",
        "lgbtq",
        "community partner",
        "co-sign",
        "sign-on",
    ],
    "Donors": [
        "donation",
        "gift",
        "contribute",
        "fundrais",
        "donor",
        "pledge",
        "matching",
        "thank you for your support",
        "tax receipt",
        "giving",
        "supporter",
    ],
    "Grants": [
        "grant",
        "foundation",
        "funder",
        "proposal",
        "application",
        "report due",
        "grant report",
        "funding",
        "regranting",
    ],
    "Media": [
        "press",
        "media",
        "reporter",
        "journalist",
        "interview",
        "statement",
        "quote",
        "coverage",
        "article",
        "news",
        "press release",
        "media inquiry",
        "comment",
    ],
    "Volunteers": ["volunteer", "volunteering", "sign up", "shift", "help needed", "opportunity"],
}

# Storage for learned sender mappings
TRIAGE_DATA_FILE = Path.home() / ".familiar" / "data" / "email_triage.json"


def _load_triage_data() -> dict:
    """Load learned sender mappings."""
    if TRIAGE_DATA_FILE.exists():
        try:
            with open(TRIAGE_DATA_FILE, encoding="utf-8") as f:
                return json.load(f)
        except (json.JSONDecodeError, IOError, OSError) as e:
            logger.warning(f"Failed to load triage data: {e}")
    return {
        "sender_categories": {},  # email -> category
        "domain_categories": {},  # domain -> category
    }


def _save_triage_data(data: dict):
    """Save learned mappings."""
    TRIAGE_DATA_FILE.parent.mkdir(parents=True, exist_ok=True)
    with open(TRIAGE_DATA_FILE, "w") as f:
        json.dump(data, f, indent=2)


def _get_email_config(account: str | None = None):
    """Get email configuration, preferring the account registry."""
    try:
        from familiar.skills.email.accounts import get_account

        acct = get_account(account)
        if acct:
            return {
                "address": acct["address"],
                "password": acct["password"],
                "imap_server": acct["imap_server"],
                "imap_port": acct["imap_port"],
            }
    except ImportError:
        pass
    return {
        "address": os.environ.get("EMAIL_ADDRESS", ""),
        "password": os.environ.get("EMAIL_PASSWORD", ""),
        "imap_server": os.environ.get("EMAIL_IMAP_SERVER", "imap.gmail.com"),
        "imap_port": int(os.environ.get("EMAIL_IMAP_PORT", "993")),
    }


# ── Gmail API helpers (mirrors email/skill.py) ─────────────────────────────


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

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


def _gmail_available() -> bool:
    token = _gmail_token_file()
    if not token.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

    token = _gmail_token_file()
    scopes = [
        "https://www.googleapis.com/auth/gmail.readonly",
        "https://www.googleapis.com/auth/gmail.modify",
    ]
    creds = Credentials.from_authorized_user_file(str(token), scopes)
    if not creds.valid:
        if creds.expired and creds.refresh_token:
            creds.refresh(Request())
            token.write_text(creds.to_json())
        else:
            raise RuntimeError("Gmail token expired. Run /connect google auth gmail.")
    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 _decode_header(header):
    """Decode email header."""
    if header is None:
        return ""
    decoded = decode_header(header)
    result = []
    for part, encoding in decoded:
        if isinstance(part, bytes):
            result.append(part.decode(encoding or "utf-8", errors="replace"))
        else:
            result.append(part)
    return "".join(result)


def _extract_email_address(from_header: str) -> str:
    """Extract just the email address from From header."""
    match = re.search(r"<([^>]+)>", from_header)
    if match:
        return match.group(1).lower()
    # No angle brackets, might just be the address
    if "@" in from_header:
        return from_header.strip().lower()
    return from_header.lower()


def _extract_sender_name(from_header: str) -> str:
    """Extract display name from From header."""
    if "<" in from_header:
        name = from_header.split("<")[0].strip().strip('"').strip("'")
        if name:
            return name
    return from_header.split("@")[0]


def _get_domain(email_addr: str) -> str:
    """Get domain from email address."""
    if "@" in email_addr:
        return email_addr.split("@")[1].lower()
    return ""


def _categorize_email(
    sender_email: str, sender_name: str, subject: str, body_preview: str = ""
) -> tuple[str, float]:
    """
    Categorize an email based on sender and content.
    Returns (category, confidence) where confidence is 0-1.
    """
    triage_data = _load_triage_data()

    sender_email = sender_email.lower()
    domain = _get_domain(sender_email)
    subject_lower = subject.lower()
    content = f"{subject_lower} {body_preview.lower()}"

    # 1. Check if we've seen this exact sender before (highest confidence)
    if sender_email in triage_data.get("sender_categories", {}):
        return triage_data["sender_categories"][sender_email], 0.95

    # 2. Check domain mapping
    if domain in triage_data.get("domain_categories", {}):
        return triage_data["domain_categories"][domain], 0.85

    # 3. Keyword matching
    best_category = "Other"
    best_score = 0

    for category, keywords in CATEGORY_KEYWORDS.items():
        score = 0
        for keyword in keywords:
            if keyword in content:
                score += 1
            if keyword in sender_name.lower():
                score += 0.5

        if score > best_score:
            best_score = score
            best_category = category

    confidence = min(0.7, best_score * 0.15) if best_score > 0 else 0.1

    return best_category, confidence


def _get_priority(subject: str, sender_category: str) -> str:
    """Determine email priority."""
    subject_lower = subject.lower()

    # Urgent indicators
    urgent_keywords = [
        "urgent",
        "asap",
        "immediate",
        "deadline today",
        "action required",
        "time sensitive",
    ]
    if any(kw in subject_lower for kw in urgent_keywords):
        return "urgent"

    # Board emails often need attention
    if sender_category == "Board":
        return "action"

    # Questions/requests
    if "?" in subject or any(w in subject_lower for w in ["request", "please", "need", "can you"]):
        return "action"

    return "fyi"


def _gmail_triage_inbox(data: dict) -> str:
    """Gmail API path for triage — uses same categorization logic as IMAP path."""
    import base64
    from datetime import datetime
    from datetime import timedelta as td

    limit = data.get("limit", 25)
    unread_only = data.get("unread_only", True)
    days_back = data.get("days_back", 3)

    svc = _get_gmail_service()
    q_parts = []
    if unread_only:
        q_parts.append("is:unread")
    if days_back:
        since = (datetime.now() - td(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 to triage."

    categorized = {cat: [] for cat in CATEGORIES}

    for m in msgs:
        try:
            full = svc.users().messages().get(userId="me", id=m["id"], format="full").execute()
            payload = full.get("payload", {})
            subject = _gmail_header(payload, "Subject") or "(no subject)"
            from_header = _gmail_header(payload, "From")
            sender_email = _extract_email_address(from_header)
            sender_name = _extract_sender_name(from_header)

            def _extract_text(p):
                if p.get("mimeType") == "text/plain":
                    raw = p.get("body", {}).get("data", "")
                    if raw:
                        return base64.urlsafe_b64decode(raw + "==").decode(
                            "utf-8", errors="replace"
                        )[:500]
                for part in p.get("parts", []):
                    r = _extract_text(part)
                    if r:
                        return r
                return ""

            body_preview = _extract_text(payload)
            category, confidence = _categorize_email(
                sender_email, sender_name, subject, body_preview
            )
            priority = _get_priority(subject, category)

            categorized[category].append(
                {
                    "id": m["id"],
                    "thread_id": full.get("threadId", m["id"]),
                    "from": sender_name,
                    "from_email": sender_email,
                    "subject": subject,
                    "priority": priority,
                    "confidence": confidence,
                }
            )
        except Exception as e:
            logger.error(f"Gmail triage msg error: {e}")
            continue

    priority_emoji = {"urgent": "🔴", "action": "🟡", "fyi": "🟢"}
    total = sum(len(v) for v in categorized.values())
    lines = [f"📬 Inbox Triage — Gmail API ({total} emails)\n"]

    # Build a flat ordered list for session_context (enables "reply to #3")
    # id_source="gmail" lets email/skill.py use the correct lookup path
    ordered_emails = []
    for category in CATEGORIES:
        emails_sorted = sorted(
            categorized[category],
            key=lambda x: {"urgent": 0, "action": 1, "fyi": 2}.get(x["priority"], 2),
        )
        for em in emails_sorted:
            ordered_emails.append(
                {
                    "index": len(ordered_emails) + 1,
                    "id": em["id"],
                    "thread_id": em.get("thread_id", em["id"]),
                    "id_source": "gmail",  # distinguishes from IMAP sequence numbers
                    "from_name": em["from"],
                    "from_email": em["from_email"],
                    "subject": em["subject"],
                    "category": category,
                    "priority": em["priority"],
                }
            )

    # Build confidence lookup by id for display
    _conf_by_id = {e["id"]: e.get("confidence", 1.0) for cat in categorized.values() for e in cat}

    for category in CATEGORIES:
        cat_emails = [e for e in ordered_emails if e["category"] == category]
        if not cat_emails:
            continue
        lines.append(f"\n━━━ {category.upper()} ({len(cat_emails)}) ━━━")
        for em in cat_emails:
            p_emoji = priority_emoji.get(em["priority"], "⚪")
            conf = "?" if _conf_by_id.get(em["id"], 1.0) < 0.5 else ""
            subj = em["subject"][:50] + "..." if len(em["subject"]) > 50 else em["subject"]
            lines.append(f"  #{em['index']} {p_emoji}{conf} {em['from_name']}")
            lines.append(f"      {subj}")

    urgent_count = sum(1 for e in ordered_emails if e["priority"] == "urgent")
    action_count = sum(1 for e in ordered_emails if e["priority"] == "action")
    if urgent_count or action_count:
        lines.append(f"\n⚡ {urgent_count} urgent, {action_count} need action")

    result_text = "\n".join(lines)

    # Phase 4: Write ordered_emails to session_context directly when available,
    # and also to thread-local as fallback for agent.py pickup.
    import datetime as _dt4t
    import threading as _threading

    _triage_payload = {"emails": ordered_emails, "total": total}
    _threading.current_thread()._familiar_triage_ctx = _triage_payload

    # Direct write path: context dict injected by tools.execute
    _ctx = data.get("_context") or {}
    _session = _ctx.get("session")
    _sessions_mgr = _ctx.get("session_manager")
    if _session is not None:
        _session.session_context["triage_emails"] = ordered_emails
        _session.session_context["triage_at"] = _dt4t.datetime.now(_dt4t.timezone.utc).isoformat()
        if _sessions_mgr is not None:
            _sessions_mgr.save_session(_session)

    return result_text


def triage_inbox(data: dict) -> str:
    """
    Fetch and categorize emails, sorted by category.
    Uses Gmail API if connected, IMAP/SMTP otherwise.
    """
    account = data.get("account")
    if _gmail_available() and (account is None or account.lower() == "gmail"):
        try:
            return _gmail_triage_inbox(data)
        except Exception as e:
            logger.warning(f"Gmail triage failed, falling back to IMAP: {e}")

    config = _get_email_config(account)

    if not config["address"] or not config["password"]:
        return (
            "❌ Email not configured. Set EMAIL_ADDRESS and EMAIL_PASSWORD environment variables."
        )

    limit = data.get("limit", 25)
    unread_only = data.get("unread_only", True)
    days_back = data.get("days_back", 3)

    try:
        mail = _imap_connect(config, timeout=IMAP_TIMEOUT)
        mail.login(config["address"], config["password"])
        mail.select("INBOX")

        # Search criteria
        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}"')

        search_str = " ".join(criteria) if criteria else "ALL"
        status, messages = mail.search(None, search_str)

        if status != "OK" or not messages[0]:
            return "📭 No emails to triage."

        email_ids = messages[0].split()[-limit:]

        # Fetch and categorize
        categorized = {cat: [] for cat in CATEGORIES}

        for eid in email_ids:
            try:
                status, msg_data = mail.fetch(eid, "(RFC822)")
                if status != "OK":
                    continue

                raw = msg_data[0][1]
                msg = email.message_from_bytes(raw)

                subject = _decode_header(msg["Subject"]) or "(no subject)"
                from_header = _decode_header(msg["From"]) or ""
                sender_email = _extract_email_address(from_header)
                sender_name = _extract_sender_name(from_header)

                # Get body preview for better categorization
                body_preview = ""
                if msg.is_multipart():
                    for part in msg.walk():
                        if part.get_content_type() == "text/plain":
                            try:
                                body_preview = part.get_payload(decode=True).decode(
                                    "utf-8", errors="replace"
                                )[:500]
                                break
                            except (UnicodeDecodeError, AttributeError, TypeError):
                                pass
                else:
                    try:
                        body_preview = msg.get_payload(decode=True).decode(
                            "utf-8", errors="replace"
                        )[:500]
                    except (UnicodeDecodeError, AttributeError, TypeError):
                        pass

                category, confidence = _categorize_email(
                    sender_email, sender_name, subject, body_preview
                )
                priority = _get_priority(subject, category)

                categorized[category].append(
                    {
                        "id": eid.decode() if isinstance(eid, bytes) else str(eid),
                        "from": sender_name,
                        "from_email": sender_email,
                        "subject": subject,
                        "priority": priority,
                        "confidence": confidence,
                    }
                )
            except Exception as e:
                logger.error(f"Error processing email: {e}")
                continue

        mail.logout()

        # Format output
        priority_emoji = {"urgent": "🔴", "action": "🟡", "fyi": "🟢"}

        lines = [f"📬 Inbox Triage ({sum(len(v) for v in categorized.values())} emails)\n"]

        # Build ordered list for session_context; id_source="imap" so resolver
        # knows these are IMAP sequence numbers, not Gmail message IDs
        ordered_emails_imap = []
        for category in CATEGORIES:
            for em in sorted(
                categorized[category],
                key=lambda x: {"urgent": 0, "action": 1, "fyi": 2}.get(x["priority"], 2),
            ):
                ordered_emails_imap.append(
                    {
                        "index": len(ordered_emails_imap) + 1,
                        "id": em["id"],
                        "id_source": "imap",
                        "from_name": em["from"],
                        "from_email": em["from_email"],
                        "subject": em["subject"],
                        "category": category,
                        "priority": em["priority"],
                    }
                )

        for category in CATEGORIES:
            cat_emails = [e for e in ordered_emails_imap if e["category"] == category]
            if not cat_emails:
                continue

            lines.append(f"\n━━━ {category.upper()} ({len(cat_emails)}) ━━━")

            _conf_imap = {
                e["id"]: categorized[category][
                    [x["id"] for x in categorized[category]].index(e["id"])
                ].get("confidence", 1.0)
                for e in cat_emails
                if e["id"] in [x["id"] for x in categorized[category]]
            }

            for em in cat_emails:
                p_emoji = priority_emoji.get(em["priority"], "⚪")
                conf = "?" if _conf_imap.get(em["id"], 1.0) < 0.5 else ""
                subj = em["subject"][:50] + "..." if len(em["subject"]) > 50 else em["subject"]
                lines.append(f"  #{em['index']} {p_emoji}{conf} {em['from_name']}")
                lines.append(f"      {subj}")

        # Summary
        urgent_count = sum(1 for e in ordered_emails_imap if e["priority"] == "urgent")
        action_count = sum(1 for e in ordered_emails_imap if e["priority"] == "action")

        if urgent_count or action_count:
            lines.append(f"\n⚡ {urgent_count} urgent, {action_count} need action")

        # Phase 4: dual write — direct + thread-local fallback
        import datetime as _dt4ti
        import threading as _threading

        _triage_imap_payload = {
            "emails": ordered_emails_imap,
            "total": sum(len(v) for v in categorized.values()),
        }
        _threading.current_thread()._familiar_triage_ctx = _triage_imap_payload

        _ictx = data.get("_context") or {}
        _isess = _ictx.get("session")
        _isess_mgr = _ictx.get("session_manager")
        if _isess is not None:
            _isess.session_context["triage_emails"] = ordered_emails_imap
            _isess.session_context["triage_at"] = _dt4ti.datetime.now(
                _dt4ti.timezone.utc
            ).isoformat()
            if _isess_mgr is not None:
                _isess_mgr.save_session(_isess)

        return "\n".join(lines)

    except Exception as e:
        logger.error(f"Triage failed: {e}")
        return "❌ Email operation failed. Check server connection."


def teach_sender(data: dict) -> str:
    """
    Teach the agent which category a sender belongs to.
    This helps improve future categorization.
    """
    sender = data.get("sender", "").lower().strip()
    category = data.get("category", "").strip()

    if not sender:
        return "Please provide the sender email address."

    # Normalize category
    category_match = None
    for cat in CATEGORIES:
        if cat.lower() == category.lower():
            category_match = cat
            break

    if not category_match:
        return f"Unknown category: {category}\nValid categories: {', '.join(CATEGORIES)}"

    triage_data = _load_triage_data()

    # Is this an email or domain?
    if "@" in sender:
        triage_data.setdefault("sender_categories", {})[sender] = category_match
        _save_triage_data(triage_data)
        return f"✅ Learned: {sender} → {category_match}"
    else:
        # Treat as domain
        domain = sender.replace("@", "").strip()
        triage_data.setdefault("domain_categories", {})[domain] = category_match
        _save_triage_data(triage_data)
        return f"✅ Learned: *@{domain} → {category_match}"


def list_learned_senders(data: dict) -> str:
    """Show all learned sender categorizations."""
    triage_data = _load_triage_data()

    senders = triage_data.get("sender_categories", {})
    domains = triage_data.get("domain_categories", {})

    if not senders and not domains:
        return "No learned categorizations yet. Use teach_sender to train me."

    lines = ["📚 Learned Categorizations:\n"]

    # Group by category
    by_category = {cat: [] for cat in CATEGORIES}

    for email_addr, cat in senders.items():
        by_category.setdefault(cat, []).append(email_addr)

    for domain, cat in domains.items():
        by_category.setdefault(cat, []).append(f"*@{domain}")

    for cat in CATEGORIES:
        items = by_category.get(cat, [])
        if items:
            lines.append(f"\n{cat}:")
            for item in sorted(items):
                lines.append(f"  • {item}")

    return "\n".join(lines)


def forget_sender(data: dict) -> str:
    """Remove a learned sender categorization."""
    sender = data.get("sender", "").lower().strip()

    if not sender:
        return "Please provide the sender to forget."

    triage_data = _load_triage_data()
    removed = False

    if sender in triage_data.get("sender_categories", {}):
        del triage_data["sender_categories"][sender]
        removed = True

    domain = sender.replace("@", "").strip()
    if domain in triage_data.get("domain_categories", {}):
        del triage_data["domain_categories"][domain]
        removed = True

    if removed:
        _save_triage_data(triage_data)
        return f"✅ Forgot categorization for {sender}"
    else:
        return f"No learned categorization found for {sender}"


def get_category_summary(data: dict) -> str:
    """Get counts by category without full triage."""
    config = _get_email_config()

    if not config["address"] or not config["password"]:
        return "❌ Email not configured."

    days_back = data.get("days_back", 7)

    try:
        mail = _imap_connect(config, timeout=IMAP_TIMEOUT)
        mail.login(config["address"], config["password"])
        mail.select("INBOX")

        since = (datetime.now() - timedelta(days=days_back)).strftime("%d-%b-%Y")
        status, messages = mail.search(None, f'UNSEEN SINCE "{since}"')

        if status != "OK" or not messages[0]:
            return "📭 No unread emails."

        email_ids = messages[0].split()

        counts = {cat: 0 for cat in CATEGORIES}
        urgent_count = 0

        for eid in email_ids:
            try:
                status, msg_data = mail.fetch(eid, "(BODY[HEADER.FIELDS (FROM SUBJECT)])")
                if status != "OK":
                    continue

                header = msg_data[0][1].decode("utf-8", errors="replace")
                msg = email.message_from_string(header)

                subject = _decode_header(msg["Subject"]) or ""
                from_header = _decode_header(msg["From"]) or ""
                sender_email = _extract_email_address(from_header)
                sender_name = _extract_sender_name(from_header)

                category, _ = _categorize_email(sender_email, sender_name, subject)
                counts[category] += 1

                if _get_priority(subject, category) == "urgent":
                    urgent_count += 1

            except (KeyError, AttributeError, TypeError) as e:
                logger.debug(f"Skipping malformed email: {e}")
                continue

        mail.logout()

        total = sum(counts.values())

        lines = [f"📊 Inbox Summary ({total} unread)\n"]

        if urgent_count:
            lines.append(f"🔴 {urgent_count} URGENT\n")

        for cat in CATEGORIES:
            if counts[cat] > 0:
                bar = "█" * min(counts[cat], 20)
                lines.append(f"  {cat}: {counts[cat]} {bar}")

        return "\n".join(lines)

    except Exception as e:
        logger.error(f"Triage operation failed: {e}")
        return "❌ Email operation failed. Check server connection."


def filter_by_category(data: dict) -> str:
    """Get emails from a specific category only."""
    config = _get_email_config()

    if not config["address"] or not config["password"]:
        return "❌ Email not configured."

    target_category = data.get("category", "").strip()

    # Match category name loosely
    matched_cat = None
    for cat in CATEGORIES:
        if cat.lower() == target_category.lower() or target_category.lower() in cat.lower():
            matched_cat = cat
            break

    if not matched_cat:
        return f"Unknown category: {target_category}\nAvailable: {', '.join(CATEGORIES)}"

    limit = data.get("limit", 20)
    days_back = data.get("days_back", 7)
    unread_only = data.get("unread_only", False)

    try:
        mail = _imap_connect(config, timeout=IMAP_TIMEOUT)
        mail.login(config["address"], config["password"])
        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}"')

        search_str = " ".join(criteria) if criteria else "ALL"
        status, messages = mail.search(None, search_str)

        if status != "OK" or not messages[0]:
            return f"📭 No {matched_cat} emails found."

        email_ids = messages[0].split()

        matching = []
        priority_emoji = {"urgent": "🔴", "action": "🟡", "fyi": "🟢"}

        for eid in reversed(email_ids):  # Most recent first
            if len(matching) >= limit:
                break

            try:
                status, msg_data = mail.fetch(eid, "(RFC822)")
                if status != "OK":
                    continue

                raw = msg_data[0][1]
                msg = email.message_from_bytes(raw)

                subject = _decode_header(msg["Subject"]) or "(no subject)"
                from_header = _decode_header(msg["From"]) or ""
                date_str = msg["Date"] or ""
                sender_email = _extract_email_address(from_header)
                sender_name = _extract_sender_name(from_header)

                # Get body preview
                body_preview = ""
                if msg.is_multipart():
                    for part in msg.walk():
                        if part.get_content_type() == "text/plain":
                            try:
                                body_preview = part.get_payload(decode=True).decode(
                                    "utf-8", errors="replace"
                                )[:300]
                                break
                            except (UnicodeDecodeError, AttributeError, TypeError):
                                pass

                category, confidence = _categorize_email(
                    sender_email, sender_name, subject, body_preview
                )

                if category == matched_cat:
                    priority = _get_priority(subject, category)
                    matching.append(
                        {
                            "from": sender_name,
                            "from_email": sender_email,
                            "subject": subject,
                            "date": date_str[:16] if date_str else "",
                            "priority": priority,
                            "confidence": confidence,
                        }
                    )
            except (KeyError, AttributeError, TypeError) as e:
                logger.debug(f"Skipping malformed email: {e}")
                continue

        mail.logout()

        if not matching:
            return f"📭 No {matched_cat} emails found in the last {days_back} days."

        lines = [f"📧 {matched_cat} ({len(matching)} emails)\n"]

        for em in matching:
            p = priority_emoji.get(em["priority"], "⚪")
            conf = " (?)" if em["confidence"] < 0.5 else ""
            subj = em["subject"][:55] + "..." if len(em["subject"]) > 55 else em["subject"]
            lines.append(f"{p} {em['from']}{conf}")
            lines.append(f"   {subj}")
            if em["date"]:
                lines.append(f"   {em['date']}")
            lines.append("")

        return "\n".join(lines)

    except Exception as e:
        logger.error(f"Triage operation failed: {e}")
        return "❌ Email operation failed. Check server connection."


# Tool definitions
TOOLS = [
    {
        "name": "triage_inbox",
        "description": (
            "Fetch and sort emails into organizational categories (Campaign, Trans Pride, GJL Operations, Staff, Board, Coalitions, Donors, Grants, Media, Volunteers) with priority flags. "
            "Uses Gmail API when connected, falling back to IMAP. "
            "Categorization combines learned sender mappings, domain rules, and keyword analysis. "
            "Results are numbered for easy reference in follow-up commands."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "limit": {"type": "integer", "description": "Max emails to process", "default": 25},
                "unread_only": {
                    "type": "boolean",
                    "default": True,
                    "description": "Only include unread emails",
                },
                "days_back": {
                    "type": "integer",
                    "description": "How many days to look back",
                    "default": 3,
                },
                "account": {
                    "type": "string",
                    "description": "Account to triage: 'gmail', 'yahoo', etc. Omit for primary.",
                },
            },
        },
        "handler": triage_inbox,
        "category": "triage",
    },
    {
        "name": "filter_emails",
        "description": (
            "Get emails from one specific category only, such as 'show me Board emails' or 'what Campaign stuff came in'. "
            "Fetches emails via IMAP, categorizes each one, and returns only those matching the target category. "
            "Supports filtering by date range and read/unread status. "
            "Results include priority flags and confidence indicators."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "category": {
                    "type": "string",
                    "description": "Category to filter: Campaign, Trans Pride, GJL Operations, Staff, Board, Coalitions, Donors, Grants, Media, Volunteers",
                },
                "limit": {
                    "type": "integer",
                    "default": 20,
                    "description": "Maximum number of emails to return",
                },
                "days_back": {
                    "type": "integer",
                    "default": 7,
                    "description": "How many days to look back",
                },
                "unread_only": {
                    "type": "boolean",
                    "default": False,
                    "description": "Only include unread emails",
                },
            },
            "required": ["category"],
        },
        "handler": filter_by_category,
        "category": "triage",
    },
    {
        "name": "inbox_summary",
        "description": (
            "Quick count of unread emails grouped by category without fetching full message contents. "
            "Faster than triage_inbox because it only reads headers. "
            "Shows urgent email count and a bar chart of category distribution. "
            "Use this for a quick glance at inbox volume."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "days_back": {
                    "type": "integer",
                    "default": 7,
                    "description": "How many days to look back",
                }
            },
        },
        "handler": get_category_summary,
        "category": "triage",
    },
    {
        "name": "teach_sender",
        "description": (
            "Teach the agent which category a sender or domain belongs to for future email categorization. "
            "Provide a full email address for sender-level mapping, or just a domain for domain-level mapping. "
            "Learned mappings take highest priority in categorization (95% confidence for sender, 85% for domain). "
            "Mappings are persisted to email_triage.json."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "sender": {
                    "type": "string",
                    "description": "Email address or domain (e.g., 'jane@example.com' or 'example.com')",
                },
                "category": {
                    "type": "string",
                    "description": "Category: Campaign, Trans Pride, GJL Operations, Staff, Board, Coalitions, Donors, Media, Other",
                },
            },
            "required": ["sender", "category"],
        },
        "handler": teach_sender,
        "category": "triage",
    },
    {
        "name": "list_learned_senders",
        "description": (
            "Show all learned sender and domain categorizations grouped by category. "
            "Displays both individual sender mappings and domain-level wildcard mappings. "
            "Use this to review and audit the trained categorization rules."
        ),
        "input_schema": {"type": "object", "properties": {}},
        "handler": list_learned_senders,
        "category": "triage",
    },
    {
        "name": "forget_sender",
        "description": (
            "Remove a previously learned sender or domain categorization. "
            "After forgetting, the sender will be categorized using keyword matching instead. "
            "Accepts either a full email address or a domain name."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "sender": {"type": "string", "description": "Email address or domain to forget"}
            },
            "required": ["sender"],
        },
        "handler": forget_sender,
        "category": "triage",
    },
]
