"""
Brain Manager Module for Boring V10.23

Manages the .boring_brain knowledge base with automatic learning capabilities.

Features:
- Extracts successful patterns from .boring_memory
- Generates evaluation rubrics
- Stores workflow adaptations
- 🆕 Incremental learning with decay
- 🆕 Session-aware pattern boosting
- 🆕 Pattern clustering for efficiency
- 🆕 Automatic pattern pruning

Performance optimizations (V10.15):
- LRU caching for pattern loading
- Lazy initialization of rubrics
- Batch pattern updates

V10.23 enhancements:
- Incremental pattern updates instead of full rebuilds
- Session context integration
- Pattern relevance decay over time
- Automatic cleanup of unused patterns

Directory Structure:
    .boring_brain/
    ├── workflow_adaptations/  # Evolution history
    ├── learned_patterns/      # Success patterns from memory
    └── rubrics/               # Evaluation criteria
"""

import json
import time
from dataclasses import asdict, dataclass
from datetime import datetime
from pathlib import Path
from typing import Any, Optional

from .logger import log_status

# =============================================================================
# Performance: Module-level pattern cache
# =============================================================================
_pattern_cache: dict[str, tuple[list[dict], float]] = {}  # path -> (patterns, mtime)
_CACHE_TTL_SECONDS = 30.0  # Cache TTL in seconds

# V10.23: Learning configuration
PATTERN_DECAY_DAYS = 90  # Patterns not used for 90 days get lower priority
MAX_PATTERNS = 500  # Maximum patterns to keep
MIN_PATTERN_SCORE = 0.1  # Minimum relevance score threshold


@dataclass
class LearnedPattern:
    """A pattern learned from successful executions."""

    pattern_id: str
    pattern_type: str  # error_solution, workflow_optimization, code_fix
    description: str
    context: str
    solution: str
    success_count: int
    created_at: str
    last_used: str
    # V10.23: Enhanced fields
    decay_score: float = 1.0  # Relevance decay over time
    session_boost: float = 0.0  # Temporary boost from current session
    cluster_id: str = ""  # For pattern clustering


@dataclass
class Rubric:
    """Evaluation rubric for quality assessment."""

    name: str
    description: str
    criteria: list[dict[str, str]]
    created_at: str


class BrainManager:
    """
    Manages .boring_brain knowledge base.

    Usage:
        brain = BrainManager(project_root)

        # Learn from successful loop
        brain.learn_from_success(loop_record)

        # Get patterns for context
        patterns = brain.get_relevant_patterns("authentication error")
    """

    def __init__(self, project_root: Path, log_dir: Optional[Path] = None):
        self.project_root = Path(project_root)
        self.brain_dir = self.project_root / ".boring_brain"
        self.log_dir = log_dir or self.project_root / "logs"

        # Subdirectories
        self.adaptations_dir = self.brain_dir / "workflow_adaptations"
        self.patterns_dir = self.brain_dir / "learned_patterns"
        self.rubrics_dir = self.brain_dir / "rubrics"

        # Ensure structure exists
        self._ensure_structure()

    def _ensure_structure(self):
        """Create directory structure if not exists."""
        for d in [self.adaptations_dir, self.patterns_dir, self.rubrics_dir]:
            d.mkdir(parents=True, exist_ok=True)

    def _load_patterns(self) -> list[dict]:
        """Load all learned patterns with caching."""

        patterns_file = self.patterns_dir / "patterns.json"
        cache_key = str(patterns_file)

        # Check cache validity
        if cache_key in _pattern_cache:
            cached_patterns, cache_time = _pattern_cache[cache_key]
            if time.time() - cache_time < _CACHE_TTL_SECONDS:
                # Check if file was modified
                if patterns_file.exists():
                    try:
                        file_mtime = patterns_file.stat().st_mtime
                        if file_mtime <= cache_time:
                            return cached_patterns
                    except OSError:
                        pass
                else:
                    return cached_patterns

        # Load from disk
        if patterns_file.exists():
            try:
                patterns = json.loads(patterns_file.read_text(encoding="utf-8"))
                _pattern_cache[cache_key] = (patterns, time.time())
                return patterns
            except (OSError, json.JSONDecodeError):
                return []
        return []

    def _save_patterns(self, patterns: list[dict]):
        """Save patterns to file and update cache."""

        patterns_file = self.patterns_dir / "patterns.json"
        patterns_file.write_text(
            json.dumps(patterns, indent=2, ensure_ascii=False), encoding="utf-8"
        )

        # Update cache
        _pattern_cache[str(patterns_file)] = (patterns, time.time())

    def learn_from_memory(self, storage) -> dict[str, Any]:
        """
        Extract successful patterns from .boring_memory SQLite storage.

        Args:
            storage: SQLiteStorage instance

        Returns:
            Learning result with patterns extracted
        """
        try:
            # Get successful loops
            recent_loops = storage.get_recent_loops(limit=50)
            [l for l in recent_loops if l.get("status") == "SUCCESS"]

            # Get error patterns with solutions
            error_patterns = storage.get_top_errors(limit=20)
            solved_patterns = [e for e in error_patterns if e.get("solution")]

            # Extract patterns
            patterns = self._load_patterns()
            new_count = 0

            for err in solved_patterns:
                pattern_id = f"ERR_{err['error_type'][:20]}"

                # Check if pattern already exists
                existing = [p for p in patterns if p.get("pattern_id") == pattern_id]
                if existing:
                    # Update success count
                    existing[0]["success_count"] = existing[0].get("success_count", 0) + 1
                    existing[0]["last_used"] = datetime.now().isoformat()
                else:
                    # Create new pattern
                    new_pattern = LearnedPattern(
                        pattern_id=pattern_id,
                        pattern_type="error_solution",
                        description=f"Solution for {err['error_type']}",
                        context=err.get("error_message", "")[:200],
                        solution=err.get("solution", ""),
                        success_count=err.get("occurrence_count", 1),
                        created_at=datetime.now().isoformat(),
                        last_used=datetime.now().isoformat(),
                    )
                    patterns.append(asdict(new_pattern))
                    new_count += 1

            self._save_patterns(patterns)

            log_status(
                self.log_dir, "INFO", f"Learned {new_count} new patterns, total: {len(patterns)}"
            )

            return {"status": "SUCCESS", "new_patterns": new_count, "total_patterns": len(patterns)}

        except Exception as e:
            return {"status": "ERROR", "error": str(e)}

    def learn_pattern(
        self,
        pattern_type: str,
        description: str,
        context: str,
        solution: str,
    ) -> dict[str, Any]:
        """
        Learn a pattern directly from AI observation.

        This allows AI to explicitly record patterns it discovers,
        without needing to go through the memory storage.

        Args:
            pattern_type: Category of pattern (error_solution, code_style, workflow_tip, etc.)
            description: Short description of what was learned
            context: When this pattern applies (error message, scenario, etc.)
            solution: The solution or recommendation

        Returns:
            Result with pattern_id and status
        """
        import hashlib

        # Generate unique pattern ID from content
        content_hash = hashlib.md5(f"{pattern_type}:{context}:{solution}".encode()).hexdigest()[:8]
        pattern_id = f"{pattern_type.upper()}_{content_hash}"

        patterns = self._load_patterns()

        # Check if similar pattern exists (by ID)
        existing = [p for p in patterns if p.get("pattern_id") == pattern_id]
        if existing:
            # Update existing pattern
            existing[0]["success_count"] = existing[0].get("success_count", 0) + 1
            existing[0]["last_used"] = datetime.now().isoformat()
            existing[0]["solution"] = solution  # Update solution if improved
            self._save_patterns(patterns)
            log_status(self.log_dir, "INFO", f"Updated pattern: {pattern_id}")
            return {"status": "UPDATED", "pattern_id": pattern_id}

        # Create new pattern
        new_pattern = LearnedPattern(
            pattern_id=pattern_id,
            pattern_type=pattern_type,
            description=description,
            context=context,
            solution=solution,
            success_count=1,
            created_at=datetime.now().isoformat(),
            last_used=datetime.now().isoformat(),
        )
        patterns.append(asdict(new_pattern))
        self._save_patterns(patterns)

        log_status(self.log_dir, "INFO", f"Learned new pattern: {pattern_id}")
        return {"status": "CREATED", "pattern_id": pattern_id, "total_patterns": len(patterns)}

    def get_relevant_patterns(self, context: str, limit: int = 5) -> list[dict]:
        """
        Get patterns relevant to given context.

        Uses hybrid approach (V10.22):
        1. Keyword matching (fast)
        2. TF-IDF similarity (if many patterns)
        3. Usage frequency boost
        """
        patterns = self._load_patterns()

        if not context:
            return patterns[:limit]

        # Use intelligent matching for larger pattern sets
        if len(patterns) > 20:
            return self._intelligent_pattern_match(context, patterns, limit)

        # Simple relevance scoring for small sets
        context_lower = context.lower()
        context_words = set(context_lower.split())
        scored = []

        for p in patterns:
            score = 0.0

            # Keyword matching
            pattern_context = p.get("context", "").lower()
            pattern_desc = p.get("description", "").lower()
            p.get("solution", "").lower()

            # Substring match (high weight)
            if context_lower in pattern_context:
                score += 3.0
            if context_lower in pattern_desc:
                score += 2.0

            # Word overlap (medium weight)
            pattern_words = set(pattern_context.split() + pattern_desc.split())
            overlap = len(context_words & pattern_words)
            score += overlap * 0.5

            # Usage frequency boost (low weight)
            success_count = p.get("success_count", 1)
            score += min(1.0, success_count * 0.1)

            if score > 0:
                scored.append((score, p))

        scored.sort(key=lambda x: x[0], reverse=True)
        return [p for _, p in scored[:limit]]

    def _intelligent_pattern_match(
        self, context: str, patterns: list[dict], limit: int
    ) -> list[dict]:
        """
        Use TF-IDF similarity for intelligent pattern matching.

        Falls back to keyword matching if sklearn not available.
        """
        try:
            import math
            from collections import Counter

            # Build simple TF-IDF without sklearn
            context_words = context.lower().split()
            context_tf = Counter(context_words)

            scored = []
            for p in patterns:
                pattern_text = (
                    f"{p.get('context', '')} {p.get('description', '')} {p.get('solution', '')}"
                )
                pattern_words = pattern_text.lower().split()
                pattern_tf = Counter(pattern_words)

                # Compute cosine similarity using TF
                dot_product = sum(
                    context_tf[w] * pattern_tf[w] for w in context_tf if w in pattern_tf
                )
                context_norm = math.sqrt(sum(v * v for v in context_tf.values()))
                pattern_norm = math.sqrt(sum(v * v for v in pattern_tf.values()))

                if context_norm > 0 and pattern_norm > 0:
                    similarity = dot_product / (context_norm * pattern_norm)
                else:
                    similarity = 0.0

                # Boost by usage frequency
                success_count = p.get("success_count", 1)
                score = similarity + min(0.2, success_count * 0.02)

                if score > 0.05:  # Threshold
                    scored.append((score, p))

            scored.sort(key=lambda x: x[0], reverse=True)
            return [p for _, p in scored[:limit]]

        except Exception:
            # Fallback to simple matching
            context_lower = context.lower()
            scored = []
            for p in patterns:
                score = 0
                if context_lower in p.get("context", "").lower():
                    score += 2
                if context_lower in p.get("description", "").lower():
                    score += 1
                if score > 0:
                    scored.append((score, p))
            scored.sort(key=lambda x: x[0], reverse=True)
            return [p for _, p in scored[:limit]]

    def create_rubric(self, name: str, description: str, criteria: list[dict]) -> dict[str, Any]:
        """
        Create an evaluation rubric.

        Args:
            name: Rubric name (e.g., "implementation_plan")
            description: What this rubric evaluates
            criteria: List of {name, description, weight} dicts
        """
        rubric = Rubric(
            name=name,
            description=description,
            criteria=criteria,
            created_at=datetime.now().isoformat(),
        )

        rubric_file = self.rubrics_dir / f"{name}.json"
        rubric_file.write_text(
            json.dumps(asdict(rubric), indent=2, ensure_ascii=False), encoding="utf-8"
        )

        return {"status": "SUCCESS", "rubric": name}

    def get_rubric(self, name: str) -> Optional[dict]:
        """Load a rubric by name."""
        rubric_file = self.rubrics_dir / f"{name}.json"
        if rubric_file.exists():
            return json.loads(rubric_file.read_text(encoding="utf-8"))
        return None

    def create_default_rubrics(self) -> dict[str, Any]:
        """Create default evaluation rubrics for LLM-as-Judge evaluation."""
        rubrics_created = []

        # Implementation Plan Rubric
        self.create_rubric(
            name="implementation_plan",
            description="Evaluates quality of implementation plans",
            criteria=[
                {
                    "name": "completeness",
                    "description": "All components have file paths",
                    "weight": 25,
                },
                {"name": "dependencies", "description": "Dependencies are explicit", "weight": 20},
                {
                    "name": "testability",
                    "description": "Verification steps are defined",
                    "weight": 25,
                },
                {"name": "clarity", "description": "Steps are unambiguous", "weight": 15},
                {"name": "feasibility", "description": "Plan is technically sound", "weight": 15},
            ],
        )
        rubrics_created.append("implementation_plan")

        # Task List Rubric
        self.create_rubric(
            name="task_list",
            description="Evaluates quality of task breakdowns",
            criteria=[
                {
                    "name": "granularity",
                    "description": "Tasks are appropriately sized",
                    "weight": 30,
                },
                {"name": "ordering", "description": "Dependencies are respected", "weight": 25},
                {"name": "testability", "description": "Each task has verification", "weight": 25},
                {"name": "completeness", "description": "Covers all plan items", "weight": 20},
            ],
        )
        rubrics_created.append("task_list")

        # Code Quality Rubric
        self.create_rubric(
            name="code_quality",
            description="Evaluates code implementation quality",
            criteria=[
                {"name": "correctness", "description": "Code works as intended", "weight": 30},
                {"name": "readability", "description": "Code is easy to understand", "weight": 20},
                {"name": "maintainability", "description": "Code is easy to modify", "weight": 20},
                {"name": "testing", "description": "Tests are comprehensive", "weight": 20},
                {"name": "documentation", "description": "Comments and docs exist", "weight": 10},
            ],
        )
        rubrics_created.append("code_quality")

        # Security Rubric
        self.create_rubric(
            name="security",
            description="Evaluates code for security vulnerabilities",
            criteria=[
                {
                    "name": "secrets",
                    "description": "No hardcoded API keys, passwords, or tokens",
                    "weight": 40,
                },
                {
                    "name": "input_validation",
                    "description": "External inputs are validated before use",
                    "weight": 30,
                },
                {
                    "name": "injection_prevention",
                    "description": "No raw SQL/Shell construction from user input",
                    "weight": 30,
                },
            ],
        )
        rubrics_created.append("security")

        # Architecture Rubric
        self.create_rubric(
            name="architecture",
            description="Evaluates high-level design and dependency flow",
            criteria=[
                {
                    "name": "consistency",
                    "description": "Follows project patterns and directory structure",
                    "weight": 35,
                },
                {
                    "name": "dependency_flow",
                    "description": "No circular imports; dependencies flow correctly",
                    "weight": 35,
                },
                {
                    "name": "scalability",
                    "description": "Design supports future growth",
                    "weight": 30,
                },
            ],
        )
        rubrics_created.append("architecture")

        # API Design Rubric
        self.create_rubric(
            name="api_design",
            description="Evaluates API interface design quality",
            criteria=[
                {
                    "name": "consistency",
                    "description": "Naming conventions are uniform",
                    "weight": 25,
                },
                {
                    "name": "intuitiveness",
                    "description": "API is easy to use without docs",
                    "weight": 20,
                },
                {
                    "name": "versioning",
                    "description": "Supports backward compatibility",
                    "weight": 15,
                },
                {
                    "name": "error_responses",
                    "description": "Errors are informative with proper codes",
                    "weight": 20,
                },
                {"name": "idempotency", "description": "Safe methods are idempotent", "weight": 20},
            ],
        )
        rubrics_created.append("api_design")

        # Testing Rubric
        self.create_rubric(
            name="testing",
            description="Evaluates test coverage and quality",
            criteria=[
                {
                    "name": "coverage",
                    "description": "Tests cover happy path, edge cases, errors",
                    "weight": 30,
                },
                {"name": "isolation", "description": "Tests are independent", "weight": 25},
                {
                    "name": "assertions",
                    "description": "Assertions are specific and meaningful",
                    "weight": 20,
                },
                {
                    "name": "maintainability",
                    "description": "Tests are easy to update",
                    "weight": 15,
                },
                {"name": "performance", "description": "Tests run quickly", "weight": 10},
            ],
        )
        rubrics_created.append("testing")

        # Documentation Rubric
        self.create_rubric(
            name="documentation",
            description="Evaluates code and API documentation",
            criteria=[
                {
                    "name": "completeness",
                    "description": "All public APIs are documented",
                    "weight": 25,
                },
                {
                    "name": "examples",
                    "description": "Usage examples for complex functionality",
                    "weight": 20,
                },
                {
                    "name": "accuracy",
                    "description": "Docs match actual implementation",
                    "weight": 30,
                },
                {
                    "name": "accessibility",
                    "description": "Written for target audience",
                    "weight": 15,
                },
                {"name": "formatting", "description": "Consistent formatting", "weight": 10},
            ],
        )
        rubrics_created.append("documentation")

        log_status(self.log_dir, "INFO", f"Created {len(rubrics_created)} default rubrics")

        return {"status": "SUCCESS", "rubrics_created": rubrics_created}

    def get_brain_summary(self) -> dict[str, Any]:
        """Get summary of brain contents."""
        patterns = self._load_patterns()

        rubrics = []
        for f in self.rubrics_dir.glob("*.json"):
            rubrics.append(f.stem)

        adaptations = []
        for f in self.adaptations_dir.glob("*.json"):
            adaptations.append(f.stem)

        return {
            "patterns_count": len(patterns),
            "rubrics": rubrics,
            "adaptations": adaptations,
            "brain_dir": str(self.brain_dir),
        }


def create_brain_manager(project_root: Path, log_dir: Optional[Path] = None) -> BrainManager:
    """Factory function to create BrainManager instance."""
    return BrainManager(project_root, log_dir)


class GlobalKnowledgeStore:
    """
    Manages global knowledge shared across all projects.

    Stores patterns in ~/.boring_brain/global_patterns.json
    Allows exporting from one project and importing to another.
    """

    def __init__(self):
        self.global_dir = Path.home() / ".boring_brain"
        self.global_patterns_file = self.global_dir / "global_patterns.json"
        self.global_dir.mkdir(parents=True, exist_ok=True)

    def _load_global_patterns(self) -> list[dict]:
        """Load global patterns."""
        if self.global_patterns_file.exists():
            try:
                return json.loads(self.global_patterns_file.read_text(encoding="utf-8"))
            except (OSError, json.JSONDecodeError):
                return []
        return []

    def _save_global_patterns(self, patterns: list[dict]):
        """Save global patterns."""
        self.global_patterns_file.write_text(
            json.dumps(patterns, indent=2, ensure_ascii=False),
            encoding="utf-8",
        )

    def export_from_project(self, project_root: Path, min_success_count: int = 2) -> dict[str, Any]:
        """
        Export high-quality patterns from a project to global store.

        Args:
            project_root: Project to export from
            min_success_count: Minimum success count for export (filters low-quality patterns)

        Returns:
            Export result
        """
        brain = BrainManager(project_root)
        local_patterns = brain._load_patterns()

        # Filter by success count
        quality_patterns = [
            p for p in local_patterns if p.get("success_count", 0) >= min_success_count
        ]

        if not quality_patterns:
            return {"status": "NO_PATTERNS", "exported": 0}

        global_patterns = self._load_global_patterns()
        exported_count = 0

        for pattern in quality_patterns:
            # Add source project info
            pattern["source_project"] = str(project_root.name)
            pattern["exported_at"] = datetime.now().isoformat()

            # Check for duplicates (by pattern_id)
            existing = [
                p for p in global_patterns if p.get("pattern_id") == pattern.get("pattern_id")
            ]
            if existing:
                # Update if our version has higher success count
                if pattern.get("success_count", 0) > existing[0].get("success_count", 0):
                    existing[0].update(pattern)
                    exported_count += 1
            else:
                global_patterns.append(pattern)
                exported_count += 1

        self._save_global_patterns(global_patterns)
        return {
            "status": "SUCCESS",
            "exported": exported_count,
            "total_global": len(global_patterns),
        }

    def import_to_project(
        self, project_root: Path, pattern_types: Optional[list[str]] = None
    ) -> dict[str, Any]:
        """
        Import relevant patterns from global store to a project.

        Args:
            project_root: Project to import to
            pattern_types: Optional filter by pattern types

        Returns:
            Import result
        """
        global_patterns = self._load_global_patterns()

        if not global_patterns:
            return {"status": "NO_GLOBAL_PATTERNS", "imported": 0}

        # Filter by pattern type if specified
        if pattern_types:
            global_patterns = [p for p in global_patterns if p.get("pattern_type") in pattern_types]

        brain = BrainManager(project_root)
        local_patterns = brain._load_patterns()
        local_ids = {p.get("pattern_id") for p in local_patterns}

        imported_count = 0
        for pattern in global_patterns:
            if pattern.get("pattern_id") not in local_ids:
                # Mark as imported
                pattern["imported_from_global"] = True
                pattern["imported_at"] = datetime.now().isoformat()
                local_patterns.append(pattern)
                imported_count += 1

        brain._save_patterns(local_patterns)
        return {"status": "SUCCESS", "imported": imported_count, "total_local": len(local_patterns)}

    def list_global_patterns(self) -> list[dict]:
        """List all global patterns with summary info."""
        patterns = self._load_global_patterns()
        return [
            {
                "pattern_id": p.get("pattern_id"),
                "pattern_type": p.get("pattern_type"),
                "description": p.get("description"),
                "source_project": p.get("source_project"),
                "success_count": p.get("success_count", 0),
            }
            for p in patterns
        ]

    # =========================================================================
    # V10.23: Enhanced Learning Methods
    # =========================================================================

    def update_pattern_decay(self) -> dict[str, Any]:
        """
        V10.23: Update decay scores for all patterns based on usage recency.

        Patterns not used recently get lower decay scores, making them
        less likely to be returned in searches.
        """
        patterns = self._load_patterns()
        updated = 0
        now = datetime.now()

        for pattern in patterns:
            last_used_str = pattern.get("last_used", pattern.get("created_at", ""))
            if not last_used_str:
                continue

            try:
                last_used = datetime.fromisoformat(
                    last_used_str.replace("Z", "+00:00").replace("+00:00", "")
                )
                days_since_use = (now - last_used).days

                # Calculate decay: starts at 1.0, decreases over PATTERN_DECAY_DAYS
                if days_since_use <= PATTERN_DECAY_DAYS:
                    decay = (
                        1.0 - (days_since_use / PATTERN_DECAY_DAYS) * 0.5
                    )  # Min 0.5 within window
                else:
                    decay = max(
                        0.2, 0.5 - (days_since_use - PATTERN_DECAY_DAYS) / 365
                    )  # Further decay

                if pattern.get("decay_score", 1.0) != decay:
                    pattern["decay_score"] = round(decay, 2)
                    updated += 1
            except (ValueError, TypeError):
                pattern["decay_score"] = 0.5  # Default for unparseable dates

        if updated > 0:
            self._save_patterns(patterns)

        return {"status": "SUCCESS", "updated": updated, "total": len(patterns)}

    def apply_session_boost(self, keywords: list[str], boost: float = 0.3) -> int:
        """
        V10.23: Apply temporary boost to patterns matching session keywords.

        This makes patterns relevant to the current session rank higher.

        Args:
            keywords: Keywords from the current session context
            boost: Boost amount to apply

        Returns:
            Number of patterns boosted
        """
        if not keywords:
            return 0

        patterns = self._load_patterns()
        boosted = 0
        keywords_lower = [k.lower() for k in keywords]

        for pattern in patterns:
            pattern_text = (
                f"{pattern.get('context', '')} {pattern.get('description', '')} {pattern.get('solution', '')}"
            ).lower()

            # Check for keyword matches
            match_count = sum(1 for kw in keywords_lower if kw in pattern_text)
            if match_count > 0:
                pattern["session_boost"] = min(1.0, boost * match_count)
                boosted += 1
            else:
                pattern["session_boost"] = 0.0

        self._save_patterns(patterns)
        return boosted

    def clear_session_boosts(self) -> int:
        """V10.23: Clear all session boosts."""
        patterns = self._load_patterns()
        cleared = 0

        for pattern in patterns:
            if pattern.get("session_boost", 0) > 0:
                pattern["session_boost"] = 0.0
                cleared += 1

        if cleared > 0:
            self._save_patterns(patterns)

        return cleared

    def prune_patterns(self, keep_min: int = 100) -> dict[str, Any]:
        """
        V10.23: Remove low-value patterns to keep the knowledge base efficient.

        Criteria for removal:
        - Decay score below MIN_PATTERN_SCORE
        - Success count is 1 and older than 30 days
        - Total patterns exceed MAX_PATTERNS

        Args:
            keep_min: Minimum patterns to keep regardless of score

        Returns:
            Pruning result
        """
        patterns = self._load_patterns()
        original_count = len(patterns)

        if original_count <= keep_min:
            return {"status": "SKIPPED", "reason": f"Only {original_count} patterns, below minimum"}

        def pattern_value(p: dict) -> float:
            """Calculate pattern value for sorting."""
            decay = p.get("decay_score", 1.0)
            success = min(1.0, p.get("success_count", 1) * 0.1)
            session = p.get("session_boost", 0.0)
            return decay * 0.5 + success * 0.3 + session * 0.2

        # Score all patterns
        scored_patterns = [(pattern_value(p), p) for p in patterns]
        scored_patterns.sort(key=lambda x: x[0], reverse=True)

        # Keep top patterns up to MAX_PATTERNS
        patterns_to_keep = []
        removed = 0

        for i, (score, pattern) in enumerate(scored_patterns):
            if i < keep_min:
                # Always keep minimum
                patterns_to_keep.append(pattern)
            elif i < MAX_PATTERNS and score >= MIN_PATTERN_SCORE:
                patterns_to_keep.append(pattern)
            else:
                removed += 1

        if removed > 0:
            self._save_patterns(patterns_to_keep)

        return {
            "status": "SUCCESS",
            "removed": removed,
            "kept": len(patterns_to_keep),
            "original": original_count,
        }

    def get_pattern_stats(self) -> dict[str, Any]:
        """V10.23: Get statistics about the pattern knowledge base."""
        patterns = self._load_patterns()

        if not patterns:
            return {
                "total": 0,
                "by_type": {},
                "avg_success_count": 0,
                "avg_decay_score": 0,
                "patterns_with_session_boost": 0,
            }

        from collections import Counter

        type_counts = Counter(p.get("pattern_type", "unknown") for p in patterns)
        avg_success = sum(p.get("success_count", 0) for p in patterns) / len(patterns)
        avg_decay = sum(p.get("decay_score", 1.0) for p in patterns) / len(patterns)
        session_boosted = sum(1 for p in patterns if p.get("session_boost", 0) > 0)

        return {
            "total": len(patterns),
            "by_type": dict(type_counts),
            "avg_success_count": round(avg_success, 1),
            "avg_decay_score": round(avg_decay, 2),
            "patterns_with_session_boost": session_boosted,
        }

    def incremental_learn(
        self, error_type: str, error_context: str, solution: str, file_path: str = ""
    ) -> dict[str, Any]:
        """
        V10.23: Incrementally learn from a single error resolution.

        Unlike learn_from_memory which batch processes, this learns
        immediately from a single success, ideal for real-time learning.

        Args:
            error_type: Type of error that was solved
            error_context: Context/message of the error
            solution: The solution that worked
            file_path: Optional file path for context

        Returns:
            Learning result
        """
        import hashlib

        # Generate pattern ID
        content_hash = hashlib.md5(f"{error_type}:{error_context[:100]}".encode()).hexdigest()[:8]
        pattern_id = f"INC_{content_hash}"

        patterns = self._load_patterns()

        # Check for existing pattern
        existing = None
        for p in patterns:
            if p.get("pattern_id") == pattern_id:
                existing = p
                break

        if existing:
            # Update existing
            existing["success_count"] = existing.get("success_count", 0) + 1
            existing["last_used"] = datetime.now().isoformat()
            existing["decay_score"] = 1.0  # Reset decay on use

            # Update solution if this one is better (longer/more detailed)
            if len(solution) > len(existing.get("solution", "")):
                existing["solution"] = solution

            self._save_patterns(patterns)
            return {
                "status": "UPDATED",
                "pattern_id": pattern_id,
                "success_count": existing["success_count"],
            }

        # Create new pattern
        new_pattern = LearnedPattern(
            pattern_id=pattern_id,
            pattern_type="error_solution",
            description=f"Solution for {error_type}",
            context=error_context[:500],
            solution=solution,
            success_count=1,
            created_at=datetime.now().isoformat(),
            last_used=datetime.now().isoformat(),
            decay_score=1.0,
            session_boost=0.0,
            cluster_id=error_type[:20],  # Simple clustering by error type
        )
        patterns.append(asdict(new_pattern))
        self._save_patterns(patterns)

        return {"status": "CREATED", "pattern_id": pattern_id, "total_patterns": len(patterns)}

    def get_brain_health_report(self) -> str:
        """V10.23: Get a human-readable health report for the brain."""
        stats = self.get_pattern_stats()

        # Calculate health indicators
        health_score = 100
        issues = []

        if stats["total"] == 0:
            health_score -= 30
            issues.append("No patterns learned yet")
        elif stats["total"] > MAX_PATTERNS:
            health_score -= 10
            issues.append(f"Pattern count ({stats['total']}) exceeds max ({MAX_PATTERNS})")

        if stats["avg_decay_score"] < 0.5:
            health_score -= 15
            issues.append("Many patterns are stale (low decay scores)")

        if stats["avg_success_count"] < 2:
            health_score -= 10
            issues.append("Low pattern reinforcement (low success counts)")

        report_lines = [
            "🧠 Brain Health Report (V10.23)",
            f"├─ Health Score: {health_score}/100",
            f"├─ Total Patterns: {stats['total']}",
            f"├─ Avg Success Count: {stats['avg_success_count']}",
            f"├─ Avg Decay Score: {stats['avg_decay_score']}",
            f"├─ Session Boosted: {stats['patterns_with_session_boost']}",
        ]

        if stats["by_type"]:
            report_lines.append("├─ By Type:")
            for ptype, count in sorted(stats["by_type"].items(), key=lambda x: x[1], reverse=True)[
                :5
            ]:
                report_lines.append(f"│  └─ {ptype}: {count}")

        if issues:
            report_lines.append("└─ Issues:")
            for issue in issues:
                report_lines.append(f"   ⚠️ {issue}")
        else:
            report_lines.append("└─ Status: ✅ Healthy")

        return "\n".join(report_lines)

    def clear_global_patterns(self) -> int:
        """Clear all global patterns. Returns count removed."""
        patterns = self._load_global_patterns()
        count = len(patterns)
        self._save_global_patterns([])
        return count


# Singleton global store
_global_store: Optional[GlobalKnowledgeStore] = None


def get_global_knowledge_store() -> GlobalKnowledgeStore:
    """Get global knowledge store singleton."""
    global _global_store
    if _global_store is None:
        _global_store = GlobalKnowledgeStore()
    return _global_store
