# 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
import time
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, safe_error

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": "Failed to read email: " + safe_error(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": "Failed to delete email: " + safe_error(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": safe_error(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": safe_error(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": safe_error(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": safe_error(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": safe_error(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": safe_error(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": safe_error(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": safe_error(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": safe_error(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": safe_error(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": safe_error(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": safe_error(e)}), 500
