"""Error Pattern Learning module for Oclawma.

This module provides error logging, pattern detection, and auto-correction
capabilities to help the system learn from mistakes and improve over time.
"""

from __future__ import annotations

import json
import re
import sqlite3
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from pathlib import Path
from re import Pattern
from typing import Any, Callable

from oclawma.tools.base import ToolResult


@dataclass
class ErrorRecord:
    """Record of a tool execution error."""

    tool_name: str
    error_message: str
    error_type: str
    timestamp: datetime
    context: dict[str, Any] = field(default_factory=dict)
    suggestion_applied: str | None = None
    resolved: bool = False

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary."""
        return {
            "tool_name": self.tool_name,
            "error_message": self.error_message,
            "error_type": self.error_type,
            "timestamp": self.timestamp.isoformat(),
            "context": self.context,
            "suggestion_applied": self.suggestion_applied,
            "resolved": self.resolved,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> ErrorRecord:
        """Create from dictionary."""
        return cls(
            tool_name=data["tool_name"],
            error_message=data["error_message"],
            error_type=data["error_type"],
            timestamp=datetime.fromisoformat(data["timestamp"]),
            context=data.get("context", {}),
            suggestion_applied=data.get("suggestion_applied"),
            resolved=data.get("resolved", False),
        )


@dataclass
class ErrorPattern:
    """Detected error pattern with metadata."""

    name: str
    category: str
    severity: str  # 'low', 'medium', 'high', 'critical'
    regex_pattern: str
    suggestion: str
    occurrences: int = 0
    first_seen: datetime | None = None
    last_seen: datetime | None = None
    auto_correct: bool = False
    correction_fn: str | None = None  # Name of correction function

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary."""
        return {
            "name": self.name,
            "category": self.category,
            "severity": self.severity,
            "regex_pattern": self.regex_pattern,
            "suggestion": self.suggestion,
            "occurrences": self.occurrences,
            "first_seen": self.first_seen.isoformat() if self.first_seen else None,
            "last_seen": self.last_seen.isoformat() if self.last_seen else None,
            "auto_correct": self.auto_correct,
            "correction_fn": self.correction_fn,
        }


# Built-in error patterns
BUILTIN_PATTERNS: list[ErrorPattern] = [
    ErrorPattern(
        name="file_not_found",
        category="file_access",
        severity="medium",
        regex_pattern=r"(?:ENOENT|no such file|File not found|does not exist)",
        suggestion="Verify file path exists before operation. Check for typos.",
        auto_correct=True,
        correction_fn="suggest_file_path_correction",
    ),
    ErrorPattern(
        name="permission_denied",
        category="permissions",
        severity="high",
        regex_pattern=r"(?:EACCES|permission denied|Permission denied)",
        suggestion="Check file permissions with 'ls -la'. Consider using appropriate user/permissions.",
        auto_correct=False,
    ),
    ErrorPattern(
        name="command_not_found",
        category="tool_usage",
        severity="medium",
        regex_pattern=r"(?:command not found|not installed|is not recognized)",
        suggestion="Verify the tool is installed and available in PATH.",
        auto_correct=False,
    ),
    ErrorPattern(
        name="invalid_directory",
        category="file_access",
        severity="medium",
        regex_pattern=r"(?:Not a directory|is a directory|not a file)",
        suggestion="Check if the path is a file or directory and use appropriate tool.",
        auto_correct=True,
        correction_fn="suggest_directory_correction",
    ),
    ErrorPattern(
        name="network_error",
        category="network",
        severity="medium",
        regex_pattern=r"(?:timeout|ECONNREFUSED|Network is unreachable|DNS resolution)",
        suggestion="Check network connectivity and retry.",
        auto_correct=False,
    ),
    ErrorPattern(
        name="syntax_error",
        category="code",
        severity="high",
        regex_pattern=r"(?:SyntaxError|Invalid syntax|Unexpected token)",
        suggestion="Review code for syntax errors. Check brackets, quotes, and indentation.",
        auto_correct=False,
    ),
    ErrorPattern(
        name="missing_parameter",
        category="validation",
        severity="medium",
        regex_pattern=r"(?:required parameter|missing argument|Missing required)",
        suggestion="Ensure all required parameters are provided.",
        auto_correct=False,
    ),
    ErrorPattern(
        name="invalid_parameter_type",
        category="validation",
        severity="low",
        regex_pattern=r"(?:must be (?:an?|the)|type error|invalid type)",
        suggestion="Check parameter types match the expected schema.",
        auto_correct=True,
        correction_fn="suggest_type_correction",
    ),
]


class ErrorPatternStore:
    """Persistent storage for error patterns and history."""

    def __init__(self, db_path: Path | None = None) -> None:
        """Initialize the error pattern store.

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

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

        self._init_db()
        self._load_builtin_patterns()

    def _init_db(self) -> None:
        """Initialize database schema."""
        with sqlite3.connect(self.db_path) as conn:
            conn.execute(
                """
                CREATE TABLE IF NOT EXISTS error_records (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    tool_name TEXT NOT NULL,
                    error_message TEXT NOT NULL,
                    error_type TEXT NOT NULL,
                    timestamp TEXT NOT NULL,
                    context TEXT,
                    suggestion_applied TEXT,
                    resolved INTEGER DEFAULT 0
                )
            """
            )

            conn.execute(
                """
                CREATE TABLE IF NOT EXISTS error_patterns (
                    name TEXT PRIMARY KEY,
                    category TEXT NOT NULL,
                    severity TEXT NOT NULL,
                    regex_pattern TEXT NOT NULL,
                    suggestion TEXT NOT NULL,
                    occurrences INTEGER DEFAULT 0,
                    first_seen TEXT,
                    last_seen TEXT,
                    auto_correct INTEGER DEFAULT 0,
                    correction_fn TEXT
                )
            """
            )

            conn.execute(
                """
                CREATE TABLE IF NOT EXISTS corrections (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    pattern_name TEXT NOT NULL,
                    original_input TEXT NOT NULL,
                    corrected_input TEXT NOT NULL,
                    success INTEGER DEFAULT 0,
                    timestamp TEXT NOT NULL
                )
            """
            )

            conn.execute(
                """
                CREATE INDEX IF NOT EXISTS idx_errors_timestamp
                ON error_records(timestamp)
            """
            )

            conn.execute(
                """
                CREATE INDEX IF NOT EXISTS idx_errors_tool
                ON error_records(tool_name)
            """
            )

            conn.execute(
                """
                CREATE INDEX IF NOT EXISTS idx_patterns_category
                ON error_patterns(category)
            """
            )

    def _load_builtin_patterns(self) -> None:
        """Load built-in patterns into database."""
        with sqlite3.connect(self.db_path) as conn:
            for pattern in BUILTIN_PATTERNS:
                conn.execute(
                    """
                    INSERT OR IGNORE INTO error_patterns
                    (name, category, severity, regex_pattern, suggestion,
                     auto_correct, correction_fn)
                    VALUES (?, ?, ?, ?, ?, ?, ?)
                    """,
                    (
                        pattern.name,
                        pattern.category,
                        pattern.severity,
                        pattern.regex_pattern,
                        pattern.suggestion,
                        int(pattern.auto_correct),
                        pattern.correction_fn,
                    ),
                )

    def log_error(self, record: ErrorRecord) -> int:
        """Log an error record.

        Args:
            record: The error record to log

        Returns:
            The ID of the inserted record
        """
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.execute(
                """
                INSERT INTO error_records
                (tool_name, error_message, error_type, timestamp, context,
                 suggestion_applied, resolved)
                VALUES (?, ?, ?, ?, ?, ?, ?)
                """,
                (
                    record.tool_name,
                    record.error_message,
                    record.error_type,
                    record.timestamp.isoformat(),
                    json.dumps(record.context),
                    record.suggestion_applied,
                    int(record.resolved),
                ),
            )
            return cursor.lastrowid or 0

    def get_recent_errors(self, hours: int = 24) -> list[ErrorRecord]:
        """Get errors from the last N hours.

        Args:
            hours: Number of hours to look back

        Returns:
            List of error records
        """
        cutoff = datetime.now() - timedelta(hours=hours)

        with sqlite3.connect(self.db_path) as conn:
            conn.row_factory = sqlite3.Row
            cursor = conn.execute(
                """
                SELECT * FROM error_records
                WHERE timestamp > ?
                ORDER BY timestamp DESC
                """,
                (cutoff.isoformat(),),
            )

            records = []
            for row in cursor.fetchall():
                records.append(
                    ErrorRecord(
                        tool_name=row["tool_name"],
                        error_message=row["error_message"],
                        error_type=row["error_type"],
                        timestamp=datetime.fromisoformat(row["timestamp"]),
                        context=json.loads(row["context"] or "{}"),
                        suggestion_applied=row["suggestion_applied"],
                        resolved=bool(row["resolved"]),
                    )
                )

            return records

    def get_pattern_counts(self, days: int = 7) -> dict[str, int]:
        """Get error counts by pattern.

        Args:
            days: Number of days to look back

        Returns:
            Dictionary mapping pattern names to occurrence counts
        """
        cutoff = datetime.now() - timedelta(days=days)

        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.execute(
                """
                SELECT error_type, COUNT(*) as count
                FROM error_records
                WHERE timestamp > ?
                GROUP BY error_type
                ORDER BY count DESC
                """,
                (cutoff.isoformat(),),
            )

            return {row[0]: row[1] for row in cursor.fetchall()}

    def get_all_patterns(self) -> list[ErrorPattern]:
        """Get all error patterns.

        Returns:
            List of error patterns
        """
        with sqlite3.connect(self.db_path) as conn:
            conn.row_factory = sqlite3.Row
            cursor = conn.execute("SELECT * FROM error_patterns")

            patterns = []
            for row in cursor.fetchall():
                patterns.append(
                    ErrorPattern(
                        name=row["name"],
                        category=row["category"],
                        severity=row["severity"],
                        regex_pattern=row["regex_pattern"],
                        suggestion=row["suggestion"],
                        occurrences=row["occurrences"],
                        first_seen=(
                            datetime.fromisoformat(row["first_seen"]) if row["first_seen"] else None
                        ),
                        last_seen=(
                            datetime.fromisoformat(row["last_seen"]) if row["last_seen"] else None
                        ),
                        auto_correct=bool(row["auto_correct"]),
                        correction_fn=row["correction_fn"],
                    )
                )

            return patterns

    def update_pattern_occurrence(self, pattern_name: str) -> None:
        """Update pattern occurrence count and timestamp.

        Args:
            pattern_name: Name of the pattern
        """
        now = datetime.now().isoformat()

        with sqlite3.connect(self.db_path) as conn:
            conn.execute(
                """
                UPDATE error_patterns
                SET occurrences = occurrences + 1,
                    first_seen = COALESCE(first_seen, ?),
                    last_seen = ?
                WHERE name = ?
                """,
                (now, now, pattern_name),
            )

    def log_correction(
        self, pattern_name: str, original: str, corrected: str, success: bool
    ) -> None:
        """Log a correction attempt.

        Args:
            pattern_name: Name of the pattern
            original: Original input
            corrected: Corrected input
            success: Whether the correction succeeded
        """
        with sqlite3.connect(self.db_path) as conn:
            conn.execute(
                """
                INSERT INTO corrections
                (pattern_name, original_input, corrected_input, success, timestamp)
                VALUES (?, ?, ?, ?, ?)
                """,
                (
                    pattern_name,
                    original,
                    corrected,
                    int(success),
                    datetime.now().isoformat(),
                ),
            )

    def get_correction_success_rate(self, pattern_name: str) -> float:
        """Get success rate for a pattern's corrections.

        Args:
            pattern_name: Name of the pattern

        Returns:
            Success rate between 0.0 and 1.0
        """
        with sqlite3.connect(self.db_path) as conn:
            cursor = conn.execute(
                """
                SELECT
                    SUM(success) as successes,
                    COUNT(*) as total
                FROM corrections
                WHERE pattern_name = ?
                """,
                (pattern_name,),
            )

            row = cursor.fetchone()
            if row and row[1] > 0:
                return row[0] / row[1]
            return 0.0


class ErrorPatternLearner:
    """Main class for error pattern learning and auto-correction."""

    def __init__(self, store: ErrorPatternStore | None = None) -> None:
        """Initialize the error pattern learner.

        Args:
            store: Optional error pattern store. Creates default if not provided.
        """
        self.store = store or ErrorPatternStore()
        self._compiled_patterns: dict[str, Pattern[str]] = {}
        self._correction_functions: dict[str, Callable[..., Any]] = {
            "suggest_file_path_correction": self._suggest_file_path_correction,
            "suggest_directory_correction": self._suggest_directory_correction,
            "suggest_type_correction": self._suggest_type_correction,
        }

    def _get_compiled_pattern(self, regex: str) -> Pattern[str]:
        """Get or compile a regex pattern."""
        if regex not in self._compiled_patterns:
            self._compiled_patterns[regex] = re.compile(regex, re.IGNORECASE)
        return self._compiled_patterns[regex]

    def log_tool_error(
        self, tool_name: str, result: ToolResult, context: dict[str, Any | None] = None
    ) -> ErrorRecord | None:
        """Log a tool execution error.

        Args:
            tool_name: Name of the tool that failed
            result: The failed tool result
            context: Additional context about the error

        Returns:
            The error record if logged, None if result was successful
        """
        if result.success:
            return None

        error_message = result.error_message or result.output
        error_type = self._classify_error(error_message)

        record = ErrorRecord(
            tool_name=tool_name,
            error_message=error_message,
            error_type=error_type,
            timestamp=datetime.now(),
            context=context or {},
        )

        self.store.log_error(record)
        self.store.update_pattern_occurrence(error_type)

        return record

    def _classify_error(self, error_message: str) -> str:
        """Classify an error message into a pattern type.

        Args:
            error_message: The error message to classify

        Returns:
            The pattern name or 'unknown'
        """
        patterns = self.store.get_all_patterns()

        for pattern in patterns:
            compiled = self._get_compiled_pattern(pattern.regex_pattern)
            if compiled.search(error_message):
                return pattern.name

        return "unknown"

    def detect_recurring_patterns(
        self, min_occurrences: int = 3, days: int = 7
    ) -> list[ErrorPattern]:
        """Detect recurring error patterns.

        Args:
            min_occurrences: Minimum number of occurrences to be considered recurring
            days: Number of days to look back

        Returns:
            List of recurring error patterns
        """
        counts = self.store.get_pattern_counts(days)
        patterns = self.store.get_all_patterns()

        recurring = []
        for pattern in patterns:
            count = counts.get(pattern.name, 0)
            if count >= min_occurrences:
                pattern.occurrences = count
                recurring.append(pattern)

        # Sort by occurrences (descending)
        recurring.sort(key=lambda p: p.occurrences, reverse=True)

        return recurring

    def suggest_correction(
        self, tool_name: str, params: dict[str, Any], error_message: str
    ) -> dict[str, Any | None]:
        """Suggest a correction for a failed tool call.

        Args:
            tool_name: Name of the tool
            params: Original parameters
            error_message: The error message

        Returns:
            Corrected parameters if a suggestion exists, None otherwise
        """
        patterns = self.store.get_all_patterns()

        for pattern in patterns:
            compiled = self._get_compiled_pattern(pattern.regex_pattern)
            if not compiled.search(error_message):
                continue

            # Check if auto-correction is enabled and reliable
            if pattern.auto_correct and pattern.correction_fn:
                success_rate = self.store.get_correction_success_rate(pattern.name)
                if success_rate >= 0.7:  # Only auto-correct if 70%+ success rate
                    correction_fn = self._correction_functions.get(pattern.correction_fn)
                    if correction_fn:
                        corrected = correction_fn(tool_name, params, error_message)
                        if corrected:
                            return {
                                "pattern": pattern.name,
                                "suggestion": pattern.suggestion,
                                "corrected_params": corrected,
                                "auto_apply": True,
                            }

            # Return suggestion without auto-correction
            return {
                "pattern": pattern.name,
                "suggestion": pattern.suggestion,
                "corrected_params": None,
                "auto_apply": False,
            }

        return None

    def try_auto_correct(
        self, tool_name: str, params: dict[str, Any], error_message: str
    ) -> dict[str, Any | None]:
        """Try to auto-correct a failed tool call.

        Args:
            tool_name: Name of the tool
            params: Original parameters
            error_message: The error message

        Returns:
            Corrected parameters if auto-correction was applied, None otherwise
        """
        suggestion = self.suggest_correction(tool_name, params, error_message)

        if suggestion and suggestion.get("auto_apply"):
            return suggestion["corrected_params"]

        return None

    def record_correction_result(
        self, pattern_name: str, original: str, corrected: str, success: bool
    ) -> None:
        """Record the result of a correction attempt.

        Args:
            pattern_name: Name of the pattern
            original: Original input
            corrected: Corrected input
            success: Whether the correction succeeded
        """
        self.store.log_correction(pattern_name, original, corrected, success)

    # Correction functions

    def _suggest_file_path_correction(
        self, tool_name: str, params: dict[str, Any], error_message: str
    ) -> dict[str, Any | None]:
        """Suggest correction for file not found errors.

        Tries to find similar files or suggest creating the file.
        """
        # Extract path from error message or params
        path = params.get("path", "")
        if not path:
            # Try to extract from error message
            match = re.search(r"['\"]([^'\"]+)['\"]", error_message)
            if match:
                path = match.group(1)

        if not path:
            return None

        path_obj = Path(path)

        # Check if parent directory exists
        if path_obj.parent.exists():
            # Parent exists, file doesn't - suggest creating it
            return {**params, "_suggestion": "create_file", "path": str(path_obj)}

        # Check for common typos
        parent = path_obj.parent
        if parent.exists():
            try:
                files = list(parent.iterdir())
                target_name = path_obj.name.lower()

                for f in files:
                    if f.name.lower() == target_name or f.stem.lower() == path_obj.stem.lower():
                        return {**params, "path": str(f)}
            except (PermissionError, OSError):
                pass

        return None

    def _suggest_directory_correction(
        self, tool_name: str, params: dict[str, Any], error_message: str
    ) -> dict[str, Any | None]:
        """Suggest correction for directory-related errors."""
        path = params.get("path", "")

        if not path:
            return None

        path_obj = Path(path)

        # If trying to read a directory, suggest listing instead
        if tool_name == "read" and path_obj.is_dir():
            return {
                "tool_switch": "exec",
                "params": {"command": f"ls -la {path}"},
            }

        return None

    def _suggest_type_correction(
        self, tool_name: str, params: dict[str, Any], error_message: str
    ) -> dict[str, Any | None]:
        """Suggest correction for type errors."""
        corrected = dict(params)

        for key, value in params.items():
            # Try to convert string numbers to integers
            if isinstance(value, str):
                if value.isdigit():
                    corrected[key] = int(value)
                elif value.lower() in ("true", "false"):
                    corrected[key] = value.lower() == "true"

        return corrected if corrected != params else None

    def get_learning_stats(self) -> dict[str, Any]:
        """Get statistics about error learning.

        Returns:
            Dictionary with learning statistics
        """
        patterns = self.store.get_all_patterns()
        recent_errors = self.store.get_recent_errors(hours=24)
        recurring = self.detect_recurring_patterns(min_occurrences=2, days=7)

        total_occurrences = sum(p.occurrences for p in patterns)
        auto_correctable = sum(1 for p in patterns if p.auto_correct)

        return {
            "total_patterns": len(patterns),
            "total_errors_24h": len(recent_errors),
            "recurring_patterns": len(recurring),
            "total_occurrences_7d": total_occurrences,
            "auto_correctable_patterns": auto_correctable,
            "top_patterns": [
                {"name": p.name, "count": p.occurrences, "severity": p.severity}
                for p in recurring[:5]
            ],
        }

    def generate_report(self) -> str:
        """Generate a human-readable error pattern report.

        Returns:
            Formatted report string
        """
        stats = self.get_learning_stats()
        recurring = self.detect_recurring_patterns(min_occurrences=2, days=7)

        lines = [
            "╔══════════════════════════════════════════════════════════╗",
            "║         ERROR PATTERN LEARNING REPORT                    ║",
            "╚══════════════════════════════════════════════════════════╝",
            "",
            f"📅 Generated: {datetime.now().isoformat()}",
            f"📊 Total Patterns Tracked: {stats['total_patterns']}",
            f"🔄 Recurring Patterns (7d): {stats['recurring_patterns']}",
            f"⚠️ Total Errors (24h): {stats['total_errors_24h']}",
            f"🤖 Auto-Correctable: {stats['auto_correctable_patterns']}",
            "",
        ]

        if recurring:
            lines.append("🔥 RECURRING PATTERNS (Last 7 Days)")
            lines.append("═" * 50)

            for pattern in recurring:
                icon = (
                    "🔴"
                    if pattern.severity == "high"
                    else "🟡" if pattern.severity == "medium" else "🟢"
                )
                lines.append(f"\n{icon} {pattern.name.upper()}")
                lines.append(f"   Category: {pattern.category}")
                lines.append(f"   Occurrences: {pattern.occurrences}")
                lines.append(f"   Severity: {pattern.severity}")
                lines.append(f"   💡 {pattern.suggestion}")
                if pattern.auto_correct:
                    success_rate = self.store.get_correction_success_rate(pattern.name)
                    lines.append(
                        f"   🤖 Auto-correct: enabled ({success_rate*100:.0f}% success rate)"
                    )
        else:
            lines.append("✅ No recurring patterns detected. Good job!")

        lines.append("")
        return "\n".join(lines)


# Global instance
_default_learner: ErrorPatternLearner | None = None


def get_default_learner() -> ErrorPatternLearner:
    """Get the default error pattern learner instance."""
    global _default_learner
    if _default_learner is None:
        _default_learner = ErrorPatternLearner()
    return _default_learner


def set_default_learner(learner: ErrorPatternLearner) -> None:
    """Set the default error pattern learner instance."""
    global _default_learner
    _default_learner = learner
