"""Pattern storage for learning from proven fixes.

This module provides persistent storage for fix patterns learned from
validated repairs. Patterns are indexed by exception type, stack signature,
and code patterns for fast similarity matching.

Based on TraceSim research: https://arxiv.org/abs/2009.12590
"""

from __future__ import annotations

import json
import logging
import sqlite3
import threading
from contextlib import contextmanager
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

logger = logging.getLogger(__name__)


@dataclass
class StackFrame:
    """A single frame in a stack trace."""

    file: str
    function: str
    line: int
    module: Optional[str] = None

    def to_signature(self) -> str:
        """Convert to a normalized signature for matching."""
        # Normalize file path to just filename
        filename = Path(self.file).name if self.file else "unknown"
        return f"{filename}:{self.function}"

    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> StackFrame:
        return cls(
            file=data.get("file", ""),
            function=data.get("function", ""),
            line=data.get("line", 0),
            module=data.get("module"),
        )


@dataclass
class ExceptionSignature:
    """Signature of an exception for matching."""

    exception_type: str
    message_pattern: str  # Anonymized message pattern
    stack_signature: str  # Normalized stack trace signature
    top_frames: List[StackFrame]  # Top N frames for quick matching

    def to_dict(self) -> Dict[str, Any]:
        return {
            "exception_type": self.exception_type,
            "message_pattern": self.message_pattern,
            "stack_signature": self.stack_signature,
            "top_frames": [f.to_dict() for f in self.top_frames],
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> ExceptionSignature:
        return cls(
            exception_type=data["exception_type"],
            message_pattern=data["message_pattern"],
            stack_signature=data["stack_signature"],
            top_frames=[StackFrame.from_dict(f) for f in data.get("top_frames", [])],
        )


@dataclass
class CodePattern:
    """A code pattern associated with a fix."""

    pattern_type: str  # e.g., "missing_await", "race_condition", "null_check"
    language: str
    code_signature: str  # Anonymized code pattern
    ast_pattern: Optional[Dict[str, Any]] = None  # AST structure pattern

    def to_dict(self) -> Dict[str, Any]:
        return asdict(self)

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> CodePattern:
        return cls(
            pattern_type=data["pattern_type"],
            language=data["language"],
            code_signature=data["code_signature"],
            ast_pattern=data.get("ast_pattern"),
        )


@dataclass
class FixPattern:
    """A learned pattern from a proven fix."""

    id: str
    created_at: str
    updated_at: str

    # Exception info
    exception_signature: ExceptionSignature

    # Code pattern
    code_pattern: Optional[CodePattern]

    # Fix info
    fix_type: str  # e.g., "add_await", "add_null_check", "add_retry"
    fix_description: str
    fix_template: Optional[str]  # Anonymized fix template
    fix_diff_pattern: str  # Anonymized diff pattern

    # Statistics
    success_count: int = 0
    failure_count: int = 0
    total_uses: int = 0

    # Metadata
    source: str = "local"  # "local" or "community"
    contributor_hash: Optional[str] = None  # Anonymized contributor
    tags: List[str] = field(default_factory=list)

    @property
    def success_rate(self) -> float:
        total = self.success_count + self.failure_count
        return self.success_count / total if total > 0 else 0.0

    @property
    def confidence(self) -> float:
        """Calculate confidence based on success rate and sample size."""
        total = self.success_count + self.failure_count
        if total == 0:
            return 0.0

        # Wilson score interval for confidence
        # Higher sample size = more confident in the success rate
        z = 1.96  # 95% confidence
        n = total
        p = self.success_rate

        denominator = 1 + z * z / n
        center = (p + z * z / (2 * n)) / denominator
        spread = z * ((p * (1 - p) + z * z / (4 * n)) / n) ** 0.5 / denominator

        # Lower bound of confidence interval
        return max(0, center - spread)

    def to_dict(self) -> Dict[str, Any]:
        return {
            "id": self.id,
            "created_at": self.created_at,
            "updated_at": self.updated_at,
            "exception_signature": self.exception_signature.to_dict(),
            "code_pattern": self.code_pattern.to_dict() if self.code_pattern else None,
            "fix_type": self.fix_type,
            "fix_description": self.fix_description,
            "fix_template": self.fix_template,
            "fix_diff_pattern": self.fix_diff_pattern,
            "success_count": self.success_count,
            "failure_count": self.failure_count,
            "total_uses": self.total_uses,
            "source": self.source,
            "contributor_hash": self.contributor_hash,
            "tags": self.tags,
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> FixPattern:
        return cls(
            id=data["id"],
            created_at=data["created_at"],
            updated_at=data["updated_at"],
            exception_signature=ExceptionSignature.from_dict(data["exception_signature"]),
            code_pattern=CodePattern.from_dict(data["code_pattern"]) if data.get("code_pattern") else None,
            fix_type=data["fix_type"],
            fix_description=data["fix_description"],
            fix_template=data.get("fix_template"),
            fix_diff_pattern=data["fix_diff_pattern"],
            success_count=data.get("success_count", 0),
            failure_count=data.get("failure_count", 0),
            total_uses=data.get("total_uses", 0),
            source=data.get("source", "local"),
            contributor_hash=data.get("contributor_hash"),
            tags=data.get("tags", []),
        )


@dataclass
class PatternMatch:
    """A match between a failure and a stored pattern."""

    pattern: FixPattern
    similarity_score: float  # 0-1, higher is more similar
    match_details: Dict[str, float]  # Breakdown of similarity components
    suggested_fix: Optional[str] = None  # Instantiated fix for this case

    @property
    def recommendation_score(self) -> float:
        """Combined score factoring in similarity and pattern success rate."""
        return self.similarity_score * 0.6 + self.pattern.confidence * 0.4


class PatternStore:
    """SQLite-backed storage for fix patterns."""

    SCHEMA_VERSION = 1

    def __init__(self, db_path: Optional[Path] = None):
        """Initialize the pattern store.

        Args:
            db_path: Path to SQLite database. Defaults to ~/.autodebug/patterns.db
        """
        if db_path is None:
            db_path = Path.home() / ".autodebug" / "patterns.db"

        db_path.parent.mkdir(parents=True, exist_ok=True)
        self.db_path = db_path
        self._local = threading.local()

        self._init_db()

    def _get_conn(self) -> sqlite3.Connection:
        """Get thread-local database connection."""
        if not hasattr(self._local, "conn") or self._local.conn is None:
            self._local.conn = sqlite3.connect(
                str(self.db_path),
                check_same_thread=False,
            )
            self._local.conn.row_factory = sqlite3.Row
        return self._local.conn

    def close(self) -> None:
        """Close the thread-local database connection."""
        if hasattr(self._local, "conn") and self._local.conn is not None:
            self._local.conn.close()
            self._local.conn = None

    @contextmanager
    def _transaction(self):
        """Context manager for database transactions."""
        conn = self._get_conn()
        try:
            yield conn
            conn.commit()
        except Exception:
            conn.rollback()
            raise

    def _init_db(self) -> None:
        """Initialize database schema."""
        with self._transaction() as conn:
            conn.executescript("""
                CREATE TABLE IF NOT EXISTS schema_version (
                    version INTEGER PRIMARY KEY
                );

                CREATE TABLE IF NOT EXISTS patterns (
                    id TEXT PRIMARY KEY,
                    created_at TEXT NOT NULL,
                    updated_at TEXT NOT NULL,
                    exception_type TEXT NOT NULL,
                    message_pattern TEXT,
                    stack_signature TEXT NOT NULL,
                    code_pattern_type TEXT,
                    fix_type TEXT NOT NULL,
                    fix_description TEXT NOT NULL,
                    fix_template TEXT,
                    fix_diff_pattern TEXT NOT NULL,
                    success_count INTEGER DEFAULT 0,
                    failure_count INTEGER DEFAULT 0,
                    total_uses INTEGER DEFAULT 0,
                    source TEXT DEFAULT 'local',
                    contributor_hash TEXT,
                    data_json TEXT NOT NULL
                );

                CREATE INDEX IF NOT EXISTS idx_patterns_exception_type
                    ON patterns(exception_type);
                CREATE INDEX IF NOT EXISTS idx_patterns_stack_signature
                    ON patterns(stack_signature);
                CREATE INDEX IF NOT EXISTS idx_patterns_fix_type
                    ON patterns(fix_type);
                CREATE INDEX IF NOT EXISTS idx_patterns_code_pattern_type
                    ON patterns(code_pattern_type);

                CREATE TABLE IF NOT EXISTS pattern_tags (
                    pattern_id TEXT NOT NULL,
                    tag TEXT NOT NULL,
                    PRIMARY KEY (pattern_id, tag),
                    FOREIGN KEY (pattern_id) REFERENCES patterns(id)
                );

                CREATE INDEX IF NOT EXISTS idx_pattern_tags_tag
                    ON pattern_tags(tag);

                CREATE TABLE IF NOT EXISTS usage_log (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    pattern_id TEXT NOT NULL,
                    used_at TEXT NOT NULL,
                    success INTEGER NOT NULL,
                    context_json TEXT,
                    FOREIGN KEY (pattern_id) REFERENCES patterns(id)
                );
            """)

            # Check schema version
            cursor = conn.execute("SELECT version FROM schema_version LIMIT 1")
            row = cursor.fetchone()
            if row is None:
                conn.execute(
                    "INSERT INTO schema_version (version) VALUES (?)",
                    (self.SCHEMA_VERSION,)
                )

    def add_pattern(self, pattern: FixPattern) -> str:
        """Add a new pattern to the store.

        Args:
            pattern: The pattern to store

        Returns:
            The pattern ID
        """
        with self._transaction() as conn:
            conn.execute("""
                INSERT OR REPLACE INTO patterns (
                    id, created_at, updated_at, exception_type, message_pattern,
                    stack_signature, code_pattern_type, fix_type, fix_description,
                    fix_template, fix_diff_pattern, success_count, failure_count,
                    total_uses, source, contributor_hash, data_json
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
            """, (
                pattern.id,
                pattern.created_at,
                pattern.updated_at,
                pattern.exception_signature.exception_type,
                pattern.exception_signature.message_pattern,
                pattern.exception_signature.stack_signature,
                pattern.code_pattern.pattern_type if pattern.code_pattern else None,
                pattern.fix_type,
                pattern.fix_description,
                pattern.fix_template,
                pattern.fix_diff_pattern,
                pattern.success_count,
                pattern.failure_count,
                pattern.total_uses,
                pattern.source,
                pattern.contributor_hash,
                json.dumps(pattern.to_dict()),
            ))

            # Add tags
            conn.execute(
                "DELETE FROM pattern_tags WHERE pattern_id = ?",
                (pattern.id,)
            )
            for tag in pattern.tags:
                conn.execute(
                    "INSERT INTO pattern_tags (pattern_id, tag) VALUES (?, ?)",
                    (pattern.id, tag)
                )

        logger.info(f"Added pattern {pattern.id}: {pattern.fix_type}")
        return pattern.id

    def get_pattern(self, pattern_id: str) -> Optional[FixPattern]:
        """Get a pattern by ID."""
        conn = self._get_conn()
        cursor = conn.execute(
            "SELECT data_json FROM patterns WHERE id = ?",
            (pattern_id,)
        )
        row = cursor.fetchone()
        if row:
            return FixPattern.from_dict(json.loads(row["data_json"]))
        return None

    def find_by_exception_type(
        self,
        exception_type: str,
        limit: int = 10,
    ) -> List[FixPattern]:
        """Find patterns matching an exception type."""
        conn = self._get_conn()
        cursor = conn.execute("""
            SELECT data_json FROM patterns
            WHERE exception_type = ?
            ORDER BY success_count DESC, total_uses DESC
            LIMIT ?
        """, (exception_type, limit))

        return [
            FixPattern.from_dict(json.loads(row["data_json"]))
            for row in cursor.fetchall()
        ]

    def find_by_stack_signature(
        self,
        stack_signature: str,
        limit: int = 10,
    ) -> List[FixPattern]:
        """Find patterns matching a stack signature."""
        conn = self._get_conn()
        cursor = conn.execute("""
            SELECT data_json FROM patterns
            WHERE stack_signature = ?
            ORDER BY success_count DESC, total_uses DESC
            LIMIT ?
        """, (stack_signature, limit))

        return [
            FixPattern.from_dict(json.loads(row["data_json"]))
            for row in cursor.fetchall()
        ]

    def find_by_fix_type(
        self,
        fix_type: str,
        limit: int = 10,
    ) -> List[FixPattern]:
        """Find patterns by fix type."""
        conn = self._get_conn()
        cursor = conn.execute("""
            SELECT data_json FROM patterns
            WHERE fix_type = ?
            ORDER BY success_count DESC, total_uses DESC
            LIMIT ?
        """, (fix_type, limit))

        return [
            FixPattern.from_dict(json.loads(row["data_json"]))
            for row in cursor.fetchall()
        ]

    def find_by_tag(self, tag: str, limit: int = 10) -> List[FixPattern]:
        """Find patterns by tag."""
        conn = self._get_conn()
        cursor = conn.execute("""
            SELECT p.data_json FROM patterns p
            JOIN pattern_tags t ON p.id = t.pattern_id
            WHERE t.tag = ?
            ORDER BY p.success_count DESC, p.total_uses DESC
            LIMIT ?
        """, (tag, limit))

        return [
            FixPattern.from_dict(json.loads(row["data_json"]))
            for row in cursor.fetchall()
        ]

    def search(
        self,
        exception_type: Optional[str] = None,
        fix_type: Optional[str] = None,
        code_pattern_type: Optional[str] = None,
        min_success_rate: float = 0.0,
        min_uses: int = 0,
        tags: Optional[List[str]] = None,
        limit: int = 20,
    ) -> List[FixPattern]:
        """Search patterns with multiple criteria."""
        conn = self._get_conn()

        query = "SELECT DISTINCT p.data_json FROM patterns p"
        params: List[Any] = []
        conditions = []

        if tags:
            query += " JOIN pattern_tags t ON p.id = t.pattern_id"
            placeholders = ",".join("?" * len(tags))
            conditions.append(f"t.tag IN ({placeholders})")
            params.extend(tags)

        if exception_type:
            conditions.append("p.exception_type = ?")
            params.append(exception_type)

        if fix_type:
            conditions.append("p.fix_type = ?")
            params.append(fix_type)

        if code_pattern_type:
            conditions.append("p.code_pattern_type = ?")
            params.append(code_pattern_type)

        if min_uses > 0:
            conditions.append("p.total_uses >= ?")
            params.append(min_uses)

        if min_success_rate > 0:
            conditions.append(
                "(p.success_count * 1.0 / NULLIF(p.success_count + p.failure_count, 0)) >= ?"
            )
            params.append(min_success_rate)

        if conditions:
            query += " WHERE " + " AND ".join(conditions)

        query += " ORDER BY p.success_count DESC, p.total_uses DESC LIMIT ?"
        params.append(limit)

        cursor = conn.execute(query, params)
        return [
            FixPattern.from_dict(json.loads(row["data_json"]))
            for row in cursor.fetchall()
        ]

    def record_usage(
        self,
        pattern_id: str,
        success: bool,
        context: Optional[Dict[str, Any]] = None,
    ) -> None:
        """Record that a pattern was used."""
        with self._transaction() as conn:
            now = datetime.now(timezone.utc).isoformat()

            # Get current pattern data
            cursor = conn.execute(
                "SELECT data_json, success_count, failure_count, total_uses FROM patterns WHERE id = ?",
                (pattern_id,)
            )
            row = cursor.fetchone()
            if not row:
                logger.warning(f"Pattern {pattern_id} not found for usage recording")
                return

            # Update counts
            new_success = row["success_count"] + (1 if success else 0)
            new_failure = row["failure_count"] + (0 if success else 1)
            new_total = row["total_uses"] + 1

            # Update the JSON data too
            data = json.loads(row["data_json"])
            data["success_count"] = new_success
            data["failure_count"] = new_failure
            data["total_uses"] = new_total
            data["updated_at"] = now

            # Update pattern stats and JSON
            conn.execute("""
                UPDATE patterns
                SET success_count = ?,
                    failure_count = ?,
                    total_uses = ?,
                    updated_at = ?,
                    data_json = ?
                WHERE id = ?
            """, (new_success, new_failure, new_total, now, json.dumps(data), pattern_id))

            # Log usage
            conn.execute("""
                INSERT INTO usage_log (pattern_id, used_at, success, context_json)
                VALUES (?, ?, ?, ?)
            """, (
                pattern_id,
                now,
                1 if success else 0,
                json.dumps(context) if context else None,
            ))

    def get_stats(self) -> Dict[str, Any]:
        """Get overall statistics about the pattern store."""
        conn = self._get_conn()

        cursor = conn.execute("SELECT COUNT(*) as count FROM patterns")
        total_patterns = cursor.fetchone()["count"]

        cursor = conn.execute("""
            SELECT SUM(success_count) as successes,
                   SUM(failure_count) as failures,
                   SUM(total_uses) as total
            FROM patterns
        """)
        row = cursor.fetchone()

        cursor = conn.execute("""
            SELECT exception_type, COUNT(*) as count
            FROM patterns
            GROUP BY exception_type
            ORDER BY count DESC
            LIMIT 10
        """)
        top_exceptions = [
            {"type": r["exception_type"], "count": r["count"]}
            for r in cursor.fetchall()
        ]

        cursor = conn.execute("""
            SELECT fix_type, COUNT(*) as count
            FROM patterns
            GROUP BY fix_type
            ORDER BY count DESC
            LIMIT 10
        """)
        top_fix_types = [
            {"type": r["fix_type"], "count": r["count"]}
            for r in cursor.fetchall()
        ]

        cursor = conn.execute("""
            SELECT source, COUNT(*) as count
            FROM patterns
            GROUP BY source
        """)
        by_source = {r["source"]: r["count"] for r in cursor.fetchall()}

        return {
            "total_patterns": total_patterns,
            "total_uses": row["total"] or 0,
            "total_successes": row["successes"] or 0,
            "total_failures": row["failures"] or 0,
            "overall_success_rate": (
                (row["successes"] or 0) / ((row["successes"] or 0) + (row["failures"] or 0))
                if (row["successes"] or 0) + (row["failures"] or 0) > 0
                else 0
            ),
            "top_exception_types": top_exceptions,
            "top_fix_types": top_fix_types,
            "by_source": by_source,
        }

    def list_patterns(
        self,
        limit: int = 50,
        offset: int = 0,
        order_by: str = "success_count",
    ) -> List[FixPattern]:
        """List all patterns with pagination."""
        valid_orders = ["success_count", "total_uses", "created_at", "updated_at"]
        if order_by not in valid_orders:
            order_by = "success_count"

        conn = self._get_conn()
        cursor = conn.execute(f"""
            SELECT data_json FROM patterns
            ORDER BY {order_by} DESC
            LIMIT ? OFFSET ?
        """, (limit, offset))

        return [
            FixPattern.from_dict(json.loads(row["data_json"]))
            for row in cursor.fetchall()
        ]

    def delete_pattern(self, pattern_id: str) -> bool:
        """Delete a pattern."""
        with self._transaction() as conn:
            conn.execute(
                "DELETE FROM pattern_tags WHERE pattern_id = ?",
                (pattern_id,)
            )
            cursor = conn.execute(
                "DELETE FROM patterns WHERE id = ?",
                (pattern_id,)
            )
            return cursor.rowcount > 0

    def export_patterns(self, output_path: Path) -> int:
        """Export all patterns to a JSON file."""
        patterns = self.list_patterns(limit=10000)
        data = {
            "version": self.SCHEMA_VERSION,
            "exported_at": datetime.now(timezone.utc).isoformat(),
            "patterns": [p.to_dict() for p in patterns],
        }
        output_path.write_text(json.dumps(data, indent=2))
        return len(patterns)

    def import_patterns(
        self,
        input_path: Path,
        source: str = "imported",
    ) -> Tuple[int, int]:
        """Import patterns from a JSON file.

        Returns:
            Tuple of (imported_count, skipped_count)
        """
        data = json.loads(input_path.read_text())
        patterns = data.get("patterns", [])

        imported = 0
        skipped = 0

        for p_data in patterns:
            try:
                pattern = FixPattern.from_dict(p_data)
                pattern.source = source
                self.add_pattern(pattern)
                imported += 1
            except Exception as e:
                logger.warning(f"Failed to import pattern: {e}")
                skipped += 1

        return imported, skipped


    def discover_emerging_patterns(
        self,
        min_occurrences: int = 3,
        min_success_rate: float = 0.7,
        days: int = 30,
    ) -> List[Dict[str, Any]]:
        """Discover emerging fix patterns from usage data.

        Queries recent usage_log entries, groups by exception_type + fix_type,
        and identifies new combinations that appear frequently with high success.

        Args:
            min_occurrences: Minimum uses to qualify
            min_success_rate: Minimum success rate to qualify
            days: Number of days to look back

        Returns:
            List of discovered pattern dicts
        """
        conn = self._get_conn()
        cutoff = datetime.now(timezone.utc)
        from datetime import timedelta
        cutoff = (cutoff - timedelta(days=days)).isoformat()

        cursor = conn.execute("""
            SELECT
                p.exception_type,
                p.fix_type,
                COUNT(*) as total_uses,
                SUM(CASE WHEN u.success = 1 THEN 1 ELSE 0 END) as successes,
                p.source
            FROM usage_log u
            JOIN patterns p ON u.pattern_id = p.id
            WHERE u.used_at >= ?
            GROUP BY p.exception_type, p.fix_type
            HAVING COUNT(*) >= ? AND
                   (SUM(CASE WHEN u.success = 1 THEN 1 ELSE 0 END) * 1.0 / COUNT(*)) >= ?
            ORDER BY total_uses DESC
        """, (cutoff, min_occurrences, min_success_rate))

        discoveries = []
        for row in cursor.fetchall():
            total = row["total_uses"]
            successes = row["successes"]
            rate = successes / total if total > 0 else 0.0

            discoveries.append({
                "exception_type": row["exception_type"],
                "fix_type": row["fix_type"],
                "total_uses": total,
                "successes": successes,
                "success_rate": rate,
                "source": row["source"] or "local",
                "discovered": True,
            })

        if discoveries:
            logger.info(
                f"Discovered {len(discoveries)} emerging patterns "
                f"(min {min_occurrences} uses, {min_success_rate:.0%} success)"
            )

        return discoveries


# Global store instance
_default_store: Optional[PatternStore] = None
_store_lock = threading.Lock()


def get_pattern_store() -> PatternStore:
    """Get the default pattern store instance."""
    global _default_store
    if _default_store is None:
        with _store_lock:
            if _default_store is None:
                _default_store = PatternStore()
    return _default_store
