# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# Licensed under the MIT License

"""Email, accounts, and contacts routes — /api/email/*, /api/accounts/*, /api/contacts/*."""

from __future__ import annotations

import json
import logging
import os
import threading
from datetime import datetime
from pathlib import Path

from flask import Blueprint, jsonify, request

from ..auth import require_auth
from ..helpers import get_agent, DATA_DIR

logger = logging.getLogger(__name__)

bp = Blueprint("email", __name__)



def _load_google_accounts():
    """Load saved Google account registrations."""
    accts_file = DATA_DIR / "google_accounts.json"
    if accts_file.exists():
        try:
            return json.loads(accts_file.read_text())
        except Exception:
            pass
    return {}

@bp.route("/api/email/summary")
@require_auth
def api_email_summary():
    """Get email triage summary."""
    try:
        import sys

        sys.path.insert(0, str(Path(__file__).parent.parent / "skills" / "triage"))
        from skill import get_category_summary

        result = get_category_summary({"days_back": 7})
        return jsonify({"summary": result})
    except Exception:
        return jsonify({"error": "Failed to generate email summary"}), 500


_triage_cache: dict = {"data": None, "timestamp": 0.0}
_triage_cache_lock = threading.Lock()
_TRIAGE_CACHE_TTL = 300  # 5 minutes
_TRIAGE_CACHE_FILE = Path.home() / ".familiar" / "data" / "cache" / "triage_cache.json"


def _load_triage_cache():
    """Load triage cache from disk on startup."""
    try:
        if _TRIAGE_CACHE_FILE.exists():
            data = json.loads(_TRIAGE_CACHE_FILE.read_text())
            _triage_cache["data"] = data.get("data")
            _triage_cache["timestamp"] = data.get("timestamp", 0.0)
    except Exception as e:
        logger.debug("Could not load triage cache: %s", e)


def _save_triage_cache():
    """Persist triage cache to disk so restarts don't lose state."""
    try:
        _TRIAGE_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
        _TRIAGE_CACHE_FILE.write_text(
            json.dumps({"data": _triage_cache["data"], "timestamp": _triage_cache["timestamp"]})
        )
    except Exception as e:
        logger.debug("Could not save triage cache: %s", e)


# Load on import
_load_triage_cache()


@bp.route("/api/email/triage")
@require_auth
def api_email_triage():
    """Get full email triage as structured JSON for the interactive dashboard."""
    force_refresh = request.args.get("refresh", "false").lower() == "true"

    with _triage_cache_lock:
        if (
            not force_refresh
            and _triage_cache["data"] is not None
            and (time.time() - _triage_cache["timestamp"]) < _TRIAGE_CACHE_TTL
        ):
            return jsonify(_triage_cache["data"])

    try:
        import threading

        from familiar.skills.triage.skill import CATEGORIES, triage_inbox

        # Run triage — side-effect: stores ordered_emails on thread-local
        text_result = triage_inbox(
            {
                "limit": int(request.args.get("limit", 25)),
                "unread_only": request.args.get("unread", "false").lower() == "true",
                "days_back": int(request.args.get("days_back", 7)),
            }
        )

        # Retrieve structured data from contextvars (set by triage_inbox)
        from familiar.core.tool_executor import _triage_ctx_var as _dash_triage_ctx

        triage_ctx = _dash_triage_ctx.get()
        if triage_ctx and triage_ctx.get("emails"):
            emails = triage_ctx["emails"]
            # Group by category
            categories = {}
            for em in emails:
                cat = em.get("category", "Other")
                categories.setdefault(cat, []).append(em)
            result = {
                "emails": emails,
                "categories": {cat: categories.get(cat, []) for cat in CATEGORIES},
                "total": triage_ctx.get("total", len(emails)),
                "triage": text_result,  # Keep text fallback
            }
        else:
            # Fallback: text-only response
            result = {"triage": text_result}

        with _triage_cache_lock:
            _triage_cache["data"] = result
            _triage_cache["timestamp"] = time.time()
            _save_triage_cache()
        return jsonify(result)
    except Exception as e:
        logger.warning("Email triage error: %s", e)
        # Serve stale cache rather than error — data is better than nothing
        with _triage_cache_lock:
            if _triage_cache["data"] is not None:
                return jsonify(_triage_cache["data"])
        # Give actionable error when Gmail OAuth is missing
        _has_google = (DATA_DIR / "google_credentials.json").exists()
        _has_gmail = (DATA_DIR / "google_token_gmail.json").exists()
        if _has_google and not _has_gmail:
            return jsonify(
                {
                    "error": "Gmail needs authorization",
                    "action": "connect_gmail",
                    "message": "Click 'Connect Gmail' to authorize email access.",
                }
            ), 401
        return jsonify({"error": "Failed to triage emails"}), 500


@bp.route("/api/email/read/<msg_id>")
@require_auth
def api_email_read(msg_id):
    """Read a specific email by Gmail message ID or IMAP ID."""
    try:
        from familiar.skills.email.skill import (
            _gmail_read_email_by_id,
            _imap_read_email_by_id,
        )

        id_source = request.args.get("source", "gmail")
        if id_source == "gmail":
            result = _gmail_read_email_by_id(msg_id)
        else:
            result = _imap_read_email_by_id(msg_id)
        return jsonify({"content": result})
    except Exception as e:
        logger.warning("Email read error: %s", e)
        return jsonify({"error": f"Failed to read email: {e}"}), 500


@bp.route("/api/email/delete/<msg_id>", methods=["POST"])
@require_auth
def api_email_delete(msg_id):
    """Move an email to Trash by Gmail message ID or IMAP UID."""
    try:
        from familiar.skills.email.skill import (
            _gmail_trash_by_id,
            _imap_trash_by_id,
        )

        body = request.get_json(silent=True) or {}
        id_source = body.get("source", request.args.get("source", "gmail"))
        if id_source == "gmail":
            result = _gmail_trash_by_id(msg_id)
        else:
            result = _imap_trash_by_id(msg_id)
        if result == "ok":
            return jsonify({"status": "ok"})
        return jsonify({"error": result}), 500
    except Exception as e:
        logger.warning("Email delete error: %s", e)
        return jsonify({"error": f"Failed to delete email: {e}"}), 500


@bp.route("/api/email/reply", methods=["POST"])
@require_auth
def api_email_reply():
    """Send a reply to an email via Gmail API or SMTP."""
    try:
        from familiar.skills.email.skill import send_email

        payload = request.get_json() or {}
        # Build send_email payload with reply context
        send_data = {
            "to": payload.get("to", ""),
            "subject": payload.get("subject", ""),
            "body": payload.get("body", ""),
            # Dashboard reply UI is the confirmation step — user reviewed the draft
            # before clicking Send, so we bypass the agent confirmation gate.
            "_confirmed": True,
        }
        # Include thread_id for Gmail threading
        if payload.get("thread_id"):
            send_data["_gmail_thread_id"] = payload["thread_id"]
        if payload.get("in_reply_to"):
            send_data["in_reply_to"] = payload["in_reply_to"]

        result = send_email(send_data)
        # send_email returns a string — treat anything not starting with ✅ as an error
        if not result.startswith("✅"):
            return jsonify({"error": result}), 500
        return jsonify({"result": result})
    except Exception as e:
        logger.warning("Email reply error: %s", e)
        return jsonify({"error": "Failed to send reply"}), 500


@bp.route("/api/email/sent")
@require_auth
def api_email_sent():
    """Fetch sent mail — Gmail API (in:sent) or IMAP Sent folder."""
    limit = int(request.args.get("limit", 25))
    days_back = int(request.args.get("days_back", 30))

    try:
        from datetime import datetime as _dt
        from datetime import timedelta as _td

        from familiar.skills.email.skill import (
            _get_gmail_service,
            _gmail_available,
            _gmail_body,
            _gmail_header,
            _imap_config,
            _imap_connect,
            decode_mime_header,
            get_email_body,
        )

        emails = []

        if _gmail_available():
            svc = _get_gmail_service()
            since = (_dt.now() - _td(days=days_back)).strftime("%Y/%m/%d")
            q = f"in:sent after:{since}"
            res = svc.users().messages().list(userId="me", q=q, maxResults=limit).execute()
            for m in res.get("messages", []):
                full = (
                    svc.users()
                    .messages()
                    .get(
                        userId="me",
                        id=m["id"],
                        format="metadata",
                        metadataHeaders=["To", "Subject", "Date"],
                    )
                    .execute()
                )
                payload = full.get("payload", {})
                to_hdr = _gmail_header(payload, "To")
                to_name = (
                    to_hdr.split("<")[0].strip().strip('"')
                    if "<" in to_hdr
                    else to_hdr.split("@")[0]
                )
                subject = _gmail_header(payload, "Subject") or "(no subject)"
                date_str = _gmail_header(payload, "Date") or ""
                emails.append(
                    {
                        "id": m["id"],
                        "thread_id": full.get("threadId", ""),
                        "id_source": "gmail",
                        "to_name": to_name,
                        "to_email": to_hdr,
                        "subject": subject,
                        "date": date_str,
                    }
                )

        else:
            # IMAP fallback — try common Sent folder names
            cfg = _imap_config()
            if cfg.get("address"):
                import email as _email_lib

                conn = _imap_connect(cfg)
                sent_folders = ["[Gmail]/Sent Mail", "Sent", "Sent Messages", "INBOX.Sent"]
                selected = False
                for folder in sent_folders:
                    try:
                        status, _ = conn.select(folder, readonly=True)
                        if status == "OK":
                            selected = True
                            break
                    except Exception:
                        continue
                if selected:
                    since_imap = (_dt.now() - _td(days=days_back)).strftime("%d-%b-%Y")
                    _, data = conn.search(None, f"SINCE {since_imap}")
                    ids = data[0].split() if data and data[0] else []
                    for uid in reversed(ids[-limit:]):
                        _, msg_data = conn.fetch(uid, "(RFC822.HEADER)")
                        if not msg_data or not msg_data[0]:
                            continue
                        raw = msg_data[0][1] if isinstance(msg_data[0], tuple) else msg_data[0]
                        msg = _email_lib.message_from_bytes(raw)
                        to_hdr = decode_mime_header(msg.get("To", ""))
                        to_name = (
                            to_hdr.split("<")[0].strip().strip('"')
                            if "<" in to_hdr
                            else to_hdr.split("@")[0]
                        )
                        subject = decode_mime_header(msg.get("Subject", "(no subject)"))
                        date_str = msg.get("Date", "")
                        emails.append(
                            {
                                "id": uid.decode() if isinstance(uid, bytes) else str(uid),
                                "id_source": "imap",
                                "to_name": to_name,
                                "to_email": to_hdr,
                                "subject": subject,
                                "date": date_str,
                            }
                        )
                conn.logout()

        return jsonify({"emails": emails, "total": len(emails)})

    except Exception as e:
        logger.warning("Email sent fetch error: %s", e)
        return jsonify({"error": str(e)}), 500


# ── Email topic flags, VIPs, and weekly digest ───────────────────────────────


@bp.route("/api/email/topic-flags", methods=["GET", "POST", "DELETE"])
@require_auth
def api_email_topic_flags():
    """Manage topic flags that auto-escalate emails to urgent."""
    try:
        from familiar.skills.triage.skill import manage_topic_flags

        if request.method == "GET":
            result = manage_topic_flags({"action": "list"})
            from familiar.skills.triage.skill import _load_triage_data

            data = _load_triage_data()
            return jsonify({"flags": data.get("topic_flags", [])})
        elif request.method == "POST":
            body = request.get_json(silent=True) or {}
            result = manage_topic_flags({"action": "add", "keyword": body.get("keyword", "")})
            return jsonify({"result": result})
        else:  # DELETE
            body = request.get_json(silent=True) or {}
            result = manage_topic_flags({"action": "remove", "keyword": body.get("keyword", "")})
            return jsonify({"result": result})
    except Exception as e:
        return jsonify({"error": str(e)}), 500


@bp.route("/api/email/monitor/config", methods=["GET", "POST"])
@require_auth
def api_email_monitor_config():
    """Get or update email monitor configuration."""
    from familiar.skills.email.monitor import get_monitor_config, save_monitor_config

    if request.method == "GET":
        return jsonify(get_monitor_config())

    body = request.json or {}
    config = get_monitor_config()
    for key in ("enabled", "interval_minutes", "quiet_hours_start", "quiet_hours_end",
                "alert_on", "smart_mode_enabled", "smart_batch_size", "smart_daily_budget_tokens"):
        if key in body:
            config[key] = body[key]
    save_monitor_config(config)
    return jsonify(config)


@bp.route("/api/email/monitor/stats")
@require_auth
def api_email_monitor_stats():
    """Get email monitor sorting statistics."""
    from familiar.skills.email.monitor import get_monitor_config, get_sorted_inbox

    config = get_monitor_config()
    cache = get_sorted_inbox()
    summary = cache.get("summary", {})

    # Training data stats
    training = {}
    try:
        from familiar.skills.email.training_data import get_stats as _get_training_stats
        training = _get_training_stats()
    except Exception:
        pass

    return jsonify({
        "enabled": config.get("enabled", False),
        "interval_minutes": config.get("interval_minutes", 10),
        "last_check_time": config.get("last_check_time"),
        "smart_mode_enabled": config.get("smart_mode_enabled", False),
        "smart_tokens_used_today": config.get("smart_tokens_used_today", 0),
        "smart_daily_budget_tokens": config.get("smart_daily_budget_tokens", 1000),
        "total_sorted": summary.get("total", 0),
        "urgent": summary.get("urgent", 0),
        "action": summary.get("action", 0),
        "fyi": summary.get("fyi", 0),
        "cache_updated_at": cache.get("updated_at"),
        "training": training,
    })


@bp.route("/api/email/sorted")
@require_auth
def api_email_sorted():
    """Get the sorted inbox cache for the Smart Sort tab."""
    from familiar.skills.email.monitor import get_sorted_inbox
    return jsonify(get_sorted_inbox())


@bp.route("/api/email/score", methods=["POST"])
@require_auth
def api_email_score():
    """Score an email using the unified six-signal scorer. For debugging/transparency."""
    data = request.json or {}
    try:
        from familiar.skills.triage.scorer import score_email

        result = score_email(
            sender_email=data.get("sender_email", ""),
            sender_name=data.get("sender_name", ""),
            subject=data.get("subject", ""),
            snippet=data.get("snippet", ""),
            has_list_unsubscribe=data.get("has_list_unsubscribe", False),
            header_signals=data.get("header_signals"),
        )
        return jsonify(result)
    except Exception as e:
        return jsonify({"error": str(e)}), 500


@bp.route("/api/email/engagement/<sender>")
@require_auth
def api_email_engagement(sender):
    """Get engagement data for a sender."""
    from familiar.skills.email.engagement import get_engagement
    return jsonify(get_engagement(sender))


@bp.route("/api/email/engagement/top")
@require_auth
def api_email_engagement_top():
    """Get top engaged senders."""
    from familiar.skills.email.engagement import get_top_engaged, get_stats
    return jsonify({"top": get_top_engaged(), "stats": get_stats()})


@bp.route("/api/email/engagement/low")
@require_auth
def api_email_engagement_low():
    """Get low-engagement senders (candidates for bulk Promotional marking)."""
    from familiar.skills.email.engagement import get_low_engaged
    return jsonify({"low": get_low_engaged()})


@bp.route("/api/email/engagement/record", methods=["POST"])
@require_auth
def api_email_engagement_record():
    """Record an engagement event (open, reply, trash)."""
    data = request.json or {}
    sender = data.get("sender", "")
    event = data.get("event", "")
    if not sender or event not in ("opened", "replied", "trashed"):
        return jsonify({"error": "Provide sender and event (opened/replied/trashed)"}), 400

    from familiar.skills.email.engagement import record_opened, record_replied, record_trashed
    if event == "opened":
        record_opened(sender)
    elif event == "replied":
        record_replied(sender)
    elif event == "trashed":
        record_trashed(sender)

    return jsonify({"recorded": True})


@bp.route("/api/email/sort-inbox", methods=["POST"])
@require_auth
def api_email_sort_inbox():
    """Start batch sorting all unread emails (background thread)."""
    from familiar.skills.email.monitor import get_sort_inbox_status, sort_inbox_batch
    import threading

    status = get_sort_inbox_status()
    if status["running"]:
        return jsonify({"error": "Sort already in progress", "status": status}), 409

    t = threading.Thread(target=sort_inbox_batch, daemon=True)
    t.start()
    return jsonify({"started": True})


@bp.route("/api/email/sort-inbox/status")
@require_auth
def api_email_sort_inbox_status():
    """Get progress of batch inbox sort."""
    from familiar.skills.email.monitor import get_sort_inbox_status
    return jsonify(get_sort_inbox_status())


@bp.route("/api/email/sort-inbox/stop", methods=["POST"])
@require_auth
def api_email_sort_inbox_stop():
    """Cancel a running inbox sort."""
    from familiar.skills.email.monitor import cancel_sort_inbox
    cancel_sort_inbox()
    return jsonify({"cancelled": True})


@bp.route("/api/email/vip-senders", methods=["GET", "POST", "DELETE"])
@require_auth
def api_email_vip_senders():
    """Manage VIP senders whose emails are always prioritized."""
    try:
        from familiar.skills.triage.skill import manage_vip_senders

        if request.method == "GET":
            from familiar.skills.triage.skill import _load_triage_data

            data = _load_triage_data()
            return jsonify({"vips": data.get("vip_senders", [])})
        elif request.method == "POST":
            body = request.get_json(silent=True) or {}
            result = manage_vip_senders({"action": "add", "sender": body.get("sender", "")})
            return jsonify({"result": result})
        else:  # DELETE
            body = request.get_json(silent=True) or {}
            result = manage_vip_senders({"action": "remove", "sender": body.get("sender", "")})
            return jsonify({"result": result})
    except Exception as e:
        return jsonify({"error": str(e)}), 500


@bp.route("/api/email/digest")
@require_auth
def api_email_digest():
    """Get the email intelligence digest."""
    from familiar.skills.email.digest import generate_digest
    return jsonify(generate_digest())


@bp.route("/api/accounts")
@require_auth
def api_accounts_list():
    """List all connected accounts (email + calendar + Google OAuth)."""
    import glob

    result = {"email": [], "calendar": [], "google_oauth": []}

    # Email accounts
    try:
        from familiar.skills.email.accounts import get_all_accounts, _load_accounts_file

        data = _load_accounts_file()
        for acct in get_all_accounts():
            result["email"].append({
                "id": acct.get("id", ""),
                "address": acct.get("address", ""),
                "provider": acct.get("provider", "custom"),
                "label": acct.get("label", ""),
                "is_primary": acct.get("id") == data.get("primary"),
            })
    except Exception as e:
        logger.debug("Email accounts list failed: %s", e)

    # Calendar accounts
    try:
        from familiar.skills.calendar.accounts import get_all_accounts as get_cal_accounts

        for acct in get_cal_accounts():
            result["calendar"].append({
                "id": acct.get("id", ""),
                "type": acct.get("type", ""),
                "email": acct.get("email", ""),
                "url": acct.get("url", ""),
                "label": acct.get("label", ""),
            })
    except Exception:
        pass

    # Google OAuth tokens (legacy single-token files)
    data_dir = Path.home() / ".familiar" / "data"
    for f in sorted(data_dir.glob("google_token_*.json")):
        service = f.stem.replace("google_token_", "")
        result["google_oauth"].append({"service": service, "file": f.name})

    # Registered Google accounts (multi-account)
    result["google_accounts"] = _load_google_accounts()

    return jsonify(result)


@bp.route("/api/accounts/email", methods=["POST"])
@require_auth
def api_accounts_email_add():
    """Add or update an email account."""
    data = request.json or {}
    address = data.get("address", "").strip()
    if not address:
        return jsonify({"error": "Email address required"}), 400

    from familiar.skills.email.accounts import add_account, _detect_provider

    provider = data.get("provider") or _detect_provider(data.get("imap_server", "imap.gmail.com"))

    account = {
        "address": address,
        "password": data.get("password", ""),
        "imap_server": data.get("imap_server", ""),
        "smtp_server": data.get("smtp_server", ""),
        "imap_port": int(data.get("imap_port", 993)),
        "smtp_port": int(data.get("smtp_port", 587)),
        "provider": provider,
        "label": data.get("label", ""),
    }
    add_account(account)
    return jsonify({"added": address, "provider": provider}), 201


@bp.route("/api/accounts/email/<account_id>", methods=["DELETE"])
@require_auth
def api_accounts_email_remove(account_id):
    """Remove an email account."""
    from familiar.skills.email.accounts import remove_account
    ok = remove_account(account_id)
    return jsonify({"removed": ok}), 200 if ok else 404


@bp.route("/api/accounts/email/<account_id>/label", methods=["PUT"])
@require_auth
def api_accounts_email_label(account_id):
    """Update an email account's label."""
    data = request.json or {}
    label = data.get("label", "")
    from familiar.skills.email.accounts import get_all_accounts, _load_accounts_file, _save_accounts_file

    file_data = _load_accounts_file()
    for acct in file_data["accounts"]:
        if acct.get("id") == account_id:
            acct["label"] = label
            _save_accounts_file(file_data)
            return jsonify({"updated": True, "label": label})
    return jsonify({"error": "Account not found"}), 404


@bp.route("/api/accounts/email/<account_id>/primary", methods=["POST"])
@require_auth
def api_accounts_email_primary(account_id):
    """Set an email account as primary (used for sending)."""
    from familiar.skills.email.accounts import _load_accounts_file, _save_accounts_file, _sync_primary_to_env

    data = _load_accounts_file()
    for acct in data["accounts"]:
        if acct.get("id") == account_id:
            data["primary"] = account_id
            _save_accounts_file(data)
            _sync_primary_to_env(acct)
            return jsonify({"primary": account_id})
    return jsonify({"error": "Account not found"}), 404


@bp.route("/api/accounts/test", methods=["POST"])
@require_auth
def api_accounts_test_connection():
    """Test email account connection (IMAP)."""
    data = request.json or {}
    server = data.get("imap_server", "")
    port = int(data.get("imap_port", 993))
    address = data.get("address", "")
    password = data.get("password", "")

    if not server or not address:
        return jsonify({"ok": False, "error": "Server and address required"}), 400

    try:
        import imaplib
        import ssl

        ctx = ssl.create_default_context()
        with imaplib.IMAP4_SSL(server, port, ssl_context=ctx) as mail:
            mail.login(address, password)
            mail.select("INBOX")
            typ, count = mail.search(None, "ALL")
            total = len(count[0].split()) if count[0] else 0
            mail.logout()
        return jsonify({"ok": True, "message": f"Connected — {total} emails in inbox"})
    except Exception as e:
        return jsonify({"ok": False, "error": str(e)[:200]})


@bp.route("/api/accounts/caldav/test", methods=["POST"])
@require_auth
def api_accounts_caldav_test():
    """Test CalDAV calendar connection."""
    data = request.json or {}
    url = data.get("url", "").strip().rstrip("/")
    user = data.get("user", "").strip()
    password = data.get("password", "")

    if not url or not user:
        return jsonify({"ok": False, "error": "URL and username required"}), 400

    try:
        import urllib.request
        import urllib.error
        import base64

        # PROPFIND on the CalDAV URL to verify credentials + connectivity
        req = urllib.request.Request(url, method="PROPFIND")
        creds = base64.b64encode(f"{user}:{password}".encode()).decode()
        req.add_header("Authorization", f"Basic {creds}")
        req.add_header("Depth", "0")
        req.add_header("Content-Type", "application/xml")
        req.data = b'<?xml version="1.0"?><d:propfind xmlns:d="DAV:"><d:prop><d:displayname/></d:prop></d:propfind>'

        with urllib.request.urlopen(req, timeout=10) as resp:
            status = resp.status
            if status in (200, 207):
                return jsonify({"ok": True, "message": "CalDAV connection successful"})
            return jsonify({"ok": False, "error": f"Unexpected status {status}"})
    except urllib.error.HTTPError as e:
        if e.code == 401:
            return jsonify({"ok": False, "error": "Authentication failed — check username and password"})
        return jsonify({"ok": False, "error": f"HTTP {e.code}: {e.reason}"})
    except Exception as e:
        return jsonify({"ok": False, "error": str(e)[:200]})


@bp.route("/api/accounts/caldav", methods=["POST"])
@require_auth
def api_accounts_caldav_add():
    """Add a CalDAV calendar account."""
    from familiar.skills.calendar.accounts import add_account, get_all_accounts

    data = request.json or {}
    url = data.get("url", "").strip().rstrip("/")
    user = data.get("user", "").strip()
    password = data.get("password", "")
    label = data.get("label", "").strip()

    if not url or not user:
        return jsonify({"ok": False, "error": "URL and username required"}), 400

    # Check for duplicate URL
    existing = get_all_accounts()
    for acct in existing:
        if acct.get("type") == "caldav" and acct.get("url", "").rstrip("/") == url:
            return jsonify({"ok": False, "error": "This CalDAV URL is already connected"}), 409

    account = {
        "type": "caldav",
        "label": label or "CalDAV",
        "url": url,
        "user": user,
        "password": password,
    }
    add_account(account)

    return jsonify({"ok": True, "message": "CalDAV calendar added"})


@bp.route("/api/accounts/calendar/<account_id>", methods=["DELETE"])
@require_auth
def api_accounts_calendar_remove(account_id):
    """Remove a calendar account (CalDAV or Google)."""
    from familiar.skills.calendar.accounts import remove_account
    ok = remove_account(account_id)
    return jsonify({"removed": ok}), 200 if ok else 404


@bp.route("/api/accounts/google/revoke", methods=["POST"])
@require_auth
def api_accounts_google_revoke():
    """Revoke a Google OAuth token and delete the token file."""
    data = request.json or {}
    service = data.get("service", "")
    if not service:
        return jsonify({"error": "Service name required (gmail, calendar, drive, contacts)"}), 400

    data_dir = Path.home() / ".familiar" / "data"
    token_path = data_dir / f"google_token_{service}.json"

    if not token_path.exists():
        return jsonify({"error": f"No token found for {service}"}), 404

    # Try to revoke the token via Google's API before deleting
    revoked = False
    try:
        import json as _json
        import urllib.request
        import urllib.error

        token_data = _json.loads(token_path.read_text())
        access_token = token_data.get("token", "")
        if access_token:
            req = urllib.request.Request(
                f"https://oauth2.googleapis.com/revoke?token={access_token}",
                method="POST",
                headers={"Content-Type": "application/x-www-form-urlencoded"},
            )
            try:
                urllib.request.urlopen(req, timeout=10)
                revoked = True
            except urllib.error.HTTPError:
                # Token may already be expired/revoked — still delete the file
                pass
    except Exception as e:
        logger.debug("Google token revoke API call failed: %s", e)

    # Delete the token file
    token_path.unlink(missing_ok=True)

    return jsonify({"revoked": revoked, "deleted": True, "service": service})


@bp.route("/api/accounts/health", methods=["GET"])
@require_auth
def api_accounts_health():
    """Check connection health for all accounts. Returns {account_id: {ok, message}}."""
    results = {}

    # Email account health (IMAP test)
    try:
        from familiar.skills.email.accounts import get_all_accounts
        for acct in get_all_accounts():
            acct_id = acct.get("id", "")
            if not acct.get("address") or not acct.get("password"):
                results[acct_id] = {"ok": False, "message": "Missing credentials"}
                continue
            try:
                import imaplib
                import ssl
                ctx = ssl.create_default_context()
                server = acct.get("imap_server", "imap.gmail.com")
                port = acct.get("imap_port", 993)
                with imaplib.IMAP4_SSL(server, port, ssl_context=ctx) as mail:
                    mail.login(acct["address"], acct["password"])
                    mail.logout()
                results[acct_id] = {"ok": True, "message": "Connected"}
            except Exception as e:
                results[acct_id] = {"ok": False, "message": str(e)[:100]}
    except Exception:
        pass

    # Google OAuth health (check if tokens are valid)
    data_dir = Path.home() / ".familiar" / "data"
    for f in sorted(data_dir.glob("google_token_*.json")):
        service = f.stem.replace("google_token_", "")
        key = f"google_{service}"
        try:
            import json as _json
            token_data = _json.loads(f.read_text())
            # Check if token has a refresh_token (can be renewed)
            has_refresh = bool(token_data.get("refresh_token"))
            # Check expiry
            expiry = token_data.get("expiry", "")
            from datetime import datetime as _dt, timezone as _tz
            if expiry:
                try:
                    exp_dt = _dt.fromisoformat(expiry.replace("Z", "+00:00"))
                    if exp_dt < _dt.now(_tz.utc):
                        if has_refresh:
                            results[key] = {"ok": True, "message": "Expired (auto-refresh)"}
                        else:
                            results[key] = {"ok": False, "message": "Expired — reconnect needed"}
                        continue
                except ValueError:
                    pass
            results[key] = {"ok": True, "message": "Valid" + (" (refreshable)" if has_refresh else "")}
        except Exception as e:
            results[key] = {"ok": False, "message": str(e)[:100]}

    return jsonify(results)


@bp.route("/api/accounts/send-from", methods=["GET"])
@require_auth
def api_accounts_send_from():
    """Get list of accounts available for sending email replies."""
    accounts = []

    # Gmail API accounts
    data_dir = Path.home() / ".familiar" / "data"
    if (data_dir / "google_token_gmail.json").exists():
        try:
            from familiar.skills.email.skill import _get_gmail_service
            svc = _get_gmail_service()
            profile = svc.users().getProfile(userId="me").execute()
            accounts.append({
                "address": profile.get("emailAddress", "gmail"),
                "provider": "gmail",
                "type": "api",
            })
        except Exception:
            accounts.append({"address": "Gmail (token expired)", "provider": "gmail", "type": "api"})

    # IMAP/SMTP accounts
    try:
        from familiar.skills.email.accounts import get_all_accounts, _load_accounts_file
        file_data = _load_accounts_file()
        primary_id = file_data.get("primary")
        for acct in get_all_accounts():
            addr = acct.get("address", "")
            # Skip if same as Gmail API address
            if any(a["address"] == addr for a in accounts):
                continue
            if acct.get("smtp_server") and acct.get("password"):
                accounts.append({
                    "address": addr,
                    "provider": acct.get("provider", "custom"),
                    "type": "smtp",
                    "is_primary": acct.get("id") == primary_id,
                    "label": acct.get("label", ""),
                })
    except Exception:
        pass

    return jsonify({"accounts": accounts})


@bp.route("/api/email/quick-setup", methods=["GET", "POST"])
@require_auth
def api_email_quick_setup():
    """Get or set work domains and family emails for quick classification setup."""
    from familiar.skills.triage.skill import _load_triage_data, _save_triage_data

    triage = _load_triage_data()

    if request.method == "GET":
        return jsonify({
            "work_domains": triage.get("work_domains", []),
            "family_emails": triage.get("family_emails", []),
        })

    data = request.json or {}

    if "work_domain" in data:
        domain = data["work_domain"].strip().lower().lstrip("@")
        if domain:
            domains = triage.setdefault("work_domains", [])
            if domain not in domains:
                domains.append(domain)
                _save_triage_data(triage)
            return jsonify({"added": domain, "type": "work_domain"})

    if "family_email" in data:
        email = data["family_email"].strip().lower()
        if email:
            emails = triage.setdefault("family_emails", [])
            if email not in emails:
                emails.append(email)
                _save_triage_data(triage)
            return jsonify({"added": email, "type": "family_email"})

    if "remove_work_domain" in data:
        domain = data["remove_work_domain"].strip().lower()
        triage["work_domains"] = [d for d in triage.get("work_domains", []) if d != domain]
        _save_triage_data(triage)
        return jsonify({"removed": domain})

    if "remove_family_email" in data:
        email = data["remove_family_email"].strip().lower()
        triage["family_emails"] = [e for e in triage.get("family_emails", []) if e != email]
        _save_triage_data(triage)
        return jsonify({"removed": email})

    return jsonify({"error": "Provide work_domain or family_email"}), 400


@bp.route("/api/email/custom-categories", methods=["GET", "POST", "DELETE"])
@require_auth
def api_email_custom_categories():
    """Manage custom email categories with keyword filters."""
    from familiar.skills.triage.skill import _load_triage_data, _save_triage_data

    triage = _load_triage_data()

    if request.method == "GET":
        return jsonify({"categories": triage.get("custom_categories", [])})

    if request.method == "POST":
        data = request.json or {}
        name = data.get("name", "").strip()
        keywords = data.get("keywords", [])
        icon = data.get("icon", "fa-tag")
        collapse = data.get("collapse", False)

        if not name:
            return jsonify({"error": "Category name required"}), 400
        if not keywords:
            return jsonify({"error": "At least one keyword required"}), 400

        cats = triage.setdefault("custom_categories", [])
        # Check for duplicate
        for c in cats:
            if c["name"].lower() == name.lower():
                # Update existing
                c["keywords"] = [k.lower().strip() for k in keywords if k.strip()]
                c["icon"] = icon
                c["collapse"] = collapse
                _save_triage_data(triage)
                return jsonify({"category": c, "updated": True})

        cat = {
            "name": name,
            "keywords": [k.lower().strip() for k in keywords if k.strip()],
            "icon": icon,
            "collapse": collapse,
        }
        cats.append(cat)
        _save_triage_data(triage)
        return jsonify({"category": cat, "created": True}), 201

    if request.method == "DELETE":
        data = request.json or {}
        name = data.get("name", "")
        cats = triage.get("custom_categories", [])
        triage["custom_categories"] = [c for c in cats if c["name"].lower() != name.lower()]
        _save_triage_data(triage)
        return jsonify({"deleted": True})


@bp.route("/api/email/suggestions")
@require_auth
def api_email_suggestions():
    """Get proactive email intelligence suggestions."""
    from familiar.skills.email.suggestions import generate_suggestions
    return jsonify({"suggestions": generate_suggestions()})


@bp.route("/api/email/suggestions/apply", methods=["POST"])
@require_auth
def api_email_suggestions_apply():
    """Apply a suggestion action."""
    data = request.json or {}
    action = data.get("action", "")
    action_data = data.get("action_data", {})

    if action == "add_vip":
        from familiar.skills.triage.skill import manage_vip_senders
        result = manage_vip_senders({"action": "add", "sender": action_data.get("sender", "")})
        return jsonify({"result": result})
    elif action == "mark_domain_promotional":
        from familiar.skills.triage.skill import _load_triage_data, _save_triage_data
        domain = action_data.get("domain", "")
        if domain:
            triage = _load_triage_data()
            triage.setdefault("domain_categories", {})[domain] = "Promotional"
            _save_triage_data(triage)
            return jsonify({"result": f"Marked @{domain} as Promotional"})
        return jsonify({"error": "No domain provided"}), 400
    elif action == "train_model":
        from familiar.skills.email.model import train_model
        return jsonify(train_model())
    elif action == "sync_contacts":
        from familiar.skills.contacts.skill import sync_google_contacts
        return jsonify({"result": sync_google_contacts({"auto_vip": True})})
    else:
        return jsonify({"error": f"Unknown action: {action}"}), 400


@bp.route("/api/email/model/train", methods=["POST"])
@require_auth
def api_email_model_train():
    """Train the TF-IDF classification model from accumulated training data."""
    from familiar.skills.email.model import train_model
    return jsonify(train_model())


@bp.route("/api/email/model/info")
@require_auth
def api_email_model_info():
    """Get TF-IDF model status and metadata."""
    from familiar.skills.email.model import get_model_info
    return jsonify(get_model_info())


@bp.route("/api/email/rate", methods=["POST"])
@require_auth
def api_email_rate():
    """Rate a classification as correct or incorrect. Builds training data.

    Body: {from_email, category, correct: bool}
    Thumbs up (correct=true): confirms the classification as a training pair.
    Thumbs down (correct=false): redirects to the feedback/correction flow.
    """
    body = request.json or {}
    from_email = body.get("from_email", "").strip().lower()
    category = body.get("category", "")
    correct = body.get("correct", True)

    if not from_email or not category:
        return jsonify({"error": "Provide from_email and category"}), 400

    try:
        from familiar.skills.email.training_data import record_rating
        record_rating(from_email, category, correct)
        return jsonify({"recorded": True, "correct": correct})
    except Exception as e:
        return jsonify({"error": str(e)}), 500


@bp.route("/api/email/feedback", methods=["POST"])
@require_auth
def api_email_feedback():
    """Submit a correction to email classification.

    Body: {email_id, from_email, correct_category?, correct_priority?, apply_to_sender?}

    When apply_to_sender is true (default), the correction is learned for
    all future emails from that sender — not just this one email.
    """
    body = request.json or {}
    from_email = body.get("from_email", "").strip().lower()
    correct_category = body.get("correct_category", "")
    correct_priority = body.get("correct_priority", "")
    apply_to_sender = body.get("apply_to_sender", True)

    if not from_email and not correct_category and not correct_priority:
        return jsonify(
            {"error": "Provide from_email and correct_category or correct_priority"}
        ), 400

    try:
        from familiar.skills.triage.skill import _load_triage_data, _save_triage_data

        triage = _load_triage_data()
        changes = []

        if correct_category and from_email:
            if apply_to_sender:
                triage.setdefault("sender_categories", {})[from_email] = correct_category
                changes.append(f"category → {correct_category} for {from_email}")
            # Also learn the domain if it's a personal domain (not gmail/outlook/etc)
            domain = from_email.split("@")[-1] if "@" in from_email else ""
            freemail = {
                "gmail.com",
                "outlook.com",
                "yahoo.com",
                "hotmail.com",
                "aol.com",
                "icloud.com",
                "protonmail.com",
                "proton.me",
                "live.com",
                "msn.com",
            }
            if apply_to_sender and domain and domain not in freemail:
                triage.setdefault("domain_categories", {})[domain] = correct_category
                changes.append(f"domain {domain} → {correct_category}")

        if correct_priority and from_email:
            if apply_to_sender:
                triage.setdefault("sender_priorities", {})[from_email] = correct_priority
                changes.append(f"priority → {correct_priority} for {from_email}")

        if changes:
            # Log the correction for review
            triage.setdefault("correction_log", []).append(
                {
                    "from_email": from_email,
                    "category": correct_category,
                    "priority": correct_priority,
                    "timestamp": datetime.now().isoformat(),
                }
            )
            # Cap correction log at 200 entries
            if len(triage["correction_log"]) > 200:
                triage["correction_log"] = triage["correction_log"][-200:]

            _save_triage_data(triage)

            # Update the sorted cache in-place so Smart Sort reflects the change immediately
            try:
                from familiar.skills.email.monitor import get_sorted_inbox, _save_sorted_cache

                cache = get_sorted_inbox()
                updated = False
                for em in cache.get("emails", []):
                    if em.get("from_email", "").lower() == from_email:
                        if correct_category:
                            em["category"] = correct_category
                            updated = True
                        if correct_priority:
                            em["priority"] = correct_priority
                            updated = True
                if updated:
                    _save_sorted_cache(cache.get("emails", []))
            except Exception as _cache_err:
                logger.debug("Cache update after feedback skipped: %s", _cache_err)

            # Record as training pair for TF-IDF model
            try:
                from familiar.skills.email.training_data import record_correction
                record_correction(from_email, "", correct_category)
            except Exception:
                pass

            # Persist to Memory Tree as a durable user preference fact.
            # Working-tier entries are injected into every LLM prompt, so
            # the agent knows the user's email preferences proactively.
            try:
                from familiar.core.memory_tree import get_memory_tree

                tree = get_memory_tree()
                parts = []
                if correct_category:
                    parts.append(f"category={correct_category}")
                if correct_priority:
                    parts.append(f"priority={correct_priority}")
                title = f"Email preference: {from_email}"
                body = (
                    f"User corrected email sorting for {from_email}: "
                    f"{', '.join(parts)}. Apply this to all future emails "
                    f"from this sender."
                )
                # Upsert — update existing preference or create new one
                existing = tree.search(f"email preference {from_email}", limit=1)
                if existing and existing[0].title == title:
                    tree.update(existing[0].memory_id, body=body)
                else:
                    tree.add(
                        title=title,
                        body=body,
                        tier="working",
                        source="email_feedback",
                        importance=0.7,
                    )
            except Exception as _tree_err:
                logger.debug("Memory tree email preference save skipped: %s", _tree_err)

            # Mark as training-eligible for the co-op corpus pipeline.
            # Email preferences are non-PHI preference data — safe to share.
            try:
                from familiar.core.memory_tree import get_memory_tree

                tree = get_memory_tree()
                entries = tree.search(f"email preference {from_email}", limit=1)
                if entries:
                    tree.update(
                        entries[0].memory_id,
                        meta={
                            "training_eligible": "true",
                            "domain": "email_preference",
                            "pair_type": "correction",
                        },
                    )
            except Exception:
                pass

        return jsonify({"changes": changes, "learned": bool(changes)})
    except Exception as e:
        return jsonify({"error": str(e)}), 500


@bp.route("/api/contacts/tiers/refresh", methods=["POST"])
@require_auth
def api_contacts_refresh_tiers():
    """Refresh relationship tiers for all contacts based on engagement."""
    from familiar.skills.contacts.skill import refresh_contact_tiers
    return jsonify(refresh_contact_tiers())


@bp.route("/api/contacts/tiers/<contact_id>", methods=["PUT"])
@require_auth
def api_contacts_set_tier(contact_id):
    """Manually set a contact's relationship tier."""
    data = request.json or {}
    tier = data.get("tier", "")
    from familiar.skills.contacts.skill import set_contact_tier
    return jsonify({"result": set_contact_tier(contact_id, tier)})


@bp.route("/api/contacts/by-tier")
@require_auth
def api_contacts_by_tier():
    """Get contacts grouped by tier."""
    tier = request.args.get("tier", "")
    from familiar.skills.contacts.skill import get_contacts_by_tier, refresh_contact_tiers
    # Auto-refresh tiers on access
    refresh_contact_tiers()
    contacts = get_contacts_by_tier(tier or None)
    tiers = {}
    for c in contacts:
        t = c.get("relationship_tier", "acquaintance")
        tiers.setdefault(t, []).append(c)
    return jsonify({"contacts": contacts, "by_tier": tiers})


@bp.route("/api/contacts/sync-google", methods=["POST"])
@require_auth
def api_contacts_sync_google():
    """Sync contacts from Google People API and record in Memory Tree."""
    try:
        from familiar.skills.contacts.skill import sync_google_contacts

        body = request.json or {}
        result = sync_google_contacts({"auto_vip": body.get("auto_vip", True)})

        # Record sync event in Memory Tree so the agent knows the user's
        # contact network. This is a working-tier fact — always available
        # in the system prompt for context-aware email handling.
        try:
            from familiar.core.memory_tree import get_memory_tree
            from familiar.skills.contacts.skill import _load_data as _load_contacts

            contacts = _load_contacts().get("contacts", [])
            contact_count = len(contacts)
            orgs = set(c.get("organization", "") for c in contacts if c.get("organization"))
            orgs_str = ", ".join(sorted(orgs)[:10]) if orgs else "none listed"

            tree = get_memory_tree()
            title = "Google Contacts network"
            body_text = (
                f"User has {contact_count} contacts synced from Google. "
                f"Key organizations: {orgs_str}. "
                f"All contact emails are VIP senders — emails from known contacts "
                f"should always be prioritized over unknown senders."
            )
            existing = tree.search("Google Contacts network", limit=1)
            if existing and existing[0].title == title:
                tree.update(existing[0].memory_id, body=body_text)
            else:
                tree.add(
                    title=title,
                    body=body_text,
                    tier="working",
                    source="contacts_sync",
                    importance=0.8,
                )
        except Exception as _tree_err:
            logger.debug("Memory tree contacts sync record skipped: %s", _tree_err)

        return jsonify({"result": result})
    except Exception as e:
        return jsonify({"error": str(e)}), 500


@bp.route("/api/email/weekly-digest")
@require_auth
def api_email_weekly_digest():
    """Generate weekly email digest for review."""
    try:
        from familiar.skills.triage.skill import get_weekly_digest

        days = int(request.args.get("days", 7))
        result = get_weekly_digest({"days_back": days})
        return jsonify(result)
    except Exception as e:
        return jsonify({"error": str(e)}), 500
