"""
Rio Local — User Pattern Model (ML)

Lightweight local ML model that learns from user behavior patterns to:
1. Predict when the user is likely to struggle
2. Adapt to user's preferred languages/frameworks
3. Track error frequency and types
4. Learn time-of-day activity patterns
5. Feed learned context into Gemini prompts

Uses SQLite for persistence and numpy for statistical modeling.
No heavy ML dependencies required — pure numpy/sqlite3.
"""

from __future__ import annotations

import json
import os
import sqlite3
import time
from collections import Counter, defaultdict
from dataclasses import dataclass, field
from typing import Optional

import numpy as np
import structlog

log = structlog.get_logger(__name__)

DEFAULT_DB_PATH = os.path.join(os.path.dirname(__file__), "..", "data", "rio_patterns.db")

# Time-of-day buckets (4-hour windows)
TIME_BUCKETS = {
    0: "late_night",     # 00:00-03:59
    1: "early_morning",  # 04:00-07:59
    2: "morning",        # 08:00-11:59
    3: "afternoon",      # 12:00-15:59
    4: "evening",        # 16:00-19:59
    5: "night",          # 20:00-23:59
}

# Common programming languages to detect
LANGUAGE_PATTERNS = {
    "python": [".py", "def ", "import ", "class ", "async ", "await "],
    "javascript": [".js", "const ", "let ", "function ", "=>", "require("],
    "typescript": [".ts", ".tsx", "interface ", "type ", ": string", ": number"],
    "rust": [".rs", "fn ", "let mut", "impl ", "pub fn", "match "],
    "go": [".go", "func ", "package ", "import (", "go func"],
    "java": [".java", "public class", "private ", "void ", "System.out"],
    "c_cpp": [".c", ".cpp", ".h", "#include", "int main", "printf"],
    "html_css": [".html", ".css", "<div", "<span", "display:", "margin:"],
    "sql": [".sql", "SELECT ", "INSERT ", "CREATE TABLE", "JOIN "],
    "shell": [".sh", "#!/bin/bash", "echo ", "export ", "alias "],
}

# Common error types to track
ERROR_CATEGORIES = {
    "syntax": ["SyntaxError", "unexpected token", "parse error"],
    "type": ["TypeError", "NoneType", "undefined is not", "type mismatch"],
    "import": ["ImportError", "ModuleNotFoundError", "cannot find module"],
    "runtime": ["RuntimeError", "segmentation fault", "core dumped"],
    "network": ["ConnectionError", "timeout", "ECONNREFUSED", "404", "500"],
    "permission": ["PermissionError", "EACCES", "forbidden", "unauthorized"],
    "memory": ["MemoryError", "out of memory", "OOM", "heap"],
    "build": ["build failed", "compile error", "make error", "cargo error"],
    "test": ["test failed", "assertion", "AssertionError", "FAIL"],
    "git": ["merge conflict", "CONFLICT", "detached HEAD", "rebase"],
}


@dataclass
class UserProfile:
    """Aggregated user behavior profile."""
    total_sessions: int = 0
    total_messages: int = 0
    total_struggles: int = 0
    avg_session_minutes: float = 0.0
    preferred_languages: list[str] = field(default_factory=list)
    common_errors: list[str] = field(default_factory=list)
    peak_hours: list[str] = field(default_factory=list)
    struggle_rate: float = 0.0  # struggles per hour
    help_acceptance_rate: float = 0.0  # % of proactive help accepted


@dataclass
class PredictionContext:
    """Context generated by the pattern model for Gemini prompts."""
    user_languages: str = ""
    recent_errors: str = ""
    struggle_prediction: float = 0.0
    time_context: str = ""
    suggestions: str = ""


class UserPatternModel:
    """Learns user behavior patterns from interaction history.
    
    Usage::
    
        model = UserPatternModel()
        
        # Record events
        model.record_activity("coding", {"language": "python", "file": "main.py"})
        model.record_error("TypeError: 'NoneType' has no attribute 'strip'")
        model.record_struggle(confidence=0.87, signals=["repeated_error"])
        model.record_help_response(accepted=True)
        
        # Get predictions
        context = model.get_prediction_context()
        profile = model.get_user_profile()
    """

    def __init__(self, db_path: str = DEFAULT_DB_PATH) -> None:
        self.db_path = os.path.abspath(db_path)
        os.makedirs(os.path.dirname(self.db_path) or ".", exist_ok=True)
        
        self._conn = sqlite3.connect(
            self.db_path,
            check_same_thread=False,
        )
        self._conn.execute("PRAGMA journal_mode=WAL")
        self._conn.execute("PRAGMA synchronous=NORMAL")
        
        self._create_tables()
        
        # In-memory caches for fast access
        self._language_counter = Counter()
        self._error_counter = Counter()
        self._hourly_activity = defaultdict(int)
        self._struggle_times: list[float] = []
        self._help_responses: list[bool] = []
        
        # Load historical data into memory
        self._load_caches()
        
        log.info(
            "user_pattern.init",
            db_path=self.db_path,
            total_events=self._total_events(),
        )

    def _create_tables(self) -> None:
        """Create pattern tracking tables."""
        self._conn.executescript("""
            CREATE TABLE IF NOT EXISTS activities (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                event_type TEXT NOT NULL,
                timestamp REAL NOT NULL,
                hour_bucket INTEGER,
                metadata TEXT DEFAULT '{}'
            );
            
            CREATE TABLE IF NOT EXISTS errors (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                error_text TEXT NOT NULL,
                category TEXT,
                language TEXT,
                timestamp REAL NOT NULL,
                metadata TEXT DEFAULT '{}'
            );
            
            CREATE TABLE IF NOT EXISTS struggles (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                confidence REAL NOT NULL,
                signals TEXT,
                timestamp REAL NOT NULL,
                context_text TEXT
            );
            
            CREATE TABLE IF NOT EXISTS help_responses (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                accepted INTEGER NOT NULL,
                timestamp REAL NOT NULL,
                context TEXT
            );
            
            CREATE TABLE IF NOT EXISTS language_detections (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                language TEXT NOT NULL,
                confidence REAL DEFAULT 1.0,
                timestamp REAL NOT NULL,
                source TEXT
            );
            
            CREATE INDEX IF NOT EXISTS idx_activities_ts ON activities(timestamp);
            CREATE INDEX IF NOT EXISTS idx_errors_ts ON errors(timestamp);
            CREATE INDEX IF NOT EXISTS idx_struggles_ts ON struggles(timestamp);
        """)
        self._conn.commit()

    def _load_caches(self) -> None:
        """Load historical data into in-memory caches for fast access."""
        # Language detections (last 30 days)
        cutoff = time.time() - 30 * 86400
        rows = self._conn.execute(
            "SELECT language, COUNT(*) FROM language_detections WHERE timestamp > ? GROUP BY language",
            (cutoff,),
        ).fetchall()
        for lang, count in rows:
            self._language_counter[lang] = count
        
        # Error categories (last 7 days)
        cutoff = time.time() - 7 * 86400
        rows = self._conn.execute(
            "SELECT category, COUNT(*) FROM errors WHERE timestamp > ? AND category IS NOT NULL GROUP BY category",
            (cutoff,),
        ).fetchall()
        for cat, count in rows:
            self._error_counter[cat] = count
        
        # Hourly activity (last 7 days)
        rows = self._conn.execute(
            "SELECT hour_bucket, COUNT(*) FROM activities WHERE timestamp > ? GROUP BY hour_bucket",
            (cutoff,),
        ).fetchall()
        for hour, count in rows:
            self._hourly_activity[hour] = count
        
        # Recent struggles
        rows = self._conn.execute(
            "SELECT timestamp FROM struggles WHERE timestamp > ? ORDER BY timestamp",
            (cutoff,),
        ).fetchall()
        self._struggle_times = [r[0] for r in rows]
        
        # Help responses
        rows = self._conn.execute(
            "SELECT accepted FROM help_responses WHERE timestamp > ?",
            (cutoff,),
        ).fetchall()
        self._help_responses = [bool(r[0]) for r in rows]

    def _total_events(self) -> int:
        """Total event count across all tables."""
        count = 0
        for table in ("activities", "errors", "struggles", "help_responses"):
            row = self._conn.execute(f"SELECT COUNT(*) FROM {table}").fetchone()
            count += row[0] if row else 0
        return count

    # ------------------------------------------------------------------
    # Recording methods
    # ------------------------------------------------------------------

    def record_activity(
        self,
        event_type: str,
        metadata: Optional[dict] = None,
    ) -> None:
        """Record a user activity event."""
        now = time.time()
        import datetime
        hour = datetime.datetime.fromtimestamp(now).hour
        hour_bucket = hour // 4
        
        self._conn.execute(
            "INSERT INTO activities (event_type, timestamp, hour_bucket, metadata) VALUES (?, ?, ?, ?)",
            (event_type, now, hour_bucket, json.dumps(metadata or {})),
        )
        self._conn.commit()
        self._hourly_activity[hour_bucket] += 1

    def record_error(
        self,
        error_text: str,
        metadata: Optional[dict] = None,
    ) -> None:
        """Record an error occurrence and categorize it."""
        now = time.time()
        category = self._categorize_error(error_text)
        language = self._detect_language_from_error(error_text)
        
        self._conn.execute(
            "INSERT INTO errors (error_text, category, language, timestamp, metadata) VALUES (?, ?, ?, ?, ?)",
            (error_text[:500], category, language, now, json.dumps(metadata or {})),
        )
        self._conn.commit()
        
        if category:
            self._error_counter[category] += 1
        log.debug("user_pattern.error_recorded", category=category, language=language)

    def record_struggle(
        self,
        confidence: float,
        signals: list[str],
        context_text: str = "",
    ) -> None:
        """Record a struggle detection event."""
        now = time.time()
        self._conn.execute(
            "INSERT INTO struggles (confidence, signals, timestamp, context_text) VALUES (?, ?, ?, ?)",
            (confidence, json.dumps(signals), now, context_text[:500]),
        )
        self._conn.commit()
        self._struggle_times.append(now)
        log.debug("user_pattern.struggle_recorded", confidence=confidence)

    def record_help_response(
        self,
        accepted: bool,
        context: str = "",
    ) -> None:
        """Record whether the user accepted proactive help."""
        now = time.time()
        self._conn.execute(
            "INSERT INTO help_responses (accepted, timestamp, context) VALUES (?, ?, ?)",
            (int(accepted), now, context[:200]),
        )
        self._conn.commit()
        self._help_responses.append(accepted)

    def record_language(
        self,
        text: str,
        source: str = "screen",
    ) -> None:
        """Detect and record programming language from text content."""
        detected = self._detect_languages(text)
        now = time.time()
        
        for lang, confidence in detected:
            self._conn.execute(
                "INSERT INTO language_detections (language, confidence, timestamp, source) VALUES (?, ?, ?, ?)",
                (lang, confidence, now, source),
            )
            self._language_counter[lang] += 1
        
        if detected:
            self._conn.commit()

    # ------------------------------------------------------------------
    # Prediction methods
    # ------------------------------------------------------------------

    def get_prediction_context(self) -> PredictionContext:
        """Generate a prediction context for Gemini prompts.
        
        Returns contextual information about the user's patterns
        that can be injected into Gemini's system prompt for more
        personalized assistance.
        """
        ctx = PredictionContext()
        
        # User's preferred languages
        top_langs = self._language_counter.most_common(3)
        if top_langs:
            ctx.user_languages = ", ".join(lang for lang, _ in top_langs)
        
        # Recent error patterns
        top_errors = self._error_counter.most_common(3)
        if top_errors:
            ctx.recent_errors = ", ".join(
                f"{cat} ({count}x)" for cat, count in top_errors
            )
        
        # Struggle prediction based on historical patterns
        ctx.struggle_prediction = self._predict_struggle_probability()
        
        # Time-of-day context
        import datetime
        hour = datetime.datetime.now().hour
        bucket = hour // 4
        bucket_name = TIME_BUCKETS.get(bucket, "unknown")
        activity_level = self._hourly_activity.get(bucket, 0)
        ctx.time_context = f"{bucket_name} (typical activity: {'high' if activity_level > 10 else 'normal'})"
        
        # Suggestions based on patterns
        suggestions = []
        if self._error_counter.get("import", 0) > 3:
            suggestions.append("User frequently has import errors — suggest checking virtual environments")
        if self._error_counter.get("type", 0) > 3:
            suggestions.append("User prone to type errors — suggest type hints")
        if self._help_responses:
            accept_rate = sum(self._help_responses) / len(self._help_responses)
            if accept_rate < 0.3:
                suggestions.append("User rarely accepts proactive help — be less intrusive")
            elif accept_rate > 0.7:
                suggestions.append("User appreciates proactive help — be more proactive")
        ctx.suggestions = "; ".join(suggestions) if suggestions else ""
        
        return ctx

    def get_user_profile(self) -> UserProfile:
        """Get an aggregated user behavior profile."""
        profile = UserProfile()
        
        # Session count
        row = self._conn.execute("SELECT COUNT(DISTINCT session_id) FROM activities WHERE event_type = 'session_start'").fetchone()
        profile.total_sessions = row[0] if row else 0
        
        # Message count
        row = self._conn.execute("SELECT COUNT(*) FROM activities WHERE event_type = 'message'").fetchone()  
        profile.total_messages = row[0] if row else 0
        
        # Struggle count
        profile.total_struggles = len(self._struggle_times)
        
        # Preferred languages
        profile.preferred_languages = [
            lang for lang, _ in self._language_counter.most_common(5)
        ]
        
        # Common errors
        profile.common_errors = [
            cat for cat, _ in self._error_counter.most_common(5)
        ]
        
        # Peak hours
        if self._hourly_activity:
            sorted_hours = sorted(
                self._hourly_activity.items(), key=lambda x: x[1], reverse=True
            )
            profile.peak_hours = [
                TIME_BUCKETS.get(h, str(h)) for h, _ in sorted_hours[:3]
            ]
        
        # Help acceptance rate
        if self._help_responses:
            profile.help_acceptance_rate = sum(self._help_responses) / len(self._help_responses)
        
        return profile

    def get_context_string(self) -> str:
        """Get a formatted context string for injection into Gemini prompts."""
        ctx = self.get_prediction_context()
        parts = []
        
        if ctx.user_languages:
            parts.append(f"User's preferred languages: {ctx.user_languages}")
        if ctx.recent_errors:
            parts.append(f"Recent error patterns: {ctx.recent_errors}")
        if ctx.time_context:
            parts.append(f"Current time context: {ctx.time_context}")
        if ctx.suggestions:
            parts.append(f"Behavioral notes: {ctx.suggestions}")
        if ctx.struggle_prediction > 0.5:
            parts.append(f"Struggle likelihood: {ctx.struggle_prediction:.0%} (elevated)")
        
        return "\n".join(parts) if parts else ""

    # ------------------------------------------------------------------
    # Internal analysis methods
    # ------------------------------------------------------------------

    def _predict_struggle_probability(self) -> float:
        """Predict probability of struggle based on historical patterns.
        
        Uses a simple Bayesian-like approach:
        - Base rate from recent struggle frequency
        - Modulated by time-of-day pattern
        - Modulated by recent error frequency
        """
        if not self._struggle_times:
            return 0.1  # Base rate
        
        now = time.time()
        
        # Base rate: struggles per hour in last 7 days
        week_ago = now - 7 * 86400
        recent_struggles = [t for t in self._struggle_times if t > week_ago]
        hours_active = max(1.0, len(self._hourly_activity) * 4)  # rough hours
        base_rate = min(1.0, len(recent_struggles) / hours_active)
        
        # Time-of-day modulation
        import datetime
        hour = datetime.datetime.now().hour
        bucket = hour // 4
        bucket_activity = self._hourly_activity.get(bucket, 0)
        total_activity = sum(self._hourly_activity.values()) or 1
        time_factor = bucket_activity / total_activity if total_activity > 0 else 0.167
        
        # Error recency modulation
        day_ago = now - 86400
        recent_errors = sum(1 for cat, count in self._error_counter.items() if count > 0)
        error_factor = min(1.0, recent_errors / 5)
        
        # Combined prediction
        probability = np.clip(
            base_rate * 0.4 + time_factor * 0.3 + error_factor * 0.3,
            0.0, 1.0,
        )
        
        return float(probability)

    def _categorize_error(self, error_text: str) -> Optional[str]:
        """Categorize an error by matching against known patterns."""
        text_lower = error_text.lower()
        for category, patterns in ERROR_CATEGORIES.items():
            for pattern in patterns:
                if pattern.lower() in text_lower:
                    return category
        return None

    def _detect_language_from_error(self, error_text: str) -> Optional[str]:
        """Try to detect the programming language from an error message."""
        text_lower = error_text.lower()
        if "python" in text_lower or "traceback" in text_lower:
            return "python"
        if "node" in text_lower or "npm" in text_lower:
            return "javascript"
        if "cargo" in text_lower or "rustc" in text_lower:
            return "rust"
        if "java" in text_lower and "javascript" not in text_lower:
            return "java"
        if "gcc" in text_lower or "g++" in text_lower:
            return "c_cpp"
        return None

    def _detect_languages(self, text: str) -> list[tuple[str, float]]:
        """Detect programming languages from text content."""
        if not text or len(text) < 10:
            return []
        
        results = []
        text_lower = text.lower()
        
        for lang, patterns in LANGUAGE_PATTERNS.items():
            matches = sum(1 for p in patterns if p.lower() in text_lower)
            if matches >= 2:
                confidence = min(1.0, matches / len(patterns))
                results.append((lang, confidence))
        
        return sorted(results, key=lambda x: x[1], reverse=True)[:3]

    def close(self) -> None:
        """Close the database connection."""
        try:
            self._conn.close()
        except Exception:
            pass
