"""Flaky pattern scanner for pre-commit hooks.

Reuses patterns from FlakyPredictor but optimized for file-level scanning.
"""

from __future__ import annotations

import re
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import List, Optional, Tuple


class Severity(str, Enum):
    """Risk severity levels."""

    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"


@dataclass
class PatternMatch:
    """A detected flaky pattern."""

    name: str
    description: str
    severity: Severity
    line: int
    file: str
    match_text: str
    recommendation: str


# Pattern definitions reused from FlakyPredictor
# Each pattern: (regex, name, description, severity, recommendation)
FLAKY_PATTERNS: List[Tuple[str, str, str, Severity, str]] = [
    # Timing patterns
    (
        r"time\.sleep\s*\(",
        "hardcoded_sleep",
        "Hardcoded sleep is fragile",
        Severity.MEDIUM,
        "Use explicit wait conditions instead of time.sleep()",
    ),
    (
        r"asyncio\.wait_for\s*\([^)]*timeout\s*=\s*[0-1]\.[0-9]",
        "short_timeout",
        "Very short timeout may cause flakiness",
        Severity.MEDIUM,
        "Increase timeout to at least 1.0 seconds",
    ),
    # Ordering patterns
    (
        r'SELECT\s+(?:(?!ORDER\s+BY).)*(?:FROM|WHERE|;|")',
        "unordered_sql",
        "SQL query without ORDER BY",
        Severity.HIGH,
        "Add ORDER BY clause to ensure deterministic results",
    ),
    (
        r"random\.(choice|sample|shuffle|randint)",
        "unseeded_random",
        "Random without seed makes tests non-deterministic",
        Severity.MEDIUM,
        "Add random.seed() or use pytest fixtures",
    ),
    # Resource patterns
    (
        r'["\'][/\\](?:tmp|var|home|Users)[^"\']*["\']',
        "hardcoded_path",
        "Hardcoded file path may not exist everywhere",
        Severity.LOW,
        "Use tempfile or pytest tmp_path fixture",
    ),
    (
        r"localhost:\d{4,5}|127\.0\.0\.1:\d{4,5}",
        "hardcoded_port",
        "Hardcoded port may conflict with other processes",
        Severity.LOW,
        "Use dynamic port allocation (port 0)",
    ),
    # State patterns
    (
        r"os\.environ\s*\[.*\]\s*=",
        "env_mutation",
        "Direct environment modification affects other tests",
        Severity.MEDIUM,
        "Use monkeypatch.setenv() fixture instead",
    ),
    (
        r"^\s*[A-Z_][A-Z_0-9]*\s*=\s*(?!.*frozen|Enum|Literal)",
        "global_mutation",
        "Global state modification",
        Severity.HIGH,
        "Use fixtures with proper teardown",
    ),
    # Network patterns
    (
        r"requests\.(get|post|put|delete|patch)\s*\(",
        "unmocked_http_requests",
        "HTTP call without mocking",
        Severity.HIGH,
        "Use responses or pytest-httpx to mock HTTP calls",
    ),
    (
        r"httpx\.(get|post|put|delete|patch|AsyncClient)\s*\(",
        "unmocked_http_httpx",
        "HTTP call without mocking",
        Severity.HIGH,
        "Use respx or pytest-httpx to mock HTTP calls",
    ),
    (
        r"aiohttp\.ClientSession\s*\(",
        "unmocked_aiohttp",
        "Async HTTP call without mocking",
        Severity.HIGH,
        "Use aioresponses to mock async HTTP calls",
    ),
    # Concurrency patterns
    (
        r"threading\.Thread\s*\(",
        "unjoined_thread",
        "Thread creation without explicit join",
        Severity.MEDIUM,
        "Always join threads before test cleanup",
    ),
]

# Patterns that indicate the code is properly mocked (skip network warnings)
MOCK_INDICATORS = [
    r"@mock\.",
    r"@patch\(",
    r"@patch\.object\(",
    r"@responses\.",
    r"@respx\.",
    r"@pytest\.fixture",
    r"with\s+mock\.patch\(",
    r"with\s+patch\(",
    r"unittest\.mock\.create_autospec\(",
    r"create_autospec\(",
    r"monkeypatch\.",
    r"pytest_httpx",
    r"aioresponses\(",
    r"mock_",
    r"_mock",
    r"@httpretty",
    r"@aioresponses",
]


class FlakPatternScanner:
    """Scans code for flaky test patterns."""

    def __init__(self, threshold: Severity = Severity.MEDIUM):
        """Initialize scanner.

        Args:
            threshold: Minimum severity to report (low, medium, high)
        """
        self.threshold = threshold
        self._severity_order = {
            Severity.LOW: 0,
            Severity.MEDIUM: 1,
            Severity.HIGH: 2,
        }

    def scan_file(self, file_path: Path) -> List[PatternMatch]:
        """Scan a single file for flaky patterns.

        Args:
            file_path: Path to file

        Returns:
            List of detected patterns
        """
        try:
            content = file_path.read_text(encoding="utf-8")
        except (UnicodeDecodeError, FileNotFoundError):
            return []

        return self.scan_content(content, str(file_path))

    def scan_content(self, content: str, file_path: str = "<stdin>") -> List[PatternMatch]:
        """Scan content string for flaky patterns.

        Args:
            content: File content
            file_path: Path for reporting

        Returns:
            List of detected patterns
        """
        matches: List[PatternMatch] = []
        lines = content.split("\n")

        # Check if file has mocking (skip network warnings)
        has_mocking = any(
            re.search(pattern, content, re.IGNORECASE) for pattern in MOCK_INDICATORS
        )
        mock_lines = []
        if has_mocking:
            for i, line in enumerate(lines, 1):
                if any(re.search(p, line, re.IGNORECASE) for p in MOCK_INDICATORS):
                    mock_lines.append(i)

        for pattern_regex, name, desc, severity, rec in FLAKY_PATTERNS:
            # Skip network patterns if file has mocking
            if has_mocking and "unmocked" in name:
                continue

            # Skip if below threshold
            if self._severity_order[severity] < self._severity_order[self.threshold]:
                continue

            for i, line in enumerate(lines, 1):
                if re.search(pattern_regex, line, re.IGNORECASE):
                    if has_mocking and "unmocked" in name:
                        if not any(abs(i - ml) <= 10 for ml in mock_lines):
                            # Mocking exists elsewhere but not near this usage
                            pass
                        else:
                            continue
                    matches.append(
                        PatternMatch(
                            name=name,
                            description=desc,
                            severity=severity,
                            line=i,
                            file=file_path,
                            match_text=line.strip()[:80],
                            recommendation=rec,
                        )
                    )

            # Extra pass for patterns that commonly span multiple lines.
            if name in {"unordered_sql"}:
                for match in re.finditer(pattern_regex, content, re.IGNORECASE | re.DOTALL):
                    start_line = content.count("\n", 0, match.start()) + 1
                    excerpt = match.group(0).strip().replace("\n", " ")
                    if not any(m.file == file_path and m.line == start_line and m.name == name for m in matches):
                        matches.append(
                            PatternMatch(
                                name=name,
                                description=desc,
                                severity=severity,
                                line=start_line,
                                file=file_path,
                                match_text=excerpt[:80],
                                recommendation=rec,
                            )
                        )

        return matches

    def scan_diff(self, diff_content: str) -> List[PatternMatch]:
        """Scan a git diff for flaky patterns in added lines.

        Args:
            diff_content: Git diff output

        Returns:
            List of patterns found in added lines only
        """
        matches: List[PatternMatch] = []
        current_file: Optional[str] = None
        current_line = 0

        for line in diff_content.split("\n"):
            # Track file
            if line.startswith("+++ b/"):
                current_file = line[6:]
                current_line = 0
                continue

            # Track line number from hunk header
            if line.startswith("@@"):
                hunk_match = re.search(r"\+(\d+)", line)
                if hunk_match:
                    current_line = int(hunk_match.group(1)) - 1
                continue

            # Only check added lines
            if line.startswith("+") and not line.startswith("+++"):
                current_line += 1
                added_content = line[1:]  # Remove the + prefix

                # Check against patterns
                for pattern_regex, name, desc, severity, rec in FLAKY_PATTERNS:
                    if self._severity_order[severity] < self._severity_order[self.threshold]:
                        continue

                    if re.search(pattern_regex, added_content, re.IGNORECASE):
                        matches.append(
                            PatternMatch(
                                name=name,
                                description=desc,
                                severity=severity,
                                line=current_line,
                                file=current_file or "<unknown>",
                                match_text=added_content.strip()[:80],
                                recommendation=rec,
                            )
                        )
            elif not line.startswith("-"):
                current_line += 1

        return matches

    def format_report(self, matches: List[PatternMatch]) -> str:
        """Format matches as a human-readable report.

        Args:
            matches: List of pattern matches

        Returns:
            Formatted report string
        """
        if not matches:
            return ""

        lines = ["\n[Hikaflow] Flaky Pattern Detection Report", "=" * 50]

        # Group by severity
        by_severity = {s: [] for s in Severity}
        for m in matches:
            by_severity[m.severity].append(m)

        for severity in [Severity.HIGH, Severity.MEDIUM, Severity.LOW]:
            group = by_severity[severity]
            if not group:
                continue

            lines.append(f"\n{severity.value.upper()} RISK ({len(group)}):")
            for m in group:
                lines.append(f"  {m.file}:{m.line} - {m.name}")
                lines.append(f"    {m.description}")
                lines.append(f"    > {m.match_text}")
                lines.append(f"    Recommendation: {m.recommendation}")

        lines.append("\n" + "=" * 50)
        high_count = len(by_severity[Severity.HIGH])
        if high_count > 0:
            lines.append(f"BLOCKED: {high_count} high-risk pattern(s) detected")
        else:
            lines.append("PASSED: No high-risk patterns (warnings only)")

        return "\n".join(lines)
