"""Pattern-based rule generator (no LLM required)."""

from __future__ import annotations

from typing import Iterable

from ..models import FailureRecord, RuleSuggestion

RuleTemplate = dict[str, str | float]

# Maps error types / categories to suggested rules
_RULES: dict[str, list[RuleTemplate]] = {
    "AttributeError": [
        {
            "category": "null-checks",
            "text": "Always check for None before attribute access: use `if obj is not None` or `hasattr(obj, 'attr')` before `obj.attr`",
            "confidence": 0.9,
        },
        {
            "category": "error-handling",
            "text": "When catching AttributeError, log the object type with `type(obj).__name__` to aid debugging",
            "confidence": 0.8,
        },
    ],
    "NameError": [
        {
            "category": "imports",
            "text": "Verify all variable names are defined before use; check for typos in variable/function names",
            "confidence": 0.9,
        },
        {
            "category": "imports",
            "text": "Ensure all imports are at the top of the file and are correct before using names",
            "confidence": 0.85,
        },
    ],
    "TypeError": [
        {
            "category": "types",
            "text": "Add type annotations and validate argument types at function boundaries",
            "confidence": 0.85,
        },
        {
            "category": "types",
            "text": "Check function signatures — passing wrong number of arguments or wrong types causes TypeError",
            "confidence": 0.8,
        },
    ],
    "ValueError": [
        {
            "category": "validation",
            "text": "Validate input values before processing; add guard clauses for unexpected values",
            "confidence": 0.85,
        },
    ],
    "ImportError": [
        {
            "category": "imports",
            "text": "Verify all dependencies are installed in the current environment before importing",
            "confidence": 0.9,
        },
        {
            "category": "imports",
            "text": "Use try/except ImportError to handle optional dependencies gracefully",
            "confidence": 0.8,
        },
    ],
    "ModuleNotFoundError": [
        {
            "category": "imports",
            "text": "Verify dependency is installed: check pyproject.toml/requirements.txt and run `pip install -e .`",
            "confidence": 0.9,
        },
    ],
    "TimeoutError": [
        {
            "category": "error-handling",
            "text": "Add explicit timeout handling for all I/O operations; use `timeout=` parameters where available",
            "confidence": 0.9,
        },
        {
            "category": "reliability",
            "text": "Implement retry logic with exponential backoff for operations that may timeout",
            "confidence": 0.75,
        },
    ],
    "RuntimeError": [
        {
            "category": "error-handling",
            "text": "Add specific error handling and meaningful error messages for RuntimeError cases",
            "confidence": 0.8,
        },
    ],
    "AssertionError": [
        {
            "category": "testing",
            "text": "Use descriptive assertion messages: `assert condition, 'expected X but got Y'`",
            "confidence": 0.85,
        },
    ],
    "KeyError": [
        {
            "category": "dict-access",
            "text": "Use `.get(key, default)` instead of direct dict access `[key]` to avoid KeyError",
            "confidence": 0.9,
        },
        {
            "category": "dict-access",
            "text": "Validate dict keys exist with `key in d` before accessing `d[key]`",
            "confidence": 0.85,
        },
    ],
    "IndexError": [
        {
            "category": "list-access",
            "text": "Check list/array bounds before indexing: `if i < len(lst)` before `lst[i]`",
            "confidence": 0.9,
        },
    ],
    "FileNotFoundError": [
        {
            "category": "file-handling",
            "text": "Check file existence with `os.path.exists()` or `Path.exists()` before opening files",
            "confidence": 0.9,
        },
    ],
    "PermissionError": [
        {
            "category": "permissions",
            "text": "Handle PermissionError explicitly; check file permissions before write operations",
            "confidence": 0.85,
        },
    ],
    "NotImplementedError": [
        {
            "category": "implementation",
            "text": "Implement all abstract methods before instantiating classes; search for NotImplementedError in code",
            "confidence": 0.9,
        },
    ],
    "RecursionError": [
        {
            "category": "recursion",
            "text": "Add base cases and depth limits to recursive functions to prevent RecursionError",
            "confidence": 0.9,
        },
    ],
    "TestFailure": [
        {
            "category": "testing",
            "text": "Review failing tests and add explicit assertions with descriptive failure messages",
            "confidence": 0.7,
        },
    ],
    "CollectionError": [
        {
            "category": "testing",
            "text": "Fix import errors and syntax issues in test files to allow pytest collection",
            "confidence": 0.85,
        },
    ],
    # Git-related
    "git-fix": [
        {
            "category": "process",
            "text": "When fixing a bug, add a regression test to prevent recurrence",
            "confidence": 0.8,
        },
    ],
    "git-bug": [
        {
            "category": "process",
            "text": "Document bug root cause in commit message and add test coverage for the fixed path",
            "confidence": 0.8,
        },
    ],
    "git-revert": [
        {
            "category": "process",
            "text": "Before reverting, document why the change was reverted to prevent re-introducing it",
            "confidence": 0.85,
        },
    ],
    "git-agent-error": [
        {
            "category": "agent-quality",
            "text": "Review agent-generated code changes carefully before committing; add automated checks",
            "confidence": 0.75,
        },
    ],
    # Note-based
    "note-null-check": [
        {
            "category": "null-checks",
            "text": "Always check for None/null before dereferencing: add explicit None guards at function boundaries",
            "confidence": 0.85,
        },
    ],
    "note-imports": [
        {
            "category": "imports",
            "text": "Verify all imports are correct and dependencies are installed before running code",
            "confidence": 0.85,
        },
    ],
    "note-timeout": [
        {
            "category": "reliability",
            "text": "Add timeout parameters to all network/I/O calls; implement retry with backoff",
            "confidence": 0.85,
        },
    ],
    "note-naming": [
        {
            "category": "naming",
            "text": "Use consistent, descriptive variable names; avoid single-letter names outside loops",
            "confidence": 0.8,
        },
    ],
    "note-oversight": [
        {
            "category": "process",
            "text": "Add pre-flight checklist: verify all required fields/files/conditions before executing",
            "confidence": 0.75,
        },
    ],
    "note-file-handling": [
        {
            "category": "file-handling",
            "text": "Always verify file paths exist before opening; use pathlib.Path for robust path handling",
            "confidence": 0.85,
        },
    ],
    "note-types": [
        {
            "category": "types",
            "text": "Add type annotations; use mypy or pyright to catch type errors before runtime",
            "confidence": 0.8,
        },
    ],
    "note-general": [
        {
            "category": "agents-rules",
            "text": "Write AGENTS.md rules as concrete operator checks: name the trigger, the source of truth, and the exact action to take",
            "confidence": 0.6,
        },
        {
            "category": "agents-rules",
            "text": "When a note describes a missed precondition, verification step, or routing fact, turn that missed check into an explicit rule",
            "confidence": 0.58,
        },
    ],
}

# Generic fallback rules
_GENERIC_RULES = [
    RuleSuggestion(
        category="agents-rules",
        text="Add a rule that names the exact source of truth the agent should consult before taking the failing action",
        confidence=0.58,
        source="generic",
    ),
    RuleSuggestion(
        category="agents-rules",
        text="Add a rule that names the precondition, verification step, or routing target the agent skipped",
        confidence=0.56,
        source="generic",
    ),
]


def _combined_record_text(record: FailureRecord) -> str:
    """Return normalized text for heuristic matching."""

    parts = [record.message, record.context, record.raw]
    return " ".join(part.strip() for part in parts if part and part.strip()).lower()


def _contains_any(text: str, phrases: Iterable[str]) -> bool:
    """Return True when any phrase appears in text."""

    return any(phrase in text for phrase in phrases)


class PatternGenerator:
    """Generate rule suggestions from FailureRecords using pattern matching."""

    def generate(self, records: list[FailureRecord]) -> list[RuleSuggestion]:
        """Generate RuleSuggestion objects from failure records."""
        if not records:
            return list(_GENERIC_RULES)

        suggestions: list[RuleSuggestion] = []
        seen_texts: set[str] = set()

        for record in records:
            matched_operational_rules = self._operational_note_rules(record)
            for suggestion in matched_operational_rules:
                self._append_suggestion(suggestions, seen_texts, suggestion)

            if matched_operational_rules and record.type.startswith("note-"):
                continue

            for rule in self._lookup_rules(record):
                self._append_suggestion(
                    suggestions,
                    seen_texts,
                    RuleSuggestion(
                        category=str(rule["category"]),
                        text=str(rule["text"]),
                        confidence=float(rule["confidence"]),
                        source=f"{record.type}: {record.message[:60]}",
                    ),
                )

        # Always add at least one suggestion
        if not suggestions:
            suggestions.extend(_GENERIC_RULES)

        # Sort by confidence descending
        suggestions.sort(key=lambda s: s.confidence, reverse=True)
        return suggestions

    def _lookup_rules(self, record: FailureRecord) -> list[RuleTemplate]:
        """Return static rule templates for a failure record."""

        rules = _RULES.get(record.type, [])

        if rules:
            return rules

        for key in _RULES:
            if record.type.startswith(key) or key.startswith(record.type):
                return _RULES[key]

        return []

    def _operational_note_rules(self, record: FailureRecord) -> list[RuleSuggestion]:
        """Return AGENTS-style operational rules inferred from note text."""

        text = _combined_record_text(record)
        if not text:
            return []

        suggestions: list[RuleSuggestion] = []
        source = f"{record.type}: {record.message[:60]}"

        if self._is_exact_fact_retrieval_failure(text):
            suggestions.append(
                RuleSuggestion(
                    category="retrieval",
                    text=(
                        "Before fuzzy retrieval, run a pinned exact-match pass over handles, "
                        "contact methods, account names, tool routing targets, and named rules "
                        "already present in repo files"
                    ),
                    confidence=0.98,
                    source=source,
                )
            )

        if self._needs_downstream_verification(text):
            suggestions.append(
                RuleSuggestion(
                    category="state-verification",
                    text=(
                        "After any irreversible action, verify the downstream system of record "
                        "and confirm the final state before reporting success"
                    ),
                    confidence=0.97,
                    source=source,
                )
            )

        if self._needs_blocked_queue_consolidation(text):
            suggestions.append(
                RuleSuggestion(
                    category="queue-management",
                    text=(
                        "If work stays blocked on a human past the follow-up threshold, "
                        "consolidate it into a single review queue instead of sending repeated "
                        "one-off pings"
                    ),
                    confidence=0.96,
                    source=source,
                )
            )

        if self._needs_exact_routing_target(text):
            suggestions.append(
                RuleSuggestion(
                    category="routing",
                    text=(
                        "Route work to the exact named handle, team, queue, or tool target from "
                        "the source material; do not infer the recipient from similarity or context"
                    ),
                    confidence=0.97,
                    source=source,
                )
            )

        return suggestions

    def _append_suggestion(
        self,
        suggestions: list[RuleSuggestion],
        seen_texts: set[str],
        suggestion: RuleSuggestion,
    ) -> None:
        """Append a suggestion if its text has not already been emitted."""

        if suggestion.text in seen_texts:
            return

        seen_texts.add(suggestion.text)
        suggestions.append(suggestion)

    def _is_exact_fact_retrieval_failure(self, text: str) -> bool:
        """Return True when the note describes missing exact facts before fuzzy retrieval."""

        has_retrieval_context = _contains_any(
            text,
            (
                "fuzzy retrieval",
                "retrieval",
                "recall",
                "lookup",
                "search",
            ),
        )
        has_exact_fact_context = _contains_any(
            text,
            (
                "exact-match",
                "exact match",
                "exact facts",
                "explicit fact",
                "explicit facts",
                "already present in files",
                "already present in the files",
                "already in files",
                "present in files",
                "named rules",
                "account names",
                "contact methods",
                "tool routing",
                "handles",
            ),
        )
        return has_retrieval_context and has_exact_fact_context

    def _needs_downstream_verification(self, text: str) -> bool:
        """Return True when the note describes missing state verification after an action."""

        has_verification_context = _contains_any(
            text,
            (
                "downstream state",
                "verify downstream",
                "verify the state",
                "confirm the state",
                "system of record",
                "actually landed",
                "actually happened",
            ),
        )
        has_irreversible_context = _contains_any(
            text,
            (
                "irreversible",
                "cannot undo",
                "can't undo",
                "sent",
                "deleted",
                "merged",
                "published",
                "transferred",
                "submitted",
                "closed",
            ),
        )
        return has_verification_context or (
            has_irreversible_context
            and _contains_any(text, ("verify", "confirm", "checked", "state"))
        )

    def _needs_blocked_queue_consolidation(self, text: str) -> bool:
        """Return True when the note describes blocked-on-human queue churn."""

        has_blocked_human_context = _contains_any(
            text,
            (
                "blocked-on-human",
                "blocked on human",
                "waiting on human",
                "blocked by human",
            ),
        )
        has_threshold_context = _contains_any(
            text,
            (
                "threshold",
                "after two follow-ups",
                "after 2 follow-ups",
                "after three follow-ups",
                "after 3 follow-ups",
                "stale queue",
            ),
        )
        has_consolidation_context = _contains_any(
            text,
            (
                "consolidate",
                "queue",
                "batched",
                "single review",
                "one-off pings",
                "follow-up",
                "nudge",
            ),
        )
        return has_blocked_human_context and (has_threshold_context or has_consolidation_context)

    def _needs_exact_routing_target(self, text: str) -> bool:
        """Return True when the note describes routing to an inferred instead of exact target."""

        has_routing_context = _contains_any(
            text,
            (
                "routing target",
                "route",
                "recipient",
                "assignee",
                "owner",
                "handoff",
                "queue",
                "tool target",
            ),
        )
        has_inference_context = _contains_any(
            text,
            (
                "inferred recipient",
                "wrong recipient",
                "wrong assignee",
                "guessed",
                "guess",
                "inferred",
                "closest match",
                "similarity",
            ),
        )
        has_exact_target_context = _contains_any(
            text,
            (
                "exact routing target",
                "exact target",
                "exact handle",
                "named handle",
                "exact recipient",
                "named queue",
                "named tool",
            ),
        )
        return has_routing_context and (has_inference_context or has_exact_target_context)
