"""Language adapters for multi-language support.

Each adapter provides:
- File extension detection
- AST parsing (where available)
- Pattern matching optimization
- Code generation for fixes
"""

from __future__ import annotations

import ast
import bisect
import io
import re
import tokenize
from abc import ABC, abstractmethod
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

from autodebug.healing.patterns import FlakyPattern, PatternMatch

SUPPORTED_LANGUAGES = ["python", "typescript", "javascript", "go", "java", "rust"]


def _compute_non_code_ranges(
    content: str,
    tree: ast.AST,
) -> List[Tuple[int, int]]:
    """Compute byte ranges for descriptive text in Python source.

    Returns a sorted list of (start, end) byte offsets covering:
    - Docstrings (string expression statements — e.g., module/class/function docstrings)
    - Comments (via tokenize)

    Standalone string constants (assignments, print args, log messages, assert
    messages) are also excluded since regex matches in those are false
    positives.  String arguments inside function calls like open("/tmp/...")
    or time.sleep(...) are NOT excluded because those represent real code
    patterns.
    """
    ranges: List[Tuple[int, int]] = []
    line_offsets = _build_line_offsets(content)

    # Build set of string nodes that are direct arguments to function calls.
    # These should NOT be excluded since e.g. open("/tmp/foo") is a real pattern.
    _call_arg_ids: set = set()
    for node in ast.walk(tree):
        if isinstance(node, ast.Call):
            for arg in node.args:
                if isinstance(arg, ast.Constant) and isinstance(arg.value, str):
                    _call_arg_ids.add(id(arg))
            for kw in node.keywords:
                if isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, str):
                    _call_arg_ids.add(id(kw.value))

    # 1. Docstrings and non-call string literals.
    for node in ast.walk(tree):
        # Docstrings: expression statements with string values
        if (isinstance(node, ast.Expr)
                and isinstance(node.value, (ast.Constant, ast.JoinedStr))):
            val = node.value
            if isinstance(val, ast.Constant) and not isinstance(val.value, str):
                continue
            if hasattr(val, 'lineno') and hasattr(val, 'end_lineno'):
                start = _linecol_to_offset(line_offsets, val.lineno, val.col_offset)
                end = _linecol_to_offset(
                    line_offsets,
                    val.end_lineno or val.lineno,
                    val.end_col_offset or (val.col_offset + 1),
                )
                if start < end:
                    ranges.append((start, end))
        # Standalone string constants NOT used as call arguments
        elif (isinstance(node, ast.Constant) and isinstance(node.value, str)
              and id(node) not in _call_arg_ids):
            if hasattr(node, 'lineno') and hasattr(node, 'end_lineno'):
                start = _linecol_to_offset(line_offsets, node.lineno, node.col_offset)
                end = _linecol_to_offset(
                    line_offsets,
                    node.end_lineno or node.lineno,
                    node.end_col_offset or (node.col_offset + 1),
                )
                if start < end:
                    ranges.append((start, end))

    # 2. Comments: use tokenize for accurate detection
    try:
        tokens = tokenize.generate_tokens(io.StringIO(content).readline)
        for tok_type, _, (srow, scol), (erow, ecol), _ in tokens:
            if tok_type == tokenize.COMMENT:
                start = _linecol_to_offset(line_offsets, srow, scol)
                end = _linecol_to_offset(line_offsets, erow, ecol)
                if start < end:
                    ranges.append((start, end))
    except tokenize.TokenError:
        pass

    ranges.sort()
    return ranges


def _build_line_offsets(content: str) -> List[int]:
    """Build a list mapping 1-based line numbers to byte offsets.

    line_offsets[0] = 0 (unused), line_offsets[1] = offset of line 1, etc.
    """
    offsets = [0, 0]  # index 0 unused; line 1 starts at offset 0
    for i, ch in enumerate(content):
        if ch == '\n':
            offsets.append(i + 1)
    return offsets


def _linecol_to_offset(line_offsets: List[int], line: int, col: int) -> int:
    """Convert 1-based line + 0-based column to byte offset."""
    if line < len(line_offsets):
        return line_offsets[line] + col
    return len(line_offsets) - 1  # fallback to end


def _offset_in_ranges(offset: int, ranges: List[Tuple[int, int]]) -> bool:
    """Check if an offset falls inside any (start, end) range using binary search."""
    if not ranges:
        return False
    starts = [r[0] for r in ranges]
    idx = bisect.bisect_right(starts, offset) - 1
    if idx >= 0 and ranges[idx][0] <= offset < ranges[idx][1]:
        return True
    return False


def _compute_anti_pattern_reduction(
    content: str,
    match_start: int,
    match_end: int,
    anti_patterns: List[str],
) -> float:
    """Compute graduated confidence reduction from anti-patterns (E4/F-07).

    Instead of a binary 500-char window check, this examines multiple
    proximity tiers and returns a confidence reduction (0.0 to 1.0):
      - Same line:     -0.6  (very likely handled)
      - Within 10 lines: -0.3
      - Within function (~120 lines): -0.15
      - Within file:   -0.05

    The maximum reduction from any single anti-pattern match is returned,
    not cumulative (to avoid over-penalizing well-handled code).

    Returns:
        Float reduction to subtract from base confidence.  0.0 means no
        anti-pattern found, >= 0.6 means almost certainly handled.
    """
    if not anti_patterns:
        return 0.0

    # Pre-compute line boundaries around the match for proximity tiers
    match_line_start = content.rfind('\n', 0, match_start) + 1
    match_line_end = content.find('\n', match_end)
    if match_line_end == -1:
        match_line_end = len(content)

    # Compute character offsets for ~10 lines and ~120 lines
    # (~80 chars/line average)
    near_start = max(0, match_start - 800)    # ~10 lines
    near_end = min(len(content), match_end + 800)
    func_start = max(0, match_start - 9600)   # ~120 lines
    func_end = min(len(content), match_end + 9600)

    best_reduction = 0.0

    for ap in anti_patterns:
        try:
            compiled = re.compile(ap)
        except re.error:
            continue

        # Tier 1: same line
        line_text = content[match_line_start:match_line_end]
        if compiled.search(line_text):
            best_reduction = max(best_reduction, 0.6)
            continue  # already max for this AP

        # Tier 2: within ~10 lines
        if compiled.search(content[near_start:near_end]):
            best_reduction = max(best_reduction, 0.3)
            continue

        # Tier 3: within ~120 lines (roughly same function)
        if compiled.search(content[func_start:func_end]):
            best_reduction = max(best_reduction, 0.15)
            continue

        # Tier 4: anywhere in file
        if compiled.search(content):
            best_reduction = max(best_reduction, 0.05)

    return best_reduction


@dataclass
class LanguageAdapter(ABC):
    """Base class for language-specific adapters."""

    name: str
    extensions: List[str]
    comment_prefix: str
    multiline_comment: Tuple[str, str]

    @abstractmethod
    def parse_file(self, file_path: Path) -> Any:
        """Parse file into AST or equivalent structure."""
        pass

    @abstractmethod
    def find_pattern_matches(
        self,
        content: str,
        file_path: Path,
        pattern: FlakyPattern,
    ) -> List[PatternMatch]:
        """Find all matches of a pattern in the given content."""
        pass

    @abstractmethod
    def apply_fix(
        self,
        content: str,
        match: PatternMatch,
        fix_template: str,
        context: Dict[str, Any],
    ) -> str:
        """Apply a fix to the content."""
        pass

    @abstractmethod
    def format_code(self, code: str) -> str:
        """Format code according to language conventions."""
        pass

    def get_context_lines(
        self,
        content: str,
        line_number: int,
        before: int = 3,
        after: int = 3,
    ) -> Tuple[List[str], List[str]]:
        """Get context lines around a match."""
        lines = content.splitlines()
        start = max(0, line_number - before - 1)
        end = min(len(lines), line_number + after)

        before_lines = lines[start:line_number - 1]
        after_lines = lines[line_number:end]

        return before_lines, after_lines

    def detect_language(self, file_path: Path) -> bool:
        """Check if this adapter handles the given file."""
        return file_path.suffix.lower() in self.extensions


class PythonAdapter(LanguageAdapter):
    """Adapter for Python code analysis."""

    def __init__(self):
        super().__init__(
            name="python",
            extensions=[".py", ".pyw"],
            comment_prefix="#",
            multiline_comment=('"""', '"""'),
        )

    def parse_file(self, file_path: Path) -> Optional[ast.AST]:
        """Parse Python file into AST."""
        try:
            content = file_path.read_text()
            return ast.parse(content, filename=str(file_path))
        except SyntaxError:
            return None

    def find_pattern_matches(
        self,
        content: str,
        file_path: Path,
        pattern: FlakyPattern,
    ) -> List[PatternMatch]:
        """Find pattern matches using both regex and AST."""
        matches = []

        # Parse AST once upfront (reused for docstring filtering AND AST patterns)
        tree = None
        non_code_ranges: List[Tuple[int, int]] = []
        try:
            tree = ast.parse(content)
            non_code_ranges = _compute_non_code_ranges(content, tree)
        except (SyntaxError, RecursionError):
            pass

        # Regex scanning with docstring/comment filtering
        for regex_pattern in pattern.code_patterns:
            try:
                for match in re.finditer(regex_pattern, content, re.MULTILINE):
                    # Skip matches inside docstrings or comments
                    if non_code_ranges and _offset_in_ranges(match.start(), non_code_ranges):
                        continue

                    # Calculate line number at the actual code, not at leading
                    # whitespace.  Regexes like ^\s+\w+ can span empty lines,
                    # so use the position of the first non-whitespace character
                    # for accurate line mapping (needed for AST suppression).
                    matched_text = match.group()
                    code_offset = len(matched_text) - len(matched_text.lstrip())
                    effective_start = match.start() + code_offset
                    line_number = content[:effective_start].count('\n') + 1
                    column = effective_start - content.rfind('\n', 0, effective_start)

                    # E4/F-07: Graduated anti-pattern proximity reduces
                    # confidence instead of binary suppression.
                    # Check at multiple distances: same line, nearby, function, file.
                    ap_reduction = _compute_anti_pattern_reduction(
                        content, match.start(), match.end(), pattern.anti_patterns
                    )

                    if ap_reduction >= 0.6:
                        # Very strong mitigation -- skip entirely
                        continue

                    before_lines, after_lines = self.get_context_lines(
                        content, line_number
                    )

                    # E4/F-06: Use per-pattern base_confidence when available
                    base_conf = pattern.base_confidence if pattern.base_confidence is not None else 0.8
                    confidence = max(0.0, base_conf - ap_reduction)

                    matches.append(PatternMatch(
                        pattern=pattern,
                        file_path=str(file_path),
                        line_number=line_number,
                        column=column,
                        matched_code=match.group(),
                        context_before=before_lines,
                        context_after=after_lines,
                        confidence=confidence,
                        language="python",
                    ))
            except re.error:
                continue

        # AST-based detection for Python-specific patterns
        if tree is not None:
            try:
                ast_matches = self._find_ast_patterns(tree, content, file_path, pattern)

                # Post-process: remove regex matches at lines AST marked as false positives
                suppress_lines = {
                    m.line_number for m in ast_matches
                    if m.additional_info.get("_suppress")
                }
                if suppress_lines:
                    matches = [m for m in matches if m.line_number not in suppress_lines]

                # Add non-suppression AST matches (e.g., time.sleep detection)
                real_ast_matches = [
                    m for m in ast_matches if not m.additional_info.get("_suppress")
                ]
                matches.extend(real_ast_matches)

                # For patterns with AST call detection (e.g., async-wait-sleep),
                # remove regex matches at lines where no real AST call was found.
                # This prevents false positives from string mentions like
                # f"Tests using time.sleep (causes flakiness)".
                if pattern.id == "async-wait-sleep":
                    ast_lines = {m.line_number for m in real_ast_matches}
                    matches = [
                        m for m in matches
                        if m.additional_info.get("_ast_detected") or m.line_number in ast_lines
                    ]
            except (SyntaxError, RecursionError):
                pass

        return matches

    def _find_ast_patterns(
        self,
        tree: ast.AST,
        content: str,
        file_path: Path,
        pattern: FlakyPattern,
    ) -> List[PatternMatch]:
        """Find patterns using AST analysis.

        Returns both additive matches (e.g., time.sleep detection) and
        suppression markers (confidence=-1.0, additional_info={"_suppress": True})
        that tell find_pattern_matches to remove false-positive regex matches.
        """
        matches = []

        class PatternVisitor(ast.NodeVisitor):
            def __init__(self, adapter: PythonAdapter):
                self.adapter = adapter
                self.matches: List[PatternMatch] = []
                # Track local variable assignments per function scope
                self._function_locals: set = set()
                self._in_function: bool = False

            def _make_suppress_marker(self, node: ast.AST) -> PatternMatch:
                """Create a suppression marker for a false-positive regex match."""
                return PatternMatch(
                    pattern=pattern,
                    file_path=str(file_path),
                    line_number=node.lineno,
                    column=getattr(node, "col_offset", 0),
                    matched_code="",
                    context_before=[],
                    context_after=[],
                    confidence=-1.0,
                    language="python",
                    additional_info={"_suppress": True},
                )

            def visit_FunctionDef(self, node: ast.FunctionDef):
                """Track function scope for local variable analysis.

                Handles closure variables: nested functions inherit the
                enclosing function's locals so that `.append()` on an
                outer-scope list is still recognised as local.
                """
                old_locals = self._function_locals
                old_in_function = self._in_function
                # Inherit enclosing scope's locals (closure variables)
                self._function_locals = set(old_locals) if old_in_function else set()
                self._in_function = True

                # Collect ALL binding forms in this function

                # 1. Function parameters
                for arg in node.args.args + node.args.posonlyargs + node.args.kwonlyargs:
                    self._function_locals.add(arg.arg)
                if node.args.vararg:
                    self._function_locals.add(node.args.vararg.arg)
                if node.args.kwarg:
                    self._function_locals.add(node.args.kwarg.arg)

                for child in ast.walk(node):
                    # 2. Assignment targets
                    if isinstance(child, ast.Assign):
                        for target in child.targets:
                            if isinstance(target, ast.Name):
                                self._function_locals.add(target.id)
                    elif isinstance(child, ast.AnnAssign) and isinstance(
                        child.target, ast.Name
                    ):
                        self._function_locals.add(child.target.id)
                    # 3. with...as var / async with...as var
                    elif isinstance(child, (ast.With, ast.AsyncWith)):
                        for item in child.items:
                            if item.optional_vars and isinstance(item.optional_vars, ast.Name):
                                self._function_locals.add(item.optional_vars.id)
                    # 4. for var in ... / async for var in ...
                    elif isinstance(child, (ast.For, ast.AsyncFor)):
                        if isinstance(child.target, ast.Name):
                            self._function_locals.add(child.target.id)
                        elif isinstance(child.target, ast.Tuple):
                            for elt in child.target.elts:
                                if isinstance(elt, ast.Name):
                                    self._function_locals.add(elt.id)
                    # 5. except E as var
                    elif isinstance(child, ast.ExceptHandler) and child.name:
                        self._function_locals.add(child.name)
                    # 6. Named expressions (walrus operator)
                    elif isinstance(child, ast.NamedExpr) and isinstance(child.target, ast.Name):
                        self._function_locals.add(child.target.id)
                    # 7. Comprehension variables
                    elif isinstance(child, ast.comprehension):
                        if isinstance(child.target, ast.Name):
                            self._function_locals.add(child.target.id)

                self.generic_visit(node)
                self._function_locals = old_locals
                self._in_function = old_in_function

            visit_AsyncFunctionDef = visit_FunctionDef

            def visit_Call(self, node: ast.Call):
                # Check for time.sleep
                if pattern.id == "async-wait-sleep":
                    if (isinstance(node.func, ast.Attribute) and
                        node.func.attr == "sleep" and
                        isinstance(node.func.value, ast.Name) and
                        node.func.value.id == "time"):

                        # Get duration argument
                        duration = None
                        if node.args:
                            try:
                                duration = ast.literal_eval(node.args[0])
                            except (ValueError, TypeError):
                                duration = ast.unparse(node.args[0])

                        before_lines, after_lines = self.adapter.get_context_lines(
                            content, node.lineno
                        )

                        self.matches.append(PatternMatch(
                            pattern=pattern,
                            file_path=str(file_path),
                            line_number=node.lineno,
                            column=node.col_offset,
                            matched_code=ast.unparse(node),
                            context_before=before_lines,
                            context_after=after_lines,
                            confidence=0.95,
                            language="python",
                            additional_info={"duration": duration, "_ast_detected": True},
                        ))

                # Suppress shared-mutable-state false positives for local variables
                if pattern.id == "order-dep-shared-mutable-state":
                    if (isinstance(node.func, ast.Attribute)
                            and node.func.attr in ("append", "extend", "update")
                            and isinstance(node.func.value, ast.Name)
                            and self._in_function
                            and node.func.value.id in self._function_locals):
                        self.matches.append(self._make_suppress_marker(node))

                    # Suppress keyword arguments like foo=[] in function calls
                    for kw in getattr(node, "keywords", []):
                        if isinstance(kw.value, (ast.List, ast.Dict, ast.Set)):
                            self.matches.append(self._make_suppress_marker(kw.value))

                self.generic_visit(node)

            def _is_empty_collection(self, node: ast.AST) -> bool:
                """Check if node is an empty [], {}, or set()."""
                if isinstance(node, ast.List) and len(node.elts) == 0:
                    return True
                if isinstance(node, ast.Dict) and len(node.keys) == 0:
                    return True
                if isinstance(node, ast.Set) and len(node.elts) == 0:
                    return True
                return False

            def visit_Assign(self, node: ast.Assign):
                """Suppress local variable inits (= []/= {}) inside functions."""
                if pattern.id == "order-dep-shared-mutable-state":
                    if (self._in_function
                            and self._is_empty_collection(node.value)):
                        for target in node.targets:
                            if isinstance(target, ast.Name) and target.id in self._function_locals:
                                self.matches.append(self._make_suppress_marker(node))
                                break
                self.generic_visit(node)

            def visit_Compare(self, node: ast.Compare):
                """Suppress float-exact-comparison for IEEE-754 exact values."""
                if pattern.id == "float-exact-comparison":
                    _EXACT_IEEE754 = {0.0, 1.0, -1.0, 0.5, -0.5, 2.0, -2.0}
                    for comparator in node.comparators:
                        if (isinstance(comparator, ast.Constant)
                                and isinstance(comparator.value, float)
                                and comparator.value in _EXACT_IEEE754):
                            self.matches.append(self._make_suppress_marker(node))
                            break
                self.generic_visit(node)

            def visit_Try(self, node: ast.Try):
                """Suppress global-state matches inside try/finally blocks."""
                if pattern.id == "order-dep-global-state" and node.finalbody:
                    # If try has a finally block, suppress os.environ matches
                    # inside the try body — the cleanup is handled.
                    for child in ast.walk(node):
                        if (isinstance(child, ast.Subscript)
                                and isinstance(child.value, ast.Attribute)
                                and isinstance(child.value.value, ast.Name)
                                and child.value.value.id == "os"
                                and child.value.attr == "environ"):
                            self.matches.append(self._make_suppress_marker(child))
                self.generic_visit(node)

        visitor = PatternVisitor(self)
        visitor.visit(tree)
        return visitor.matches

    def apply_fix(
        self,
        content: str,
        match: PatternMatch,
        fix_template: str,
        context: Dict[str, Any],
    ) -> str:
        """Apply fix template to content."""
        # Simple line replacement for now
        lines = content.splitlines()

        # Format the fix template
        formatted_fix = fix_template.format(**context)

        # Find the line to replace
        line_idx = match.line_number - 1

        # Get indentation of original line
        original_line = lines[line_idx]
        indent = len(original_line) - len(original_line.lstrip())
        indent_str = original_line[:indent]

        # Apply indentation to fix
        fix_lines = formatted_fix.splitlines()
        indented_fix = '\n'.join(
            indent_str + line if line.strip() else line
            for line in fix_lines
        )

        # Replace the line
        lines[line_idx] = indented_fix

        return '\n'.join(lines)

    def format_code(self, code: str) -> str:
        """Format Python code using black if available."""
        try:
            import black
            return black.format_str(code, mode=black.Mode())
        except ImportError:
            return code


class TypeScriptAdapter(LanguageAdapter):
    """Adapter for TypeScript/JavaScript code analysis."""

    def __init__(self, is_typescript: bool = True):
        extensions = [".ts", ".tsx"] if is_typescript else [".js", ".jsx", ".mjs"]
        super().__init__(
            name="typescript" if is_typescript else "javascript",
            extensions=extensions,
            comment_prefix="//",
            multiline_comment=("/*", "*/"),
        )

    def parse_file(self, file_path: Path) -> Optional[Dict[str, Any]]:
        """Parse TypeScript/JavaScript file.

        Uses regex-based parsing since we don't want heavy dependencies.
        For full AST, would need to shell out to tsc or use a JS parser.
        """
        try:
            content = file_path.read_text()
            return {"content": content, "path": str(file_path)}
        except Exception:
            return None

    def find_pattern_matches(
        self,
        content: str,
        file_path: Path,
        pattern: FlakyPattern,
    ) -> List[PatternMatch]:
        """Find pattern matches using regex."""
        matches = []

        for regex_pattern in pattern.code_patterns:
            try:
                for match in re.finditer(regex_pattern, content, re.MULTILINE):
                    line_number = content[:match.start()].count('\n') + 1
                    column = match.start() - content.rfind('\n', 0, match.start())

                    # E4/F-07: Graduated anti-pattern proximity
                    ap_reduction = _compute_anti_pattern_reduction(
                        content, match.start(), match.end(), pattern.anti_patterns
                    )

                    if ap_reduction >= 0.6:
                        continue

                    before_lines, after_lines = self.get_context_lines(
                        content, line_number
                    )

                    # E4/F-06: Per-pattern base_confidence
                    base_conf = pattern.base_confidence if pattern.base_confidence is not None else 0.75
                    confidence = max(0.0, base_conf - ap_reduction)

                    matches.append(PatternMatch(
                        pattern=pattern,
                        file_path=str(file_path),
                        line_number=line_number,
                        column=column,
                        matched_code=match.group(),
                        context_before=before_lines,
                        context_after=after_lines,
                        confidence=confidence,
                        language=self.name,
                    ))
            except re.error:
                continue

        return matches

    def apply_fix(
        self,
        content: str,
        match: PatternMatch,
        fix_template: str,
        context: Dict[str, Any],
    ) -> str:
        """Apply fix template to content."""
        lines = content.splitlines()
        line_idx = match.line_number - 1

        original_line = lines[line_idx]
        indent = len(original_line) - len(original_line.lstrip())
        indent_str = original_line[:indent]

        formatted_fix = fix_template.format(**context)
        fix_lines = formatted_fix.splitlines()
        indented_fix = '\n'.join(
            indent_str + line if line.strip() else line
            for line in fix_lines
        )

        lines[line_idx] = indented_fix
        return '\n'.join(lines)

    def format_code(self, code: str) -> str:
        """Format code using prettier if available via subprocess."""
        try:
            import subprocess
            result = subprocess.run(
                ["npx", "prettier", "--parser", "typescript", "--stdin-filepath", "temp.ts"],
                input=code,
                capture_output=True,
                text=True,
                timeout=5,
            )
            if result.returncode == 0:
                return result.stdout
        except Exception:
            pass
        return code


class GoAdapter(LanguageAdapter):
    """Adapter for Go code analysis."""

    def __init__(self):
        super().__init__(
            name="go",
            extensions=[".go"],
            comment_prefix="//",
            multiline_comment=("/*", "*/"),
        )

    def parse_file(self, file_path: Path) -> Optional[Dict[str, Any]]:
        """Parse Go file (regex-based)."""
        try:
            content = file_path.read_text()
            return {"content": content, "path": str(file_path)}
        except Exception:
            return None

    def find_pattern_matches(
        self,
        content: str,
        file_path: Path,
        pattern: FlakyPattern,
    ) -> List[PatternMatch]:
        """Find pattern matches using regex."""
        matches = []

        for regex_pattern in pattern.code_patterns:
            try:
                for match in re.finditer(regex_pattern, content, re.MULTILINE):
                    line_number = content[:match.start()].count('\n') + 1
                    column = match.start() - content.rfind('\n', 0, match.start())

                    # E4/F-07: Graduated anti-pattern proximity
                    ap_reduction = _compute_anti_pattern_reduction(
                        content, match.start(), match.end(), pattern.anti_patterns
                    )

                    if ap_reduction >= 0.6:
                        continue

                    before_lines, after_lines = self.get_context_lines(
                        content, line_number
                    )

                    # E4/F-06: Per-pattern base_confidence
                    base_conf = pattern.base_confidence if pattern.base_confidence is not None else 0.7
                    confidence = max(0.0, base_conf - ap_reduction)

                    matches.append(PatternMatch(
                        pattern=pattern,
                        file_path=str(file_path),
                        line_number=line_number,
                        column=column,
                        matched_code=match.group(),
                        context_before=before_lines,
                        context_after=after_lines,
                        confidence=confidence,
                        language="go",
                    ))
            except re.error:
                continue

        return matches

    def apply_fix(
        self,
        content: str,
        match: PatternMatch,
        fix_template: str,
        context: Dict[str, Any],
    ) -> str:
        """Apply fix template to content."""
        lines = content.splitlines()
        line_idx = match.line_number - 1

        original_line = lines[line_idx]
        indent = len(original_line) - len(original_line.lstrip())
        indent_str = original_line[:indent]

        formatted_fix = fix_template.format(**context)
        fix_lines = formatted_fix.splitlines()
        indented_fix = '\n'.join(
            indent_str + line if line.strip() else line
            for line in fix_lines
        )

        lines[line_idx] = indented_fix
        return '\n'.join(lines)

    def format_code(self, code: str) -> str:
        """Format Go code using gofmt."""
        try:
            import subprocess
            result = subprocess.run(
                ["gofmt"],
                input=code,
                capture_output=True,
                text=True,
                timeout=5,
            )
            if result.returncode == 0:
                return result.stdout
        except Exception:
            pass
        return code


class RustAdapter(LanguageAdapter):
    """Adapter for Rust code analysis."""

    def __init__(self):
        super().__init__(
            name="rust",
            extensions=[".rs"],
            comment_prefix="//",
            multiline_comment=("/*", "*/"),
        )

    def parse_file(self, file_path: Path) -> Optional[Dict[str, Any]]:
        """Parse Rust file (regex-based)."""
        try:
            content = file_path.read_text()
            return {"content": content, "path": str(file_path)}
        except Exception:
            return None

    def find_pattern_matches(
        self,
        content: str,
        file_path: Path,
        pattern: FlakyPattern,
    ) -> List[PatternMatch]:
        """Find pattern matches using regex."""
        matches = []

        for regex_pattern in pattern.code_patterns:
            try:
                for match in re.finditer(regex_pattern, content, re.MULTILINE):
                    line_number = content[:match.start()].count('\n') + 1
                    column = match.start() - content.rfind('\n', 0, match.start())

                    # E4/F-07: Graduated anti-pattern proximity
                    ap_reduction = _compute_anti_pattern_reduction(
                        content, match.start(), match.end(), pattern.anti_patterns
                    )

                    if ap_reduction >= 0.6:
                        continue

                    before_lines, after_lines = self.get_context_lines(
                        content, line_number
                    )

                    # E4/F-06: Per-pattern base_confidence
                    base_conf = pattern.base_confidence if pattern.base_confidence is not None else 0.7
                    confidence = max(0.0, base_conf - ap_reduction)

                    matches.append(PatternMatch(
                        pattern=pattern,
                        file_path=str(file_path),
                        line_number=line_number,
                        column=column,
                        matched_code=match.group(),
                        context_before=before_lines,
                        context_after=after_lines,
                        confidence=confidence,
                        language="rust",
                    ))
            except re.error:
                continue

        return matches

    def apply_fix(
        self,
        content: str,
        match: PatternMatch,
        fix_template: str,
        context: Dict[str, Any],
    ) -> str:
        """Apply fix template to content."""
        lines = content.splitlines()
        line_idx = match.line_number - 1

        original_line = lines[line_idx]
        indent = len(original_line) - len(original_line.lstrip())
        indent_str = original_line[:indent]

        formatted_fix = fix_template.format(**context)
        fix_lines = formatted_fix.splitlines()
        indented_fix = '\n'.join(
            indent_str + line if line.strip() else line
            for line in fix_lines
        )

        lines[line_idx] = indented_fix
        return '\n'.join(lines)

    def format_code(self, code: str) -> str:
        """Format Rust code using rustfmt."""
        try:
            import subprocess
            result = subprocess.run(
                ["rustfmt"],
                input=code,
                capture_output=True,
                text=True,
                timeout=5,
            )
            if result.returncode == 0:
                return result.stdout
        except Exception:
            pass
        return code


class JavaAdapter(LanguageAdapter):
    """Adapter for Java code analysis."""

    def __init__(self):
        super().__init__(
            name="java",
            extensions=[".java"],
            comment_prefix="//",
            multiline_comment=("/*", "*/"),
        )

    def parse_file(self, file_path: Path) -> Optional[Dict[str, Any]]:
        """Parse Java file (regex-based)."""
        try:
            content = file_path.read_text()
            return {"content": content, "path": str(file_path)}
        except Exception:
            return None

    def find_pattern_matches(
        self,
        content: str,
        file_path: Path,
        pattern: FlakyPattern,
    ) -> List[PatternMatch]:
        """Find pattern matches using regex."""
        matches = []

        for regex_pattern in pattern.code_patterns:
            try:
                for match in re.finditer(regex_pattern, content, re.MULTILINE):
                    line_number = content[:match.start()].count('\n') + 1
                    column = match.start() - content.rfind('\n', 0, match.start())

                    # E4/F-07: Graduated anti-pattern proximity
                    ap_reduction = _compute_anti_pattern_reduction(
                        content, match.start(), match.end(), pattern.anti_patterns
                    )

                    if ap_reduction >= 0.6:
                        continue

                    before_lines, after_lines = self.get_context_lines(
                        content, line_number
                    )

                    # E4/F-06: Per-pattern base_confidence
                    base_conf = pattern.base_confidence if pattern.base_confidence is not None else 0.7
                    confidence = max(0.0, base_conf - ap_reduction)

                    matches.append(PatternMatch(
                        pattern=pattern,
                        file_path=str(file_path),
                        line_number=line_number,
                        column=column,
                        matched_code=match.group(),
                        context_before=before_lines,
                        context_after=after_lines,
                        confidence=confidence,
                        language="java",
                    ))
            except re.error:
                continue

        return matches

    def apply_fix(
        self,
        content: str,
        match: PatternMatch,
        fix_template: str,
        context: Dict[str, Any],
    ) -> str:
        """Apply fix template to content."""
        lines = content.splitlines()
        line_idx = match.line_number - 1

        original_line = lines[line_idx]
        indent = len(original_line) - len(original_line.lstrip())
        indent_str = original_line[:indent]

        formatted_fix = fix_template.format(**context)
        fix_lines = formatted_fix.splitlines()
        indented_fix = '\n'.join(
            indent_str + line if line.strip() else line
            for line in fix_lines
        )

        lines[line_idx] = indented_fix
        return '\n'.join(lines)

    def format_code(self, code: str) -> str:
        """Format Java code (no auto-formatting without heavy deps)."""
        return code


# Adapter registry
_ADAPTERS: Dict[str, LanguageAdapter] = {
    "python": PythonAdapter(),
    "typescript": TypeScriptAdapter(is_typescript=True),
    "javascript": TypeScriptAdapter(is_typescript=False),
    "go": GoAdapter(),
    "java": JavaAdapter(),
    "rust": RustAdapter(),
}


def get_adapter(language: str) -> Optional[LanguageAdapter]:
    """Get adapter for a language."""
    return _ADAPTERS.get(language.lower())


def get_adapter_for_file(file_path: Path) -> Optional[LanguageAdapter]:
    """Get adapter based on file extension."""
    for adapter in _ADAPTERS.values():
        if adapter.detect_language(file_path):
            return adapter
    return None


def register_adapter(adapter: LanguageAdapter) -> None:
    """Register a custom language adapter."""
    _ADAPTERS[adapter.name] = adapter
