# 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

"""
Inbox Monitor — real-time background email watcher.

Polls Gmail or IMAP on a configurable interval, classifies new emails
through the Themis triage engine, and returns priority hits for delivery
to the user's configured notification channel.

State persisted at: ~/.familiar/data/email_monitor.json
  {
    "enabled": false,
    "interval_minutes": 10,
    "last_check_time": null,           # ISO8601 UTC
    "alert_on": ["urgent"],            # priority levels that trigger alerts
    "watch_senders": [],               # always alert on these email addresses or domains
    "watch_topics": [],                # always alert on these keywords in subject/snippet
    "quiet_hours_start": "22:00",
    "quiet_hours_end": "07:00",
    "task_id": null                    # scheduler task ID (set by watch_inbox tool)
  }
"""

import json
import logging
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Optional

logger = logging.getLogger(__name__)

_DEFAULT_CONFIG: dict = {
    "enabled": False,
    "interval_minutes": 10,
    "last_check_time": None,
    "alert_on": ["urgent"],
    "watch_senders": [],
    "watch_topics": [],
    "quiet_hours_start": "22:00",
    "quiet_hours_end": "07:00",
    "task_id": None,
}


# ── Config helpers ─────────────────────────────────────────────────────────────


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

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


def _monitor_file() -> Path:
    return _data_dir() / "email_monitor.json"


def get_monitor_config() -> dict:
    """Load monitor config from disk, filling in defaults for missing keys."""
    path = _monitor_file()
    if path.exists():
        try:
            data = json.loads(path.read_text())
            return {**_DEFAULT_CONFIG, **data}
        except (json.JSONDecodeError, OSError) as e:
            logger.warning("Failed to load monitor config: %s", e)
    return dict(_DEFAULT_CONFIG)


def save_monitor_config(config: dict):
    """Persist monitor config at chmod 0o600."""
    path = _monitor_file()
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(config, indent=2) + "\n")
    path.chmod(0o600)


# ── Quiet-hours check ──────────────────────────────────────────────────────────


def _in_quiet_hours(config: dict) -> bool:
    """Return True if the current local time falls inside the quiet window."""
    now = datetime.now()
    start_str = config.get("quiet_hours_start", "22:00")
    end_str = config.get("quiet_hours_end", "07:00")
    try:
        qs = datetime.strptime(start_str, "%H:%M").replace(
            year=now.year, month=now.month, day=now.day
        )
        qe = datetime.strptime(end_str, "%H:%M").replace(
            year=now.year, month=now.month, day=now.day
        )
        # Overnight window (e.g. 22:00 – 07:00)
        if qs > qe:
            return now >= qs or now <= qe
        return qs <= now <= qe
    except ValueError:
        return False


# ── Email classification ───────────────────────────────────────────────────────


def _classify_email(
    from_header: str,
    subject: str,
    snippet: str,
    has_list_unsubscribe: bool = False,
) -> tuple:
    """
    Classify an email through the Themis triage engine.
    Returns (category: str, priority: str).
    Falls back to ("Other", "fyi") if triage is unavailable.
    """
    try:
        from familiar.skills.triage.skill import (
            _categorize_email as _cat,
            _get_priority as _pri,
        )

        category, _ = _cat(from_header, subject, snippet, has_list_unsubscribe)
        priority = _pri(subject, category, from_header)
        return category, priority
    except Exception as e:
        logger.debug("Triage classification unavailable: %s", e)
        return "Other", "fyi"


def _should_alert(email_data: dict, config: dict) -> bool:
    """Return True if this email warrants a push alert."""
    priority = email_data.get("priority", "fyi")
    alert_on = set(config.get("alert_on", ["urgent"]))

    if priority in alert_on:
        return True

    # Watch-sender match (exact address or domain suffix)
    sender = email_data.get("from_email", "").lower()
    for ws in config.get("watch_senders", []):
        ws_lower = ws.lower().lstrip("@")
        if sender == ws_lower or sender.endswith("@" + ws_lower):
            return True

    # Watch-topic keyword in subject or snippet
    subject_lower = email_data.get("subject", "").lower()
    snippet_lower = email_data.get("snippet", "").lower()
    for wt in config.get("watch_topics", []):
        wt_lower = wt.lower()
        if wt_lower in subject_lower or wt_lower in snippet_lower:
            return True

    return False


# ── Gmail fetch ────────────────────────────────────────────────────────────────


def _fetch_gmail_since(service, since_dt: datetime) -> list:
    """Fetch unread Gmail messages received after since_dt (max 50)."""
    since_epoch = int(since_dt.timestamp())
    query = f"is:unread after:{since_epoch}"

    try:
        result = service.users().messages().list(
            userId="me", q=query, maxResults=50
        ).execute()
    except Exception as e:
        logger.warning("Gmail list failed: %s", e)
        return []

    message_ids = [m["id"] for m in result.get("messages", [])]
    emails = []

    for msg_id in message_ids:
        try:
            msg = service.users().messages().get(
                userId="me",
                id=msg_id,
                format="metadata",
                metadataHeaders=["From", "Subject", "Date", "List-Unsubscribe"],
            ).execute()
            headers = {
                h["name"].lower(): h["value"]
                for h in msg.get("payload", {}).get("headers", [])
            }
            emails.append({
                "id": msg_id,
                "from": headers.get("from", ""),
                "subject": headers.get("subject", "(no subject)"),
                "snippet": msg.get("snippet", "")[:200],
                "has_list_unsubscribe": "list-unsubscribe" in headers,
                "source": "gmail",
            })
        except Exception as e:
            logger.debug("Failed to fetch Gmail message %s: %s", msg_id, e)

    return emails


# ── IMAP fetch ─────────────────────────────────────────────────────────────────


def _fetch_imap_since(imap_cfg: dict, since_dt: datetime) -> list:
    """Fetch unseen IMAP messages received on or after since_dt (max 50)."""
    try:
        from familiar.skills.triage.skill import _decode_header, _imap_connect
    except ImportError as e:
        logger.debug("IMAP helpers not available: %s", e)
        return []

    since_str = since_dt.strftime("%d-%b-%Y")
    emails = []
    mail = None

    try:
        import email as _email_mod

        mail = _imap_connect(imap_cfg)
        mail.login(imap_cfg["address"], imap_cfg["password"])
        mail.select("INBOX", readonly=True)

        _, data = mail.search(None, f'(UNSEEN SINCE "{since_str}")')
        if not data or not data[0]:
            return []

        # Most recent 50 to avoid overwhelming the scheduler
        msg_ids = data[0].split()[-50:]

        for msg_id in msg_ids:
            try:
                _, msg_data = mail.fetch(
                    msg_id,
                    "(BODY.PEEK[HEADER.FIELDS (FROM SUBJECT LIST-UNSUBSCRIBE)])",
                )
                raw = msg_data[0][1] if msg_data and msg_data[0] else b""
                msg = _email_mod.message_from_bytes(raw)
                emails.append({
                    "id": msg_id.decode(),
                    "from": _decode_header(msg.get("From", "")),
                    "subject": _decode_header(msg.get("Subject", "(no subject)")),
                    "snippet": "",
                    "has_list_unsubscribe": bool(msg.get("List-Unsubscribe")),
                    "source": "imap",
                })
            except Exception as e:
                logger.debug("Failed to fetch IMAP message %s: %s", msg_id, e)

    except Exception as e:
        logger.warning("IMAP monitor fetch failed: %s", e)
    finally:
        if mail:
            try:
                mail.logout()
            except Exception:
                pass

    return emails


# ── Core check ─────────────────────────────────────────────────────────────────


def check_new_priority_emails() -> list:
    """
    Fetch emails since last check, classify them, and return those that
    match the configured alert criteria.

    Always updates last_check_time on return so the next poll starts fresh.
    Returns an empty list during quiet hours or when no priority hits exist.
    """
    config = get_monitor_config()

    if _in_quiet_hours(config):
        logger.debug("Inbox monitor: quiet hours — skipping")
        return []

    # Determine the look-back window
    last_check_str = config.get("last_check_time")
    if last_check_str:
        try:
            since_dt = datetime.fromisoformat(last_check_str.replace("Z", ""))
        except ValueError:
            since_dt = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(
                minutes=config.get("interval_minutes", 10) + 5
            )
    else:
        since_dt = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta(
            minutes=config.get("interval_minutes", 10) + 5
        )

    # Fetch — try Gmail API first, fall back to IMAP
    raw_emails: list = []

    try:
        from familiar.skills.triage.skill import _get_gmail_service, _gmail_available

        if _gmail_available():
            svc = _get_gmail_service()
            raw_emails = _fetch_gmail_since(svc, since_dt)
    except Exception as e:
        logger.debug("Gmail monitor path failed, trying IMAP: %s", e)

    if not raw_emails:
        try:
            from familiar.skills.email.accounts import get_account

            acct = get_account()
            if acct and acct.get("address") and acct.get("password"):
                imap_cfg = {
                    "address": acct["address"],
                    "password": acct["password"],
                    "imap_server": acct.get("imap_server", "imap.gmail.com"),
                    "imap_port": acct.get("imap_port", 993),
                }
                raw_emails = _fetch_imap_since(imap_cfg, since_dt)
        except Exception as e:
            logger.debug("IMAP monitor path failed: %s", e)

    # Always advance the cursor regardless of fetch outcome
    config["last_check_time"] = datetime.now(timezone.utc).replace(tzinfo=None).isoformat()
    save_monitor_config(config)

    if not raw_emails:
        return []

    # Classify and filter for alert-worthy emails
    try:
        from familiar.skills.triage.skill import (
            _extract_email_address,
            _extract_sender_name,
        )
    except ImportError:

        def _extract_email_address(h):
            import re

            m = re.search(r"<([^>]+)>", h)
            return (m.group(1) if m else h).lower().strip()

        def _extract_sender_name(h):
            return h.split("<")[0].strip().strip('"') or h.split("@")[0]

    hits = []
    for em in raw_emails:
        from_header = em.get("from", "")
        subject = em.get("subject", "")
        snippet = em.get("snippet", "")

        category, priority = _classify_email(
            from_header, subject, snippet, em.get("has_list_unsubscribe", False)
        )

        email_data = {
            "id": em["id"],
            "from_email": _extract_email_address(from_header),
            "from_name": _extract_sender_name(from_header),
            "subject": subject,
            "snippet": snippet[:120],
            "category": category,
            "priority": priority,
            "source": em.get("source", "unknown"),
        }

        if _should_alert(email_data, config):
            hits.append(email_data)

    return hits


# ── Alert formatting ───────────────────────────────────────────────────────────


def format_alert_message(hits: list) -> str:
    """Format a concise push notification for a list of priority email hits."""
    if not hits:
        return ""

    urgent = [h for h in hits if h["priority"] == "urgent"]
    action = [h for h in hits if h["priority"] == "action"]
    watched = [h for h in hits if h["priority"] == "fyi"]

    count = len(hits)
    noun = "email" if count == 1 else "emails"
    lines = [f"📬 **{count} priority {noun}:**\n"]

    for em in urgent[:4]:
        subj = em["subject"][:52] + ("…" if len(em["subject"]) > 52 else "")
        lines.append(f"  🔴 {em['from_name']} — {subj}")

    for em in action[:3]:
        subj = em["subject"][:52] + ("…" if len(em["subject"]) > 52 else "")
        lines.append(f"  🟡 {em['from_name']} — {subj}")

    for em in watched[:2]:
        subj = em["subject"][:52] + ("…" if len(em["subject"]) > 52 else "")
        lines.append(f"  👁️  {em['from_name']} — {subj}")

    remaining = count - len(urgent[:4]) - len(action[:3]) - len(watched[:2])
    if remaining > 0:
        lines.append(f"  … and {remaining} more")

    lines.append("\nRun `triage_inbox` to see the full list.")
    return "\n".join(lines)
