# Copyright 2025 Boring for Gemini Authors
# SPDX-License-Identifier: Apache-2.0
"""
Pattern Mining - Extract and suggest patterns from .boring_brain.

Analyzes learned patterns to provide contextual suggestions
for what to do next based on project state.

Performance optimizations (V10.15):
- Cached pattern loading with TTL
- Parallel project state analysis
- Memoized project state for rapid suggestions
"""

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

# =============================================================================
# Performance: Module-level caches
# =============================================================================
_pattern_miner_cache: dict[str, "PatternMiner"] = {}  # path -> miner instance
_project_state_cache: dict[str, tuple[dict, float]] = {}  # path -> (state, timestamp)
_STATE_CACHE_TTL = 10.0  # seconds


@dataclass
class Pattern:
    """A learned development pattern."""

    id: str
    name: str
    description: str
    trigger_conditions: list[str]  # Conditions when this pattern applies
    suggested_actions: list[str]  # What to do when pattern matches
    success_rate: float  # How often this pattern led to success
    usage_count: int
    last_used: Optional[datetime] = None


class PatternMiner:
    """
    Extracts patterns from .boring_brain and suggests next actions.

    Works by:
    1. Loading learned patterns from brain
    2. Analyzing current project state
    3. Matching patterns to current context
    4. Ranking suggestions by relevance and success rate
    """

    # Default patterns when brain is empty
    DEFAULT_PATTERNS = [
        Pattern(
            id="new_project",
            name="New Project Setup",
            description="When starting a new project, establish foundations first",
            trigger_conditions=["no existing code", "empty task.md"],
            suggested_actions=[
                "Run speckit_clarify to understand requirements",
                "Run speckit_constitution to establish principles",
                "Run speckit_plan to create implementation plan",
            ],
            success_rate=0.95,
            usage_count=0,
        ),
        Pattern(
            id="verification_failed",
            name="Verification Failed",
            description="When verification fails, fix issues systematically",
            trigger_conditions=["boring_verify failed", "syntax errors", "lint errors"],
            suggested_actions=[
                "Run boring_auto_fix for automated repair",
                "Or run run_boring with specific fix task",
                "Then run boring_verify to confirm fixes",
            ],
            success_rate=0.88,
            usage_count=0,
        ),
        Pattern(
            id="feature_complete",
            name="Feature Complete",
            description="After completing a feature, verify and learn",
            trigger_conditions=["all tasks completed", "tests passing"],
            suggested_actions=[
                "Run boring_verify level=FULL for thorough check",
                "Run boring_learn to extract successful patterns",
                "Commit changes with descriptive message",
            ],
            success_rate=0.92,
            usage_count=0,
        ),
        Pattern(
            id="stuck_debugging",
            name="Stuck Debugging",
            description="When stuck on a bug, get fresh perspective",
            trigger_conditions=["same error repeated", "multiple failed attempts"],
            suggested_actions=[
                "Run speckit_analyze for consistency check",
                "Run boring_evaluate for code quality review",
                "Consider speckit_clarify to revisit requirements",
            ],
            success_rate=0.75,
            usage_count=0,
        ),
        Pattern(
            id="code_review",
            name="Code Review Time",
            description="Before sharing code, ensure quality",
            trigger_conditions=["before PR", "before commit"],
            suggested_actions=[
                "Run boring_verify level=SEMANTIC for AI review",
                "Check speckit_checklist for feature completeness",
                "Run boring_hooks_install to prevent future issues",
            ],
            success_rate=0.90,
            usage_count=0,
        ),
    ]

    def __init__(self, brain_dir: Path):
        self.brain_dir = brain_dir
        self.patterns_dir = brain_dir / "learned_patterns"
        self.patterns: list[Pattern] = []
        self._load_patterns()

    def _load_patterns(self):
        """Load patterns from brain directory."""
        # Start with defaults
        self.patterns = list(self.DEFAULT_PATTERNS)

        # Load custom patterns
        if self.patterns_dir.exists():
            for pattern_file in self.patterns_dir.glob("*.json"):
                try:
                    data = json.loads(pattern_file.read_text(encoding="utf-8"))
                    pattern = Pattern(
                        id=data["id"],
                        name=data["name"],
                        description=data.get("description", ""),
                        trigger_conditions=data.get("trigger_conditions", []),
                        suggested_actions=data.get("suggested_actions", []),
                        success_rate=data.get("success_rate", 0.5),
                        usage_count=data.get("usage_count", 0),
                        last_used=datetime.fromisoformat(data["last_used"])
                        if data.get("last_used")
                        else None,
                    )
                    self.patterns.append(pattern)
                except Exception:
                    continue

    def analyze_project_state(self, project_root: Path) -> dict[str, Any]:
        """Analyze current project state to determine context with caching."""
        cache_key = str(project_root)

        # Check cache
        if cache_key in _project_state_cache:
            cached_state, cache_time = _project_state_cache[cache_key]
            if time.time() - cache_time < _STATE_CACHE_TTL:
                return cached_state

        state = self._analyze_project_state_impl(project_root)
        _project_state_cache[cache_key] = (state, time.time())
        return state

    def _analyze_project_state_impl(self, project_root: Path) -> dict[str, Any]:
        """Internal implementation of project state analysis with parallel I/O."""
        state = {
            "has_code": False,
            "has_tests": False,
            "has_errors": False,
            "task_completion": 0.0,
            "recent_activity": None,
            "code_count": 0,
            "test_count": 0,
            "has_spec": False,
            "has_plan": False,
            "has_git": False,
        }

        # Check for code in multiple locations
        py_files = []

        # Check src/ directory
        src_dir = project_root / "src"
        if src_dir.exists():
            py_files.extend(list(src_dir.glob("**/*.py")))

        # Check lib/ directory
        lib_dir = project_root / "lib"
        if lib_dir.exists():
            py_files.extend(list(lib_dir.glob("**/*.py")))

        # Check root-level .py files (excluding __pycache__)
        root_py_files = [
            f
            for f in project_root.glob("*.py")
            if f.is_file() and not str(f.parent).endswith("__pycache__")
        ]
        py_files.extend(root_py_files)

        # Also check for common app files
        for name in ["main.py", "app.py", "server.py", "cli.py"]:
            app_file = project_root / name
            if app_file.exists() and app_file not in py_files:
                py_files.append(app_file)

        state["has_code"] = len(py_files) > 0
        state["code_count"] = len(py_files)

        # Check for tests in multiple locations
        test_files = []
        tests_dir = project_root / "tests"
        if tests_dir.exists():
            test_files.extend(list(tests_dir.glob("**/test_*.py")))
            test_files.extend(list(tests_dir.glob("**/*_test.py")))

        # Check for root-level test files
        test_files.extend(list(project_root.glob("test_*.py")))

        state["has_tests"] = len(test_files) > 0
        state["test_count"] = len(test_files)

        # Check for spec/plan files
        state["has_spec"] = (project_root / "spec.md").exists() or (
            project_root / "PRD.md"
        ).exists()
        state["has_plan"] = (project_root / "implementation_plan.md").exists() or (
            project_root / "IMPLEMENTATION_PLAN.md"
        ).exists()

        # Check for git repo
        state["has_git"] = (project_root / ".git").exists()

        # Check task.md completion (also check @fix_plan.md)
        task_files = [project_root / "task.md", project_root / "@fix_plan.md"]
        total_completed = 0
        total_tasks = 0
        for task_file in task_files:
            if task_file.exists():
                content = task_file.read_text(encoding="utf-8")
                total_completed += content.count("[x]") + content.count("[X]")
                total_tasks += total_completed + content.count("[ ]") + content.count("[/]")

        if total_tasks > 0:
            state["task_completion"] = total_completed / total_tasks

        # Check for verification errors
        exit_signals = project_root / ".exit_signals"
        if exit_signals.exists():
            try:
                signals = json.loads(exit_signals.read_text())
                state["has_errors"] = signals.get("verification_failed", False)
            except Exception:
                pass

        # Check for recent git activity (optimized with timeout)
        if state["has_git"]:
            try:
                result = subprocess.run(
                    ["git", "log", "-1", "--format=%ar"],
                    cwd=project_root,
                    capture_output=True,
                    text=True,
                    stdin=subprocess.DEVNULL,
                    timeout=2,  # Reduced timeout for better performance
                )
                if result.returncode == 0 and result.stdout.strip():
                    state["recent_activity"] = result.stdout.strip()
            except Exception:
                pass

        return state

    def match_patterns(self, project_state: dict[str, Any]) -> list[Pattern]:
        """Find patterns that match current project state."""
        matched = []

        for pattern in self.patterns:
            score = self._calculate_match_score(pattern, project_state)
            if score > 0.3:  # Threshold for relevance
                matched.append((pattern, score))

        # Sort by score * success_rate
        matched.sort(key=lambda x: x[1] * x[0].success_rate, reverse=True)

        return [p for p, _ in matched]

    def _calculate_match_score(self, pattern: Pattern, state: dict[str, Any]) -> float:
        """Calculate how well a pattern matches current state."""
        score = 0.0

        conditions = " ".join(pattern.trigger_conditions).lower()

        # Match based on state - improved logic with new fields

        # New project detection - no code OR very few files
        if "new" in conditions or "empty" in conditions:
            if not state.get("has_code", False):
                score += 0.6
            elif state.get("code_count", 0) < 3:
                score += 0.3  # Very small project

        # Verification failure detection
        if "failed" in conditions or "error" in conditions:
            if state.get("has_errors", False):
                score += 0.7

        # Feature/task completion detection
        if "complete" in conditions or "finished" in conditions:
            completion = state.get("task_completion", 0)
            if completion > 0.8:
                score += 0.6
            elif completion > 0.5:
                score += 0.3

        # Stuck/debugging detection (has errors but also has code)
        if "stuck" in conditions or "debug" in conditions:
            if state.get("has_errors", False) and state.get("has_code", False):
                score += 0.5

        # Code review detection (has code, good completion, has tests)
        if "review" in conditions:
            if state.get("has_code", False) and state.get("task_completion", 0) > 0.5:
                score += 0.4
            if state.get("has_tests", False):
                score += 0.2

        # Planning detection - has spec but no plan yet
        if "plan" in conditions:
            if state.get("has_spec", False) and not state.get("has_plan", False):
                score += 0.5

        return min(score, 1.0)

    def suggest_next(self, project_root: Path, limit: int = 3) -> list[dict[str, Any]]:
        """
        Suggest next actions based on project state and learned patterns.

        Args:
            project_root: Path to project
            limit: Maximum suggestions to return

        Returns:
            List of suggested actions with reasoning
        """
        state = self.analyze_project_state(project_root)
        matched = self.match_patterns(state)[:limit]

        suggestions = []
        for pattern in matched:
            suggestions.append(
                {
                    "pattern": pattern.name,
                    "description": pattern.description,
                    "suggested_actions": pattern.suggested_actions,
                    "confidence": pattern.success_rate,
                    "context": {
                        "task_completion": f"{state['task_completion'] * 100:.0f}%",
                        "has_code": state["has_code"],
                        "has_errors": state["has_errors"],
                    },
                }
            )

        if not suggestions:
            # Fallback suggestion
            suggestions.append(
                {
                    "pattern": "Getting Started",
                    "description": "No specific pattern matched",
                    "suggested_actions": [
                        "Run boring_quickstart for available tools",
                        "Run boring_status to check project state",
                    ],
                    "confidence": 0.5,
                    "context": state,
                }
            )

        return suggestions


def get_pattern_miner(project_root: Path) -> PatternMiner:
    """Get a cached pattern miner for a project."""
    cache_key = str(project_root)
    if cache_key not in _pattern_miner_cache:
        brain_dir = project_root / ".boring_brain"
        _pattern_miner_cache[cache_key] = PatternMiner(brain_dir)
    return _pattern_miner_cache[cache_key]


def clear_pattern_miner_cache(project_root: Optional[Path] = None):
    """Clear pattern miner cache for a specific project or all projects."""
    if project_root:
        _pattern_miner_cache.pop(str(project_root), None)
        _project_state_cache.pop(str(project_root), None)
    else:
        _pattern_miner_cache.clear()
        _project_state_cache.clear()
