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

"""
Unified Email Scorer — six-signal weighted classification.

Combines header intelligence, sender reputation, content analysis,
engagement history, user corrections, and contact data into a single
weighted score per category.

Each signal produces a (category, confidence) tuple. The scorer
combines them with configurable weights. The winning category is
the one with the highest weighted score.

Signals:
  S0: Headers     (0.15) — Precedence, X-Mailer, X-Campaign-Id, Return-Path
  S1: Sender      (0.20) — domain lists, dynamic reputation
  S2: Content     (0.15) — keyword matching (+ TF-IDF model in Sprint 4)
  S3: Engagement  (0.25) — opens, replies, trashes per sender
  S4: Corrections (0.20) — explicit user feedback (highest per-signal)
  S5: Contacts    (0.05) — address book, relationship tier
"""

import logging
from typing import Optional

logger = logging.getLogger(__name__)

# Signal weights — must sum to 1.0
WEIGHTS = {
    "header": 0.15,
    "sender": 0.20,
    "content": 0.15,
    "engagement": 0.25,
    "correction": 0.20,
    "contact": 0.05,
}

CATEGORIES = ["Work", "Personal", "Notifications", "Promotional", "Campaign", "Other"]
PRIORITIES = ["urgent", "action", "fyi"]


def score_email(
    sender_email: str,
    sender_name: str,
    subject: str,
    snippet: str = "",
    has_list_unsubscribe: bool = False,
    header_signals: Optional[dict] = None,
) -> dict:
    """Score an email using all 6 signals.

    Returns:
        {
            "category": "Promotional",
            "priority": "fyi",
            "confidence": 0.87,
            "signals": {
                "header": {"category": "Promotional", "confidence": 0.90, "reason": "Precedence: bulk"},
                "sender": {"category": "Promotional", "confidence": 0.85, "reason": "humblebundle.com in promo list"},
                "content": {"category": "Other", "confidence": 0.10, "reason": "No keywords matched"},
                "engagement": {"category": "Promotional", "confidence": 0.80, "reason": "0 opens in 12 received"},
                "correction": {"category": null, "confidence": 0, "reason": "No corrections for this sender"},
                "contact": {"category": null, "confidence": 0, "reason": "Not in contacts"},
            },
        }
    """
    sender_email = (sender_email or "").lower().strip()
    domain = sender_email.split("@")[-1] if "@" in sender_email else ""

    signals = {}
    category_votes = {}  # category → weighted score

    # ── S0: Header Intelligence ───────────────────────────────────────
    signals["header"] = _score_headers(header_signals, has_list_unsubscribe)

    # ── S1: Sender Identity (domain lists + learned) ──────────────────
    signals["sender"] = _score_sender(sender_email, sender_name, domain)

    # ── S2: Content / Keywords ────────────────────────────────────────
    signals["content"] = _score_content(sender_email, sender_name, subject, snippet, has_list_unsubscribe)

    # ── S3: Engagement ────────────────────────────────────────────────
    signals["engagement"] = _score_engagement(sender_email)

    # ── S4: User Corrections ──────────────────────────────────────────
    signals["correction"] = _score_corrections(sender_email, domain)

    # ── S5: Contacts ──────────────────────────────────────────────────
    signals["contact"] = _score_contacts(sender_email)

    # ── Combine weighted scores ───────────────────────────────────────
    for signal_name, signal in signals.items():
        cat = signal.get("category")
        conf = signal.get("confidence", 0)
        weight = WEIGHTS.get(signal_name, 0)
        if cat and conf > 0:
            category_votes[cat] = category_votes.get(cat, 0) + (conf * weight)

    # Pick winner
    if category_votes:
        best_cat = max(category_votes, key=category_votes.get)
        total_score = sum(category_votes.values())
        confidence = round(category_votes[best_cat] / max(total_score, 0.01), 2)
    else:
        best_cat = "Other"
        confidence = 0.10

    # Priority from content analysis
    priority = _determine_priority(subject, best_cat, sender_email)

    return {
        "category": best_cat,
        "priority": priority,
        "confidence": confidence,
        "signals": signals,
    }


# ── Signal Scorers ────────────────────────────────────────────────────────────


def _score_headers(header_signals: Optional[dict], has_list_unsubscribe: bool) -> dict:
    """S0: Score based on email headers."""
    if not header_signals and not has_list_unsubscribe:
        return {"category": None, "confidence": 0, "reason": "No header data"}

    hs = header_signals or {}
    precedence = (hs.get("precedence") or "").lower()
    x_mailer = (hs.get("x_mailer") or "").lower()
    x_campaign = hs.get("x_campaign_id") or ""
    return_path = (hs.get("return_path") or "").lower()

    if precedence in ("bulk", "list", "junk"):
        return {"category": "Promotional", "confidence": 0.90, "reason": f"Precedence: {precedence}"}

    _BULK_MAILERS = (
        "mailchimp", "sendgrid", "constant contact", "hubspot",
        "marketo", "pardot", "activecampaign", "brevo", "klaviyo",
        "mailgun", "sparkpost", "sendinblue", "campaign monitor",
        "drip", "convertkit", "aweber", "getresponse",
    )
    for m in _BULK_MAILERS:
        if m in x_mailer:
            return {"category": "Promotional", "confidence": 0.85, "reason": f"X-Mailer: {m}"}

    if x_campaign:
        return {"category": "Promotional", "confidence": 0.80, "reason": "X-Campaign-Id present"}

    if has_list_unsubscribe:
        return {"category": "Promotional", "confidence": 0.60, "reason": "List-Unsubscribe header"}

    return {"category": None, "confidence": 0, "reason": "No bulk header signals"}


def _score_sender(sender_email: str, sender_name: str, domain: str) -> dict:
    """S1: Score based on sender domain lists and reputation."""
    try:
        from familiar.skills.triage.skill import (
            NOTIFICATION_DOMAINS,
            PROMOTIONAL_DOMAINS,
            _POLITICAL_FUNDRAISING_PATTERNS,
        )

        # Check promotional domains
        if domain in PROMOTIONAL_DOMAINS:
            return {"category": "Promotional", "confidence": 0.85, "reason": f"{domain} in promo list"}
        parts = domain.split(".")
        if len(parts) > 2:
            parent = ".".join(parts[-2:])
            if parent in PROMOTIONAL_DOMAINS:
                return {"category": "Promotional", "confidence": 0.85, "reason": f"{parent} in promo list"}

        # Check notification domains
        if domain in NOTIFICATION_DOMAINS:
            return {"category": "Notifications", "confidence": 0.80, "reason": f"{domain} in notification list"}
        if len(parts) > 2:
            parent = ".".join(parts[-2:])
            if parent in NOTIFICATION_DOMAINS:
                return {"category": "Notifications", "confidence": 0.80, "reason": f"{parent} in notification list"}

        # Political fundraising by sender name
        sender_lower = sender_name.lower()
        if any(pat in sender_lower for pat in _POLITICAL_FUNDRAISING_PATTERNS):
            return {"category": "Promotional", "confidence": 0.75, "reason": f"Political fundraising pattern in sender name"}

    except ImportError:
        pass

    return {"category": None, "confidence": 0, "reason": "Unknown sender domain"}


def _score_content(sender_email: str, sender_name: str, subject: str, snippet: str, has_unsub: bool) -> dict:
    """S2: Score based on content keywords."""
    try:
        from familiar.skills.triage.skill import _categorize_email

        category, confidence = _categorize_email(sender_email, sender_name, subject, snippet, has_unsub)
        if category != "Other":
            return {"category": category, "confidence": confidence, "reason": f"Keyword match → {category}"}
        return {"category": "Other", "confidence": 0.10, "reason": "No keywords matched"}
    except Exception:
        return {"category": "Other", "confidence": 0.10, "reason": "Content scorer unavailable"}


def _score_engagement(sender_email: str) -> dict:
    """S3: Score based on interaction history."""
    try:
        from familiar.skills.email.engagement import get_engagement

        eng = get_engagement(sender_email)
        if not eng:
            return {"category": None, "confidence": 0, "reason": "New sender — no history"}

        score = eng.get("engagement_score", 0)
        total = eng.get("total_received", 0)
        opens = eng.get("opened", 0)
        replies = eng.get("replied", 0)
        trashed = eng.get("trashed", 0)

        if total < 2:
            return {"category": None, "confidence": 0, "reason": f"Too few emails ({total}) for engagement signal"}

        # High engagement → likely important (Work/Personal)
        if score > 2.0:
            cat = "Work" if replies > 0 else "Personal"
            return {
                "category": cat,
                "confidence": min(0.90, score / 5.0),
                "reason": f"High engagement ({replies} replies, {opens} opens in {total} received)",
            }

        # Medium engagement → notifications worth seeing
        if score >= 0.5:
            return {
                "category": "Notifications",
                "confidence": 0.50,
                "reason": f"Medium engagement ({opens} opens in {total} received)",
            }

        # Low engagement + many received → promotional/ignorable
        if total >= 5 and score < 0.3:
            trash_rate = trashed / total if total > 0 else 0
            conf = min(0.85, 0.50 + trash_rate * 0.5)
            return {
                "category": "Promotional",
                "confidence": conf,
                "reason": f"Low engagement ({opens} opens, {trashed} trashed in {total} received)",
            }

        return {"category": None, "confidence": 0, "reason": f"Engagement score {score:.2f} — inconclusive"}

    except Exception:
        return {"category": None, "confidence": 0, "reason": "Engagement data unavailable"}


def _score_corrections(sender_email: str, domain: str) -> dict:
    """S4: Score based on user corrections (highest per-signal weight)."""
    try:
        from familiar.skills.triage.skill import _load_triage_data

        triage = _load_triage_data()

        # Exact sender match
        if sender_email in triage.get("sender_categories", {}):
            cat = triage["sender_categories"][sender_email]
            return {"category": cat, "confidence": 0.95, "reason": f"User corrected {sender_email} → {cat}"}

        # Domain match
        if domain in triage.get("domain_categories", {}):
            cat = triage["domain_categories"][domain]
            return {"category": cat, "confidence": 0.85, "reason": f"User corrected *@{domain} → {cat}"}

    except Exception:
        pass

    return {"category": None, "confidence": 0, "reason": "No corrections for this sender"}


def _score_contacts(sender_email: str) -> dict:
    """S5: Score based on address book presence + relationship tier."""
    try:
        from familiar.skills.triage.skill import _load_triage_data

        triage = _load_triage_data()
        vips = set(triage.get("vip_senders", []))

        if sender_email in vips:
            return {"category": "Work", "confidence": 0.70, "reason": "VIP sender"}

        domain = sender_email.split("@")[-1] if "@" in sender_email else ""
        if domain and domain in vips:
            return {"category": "Work", "confidence": 0.60, "reason": f"VIP domain: @{domain}"}

    except Exception:
        pass

    # Check contacts store with relationship tier
    try:
        from familiar.skills.contacts.skill import _load_data as _load_contacts, _normalize_email

        contacts = _load_contacts().get("contacts", [])
        for c in contacts:
            if _normalize_email(c.get("email", "")) == sender_email:
                tier = c.get("relationship_tier", "acquaintance")
                name = c.get("name", "")
                if tier == "close":
                    return {"category": "Personal", "confidence": 0.80, "reason": f"Close contact: {name}"}
                elif tier == "professional":
                    return {"category": "Work", "confidence": 0.65, "reason": f"Professional contact: {name}"}
                else:
                    return {"category": "Personal", "confidence": 0.40, "reason": f"Acquaintance: {name}"}
    except Exception:
        pass

    return {"category": None, "confidence": 0, "reason": "Not in contacts"}


def _determine_priority(subject: str, category: str, sender_email: str) -> str:
    """Determine priority from subject + category + sender rules."""
    try:
        from familiar.skills.triage.skill import _get_priority
        return _get_priority(subject, category, sender_email)
    except Exception:
        subject_lower = subject.lower()
        if any(w in subject_lower for w in ("urgent", "asap", "immediate", "critical")):
            return "urgent"
        if category in ("Work", "Personal"):
            return "action"
        return "fyi"
