"""Service: signal_scorer — Weighted composite scoring per prospect with freshness decay.

Computes a 0.0-1.0 composite signal score for each prospect based on:
- Signal type weights (job_change=0.5, competitor=0.4, etc.)
- Signal confidence from classification
- Freshness decay (signals lose value over time)
- Signal stacking bonus (multiple signal types = compound intent)

Scores are stored in the signal_accounts table for fast dashboard lookups.
"""

from __future__ import annotations

import logging
import time
from typing import Any

from ..constants import (
    COMPOUND_INTENT_LOOKBACK_DAYS,
    COMPOUND_INTENT_MIN_TYPES,
    COMPOUND_INTENT_PATTERNS,
    SIGNAL_COMMENTER_MATCH,
    SIGNAL_COMPETITOR_MENTION,
    SIGNAL_FUNDING_EVENT,
    SIGNAL_HIRING_SURGE,
    SIGNAL_JOB_CHANGE,
    SIGNAL_KEYWORD_MENTION,
    SIGNAL_NEWS_EVENT,
    SIGNAL_POST_ENGAGEMENT,
    SIGNAL_PROFILE_VIEW,
    SIGNAL_PROSPECT_POST,
    SIGNAL_STATUS_CLASSIFIED,
    SIGNAL_STATUS_NEW,
    SIGNAL_WEIGHT_COMMENTER_MATCH,
    SIGNAL_WEIGHT_COMPETITOR_MENTION,
    SIGNAL_WEIGHT_FUNDING_EVENT,
    SIGNAL_WEIGHT_HIRING_SURGE,
    SIGNAL_WEIGHT_JOB_CHANGE,
    SIGNAL_WEIGHT_KEYWORD_MENTION,
    SIGNAL_WEIGHT_NEWS_EVENT,
    SIGNAL_WEIGHT_POST_ENGAGEMENT,
    SIGNAL_WEIGHT_PROFILE_VIEW,
    SIGNAL_WEIGHT_PROSPECT_POST,
)

logger = logging.getLogger(__name__)

# Signal type → base weight mapping
SIGNAL_WEIGHTS: dict[str, float] = {
    SIGNAL_KEYWORD_MENTION: SIGNAL_WEIGHT_KEYWORD_MENTION,
    SIGNAL_PROSPECT_POST: SIGNAL_WEIGHT_PROSPECT_POST,
    SIGNAL_JOB_CHANGE: SIGNAL_WEIGHT_JOB_CHANGE,
    SIGNAL_COMPETITOR_MENTION: SIGNAL_WEIGHT_COMPETITOR_MENTION,
    SIGNAL_HIRING_SURGE: SIGNAL_WEIGHT_HIRING_SURGE,
    SIGNAL_FUNDING_EVENT: SIGNAL_WEIGHT_FUNDING_EVENT,
    SIGNAL_NEWS_EVENT: SIGNAL_WEIGHT_NEWS_EVENT,
    SIGNAL_PROFILE_VIEW: SIGNAL_WEIGHT_PROFILE_VIEW,
    SIGNAL_POST_ENGAGEMENT: SIGNAL_WEIGHT_POST_ENGAGEMENT,
    SIGNAL_COMMENTER_MATCH: SIGNAL_WEIGHT_COMMENTER_MATCH,
}

# Freshness decay thresholds (age_in_days → multiplier)
FRESHNESS_DECAY: list[tuple[int, float]] = [
    (1, 1.0),     # < 1 day: full value
    (3, 0.85),    # 1-3 days
    (7, 0.65),    # 3-7 days
    (14, 0.40),   # 7-14 days
    (30, 0.20),   # 14-30 days
]
# Beyond 30 days: 0.1 (nearly expired)
DECAY_FLOOR = 0.1

# Stacking bonus: having multiple signal types increases score
STACKING_BONUS_THRESHOLD = 2  # 2+ different signal types
STACKING_BONUS_FACTOR = 0.15  # +15% bonus per extra type


def compute_freshness_decay(detected_at: int, now: int | None = None) -> float:
    """Compute freshness decay multiplier for a signal.

    Args:
        detected_at: Unix timestamp when signal was detected.
        now: Current Unix timestamp (defaults to time.time()).

    Returns:
        Decay multiplier between DECAY_FLOOR and 1.0.
    """
    if now is None:
        now = int(time.time())

    age_days = max(0, (now - detected_at) / 86400)

    for threshold_days, multiplier in FRESHNESS_DECAY:
        if age_days <= threshold_days:
            return multiplier

    return DECAY_FLOOR


def compute_signal_score(signal: dict[str, Any], now: int | None = None) -> float:
    """Compute the weighted score for a single signal.

    Formula: weight * confidence * freshness_decay

    Args:
        signal: Signal dict from the DB.
        now: Current Unix timestamp.

    Returns:
        Score between 0.0 and 1.0.
    """
    signal_type = signal.get("signal_type", "")
    confidence = signal.get("confidence", 0.0) or 0.0
    detected_at = signal.get("detected_at", 0) or 0

    # Get base weight for signal type
    weight = SIGNAL_WEIGHTS.get(signal_type, 0.2)

    # Freshness decay
    decay = compute_freshness_decay(detected_at, now)

    # For unclassified signals, use a moderate confidence
    if not confidence or signal.get("status") == SIGNAL_STATUS_NEW:
        confidence = 0.5

    return weight * confidence * decay


def compute_prospect_signal_score(linkedin_id: str, now: int | None = None) -> dict[str, Any]:
    """Compute composite signal score for a prospect across all signal types.

    Formula:
    1. For each signal: score = weight * confidence * freshness_decay
    2. Sum all scores, normalize to [0, 1]
    3. Apply stacking bonus for multiple signal types

    Args:
        linkedin_id: The prospect's LinkedIn provider ID.
        now: Current Unix timestamp.

    Returns:
        Dict with composite_score, total_signals, signal_types, top_signal_type, details.
    """
    from ..db.signal_queries import list_signals

    if now is None:
        now = int(time.time())

    # Get all non-expired signals for this prospect
    signals = list_signals(linkedin_id=linkedin_id, limit=100)

    if not signals:
        return {
            "composite_score": 0.0,
            "total_signals": 0,
            "signal_types": [],
            "top_signal_type": "",
            "details": [],
        }

    # Compute individual scores
    signal_scores: list[dict[str, Any]] = []
    type_set: set[str] = set()
    type_max: dict[str, float] = {}

    for sig in signals:
        # Skip expired signals
        expires_at = sig.get("expires_at", 0) or 0
        if expires_at and expires_at < now:
            continue

        score = compute_signal_score(sig, now)
        sig_type = sig.get("signal_type", "")
        type_set.add(sig_type)

        # Track highest score per type
        if sig_type not in type_max or score > type_max[sig_type]:
            type_max[sig_type] = score

        signal_scores.append({
            "signal_id": sig.get("id", ""),
            "signal_type": sig_type,
            "score": score,
            "confidence": sig.get("confidence", 0),
            "detected_at": sig.get("detected_at", 0),
            "intent": sig.get("intent", ""),
        })

    if not signal_scores:
        return {
            "composite_score": 0.0,
            "total_signals": 0,
            "signal_types": [],
            "top_signal_type": "",
            "details": [],
        }

    # Composite score: sum of best score per type (avoids over-counting same type)
    raw_score = sum(type_max.values())

    # Normalize: max possible is sum of all weights (if every type fired with 1.0 confidence)
    max_possible = sum(SIGNAL_WEIGHTS.values())
    normalized = min(raw_score / max_possible, 1.0) if max_possible > 0 else 0.0

    # Stacking bonus: multiple signal types compound
    num_types = len(type_set)
    if num_types >= STACKING_BONUS_THRESHOLD:
        bonus = (num_types - 1) * STACKING_BONUS_FACTOR
        normalized = min(normalized + bonus, 1.0)

    # Find top signal type
    top_type = max(type_max, key=type_max.get) if type_max else ""

    return {
        "composite_score": round(normalized, 3),
        "total_signals": len(signal_scores),
        "signal_types": sorted(type_set),
        "top_signal_type": top_type,
        "details": sorted(signal_scores, key=lambda x: x["score"], reverse=True)[:10],
    }


def recompute_all_signal_accounts() -> str:
    """Recompute composite scores for all prospects in signal_accounts.

    Called periodically by the scheduler (e.g., every hour) or manually.

    Returns:
        Summary string of results.
    """
    from ..db.signal_queries import list_signal_accounts, upsert_signal_account

    accounts = list_signal_accounts(limit=500)
    if not accounts:
        return "No signal accounts to recompute."

    now = int(time.time())
    updated = 0

    for acct in accounts:
        linkedin_id = acct.get("linkedin_id", "")
        if not linkedin_id:
            continue

        result = compute_prospect_signal_score(linkedin_id, now)

        # Update signal_accounts table
        upsert_signal_account(
            linkedin_id=linkedin_id,
            prospect_name=acct.get("prospect_name", ""),
            company=acct.get("company", ""),
        )

        # Now update the composite score directly
        from ..db.signal_queries import _update_signal_account_score

        _update_signal_account_score(
            linkedin_id=linkedin_id,
            composite_score=result["composite_score"],
            total_signals=result["total_signals"],
            top_signal_type=result["top_signal_type"],
        )
        updated += 1

    summary = f"Recomputed scores for {updated} signal accounts"
    logger.info(summary)
    return summary


# ──────────────────────────────────────────────
# Compound Intent Detection
# ──────────────────────────────────────────────


def detect_compound_intent(linkedin_id: str, now: int | None = None) -> list[dict[str, Any]]:
    """Detect compound intent events from stacked signals for a single prospect.

    Scans all non-expired signals within the lookback window and matches
    against known compound patterns (e.g., job_change + hiring_surge →
    "new_leader_building"). Creates intent_events in the DB for new matches.

    Compound patterns (defined in constants.COMPOUND_INTENT_PATTERNS):
      - job_change + hiring_surge → "new_leader_building" (0.90)
      - competitor_mention + keyword_mention → "active_evaluation" (0.85)
      - prospect_post + commenter_match → "engaged_thought_leader" (0.70)
      - funding_event + hiring_surge → "growth_mode" (0.80)
      - job_change + keyword_mention → "new_role_exploring" (0.80)
      - job_change + competitor_mention → "new_role_evaluating" (0.85)
      - funding_event + keyword_mention → "funded_and_searching" (0.75)
      - competitor_mention + prospect_post → "vocal_evaluator" (0.80)

    Args:
        linkedin_id: The prospect's LinkedIn provider ID.
        now: Current Unix timestamp (defaults to time.time()).

    Returns:
        List of intent event dicts that were created (empty if no new events).
    """
    from ..db.signal_queries import (
        intent_event_exists,
        list_signals,
        save_intent_event,
    )

    if now is None:
        now = int(time.time())

    # Fetch signals within the lookback window
    cutoff = now - (COMPOUND_INTENT_LOOKBACK_DAYS * 86400)
    signals = list_signals(linkedin_id=linkedin_id, limit=200)

    # Filter to recent, non-expired signals
    recent_signals: list[dict[str, Any]] = []
    for sig in signals:
        detected_at = sig.get("detected_at", 0) or 0
        expires_at = sig.get("expires_at", 0) or 0
        if detected_at >= cutoff and (not expires_at or expires_at > now):
            recent_signals.append(sig)

    if not recent_signals:
        return []

    # Group signals by type
    by_type: dict[str, list[dict[str, Any]]] = {}
    for sig in recent_signals:
        sig_type = sig.get("signal_type", "")
        if sig_type:
            by_type.setdefault(sig_type, []).append(sig)

    # Need at least 2 distinct signal types for any compound event
    if len(by_type) < COMPOUND_INTENT_MIN_TYPES:
        return []

    # Check against known compound patterns
    prospect_types = set(by_type.keys())
    created_events: list[dict[str, Any]] = []

    for pattern_types, (event_type, score_boost) in COMPOUND_INTENT_PATTERNS.items():
        # Check if the prospect has all signal types in this pattern
        if not pattern_types.issubset(prospect_types):
            continue

        # Dedup: skip if this compound event was already created recently
        if intent_event_exists(linkedin_id, event_type):
            continue

        # Gather contributing signal IDs and types
        contributing_ids: list[str] = []
        contributing_types: list[str] = []
        company: str | None = None

        for sig_type in pattern_types:
            # Pick the highest-confidence signal of this type
            type_signals = by_type[sig_type]
            best_sig = max(
                type_signals,
                key=lambda s: (s.get("confidence", 0) or 0, s.get("detected_at", 0) or 0),
            )
            sig_id = best_sig.get("id", "")
            if sig_id:
                contributing_ids.append(sig_id)
            contributing_types.append(sig_type)

            # Extract company from any signal that has it
            if not company:
                meta = best_sig.get("metadata_json")
                if meta:
                    try:
                        import json
                        meta_dict = json.loads(meta)
                        company = meta_dict.get("company") or meta_dict.get("new_company")
                    except (ValueError, TypeError):
                        pass

        # Create the intent event
        event_id = save_intent_event(
            linkedin_id=linkedin_id,
            event_type=event_type,
            signal_ids=contributing_ids,
            signal_types=contributing_types,
            composite_score=score_boost,
            company=company,
        )

        event = {
            "id": event_id,
            "linkedin_id": linkedin_id,
            "event_type": event_type,
            "signal_ids": contributing_ids,
            "signal_types": contributing_types,
            "composite_score": score_boost,
            "company": company,
        }
        created_events.append(event)
        logger.info(
            "Compound intent detected: %s for %s (score=%.2f, signals=%s)",
            event_type, linkedin_id, score_boost, contributing_types,
        )

    return created_events


def detect_all_compound_intents() -> str:
    """Scan all signal accounts for compound intent events.

    Called periodically by the scheduler. Iterates through accounts
    with 2+ signal types and runs detect_compound_intent() on each.

    Returns:
        Summary string of results.
    """
    from ..db.signal_queries import list_signal_accounts

    accounts = list_signal_accounts(limit=500)
    if not accounts:
        return "No signal accounts to scan for compound intents."

    now = int(time.time())
    total_events = 0
    scanned = 0

    for acct in accounts:
        linkedin_id = acct.get("linkedin_id", "")
        if not linkedin_id:
            continue

        # Quick check: need at least 2 signals to form a compound
        total_signals = acct.get("total_signals", 0)
        if total_signals < COMPOUND_INTENT_MIN_TYPES:
            continue

        scanned += 1
        try:
            events = detect_compound_intent(linkedin_id, now)
            total_events += len(events)
        except Exception as e:
            logger.warning("Compound intent detection failed for %s: %s", linkedin_id, e)

    summary = (
        f"Scanned {scanned} accounts for compound intents, "
        f"created {total_events} new intent events"
    )
    logger.info(summary)
    return summary
