# 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.
"""

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


@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."""
        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
        if state["has_git"]:
            try:
                import subprocess

                result = subprocess.run(
                    ["git", "log", "-1", "--format=%ar"],
                    cwd=project_root,
                    capture_output=True,
                    text=True,
                    stdin=subprocess.DEVNULL,
                )
                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 pattern miner for a project."""
    brain_dir = project_root / ".boring_brain"
    return PatternMiner(brain_dir)
