"""PMP Topic Predictor — predicts likely-needed topics for proactive priming.

Uses three signals with weighted scoring:
1. Time-of-day patterns (what topics at current hour) — weight 0.3
2. Topic co-occurrence (what follows recently-accessed topics) — weight 0.4
3. Recency bias (recently active topics are more likely needed) — weight 0.3

Pattern analysis runs in the background scheduler (every 5 min).
Predictions read from in-memory cache — purely CPU, never hits DB.
"""

import json
import logging
import time
from datetime import datetime, timezone

from soagila.db.sqlite_init import get_db

logger = logging.getLogger("soagila.pmp.predictor")

# Weights for combining signals
W_TIME = 0.3
W_COOCCURRENCE = 0.4
W_RECENCY = 0.3

# Pattern cache: user_id -> (patterns_dict, expires_at)
_pattern_cache: dict[str, tuple[dict, float]] = {}
PATTERN_CACHE_TTL = 600  # 10 minutes

# Prediction metrics
_prediction_metrics = {
    "total_predictions": 0,
    "predictions_used": 0,
    "last_predicted_topics": [],
    "last_refresh_at": None,
}


class TopicPrediction:
    """A predicted topic with a confidence score."""
    def __init__(self, topic: str, score: float, reason: str):
        self.topic = topic
        self.score = min(max(score, 0.0), 1.0)
        self.reason = reason

    def to_dict(self) -> dict:
        return {
            "topic": self.topic,
            "score": round(self.score, 3),
            "reason": self.reason,
        }


async def refresh_patterns(user_id: str) -> dict:
    """Refresh pattern analysis (called by background scheduler).

    Runs all 3 pattern detector functions and caches results.
    This is the only function that hits the DB.
    """
    from soagila.core.pmp.pattern_detector import (
        detect_usage_patterns,
        detect_topic_cooccurrence,
        detect_time_topic_patterns,
    )

    try:
        usage = await detect_usage_patterns(user_id)
        cooccurrence = await detect_topic_cooccurrence(user_id)
        time_topics = await detect_time_topic_patterns(user_id)
    except Exception as e:
        logger.warning(f"Pattern refresh failed: {e}")
        return {}

    combined = {
        "usage": usage,
        "cooccurrence": cooccurrence,
        "time_topics": time_topics,
        "refreshed_at": datetime.now(timezone.utc).isoformat(),
    }

    _pattern_cache[user_id] = (combined, time.time() + PATTERN_CACHE_TTL)
    _prediction_metrics["last_refresh_at"] = combined["refreshed_at"]

    logger.debug(
        f"Patterns refreshed for {user_id}: "
        f"{len(usage.get('common_topics', []))} topics, "
        f"{cooccurrence.get('total_pairs', 0)} co-occurrences, "
        f"{time_topics.get('hours_with_data', 0)} hours"
    )

    return combined


def _get_cached_patterns(user_id: str) -> dict | None:
    """Get cached patterns (O(1) lookup)."""
    entry = _pattern_cache.get(user_id)
    if entry is None:
        return None
    patterns, expires_at = entry
    if time.time() > expires_at:
        del _pattern_cache[user_id]
        return None
    return patterns


async def predict_topics(
    user_id: str,
    current_hour: int | None = None,
    recent_topics: list[str] | None = None,
    max_predictions: int = 5,
) -> list[TopicPrediction]:
    """Predict which topics the user is likely to need next.

    Combines 3 signals:
    - Time-of-day patterns (weight 0.3)
    - Topic co-occurrence with recent topics (weight 0.4)
    - Recency bias from common topics (weight 0.3)

    Reads from in-memory cache only — no DB queries.
    """
    patterns = _get_cached_patterns(user_id)
    if patterns is None:
        # No cached patterns — trigger refresh if possible
        patterns = await refresh_patterns(user_id)
        if not patterns:
            return []

    if current_hour is None:
        current_hour = datetime.now().hour

    # If no recent topics provided, use the top common topics
    if recent_topics is None:
        usage = patterns.get("usage", {})
        common = usage.get("common_topics", [])
        recent_topics = [t["topic"] for t in common[:5]]

    # Score each candidate topic
    topic_scores: dict[str, dict] = {}  # topic -> {score, reasons}

    # Signal 1: Time-of-day patterns
    time_data = patterns.get("time_topics", {}).get("hour_topics", {})
    hour_topics = time_data.get(current_hour, time_data.get(str(current_hour), []))
    if hour_topics:
        max_count = max(t["count"] for t in hour_topics) if hour_topics else 1
        for t in hour_topics:
            topic = t["topic"]
            score = (t["count"] / max_count) * W_TIME
            if topic not in topic_scores:
                topic_scores[topic] = {"score": 0.0, "reasons": []}
            topic_scores[topic]["score"] += score
            topic_scores[topic]["reasons"].append("time_pattern")

    # Signal 2: Co-occurrence with recent topics
    cooc_data = patterns.get("cooccurrence", {}).get("cooccurrences", [])
    if cooc_data and recent_topics:
        recent_set = set(recent_topics)
        max_count = max(c["count"] for c in cooc_data) if cooc_data else 1
        for c in cooc_data:
            # If topic_a is recent, topic_b is predicted (and vice versa)
            predicted = None
            if c["topic_a"] in recent_set:
                predicted = c["topic_b"]
            elif c["topic_b"] in recent_set:
                predicted = c["topic_a"]

            if predicted and predicted not in recent_set:
                score = (c["count"] / max_count) * W_COOCCURRENCE
                if predicted not in topic_scores:
                    topic_scores[predicted] = {"score": 0.0, "reasons": []}
                topic_scores[predicted]["score"] += score
                topic_scores[predicted]["reasons"].append("cooccurrence")

    # Signal 3: Recency bias (common topics)
    usage = patterns.get("usage", {})
    common_topics = usage.get("common_topics", [])
    if common_topics:
        max_freq = max(t["frequency"] for t in common_topics) if common_topics else 1
        for t in common_topics:
            topic = t["topic"]
            score = (t["frequency"] / max_freq) * W_RECENCY
            if topic not in topic_scores:
                topic_scores[topic] = {"score": 0.0, "reasons": []}
            topic_scores[topic]["score"] += score
            if "recency" not in topic_scores[topic]["reasons"]:
                topic_scores[topic]["reasons"].append("recency")

    # Sort by score and return top predictions
    sorted_topics = sorted(
        topic_scores.items(),
        key=lambda x: x[1]["score"],
        reverse=True,
    )

    predictions = []
    for topic, data in sorted_topics[:max_predictions]:
        reason = "+".join(data["reasons"]) if data["reasons"] else "combined"
        predictions.append(TopicPrediction(
            topic=topic,
            score=data["score"],
            reason=reason,
        ))

    # Track metrics
    _prediction_metrics["total_predictions"] += 1
    _prediction_metrics["last_predicted_topics"] = [p.topic for p in predictions]

    return predictions


async def persist_prediction_metrics():
    """Persist PMP prediction metrics to SQLite."""
    db = await get_db()
    now = datetime.now(timezone.utc).isoformat()

    from soagila.core.pmp.primer import get_prime_utilization
    utilization = get_prime_utilization()

    metrics_json = json.dumps({
        **_prediction_metrics,
        "utilization": round(utilization, 3),
        "updated_at": now,
    })

    await db.execute(
        """INSERT OR REPLACE INTO pmp_metrics
           (id, total_primes, predictions_made, avg_prime_latency_ms,
            last_patterns_json, updated_at)
           VALUES ('pmp_singleton', ?, ?, 0.0, ?, ?)""",
        (
            _prediction_metrics.get("predictions_used", 0),
            _prediction_metrics.get("total_predictions", 0),
            metrics_json,
            now,
        ),
    )
    await db.commit()
    logger.debug("PMP prediction metrics persisted")


async def load_prediction_metrics():
    """Load persisted PMP prediction metrics from SQLite."""
    db = await get_db()
    cursor = await db.execute(
        "SELECT last_patterns_json FROM pmp_metrics WHERE id = 'pmp_singleton'"
    )
    row = await cursor.fetchone()
    if row and row[0]:
        try:
            data = json.loads(row[0])
            _prediction_metrics["total_predictions"] = data.get("total_predictions", 0)
            _prediction_metrics["predictions_used"] = data.get("predictions_used", 0)
            logger.info(f"PMP metrics loaded: {_prediction_metrics['total_predictions']} predictions made")
        except (json.JSONDecodeError, TypeError):
            pass


def get_prediction_metrics() -> dict:
    """Get current prediction metrics."""
    from soagila.core.pmp.primer import get_prime_utilization
    return {
        **_prediction_metrics,
        "utilization": round(get_prime_utilization(), 3),
    }
