from __future__ import annotations

import ast
import json
import re
import shlex
from pathlib import Path

from ai_orchestrator.config.settings import Settings
from ai_orchestrator.schemas.task_state import TaskState
from ai_orchestrator.schemas.workflow_models import (
    StructuredError,
    ValidationCheckResult,
    ValidationSummary,
)
from ai_orchestrator.services.safety_guardrails import SafetyGuardrails
from ai_orchestrator.tools.shell_runner import ShellRunner
from ai_orchestrator.tools.test_runner import TestRunner


class ValidationEngine:
    def __init__(
        self,
        *,
        settings: Settings,
        shell_runner: ShellRunner,
        test_runner: TestRunner,
        safety_guardrails: SafetyGuardrails,
        rag_knowledge_base = None,
        embedding_service = None,
    ) -> None:
        self.settings = settings
        self.shell_runner = shell_runner
        self.test_runner = test_runner
        self.safety_guardrails = safety_guardrails
        self.rag_knowledge_base = rag_knowledge_base
        self.embedding_service = embedding_service

    async def validate(self, task: TaskState, repo_context: dict) -> ValidationSummary:
        checks: list[ValidationCheckResult] = []
        repo_path = task.execution_repo_path()

        runtime_command = self._default_python_runtime_command(task, repo_context, repo_path)
        command_plan: list[tuple[str, str, bool]] = []
        for name, command, is_test in self._resolve_commands(task, repo_context):
            command_plan.append((name, command, is_test))
            if name == "syntax" and runtime_command:
                command_plan.append(("runtime", runtime_command, False))

        inferred_test_failed = False
        for name, command, is_test in command_plan:
            if is_test and inferred_test_failed and not task.constraints.test_command:
                continue
            check = await self._run_check(
                name=name,
                command=command,
                is_test=is_test,
                task=task,
                repo_path=repo_path,
            )
            if check is not None and not self._should_skip_empty_inferred_test_check(
                check=check,
                name=name,
                command=command,
                task=task,
            ):
                checks.append(check)
                if is_test and not check.success and not task.constraints.test_command:
                    inferred_test_failed = True

        synthesized_check = await self._run_synthesized_focus_check(task, repo_context, repo_path)
        if synthesized_check is not None:
            checks.append(synthesized_check)

        heuristic_check = self._bug_marker_check(task, repo_context, repo_path)
        if heuristic_check is not None:
            checks.append(heuristic_check)

        edge_case_check = await self._edge_case_type_check(task, repo_context, repo_path)
        if edge_case_check is not None:
            checks.append(edge_case_check)

        edge_case_runtime_test = await self._run_edge_case_runtime_tests(task, repo_context, repo_path)
        if edge_case_runtime_test is not None:
            checks.append(edge_case_runtime_test)

        # Run Ruff linter if available (fast, comprehensive Python linting)
        ruff_check = await self._run_ruff_lint(task, repo_context, repo_path)
        if ruff_check is not None:
            checks.append(ruff_check)

        # Enhanced semantic analysis via AST
        semantic_check = self._semantic_analysis_check(task, repo_context, repo_path)
        if semantic_check is not None:
            checks.append(semantic_check)

        # RAG-based pattern matching from known bug-fix datasets
        rag_check = await self._rag_pattern_check(task, repo_context, repo_path)
        if rag_check is not None:
            checks.append(rag_check)

        failed_checks = [check.name for check in checks if not check.success]
        return ValidationSummary(valid=not failed_checks, failed_checks=failed_checks, checks=checks)

    @staticmethod
    def _should_skip_empty_inferred_test_check(
        *,
        check: ValidationCheckResult,
        name: str,
        command: str,
        task: TaskState,
    ) -> bool:
        if not name.startswith("tests") or task.constraints.test_command:
            return False
        if "pytest" not in command.lower():
            return False
        if check.structured_errors:
            return False
        output = "\n".join(part for part in (check.stdout, check.stderr) if part).lower()
        if any(marker in output for marker in ("traceback", "importerror", "modulenotfounderror", "syntaxerror", "1 error")):
            return False
        return "no tests ran" in output or "collected 0 items" in output

    async def _run_check(
        self,
        *,
        name: str,
        command: str,
        is_test: bool,
        task: TaskState,
        repo_path,
    ) -> ValidationCheckResult | None:
        if not command:
            return None
        decision = self.safety_guardrails.validate_shell_command(command)
        if not bool(decision["allowed"]):
            return ValidationCheckResult(
                name=name,
                success=False,
                command=command,
                stderr=str(decision["reason"]),
                structured_errors=[
                    StructuredError(
                        error_type="CommandBlocked",
                        message=str(decision["reason"]),
                    )
                ],
            )

        if is_test:
            result = await self.test_runner.run_tests(
                command=command,
                repo_path=repo_path,
                timeout_seconds=task.constraints.command_timeout_seconds,
                use_docker=task.constraints.use_docker,
                sandbox_image=task.constraints.sandbox_image,
            )
        else:
            result = await self.shell_runner.run_shell(
                command=command,
                repo_path=repo_path,
                timeout_seconds=task.constraints.command_timeout_seconds,
                use_docker=task.constraints.use_docker,
                sandbox_image=task.constraints.sandbox_image,
            )

        return ValidationCheckResult(
            name=name,
            success=result.success,
            command=command,
            exit_code=result.exit_code,
            stdout=result.stdout,
            stderr=result.stderr,
            structured_errors=result.structured_errors,
        )

    @staticmethod
    def _resolve_commands(task: TaskState, repo_context: dict) -> list[tuple[str, str, bool]]:
        first_build_command = next(iter(repo_context.get("build_commands", [])), "")
        commands: list[tuple[str, str, bool]] = []

        syntax_command = ValidationEngine._default_python_syntax_command(task, repo_context)
        if syntax_command:
            commands.append(("syntax", syntax_command, False))

        commands.extend(
            [
                ("build", task.constraints.build_command or first_build_command, False),
                ("lint", task.constraints.lint_command or "", False),
                ("type_check", task.constraints.type_check_command or "", False),
                ("security", task.constraints.security_command or "", False),
            ]
        )
        test_commands = ValidationEngine._candidate_test_commands(task, repo_context)
        for index, test_command in enumerate(test_commands):
            name = "tests" if index == 0 else f"tests_fallback_{index}"
            commands.append((name, test_command, True))
        return [item for item in commands if item[1]]

    @staticmethod
    def _candidate_test_commands(task: TaskState, repo_context: dict) -> list[str]:
        if task.constraints.test_command:
            return [task.constraints.test_command]

        commands: list[str] = []
        seen: set[str] = set()
        for key in ("retry_test_commands", "test_commands"):
            values = repo_context.get(key, [])
            if not isinstance(values, list):
                continue
            for value in values:
                command = str(value).strip()
                if not command or command in seen:
                    continue
                seen.add(command)
                commands.append(command)
        return commands[:4]

    @staticmethod
    def _default_python_syntax_command(task: TaskState, repo_context: dict) -> str:
        if task.constraints.test_command:
            return ""

        contains_python = bool(repo_context.get("contains_python"))
        if not contains_python:
            mentioned_files = repo_context.get("mentioned_files", [])
            contains_python = any(str(path).endswith(".py") for path in mentioned_files)
        if not contains_python:
            return ""

        mentioned_python_files = [
            str(path)
            for path in repo_context.get("mentioned_files", [])
            if str(path).endswith(".py")
        ]
        if mentioned_python_files:
            quoted_paths = " ".join(shlex.quote(path) for path in mentioned_python_files)
            return f"python -m py_compile {quoted_paths}"

        return "python -m compileall ."

    @staticmethod
    def _default_python_runtime_command(task: TaskState, repo_context: dict, repo_path: Path) -> str:
        if task.constraints.test_command:
            return ""

        mentioned_python_files = [
            str(path)
            for path in repo_context.get("mentioned_files", [])
            if str(path).endswith(".py")
        ]
        if len(mentioned_python_files) != 1:
            return ""

        target_path = repo_path / mentioned_python_files[0]
        try:
            content = target_path.read_text(encoding="utf-8")
        except (OSError, UnicodeDecodeError):
            return ""
        if '__name__ == "__main__"' not in content and "__name__ == '__main__'" not in content:
            return ""
        return f"python {shlex.quote(mentioned_python_files[0])}"

    def _bug_marker_check(self, task: TaskState, repo_context: dict, repo_path: Path) -> ValidationCheckResult | None:
        if not self._requires_code_changes(task.goal):
            return None

        mentioned_python_files = [
            Path(str(path))
            for path in repo_context.get("mentioned_files", [])
            if str(path).endswith(".py")
        ]
        if not mentioned_python_files:
            return None

        unresolved: list[StructuredError] = []
        saw_marker = False
        for relative_path in mentioned_python_files:
            original_path = task.repo_path / relative_path
            updated_path = repo_path / relative_path
            try:
                original_lines = original_path.read_text(encoding="utf-8").splitlines()
                updated_text = updated_path.read_text(encoding="utf-8")
            except (OSError, UnicodeDecodeError):
                continue

            for index, line in enumerate(original_lines):
                if not re.search(r"#\s*(BUG|FIXME)\b", line):
                    continue
                saw_marker = True
                window = "\n".join(original_lines[index : min(len(original_lines), index + 5)])
                if window and window in updated_text:
                    unresolved.append(
                        StructuredError(
                            error_type="BugMarkerUnchanged",
                            message=line.strip(),
                            file=relative_path.as_posix(),
                            line=index + 1,
                        )
                    )

        if not saw_marker:
            return None
        if unresolved:
            return ValidationCheckResult(
                name="bug_markers",
                success=False,
                command="heuristic bug marker check",
                stderr="Unresolved BUG/FIXME markers remain near unchanged code regions.",
                structured_errors=unresolved,
            )
        return ValidationCheckResult(
            name="bug_markers",
            success=True,
            command="heuristic bug marker check",
            stdout="Focused BUG/FIXME markers were addressed or modified.",
        )

    async def _run_synthesized_focus_check(
        self,
        task: TaskState,
        repo_context: dict,
        repo_path: Path,
    ) -> ValidationCheckResult | None:
        if task.task_mode != "focused_file_repair":
            return None
        inventory = task.focus_issue_inventory
        if inventory is None or not inventory.focus_paths:
            return None

        supported_issues = [
            issue.model_dump(mode="json")
            for issue in inventory.issues
            if issue.issue_type in {
                "unfiltered_status_flag_loop",
                "duplicate_condition_fragment",
                "bug_marker",
                "hardcoded_threshold_ignores_parameter",
                "processed_count_uses_unfiltered_collection",
                "unguarded_max_call",
                "type_safety_arithmetic_without_validation",
                "output_formatting_raw_dict_string",
                "mutable_default_argument",
                "unguarded_min_max_on_empty",
                "bare_except_clause",
                "bare_except_exception_pass",
            }
        ]
        if not supported_issues:
            return None

        target_path = inventory.focus_paths[0]
        script_path = repo_path / ".aiiq_tmp" / f"focused_repair_check_{task.task_id}_{task.attempt}.py"
        script_path.parent.mkdir(parents=True, exist_ok=True)
        script_path.write_text(
            self._build_focused_repair_script(target_path=target_path, issues=supported_issues),
            encoding="utf-8",
            newline="\n",
        )

        command = f"python {shlex.quote(script_path.relative_to(repo_path).as_posix())}"
        try:
            return await self._run_check(
                name="focused_acceptance",
                command=command,
                is_test=True,
                task=task,
                repo_path=repo_path,
            )
        finally:
            try:
                script_path.unlink(missing_ok=True)
            except OSError:
                pass
            try:
                script_path.parent.rmdir()
            except OSError:
                pass

    @staticmethod
    def _build_focused_repair_script(*, target_path: str, issues: list[dict]) -> str:
        issue_payload = json.dumps(issues, indent=2)
        return (
            "from __future__ import annotations\n\n"
            "import ast\n"
            "import json\n"
            "from pathlib import Path\n\n"
            f"TARGET_PATH = Path({target_path!r})\n"
            f"ISSUES = json.loads({issue_payload!r})\n\n"
            "def literal_string(node):\n"
            "    if isinstance(node, ast.Constant) and isinstance(node.value, str):\n"
            "        return node.value\n"
            "    return None\n\n"
            "def flag_access(node):\n"
            "    if isinstance(node, ast.Subscript) and isinstance(node.value, ast.Name):\n"
            "        key = literal_string(node.slice)\n"
            "        if key in {'active', 'enabled', 'disabled', 'archived', 'deleted'}:\n"
            "            return node.value.id, key\n"
            "    if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == 'get' and isinstance(node.func.value, ast.Name) and node.args:\n"
            "        key = literal_string(node.args[0])\n"
            "        if key in {'active', 'enabled', 'disabled', 'archived', 'deleted'}:\n"
            "            return node.func.value.id, key\n"
            "    return None\n\n"
            "def target_names(node):\n"
            "    if isinstance(node, ast.Name):\n"
            "        return [node.id]\n"
            "    if isinstance(node, (ast.Tuple, ast.List)):\n"
            "        names = []\n"
            "        for child in node.elts:\n"
            "            names.extend(target_names(child))\n"
            "        return names\n"
            "    return []\n\n"
            "def called_name(node):\n"
            "    if isinstance(node, ast.Name):\n"
            "        return node.id\n"
            "    if isinstance(node, ast.Attribute):\n"
            "        return node.attr\n"
            "    return None\n\n"
            "def compare_mentions_name(node, name):\n"
            "    for child in ast.walk(node):\n"
            "        if isinstance(child, ast.Name) and child.id == name:\n"
            "            return True\n"
            "    return False\n\n"
            "def compare_has_numeric_literal(node):\n"
            "    for child in ast.walk(node):\n"
            "        if isinstance(child, ast.Constant) and isinstance(child.value, (int, float)):\n"
            "            return True\n"
            "    return False\n\n"
            "def test_references_flag(node, loop_name, flag_keys):\n"
            "    for child in ast.walk(node):\n"
            "        match = flag_access(child)\n"
            "        if match is None:\n"
            "            continue\n"
            "        variable_name, key = match\n"
            "        if variable_name == loop_name and key in flag_keys:\n"
            "            return True\n"
            "    return False\n\n"
            "def loop_has_flag_guard(node, loop_name, flag_keys):\n"
            "    for child in ast.walk(node):\n"
            "        if isinstance(child, ast.If) and test_references_flag(child.test, loop_name, flag_keys):\n"
            "            return True\n"
            "    return False\n\n"
            "def flag_sensitive_functions(tree):\n"
            "    functions = {}\n"
            "    for node in ast.walk(tree):\n"
            "        if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):\n"
            "            continue\n"
            "        parameter_names = {argument.arg for argument in node.args.args}\n"
            "        sensitive_keys = set()\n"
            "        for child in ast.walk(node):\n"
            "            match = flag_access(child)\n"
            "            if match is None:\n"
            "                continue\n"
            "            variable_name, key = match\n"
            "            if variable_name in parameter_names:\n"
            "                sensitive_keys.add(key)\n"
            "        if sensitive_keys:\n"
            "            functions[node.name] = sensitive_keys\n"
            "    return functions\n\n"
            "def flag_keys_for_names(node, names):\n"
            "    keys = set()\n"
            "    for child in ast.walk(node):\n"
            "        match = flag_access(child)\n"
            "        if match is None:\n"
            "            continue\n"
            "        variable_name, key = match\n"
            "        if variable_name in names:\n"
            "            keys.add(key)\n"
            "    return keys\n\n"
            "def filtered_iter_flag_keys(expression):\n"
            "    if isinstance(expression, (ast.ListComp, ast.SetComp, ast.GeneratorExp)):\n"
            "        keys = set()\n"
            "        for generator in expression.generators:\n"
            "            generator_names = set(target_names(generator.target))\n"
            "            for condition in generator.ifs:\n"
            "                keys.update(flag_keys_for_names(condition, generator_names))\n"
            "        return keys\n"
            "    return set()\n\n"
            "def assigned_filtered_alias(statement):\n"
            "    if not isinstance(statement, ast.Assign) or len(statement.targets) != 1:\n"
            "        return None\n"
            "    target = statement.targets[0]\n"
            "    if not isinstance(target, ast.Name):\n"
            "        return None\n"
            "    keys = filtered_iter_flag_keys(statement.value)\n"
            "    if not keys:\n"
            "        return None\n"
            "    return target.id, keys\n\n"
            "def loop_sensitivity_keys(node, loop_names, sensitive_functions):\n"
            "    keys = set()\n"
            "    loop_name_set = set(loop_names)\n"
            "    for child in ast.walk(node):\n"
            "        match = flag_access(child)\n"
            "        if match is not None:\n"
            "            variable_name, key = match\n"
            "            if variable_name in loop_name_set:\n"
            "                keys.add(key)\n"
            "        if not isinstance(child, ast.Call):\n"
            "            continue\n"
            "        name = called_name(child.func)\n"
            "        sensitive_keys = sensitive_functions.get(name, set())\n"
            "        if not sensitive_keys:\n"
            "            continue\n"
            "        arguments = list(child.args) + [keyword.value for keyword in child.keywords if keyword.arg is not None]\n"
            "        for argument in arguments:\n"
            "            if isinstance(argument, ast.Name) and argument.id in loop_name_set:\n"
            "                keys.update(sensitive_keys)\n"
            "    return keys\n\n"
            "def loop_has_status_filter(node, loop_names, flag_keys, filtered_aliases):\n"
            "    for loop_name in loop_names:\n"
            "        if loop_has_flag_guard(node, loop_name, flag_keys):\n"
            "            return True\n"
            "    if filtered_iter_flag_keys(node.iter) & flag_keys:\n"
            "        return True\n"
            "    if isinstance(node.iter, ast.Name) and filtered_aliases.get(node.iter.id, set()) & flag_keys:\n"
            "        return True\n"
            "    return False\n\n"
            "def collect_status_flag_loop_issues(body, sensitive_functions, issues, filtered_aliases):\n"
            "    local_aliases = dict(filtered_aliases)\n"
            "    for statement in body:\n"
            "        alias = assigned_filtered_alias(statement)\n"
            "        if alias is not None:\n"
            "            alias_name, alias_keys = alias\n"
            "            local_aliases[alias_name] = alias_keys\n"
            "        if isinstance(statement, ast.For):\n"
            "            loop_names = target_names(statement.target)\n"
            "            sensitivity_keys = loop_sensitivity_keys(statement, loop_names, sensitive_functions)\n"
            "            if sensitivity_keys and not loop_has_status_filter(statement, loop_names, sensitivity_keys, local_aliases):\n"
            "                issues.append({'type': 'unfiltered_status_flag_loop', 'line': getattr(statement, 'lineno', None), 'flag_key': sorted(sensitivity_keys)[0]})\n"
            "            collect_status_flag_loop_issues(statement.body, sensitive_functions, issues, local_aliases)\n"
            "            collect_status_flag_loop_issues(statement.orelse, sensitive_functions, issues, local_aliases)\n"
            "            continue\n"
            "        if isinstance(statement, ast.If):\n"
            "            collect_status_flag_loop_issues(statement.body, sensitive_functions, issues, local_aliases)\n"
            "            collect_status_flag_loop_issues(statement.orelse, sensitive_functions, issues, local_aliases)\n"
            "            continue\n"
            "        if isinstance(statement, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):\n"
            "            collect_status_flag_loop_issues(statement.body, sensitive_functions, issues, {})\n\n"
            "def detect_status_flag_loop_issues(tree):\n"
            "    issues = []\n"
            "    collect_status_flag_loop_issues(tree.body, flag_sensitive_functions(tree), issues, {})\n"
            "    return issues\n\n"
            "def detect_hardcoded_threshold_issues(tree):\n"
            "    issues = []\n"
            "    for node in ast.walk(tree):\n"
            "        if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):\n"
            "            continue\n"
            "        parameter_names = {argument.arg for argument in node.args.args}\n"
            "        if 'passing_score' not in parameter_names:\n"
            "            continue\n"
            "        compares_using_parameter = False\n"
            "        compares_using_literal = False\n"
            "        for child in ast.walk(node):\n"
            "            if not isinstance(child, ast.Compare):\n"
            "                continue\n"
            "            if compare_mentions_name(child, 'passing_score'):\n"
            "                compares_using_parameter = True\n"
            "            if compare_has_numeric_literal(child):\n"
            "                compares_using_literal = True\n"
            "        if compares_using_literal and not compares_using_parameter:\n"
            "            issues.append({'type': 'hardcoded_threshold_ignores_parameter', 'function_name': node.name})\n"
            "    return issues\n\n"
            "def filtered_iter_source_name(expression):\n"
            "    if not isinstance(expression, (ast.ListComp, ast.SetComp, ast.GeneratorExp)) or not expression.generators:\n"
            "        return None\n"
            "    generator = expression.generators[0]\n"
            "    if not isinstance(generator.iter, ast.Name) or not generator.ifs:\n"
            "        return None\n"
            "    return generator.iter.id\n\n"
            "def filtered_source_names(function_node):\n"
            "    filtered_sources = set()\n"
            "    filtered_alias_sources = {}\n"
            "    for statement in getattr(function_node, 'body', []):\n"
            "        if isinstance(statement, ast.Assign) and len(statement.targets) == 1 and isinstance(statement.targets[0], ast.Name):\n"
            "            source = filtered_iter_source_name(statement.value)\n"
            "            if source:\n"
            "                filtered_alias_sources[statement.targets[0].id] = source\n"
            "                filtered_sources.add(source)\n"
            "        if isinstance(statement, ast.For):\n"
            "            iter_name = statement.iter.id if isinstance(statement.iter, ast.Name) else None\n"
            "            loop_names = target_names(statement.target)\n"
            "            if iter_name and loop_names and loop_has_flag_guard(statement, loop_names[0], {'active', 'enabled', 'disabled', 'archived', 'deleted'}):\n"
            "                filtered_sources.add(iter_name)\n"
            "            if iter_name and iter_name in filtered_alias_sources:\n"
            "                filtered_sources.add(filtered_alias_sources[iter_name])\n"
            "    return filtered_sources\n\n"
            "def detect_processed_count_issues(tree):\n"
            "    issues = []\n"
            "    for node in ast.walk(tree):\n"
            "        if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):\n"
            "            continue\n"
            "        filtered_sources = filtered_source_names(node)\n"
            "        if not filtered_sources:\n"
            "            continue\n"
            "        for child in ast.walk(node):\n"
            "            if not isinstance(child, ast.Call) or not isinstance(child.func, ast.Name) or child.func.id != 'len' or not child.args:\n"
            "                continue\n"
            "            if isinstance(child.args[0], ast.Name) and child.args[0].id in filtered_sources:\n"
            "                issues.append({'type': 'processed_count_uses_unfiltered_collection', 'source_name': child.args[0].id})\n"
            "                break\n"
            "    return issues\n\n"
            "def if_chain(node):\n"
            "    chain = []\n"
            "    current = node\n"
            "    while current is not None:\n"
            "        chain.append(current)\n"
            "        if len(current.orelse) == 1 and isinstance(current.orelse[0], ast.If):\n"
            "            current = current.orelse[0]\n"
            "        else:\n"
            "            current = None\n"
            "    return chain\n\n"
            "def compare_fragments(node):\n"
            "    fragments = []\n"
            "    for child in ast.walk(node):\n"
            "        if isinstance(child, ast.Compare):\n"
            "            fragments.append(' '.join(ast.unparse(child).split()))\n"
            "    return sorted(set(fragment for fragment in fragments if fragment))\n\n"
            "def detect_duplicate_condition_issues(tree):\n"
            "    issues = []\n"
            "    for node in ast.walk(tree):\n"
            "        if not isinstance(node, ast.If):\n"
            "            continue\n"
            "        fragments = {}\n"
            "        for branch in if_chain(node):\n"
            "            for fragment in compare_fragments(branch.test):\n"
            "                fragments[fragment] = fragments.get(fragment, 0) + 1\n"
            "        for fragment, count in fragments.items():\n"
            "            if count > 1:\n"
            "                issues.append({'type': 'duplicate_condition_fragment', 'fragment': fragment, 'line': getattr(node, 'lineno', None)})\n"
            "    return issues\n\n"
            "def assigned_name_targets(function_node):\n"
            "    names = set()\n"
            "    for child in ast.walk(function_node):\n"
            "        if isinstance(child, ast.Assign):\n"
            "            for target in child.targets:\n"
            "                if isinstance(target, ast.Name):\n"
            "                    names.add(target.id)\n"
            "    return names\n\n"
            "def function_has_guard_before_line(lines, function_node, name, line_number):\n"
            "    function_start = max(getattr(function_node, 'lineno', 1), 1)\n"
            "    check_end = max(line_number - 1, function_start)\n"
            "    for line in lines[function_start - 1:check_end]:\n"
            "        stripped = line.strip()\n"
            "        if stripped.startswith(f'if not {name}') or stripped.startswith(f'if {name}:') or stripped.startswith(f'if len({name})'):\n"
            "            return True\n"
            "    return False\n\n"
            "def detect_unguarded_max_issues(tree, lines):\n"
            "    issues = []\n"
            "    for node in ast.walk(tree):\n"
            "        if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):\n"
            "            continue\n"
            "        parameters = {argument.arg for argument in node.args.args}\n"
            "        assigned_names = assigned_name_targets(node)\n"
            "        for child in ast.walk(node):\n"
            "            if not isinstance(child, ast.Call) or not isinstance(child.func, ast.Name) or child.func.id != 'max' or not child.args:\n"
            "                continue\n"
            "            target = child.args[0]\n"
            "            if not isinstance(target, ast.Name):\n"
            "                continue\n"
            "            if target.id not in parameters and target.id not in assigned_names:\n"
            "                continue\n"
            "            if function_has_guard_before_line(lines, node, target.id, getattr(child, 'lineno', 0)):\n"
            "                continue\n"
            "            issues.append({'type': 'unguarded_max_call', 'collection_name': target.id})\n"
            "    return issues\n\n"
            "def detect_type_safety_issues(tree):\n"
            "    issues = []\n"
            "    for node in ast.walk(tree):\n"
            "        if isinstance(node, ast.AugAssign) and isinstance(node.op, (ast.Add, ast.Sub)):\n"
            "            if isinstance(node.target, ast.Name) and node.target.id in {'total', 'sum', 'count'}:\n"
            "                if isinstance(node.value, ast.Name):\n"
            "                    issues.append({'type': 'type_safety_arithmetic_without_validation', 'variable': node.value.id, 'line': getattr(node, 'lineno', None)})\n"
            "    return issues\n\n"
            "def detect_output_formatting_issues(tree):\n"
            "    issues = []\n"
            "    write_calls = []\n"
            "    for node in ast.walk(tree):\n"
            "        if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == 'write':\n"
            "            write_calls.append(node)\n"
            "    for write_node in write_calls:\n"
            "        for arg in write_node.args:\n"
            "            for nested_node in ast.walk(arg):\n"
            "                if isinstance(nested_node, ast.Call) and isinstance(nested_node.func, ast.Name) and nested_node.func.id == 'str':\n"
            "                    if nested_node.args and isinstance(nested_node.args[0], ast.Name):\n"
            "                        issues.append({'type': 'output_formatting_raw_dict_string', 'variable': nested_node.args[0].id, 'line': getattr(nested_node, 'lineno', None)})\n"
            "    return issues\n\n"
            "content = TARGET_PATH.read_text(encoding='utf-8')\n"
            "lines = content.splitlines()\n"
            "tree = ast.parse(content)\n"
            "current_status_issues = detect_status_flag_loop_issues(tree)\n"
            "current_threshold_issues = detect_hardcoded_threshold_issues(tree)\n"
            "current_processed_count_issues = detect_processed_count_issues(tree)\n"
            "current_duplicate_issues = detect_duplicate_condition_issues(tree)\n"
            "current_unguarded_max_issues = detect_unguarded_max_issues(tree, lines)\n"
            "current_type_safety_issues = detect_type_safety_issues(tree)\n"
            "current_output_formatting_issues = detect_output_formatting_issues(tree)\n"
            "failures = []\n"
            "for issue in ISSUES:\n"
            "    issue_type = issue.get('issue_type')\n"
            "    if issue_type == 'bug_marker':\n"
            "        marker = issue.get('metadata', {}).get('marker') or issue.get('evidence') or ''\n"
            "        if marker and marker in content:\n"
            "            failures.append(f'BUG/FIXME marker still present: {marker}')\n"
            "    elif issue_type == 'unfiltered_status_flag_loop':\n"
            "        flag_key = issue.get('metadata', {}).get('flag_key')\n"
            "        if any(item.get('flag_key') == flag_key for item in current_status_issues):\n"
            "            failures.append(f'Status-flagged items with `{flag_key}` are still processed without a guard.')\n"
            "    elif issue_type == 'hardcoded_threshold_ignores_parameter':\n"
            "        function_name = issue.get('metadata', {}).get('function_name')\n"
            "        if any(item.get('function_name') == function_name for item in current_threshold_issues):\n"
            "            failures.append('Threshold logic still ignores the configurable passing_score parameter.')\n"
            "    elif issue_type == 'processed_count_uses_unfiltered_collection':\n"
            "        source_name = issue.get('metadata', {}).get('source_name') or issue.get('evidence', '').removeprefix('len(').removesuffix(')')\n"
            "        if any(item.get('source_name') == source_name for item in current_processed_count_issues):\n"
            "            failures.append(f'Processed count still uses the unfiltered collection `{source_name}`.')\n"
            "    elif issue_type == 'unguarded_max_call':\n"
            "        collection_name = issue.get('metadata', {}).get('collection_name')\n"
            "        if any(item.get('collection_name') == collection_name for item in current_unguarded_max_issues):\n"
            "            failures.append(f'max() is still unguarded for `{collection_name}`.')\n"
            "    elif issue_type == 'duplicate_condition_fragment':\n"
            "        fragment = issue.get('metadata', {}).get('fragment') or issue.get('evidence')\n"
            "        if any(item.get('fragment') == fragment for item in current_duplicate_issues):\n"
            "            failures.append(f'Duplicate conditional fragment still present: {fragment}')\n"
            "    elif issue_type == 'type_safety_arithmetic_without_validation':\n"
            "        variable = issue.get('metadata', {}).get('variable') or issue.get('evidence', '').split(' += ')[-1] if ' += ' in issue.get('evidence', '') else ''\n"
            "        if any(item.get('variable') == variable for item in current_type_safety_issues):\n"
            "            failures.append(f'Arithmetic operation on variable `{variable}` without type validation still present.')\n"
            "    elif issue_type == 'output_formatting_raw_dict_string':\n"
            "        variable = issue.get('metadata', {}).get('variable') or ''\n"
            "        if any(item.get('variable') == variable for item in current_output_formatting_issues):\n"
            "            failures.append(f'Raw dict string output for `{variable}` still present - use json.dumps() or f-strings.')\n"
            "if failures:\n"
            "    raise SystemExit('\\n'.join(failures))\n"
            "print('focused acceptance checks passed')\n"
        )

    async def _edge_case_type_check(
        self,
        task: TaskState,
        repo_context: dict,
        repo_path: Path,
    ) -> ValidationCheckResult | None:
        """Check for type safety issues and edge cases that could cause runtime errors."""
        mentioned_python_files = [
            Path(str(path))
            for path in repo_context.get("mentioned_files", [])
            if str(path).endswith(".py")
        ]
        if not mentioned_python_files:
            return None

        issues: list[StructuredError] = []
        for relative_path in mentioned_python_files:
            target_path = repo_path / relative_path
            try:
                content = target_path.read_text(encoding="utf-8")
                tree = ast.parse(content)
            except (OSError, UnicodeDecodeError, SyntaxError):
                continue

            issues.extend(self._detect_type_safety_issues(tree, content, relative_path))
            issues.extend(self._detect_output_formatting_issues(tree, content, relative_path))

        if not issues:
            return None

        return ValidationCheckResult(
            name="edge_case_type_check",
            success=False,
            command="static type safety analysis",
            stderr="Potential type safety or edge case issues detected.",
            structured_errors=issues,
        )

    def _detect_type_safety_issues(self, tree: ast.AST, content: str, relative_path: Path) -> list[StructuredError]:
        """Detect operations that assume specific types without validation."""
        issues: list[StructuredError] = []

        for node in ast.walk(tree):
            # Check for arithmetic operations on loop variables that might not be numeric
            if isinstance(node, ast.AugAssign) and isinstance(node.op, (ast.Add, ast.Sub)):
                # Look for patterns like: total += item where item is from a loop
                if isinstance(node.target, ast.Name) and node.target.id in {"total", "sum", "count"}:
                    if isinstance(node.value, ast.Name):
                        issues.append(
                            StructuredError(
                                error_type="PotentialTypeError",
                                message=f"Arithmetic operation '{node.op.__name__}' on variable '{node.value.id}' without type validation - will fail if value is a string",
                                file=relative_path.as_posix(),
                                line=getattr(node, "lineno", None),
                            )
                        )

        return issues

    def _detect_output_formatting_issues(self, tree: ast.AST, content: str, relative_path: Path) -> list[StructuredError]:
        """Detect poor output formatting like raw dict strings."""
        issues: list[StructuredError] = []
        lines = content.splitlines()

        # First, find all nodes that write to files
        write_calls = []
        for node in ast.walk(tree):
            if isinstance(node, ast.Call):
                # Check for f.write(...) pattern
                if isinstance(node.func, ast.Attribute) and node.func.attr == "write":
                    write_calls.append(node)

        if not write_calls:
            return issues

        # Now find str() calls within write() arguments
        for write_node in write_calls:
            for arg in write_node.args:
                for nested_node in ast.walk(arg):
                    if isinstance(nested_node, ast.Call):
                        if isinstance(nested_node.func, ast.Name) and nested_node.func.id == "str":
                            if nested_node.args and isinstance(nested_node.args[0], ast.Name):
                                var_name = nested_node.args[0].id
                                issues.append(
                                    StructuredError(
                                        error_type="OutputFormattingIssue",
                                        message=f"Using str() on variable '{var_name}' in file output - produces unreadable dict strings instead of formatted output (use json.dumps() or f-strings)",
                                        file=relative_path.as_posix(),
                                        line=getattr(nested_node, "lineno", None),
                                    )
                                )

        return issues

    async def _run_edge_case_runtime_tests(
        self,
        task: TaskState,
        repo_context: dict,
        repo_path: Path,
    ) -> ValidationCheckResult | None:
        """Run edge case tests by generating synthetic inputs and executing the code."""
        mentioned_python_files = [
            Path(str(path))
            for path in repo_context.get("mentioned_files", [])
            if str(path).endswith(".py")
        ]
        if not mentioned_python_files:
            return None

        # Only run edge case tests for single-file Python scripts with main()
        if len(mentioned_python_files) != 1:
            return None

        target_path = repo_path / mentioned_python_files[0]
        try:
            content = target_path.read_text(encoding="utf-8")
        except (OSError, UnicodeDecodeError):
            return None

        # The synthesized edge-case harness is only valid for a narrow class of
        # single-file dataset scripts. Skip generic scripts to avoid false failures.
        if not self._supports_edge_case_runtime_tests(content):
            return None

        # Generate edge case test script
        test_script = self._build_edge_case_test_script(mentioned_python_files[0], content)
        try:
            ast.parse(test_script)
        except SyntaxError:
            return None
        test_script_path = repo_path / ".aiiq_tmp" / f"edge_case_test_{task.task_id}.py"

        try:
            test_script_path.parent.mkdir(parents=True, exist_ok=True)
            test_script_path.write_text(test_script, encoding="utf-8", newline="\n")

            command = f"python {shlex.quote(test_script_path.relative_to(repo_path).as_posix())}"
            result = await self._run_check(
                name="edge_case_runtime",
                command=command,
                is_test=True,
                task=task,
                repo_path=repo_path,
            )

            # Transform the result to indicate edge case testing
            if result:
                return ValidationCheckResult(
                    name="edge_case_runtime",
                    success=result.success,
                    command=result.command,
                    exit_code=result.exit_code,
                    stdout=result.stdout,
                    stderr=result.stderr,
                    structured_errors=result.structured_errors,
                )
        finally:
            try:
                test_script_path.unlink(missing_ok=True)
            except OSError:
                pass
            try:
                test_script_path.parent.rmdir()
            except OSError:
                pass

        return None

    @staticmethod
    def _supports_edge_case_runtime_tests(content: str) -> bool:
        has_main = '__name__ == "__main__"' in content or "__name__ == '__main__'" in content
        return has_main and "STUDENT_DATA" in content

    @staticmethod
    def _build_edge_case_test_script(file_path: str, original_content: str) -> str:
        """Build a test script that generates synthetic edge case inputs."""
        return f'''import sys
import json
import subprocess
from pathlib import Path
import re

# Edge case inputs to test
edge_cases = [
    {{"name": "Alice", "scores": [], "active": True}},
    {{"name": "Bob", "scores": [72, 67, 70], "active": True}},
    {{"name": "Charlie", "scores": ["89", "90", "91"], "active": False}},
    {{"name": "Dana", "scores": [None, None, None], "active": True}},
]

# Run the original script with each edge case
for i, test_data in enumerate(edge_cases):
    print(f"Testing edge case {{i+1}}: {{test_data}}")

    # Create a modified version of the script with test data
    test_script = Path("{file_path}").read_text()

    # Replace STUDENT_DATA with test data
    test_script = re.sub(
        r"STUDENT_DATA\\s*=\\s*(?:\\\"[^\\\"\\n]*\\\"|'[^'\\n]*')",
        "STUDENT_DATA = " + repr(json.dumps([test_data])),
        test_script
    )

    # Write to temp file and execute
    temp_path = Path("temp_test.py")
    temp_path.write_text(test_script)

    try:
        result = subprocess.run(
            [sys.executable, "temp_test.py"],
            capture_output=True,
            text=True,
            timeout=5
        )

        if result.returncode != 0:
            print(f"  FAILED: {{result.stderr}}")
        else:
            print(f"  PASSED")
    except subprocess.TimeoutExpired:
        print(f"  FAILED: Timeout")
    except Exception as e:
        print(f"  FAILED: {{e}}")
    finally:
        temp_path.unlink(missing_ok=True)

print("Edge case testing complete")
'''

    @staticmethod
    def _requires_code_changes(goal: str) -> bool:
        lowered = goal.lower()
        return any(keyword in lowered for keyword in ("fix", "repair", "resolve", "patch", "correct", "debug"))

    async def _run_ruff_lint(
        self,
        task: TaskState,
        repo_context: dict,
        repo_path: Path,
    ) -> ValidationCheckResult | None:
        """Run Ruff linter for fast, comprehensive Python linting."""
        mentioned_python_files = [
            str(path)
            for path in repo_context.get("mentioned_files", [])
            if str(path).endswith(".py")
        ]

        # Fall back to all Python files if no specific files mentioned
        if not mentioned_python_files:
            python_files = list(repo_path.rglob("*.py"))
            # Filter out test files and virtual environment
            python_files = [
                f for f in python_files
                if "test" not in f.parts
                and "tests" not in f.parts
                and "venv" not in f.parts
                and ".venv" not in f.parts
                and "__pycache__" not in f.parts
            ]
            if len(python_files) > self.settings.validation_ruff_repo_file_cap:
                return None
            mentioned_python_files = [
                str(f.relative_to(repo_path)) for f in python_files[: self.settings.validation_ruff_target_file_limit]
            ]

        if not mentioned_python_files:
            return None

        # Check if ruff is available
        check_result = await self.shell_runner.run_shell(
            command="ruff --version",
            repo_path=repo_path,
            timeout_seconds=self.settings.validation_ruff_version_timeout_seconds,
            use_docker=False,
        )
        if not check_result.success:
            return None  # Ruff not available, skip silently

        issues: list[StructuredError] = []

        for file_path in mentioned_python_files:
            # Run ruff with JSON output for structured parsing
            target_path = repo_path / file_path
            if not target_path.exists():
                continue
            try:
                ast.parse(target_path.read_text(encoding="utf-8"))
            except (OSError, UnicodeDecodeError, SyntaxError):
                continue

            result = await self.shell_runner.run_shell(
                command=f"ruff check --output-format=json {shlex.quote(file_path)}",
                repo_path=repo_path,
                timeout_seconds=self.settings.validation_ruff_check_timeout_seconds,
                use_docker=False,
            )

            if result.stdout:
                try:
                    ruff_output = json.loads(result.stdout)
                    for item in ruff_output:
                        issues.append(
                            StructuredError(
                                error_type=f"Ruff:{item.get('code', 'unknown')}",
                                message=item.get('message', ''),
                                file=item.get('filename', file_path),
                                line=item.get('location', {}).get('row'),
                            )
                        )
                except json.JSONDecodeError:
                    # Fallback: parse non-JSON output
                    for line in result.stdout.splitlines():
                        if ":" in line and line.strip():
                            issues.append(
                                StructuredError(
                                    error_type="Ruff",
                                    message=line.strip(),
                                    file=file_path,
                                )
                            )

        if not issues:
            return None

        return ValidationCheckResult(
            name="ruff_lint",
            success=False,
            command="ruff check",
            stderr=f"Ruff found {len(issues)} issue(s)",
            structured_errors=issues,
        )

    def _semantic_analysis_check(
        self,
        task: TaskState,
        repo_context: dict,
        repo_path: Path,
    ) -> ValidationCheckResult | None:
        """Enhanced AST-based semantic analysis for Python-specific issues."""
        mentioned_python_files = [
            Path(str(path))
            for path in repo_context.get("mentioned_files", [])
            if str(path).endswith(".py")
        ]
        if not mentioned_python_files:
            return None

        all_issues: list[StructuredError] = []
        for relative_path in mentioned_python_files:
            target_path = repo_path / relative_path
            try:
                content = target_path.read_text(encoding="utf-8")
                tree = ast.parse(content)
            except (OSError, UnicodeDecodeError, SyntaxError):
                continue

            # Run all semantic checks
            all_issues.extend(self._detect_unreachable_code(tree, content, relative_path))
            all_issues.extend(self._detect_unused_imports(tree, content, relative_path))
            all_issues.extend(self._detect_missing_error_handling(tree, content, relative_path))
            all_issues.extend(self._detect_resource_leaks(tree, content, relative_path))
            all_issues.extend(self._detect_unsafe_defaults(tree, content, relative_path))
            all_issues.extend(self._detect_none_handling_issues(tree, content, relative_path))
            all_issues.extend(self._detect_mapping_contract_issues(tree, content, relative_path))
            all_issues.extend(self._detect_optional_contract_attribute_issues(tree, content, relative_path))
            all_issues.extend(self._detect_text_encoding_contract_issues(tree, content, relative_path))

        if not all_issues:
            return None

        return ValidationCheckResult(
            name="semantic_analysis",
            success=False,
            command="ast semantic analysis",
            stderr=f"Found {len(all_issues)} semantic issue(s)",
            structured_errors=all_issues,
        )

    def _detect_unreachable_code(self, tree: ast.AST, content: str, relative_path: Path) -> list[StructuredError]:
        """Detect code that can never be executed (after return, raise, etc.)."""
        issues: list[StructuredError] = []

        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                if not node.body:
                    continue
                for i, stmt in enumerate(node.body):
                    # Check for return/raise followed by more code
                    if isinstance(stmt, (ast.Return, ast.Raise)):
                        # Check if there are statements after this
                        if i + 1 < len(node.body):
                            next_stmt = node.body[i + 1]
                            issues.append(
                                StructuredError(
                                    error_type="UnreachableCode",
                                    message=f"Unreachable code after {stmt.__class__.__name__} statement at line {getattr(stmt, 'lineno', '?')}",
                                    file=relative_path.as_posix(),
                                    line=getattr(next_stmt, "lineno", None),
                                )
                            )
                        break  # Only report first unreachable code

        return issues

    def _detect_unused_imports(self, tree: ast.AST, content: str, relative_path: Path) -> list[StructuredError]:
        """Detect imports that are never used in the code."""
        issues: list[StructuredError] = []

        # Collect all imports
        imports: dict[str, int] = {}
        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    name = alias.asname if alias.asname else alias.name.split('.')[0]
                    imports[name] = getattr(node, "lineno", None)
            elif isinstance(node, ast.ImportFrom):
                for alias in node.names:
                    name = alias.asname if alias.asname else alias.name
                    imports[name] = getattr(node, "lineno", None)

        if not imports:
            return issues

        # Collect all names used in the code
        used_names: set[str] = set()
        for node in ast.walk(tree):
            if isinstance(node, ast.Name):
                used_names.add(node.id)
            elif isinstance(node, ast.Attribute):
                # For attr.access, get the base name
                if isinstance(node.value, ast.Name):
                    used_names.add(node.value.id)

        # Find unused imports
        for import_name, line_num in imports.items():
            if import_name not in used_names:
                issues.append(
                    StructuredError(
                        error_type="UnusedImport",
                        message=f"Import '{import_name}' is never used",
                        file=relative_path.as_posix(),
                        line=line_num,
                    )
                )

        return issues

    def _detect_missing_error_handling(self, tree: ast.AST, content: str, relative_path: Path) -> list[StructuredError]:
        """Detect I/O operations and risky calls without error handling."""
        issues: list[StructuredError] = []

        # Risky operations that should have error handling
        risky_operations = {
            "Path.unlink": "file deletion",
            "Path.mkdir": "directory creation",
            "subprocess.run": "subprocess execution",
            "subprocess.call": "subprocess execution",
            "requests.get": "HTTP request",
            "requests.post": "HTTP request",
        }

        for node in ast.walk(tree):
            if isinstance(node, ast.Call):
                func_name = None
                if isinstance(node.func, ast.Name):
                    func_name = node.func.id
                elif isinstance(node.func, ast.Attribute):
                    if isinstance(node.func.value, ast.Name):
                        func_name = f"{node.func.value.id}.{node.func.attr}"

                if func_name in risky_operations:
                    # Check if this call is within a try-except block
                    in_try = False
                    for parent in ast.walk(tree):
                        if isinstance(parent, ast.Try):
                            # Check if our node is in the try body
                            for body_stmt in parent.body:
                                if node == body_stmt or self._node_contains(body_stmt, node):
                                    in_try = True
                                    break
                            if in_try:
                                break

                    if not in_try:
                        issues.append(
                            StructuredError(
                                error_type="MissingErrorHandling",
                                message=f"{risky_operations[func_name]} operation '{func_name}' without error handling (wrap in try-except)",
                                file=relative_path.as_posix(),
                                line=getattr(node, "lineno", None),
                            )
                        )

        return issues

    def _node_contains(self, parent: ast.AST, child: ast.AST) -> bool:
        """Check if a parent node contains a child node."""
        for node in ast.walk(parent):
            if node is child:
                return True
        return False

    def _detect_resource_leaks(self, tree: ast.AST, content: str, relative_path: Path) -> list[StructuredError]:
        """Detect potential resource leaks (unclosed files, connections)."""
        issues: list[StructuredError] = []

        for node in ast.walk(tree):
            if isinstance(node, ast.Call):
                if isinstance(node.func, ast.Name) and node.func.id == "open":
                    if not self._is_call_within_with_context(tree, node):
                        issues.append(
                            StructuredError(
                                error_type="PotentialResourceLeak",
                                message="File opened with open() but not used with context manager (use 'with open(...)' to ensure cleanup)",
                                file=relative_path.as_posix(),
                                line=getattr(node, "lineno", None),
                            )
                        )

        return issues

    def _is_call_within_with_context(self, tree: ast.AST, target_call: ast.Call) -> bool:
        for parent in ast.walk(tree):
            if not isinstance(parent, (ast.With, ast.AsyncWith)):
                continue
            for item in parent.items:
                if target_call is item.context_expr or self._node_contains(item.context_expr, target_call):
                    return True
        return False

    def _detect_unsafe_defaults(self, tree: ast.AST, content: str, relative_path: Path) -> list[StructuredError]:
        """Detect mutable default arguments (classic Python gotcha)."""
        issues: list[StructuredError] = []

        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                for default in node.args.defaults:
                    if isinstance(default, (ast.List, ast.Dict, ast.Set)):
                        issues.append(
                            StructuredError(
                                error_type="MutableDefaultArgument",
                                message=f"Function '{node.name}' has mutable default argument - will be shared across all calls (use None and initialize inside function)",
                                file=relative_path.as_posix(),
                                line=getattr(node, "lineno", None),
                            )
                        )

        return issues

    def _detect_none_handling_issues(self, tree: ast.AST, content: str, relative_path: Path) -> list[StructuredError]:
        """Detect potential None-related errors (missing None checks)."""
        issues: list[StructuredError] = []

        # Look for patterns like: optional_value.method() without None check
        for node in ast.walk(tree):
            if isinstance(node, ast.Call):
                if isinstance(node.func, ast.Attribute):
                    # This is a method call like value.method()
                    if node.func.attr == "get":
                        continue
                    # Check if the value could be None
                    if isinstance(node.func.value, ast.Name):
                        var_name = node.func.value.id
                        # Check if this variable is assigned from a function that might return None
                        # This is a simplified heuristic
                        for assign_node in ast.walk(tree):
                            if isinstance(assign_node, ast.Assign):
                                for target in assign_node.targets:
                                    if isinstance(target, ast.Name) and target.id == var_name:
                                        if isinstance(assign_node.value, ast.Call):
                                            if self._has_optional_value_guard_before_line(
                                                lines=content.splitlines(),
                                                function_node=next(
                                                    (
                                                        parent
                                                        for parent in ast.walk(tree)
                                                        if isinstance(parent, (ast.FunctionDef, ast.AsyncFunctionDef))
                                                        and self._node_contains(parent, node)
                                                    ),
                                                    tree,
                                                ),
                                                name=var_name,
                                                line_number=getattr(node, "lineno", 0),
                                                include_current_line=True,
                                            ):
                                                continue
                                            # Function call that might return None
                                            issues.append(
                                                StructuredError(
                                                    error_type="PotentialNoneError",
                                                    message=f"Method call on '{var_name}' without None check - if '{var_name}' is None, this will raise AttributeError",
                                                    file=relative_path.as_posix(),
                                                    line=getattr(node, "lineno", None),
                                                )
                                            )

        return issues

    def _detect_mapping_contract_issues(self, tree: ast.AST, content: str, relative_path: Path) -> list[StructuredError]:
        issues: list[StructuredError] = []
        parents = self._build_parent_map(tree)
        lines = content.splitlines()

        for function_node in ast.walk(tree):
            if not isinstance(function_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                continue
            parameter_names = {argument.arg for argument in function_node.args.args}
            if not parameter_names:
                continue
            contract_aliases = self._contract_alias_names(function_node, parameter_names)

            for child in ast.walk(function_node):
                if not isinstance(child, ast.Subscript) or not isinstance(child.ctx, ast.Load):
                    continue
                parent = parents.get(child)
                if isinstance(parent, ast.Subscript) and parent.value is child:
                    continue
                root_name = self._contract_root_name(child.value)
                if not root_name or (root_name not in parameter_names and root_name not in contract_aliases):
                    continue
                access_path = self._contract_access_path(child)
                if not access_path:
                    continue
                if len(access_path) == 1 and access_path[0] in {"active", "enabled", "disabled", "archived", "deleted"}:
                    continue
                if self._is_sequence_index_access(
                    function_node=function_node,
                    root_name=root_name,
                    key_name=access_path[0],
                ):
                    continue
                if self._has_contract_key_guard_before_line(
                    lines=lines,
                    function_node=function_node,
                    root_name=root_name,
                    key_name=access_path[0],
                    line_number=getattr(child, "lineno", 0),
                ):
                    continue
                issues.append(
                    StructuredError(
                        error_type="PotentialKeyError",
                        message=f"Caller-provided mapping access `{'.'.join(access_path)}` assumes required keys are present",
                        file=relative_path.as_posix(),
                        line=getattr(child, "lineno", None),
                    )
                )
        return issues

    def _detect_optional_contract_attribute_issues(
        self,
        tree: ast.AST,
        content: str,
        relative_path: Path,
    ) -> list[StructuredError]:
        issues: list[StructuredError] = []
        lines = content.splitlines()
        safe_attributes = {"get", "keys", "items", "values", "copy"}

        for function_node in ast.walk(tree):
            if not isinstance(function_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                continue
            parameter_names = {argument.arg for argument in function_node.args.args}
            if not parameter_names:
                continue
            contract_aliases = self._contract_alias_names(function_node, parameter_names)

            for child in ast.walk(function_node):
                if not isinstance(child, ast.Call) or not isinstance(child.func, ast.Attribute):
                    continue
                if child.func.attr in safe_attributes:
                    continue
                root_name = self._contract_root_name(child.func.value)
                if not root_name or (root_name not in parameter_names and root_name not in contract_aliases):
                    continue
                access_path = self._contract_access_path(child.func.value)
                if len(access_path) == 1 and access_path[0] in {"active", "enabled", "disabled", "archived", "deleted"}:
                    continue
                if access_path and self._has_contract_key_guard_before_line(
                    lines=lines,
                    function_node=function_node,
                    root_name=root_name,
                    key_name=access_path[0],
                    line_number=getattr(child, "lineno", 0),
                ):
                    continue
                if self._has_optional_value_guard_before_line(
                    lines=lines,
                    function_node=function_node,
                    name=root_name,
                    line_number=getattr(child, "lineno", 0),
                    include_current_line=True,
                ):
                    continue
                subject = ".".join(access_path) if access_path else root_name
                issues.append(
                    StructuredError(
                        error_type="PotentialAttributeError",
                        message=f"Method `{child.func.attr}()` assumes caller-provided value `{subject}` is present and non-None",
                        file=relative_path.as_posix(),
                        line=getattr(child, "lineno", None),
                    )
                )
        return issues

    def _detect_text_encoding_contract_issues(
        self,
        tree: ast.AST,
        content: str,
        relative_path: Path,
    ) -> list[StructuredError]:
        issues: list[StructuredError] = []
        for child in ast.walk(tree):
            if not isinstance(child, ast.Call):
                continue
            if self._is_text_open_without_encoding(child) or self._is_read_text_without_encoding(child):
                issues.append(
                    StructuredError(
                        error_type="PotentialUnicodeDecodeError",
                        message="Text file read relies on the platform default encoding",
                        file=relative_path.as_posix(),
                        line=getattr(child, "lineno", None),
                    )
                )
        return issues

    @staticmethod
    def _build_parent_map(tree: ast.AST) -> dict[ast.AST, ast.AST]:
        parents: dict[ast.AST, ast.AST] = {}
        for parent in ast.walk(tree):
            for child in ast.iter_child_nodes(parent):
                parents[child] = parent
        return parents

    def _contract_alias_names(self, function_node: ast.AST, parameter_names: set[str]) -> set[str]:
        aliases: set[str] = set()
        changed = True
        while changed:
            changed = False
            for child in ast.walk(function_node):
                if not isinstance(child, ast.Assign) or len(child.targets) != 1:
                    continue
                target = child.targets[0]
                if not isinstance(target, ast.Name):
                    continue
                root_name = self._contract_root_name(child.value)
                if not root_name or (root_name not in parameter_names and root_name not in aliases):
                    continue
                if target.id not in aliases:
                    aliases.add(target.id)
                    changed = True
        return aliases

    @staticmethod
    def _contract_root_name(node: ast.AST) -> str | None:
        if isinstance(node, ast.Name):
            return node.id
        if isinstance(node, ast.Subscript):
            return ValidationEngine._contract_root_name(node.value)
        if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
            return ValidationEngine._contract_root_name(node.func.value)
        if isinstance(node, ast.Attribute):
            return ValidationEngine._contract_root_name(node.value)
        return None

    @staticmethod
    def _contract_access_path(node: ast.AST) -> list[str]:
        if isinstance(node, ast.Name):
            return []
        if isinstance(node, ast.Attribute):
            return ValidationEngine._contract_access_path(node.value)
        if isinstance(node, ast.Subscript):
            key = ValidationEngine._literal_key_or_placeholder(node.slice)
            if key is None:
                return ValidationEngine._contract_access_path(node.value)
            return [*ValidationEngine._contract_access_path(node.value), key]
        if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "get":
            if not node.args:
                return ValidationEngine._contract_access_path(node.func.value)
            key = ValidationEngine._literal_key_or_placeholder(node.args[0])
            if key is None:
                return ValidationEngine._contract_access_path(node.func.value)
            return [*ValidationEngine._contract_access_path(node.func.value), key]
        return []

    @staticmethod
    def _literal_key_or_placeholder(node: ast.AST) -> str | None:
        if isinstance(node, ast.Constant) and isinstance(node.value, str):
            return node.value
        if isinstance(node, ast.Name):
            return f"<dynamic:{node.id}>"
        return None

    @staticmethod
    def _has_contract_key_guard_before_line(
        *,
        lines: list[str],
        function_node: ast.AST,
        root_name: str,
        key_name: str,
        line_number: int,
    ) -> bool:
        function_start = max(getattr(function_node, "lineno", 1), 1)
        check_end = max(line_number - 1, function_start)
        dynamic_key = None
        if key_name.startswith("<dynamic:") and key_name.endswith(">"):
            dynamic_key = key_name[len("<dynamic:") : -1]
        for line in lines[function_start - 1 : check_end]:
            stripped = line.strip()
            if (
                stripped.startswith(f"if not {root_name}")
                or stripped.startswith(f"if {root_name}:")
                or stripped.startswith(f"if {root_name} is None")
                or stripped.startswith(f"if {root_name} is not None")
            ):
                return True
            if dynamic_key:
                if (
                    f"{root_name}.get({dynamic_key}" in stripped
                    or f"{dynamic_key} in {root_name}" in stripped
                    or f"{dynamic_key} not in {root_name}" in stripped
                ):
                    return True
                continue
            if (
                f"{root_name}.get('{key_name}'" in stripped
                or f'{root_name}.get("{key_name}"' in stripped
                or f"'{key_name}' in {root_name}" in stripped
                or f'"{key_name}" in {root_name}' in stripped
                or f"'{key_name}' not in {root_name}" in stripped
                or f'"{key_name}" not in {root_name}' in stripped
            ):
                return True
        return False

    @staticmethod
    def _has_optional_value_guard_before_line(
        *,
        lines: list[str],
        function_node: ast.AST,
        name: str,
        line_number: int,
        include_current_line: bool = False,
    ) -> bool:
        function_start = max(getattr(function_node, "lineno", 1), 1)
        check_end = max(line_number, function_start) if include_current_line else max(line_number - 1, function_start)
        for line in lines[function_start - 1 : check_end]:
            if ValidationEngine._line_has_optional_value_guard(line, name):
                return True
        return False

    @staticmethod
    def _line_has_optional_value_guard(line: str, name: str) -> bool:
        stripped = line.strip()
        escaped_name = re.escape(name)
        patterns = (
            rf"\bif\s+not\s+{escaped_name}\b",
            rf"\bif\s+{escaped_name}\s*:",
            rf"\bif\s+{escaped_name}\s+(?:and|or)\b",
            rf"\bif\s+{escaped_name}\s+is\s+None\b",
            rf"\bif\s+{escaped_name}\s+is\s+not\s+None\b",
            rf"\bif\s+{escaped_name}\b.*\belse\b",
        )
        return any(re.search(pattern, stripped) for pattern in patterns)

    @staticmethod
    def _loop_target_names(node: ast.AST) -> set[str]:
        if isinstance(node, ast.Name):
            return {node.id}
        if isinstance(node, (ast.Tuple, ast.List)):
            names: set[str] = set()
            for child in node.elts:
                names.update(ValidationEngine._loop_target_names(child))
            return names
        return set()

    @staticmethod
    def _dynamic_placeholder_name(key_name: str) -> str | None:
        if key_name.startswith("<dynamic:") and key_name.endswith(">"):
            return key_name[len("<dynamic:") : -1]
        return None

    @staticmethod
    def _is_range_len_iter(node: ast.AST, root_name: str) -> bool:
        return (
            isinstance(node, ast.Call)
            and isinstance(node.func, ast.Name)
            and node.func.id == "range"
            and len(node.args) == 1
            and isinstance(node.args[0], ast.Call)
            and isinstance(node.args[0].func, ast.Name)
            and node.args[0].func.id == "len"
            and len(node.args[0].args) == 1
            and isinstance(node.args[0].args[0], ast.Name)
            and node.args[0].args[0].id == root_name
        )

    @staticmethod
    def _is_enumerate_iter(node: ast.AST, root_name: str) -> bool:
        return (
            isinstance(node, ast.Call)
            and isinstance(node.func, ast.Name)
            and node.func.id == "enumerate"
            and bool(node.args)
            and isinstance(node.args[0], ast.Name)
            and node.args[0].id == root_name
        )

    @staticmethod
    def _is_sequence_index_access(*, function_node: ast.AST, root_name: str, key_name: str) -> bool:
        dynamic_name = ValidationEngine._dynamic_placeholder_name(key_name)
        if dynamic_name is None:
            return False
        for child in ast.walk(function_node):
            if not isinstance(child, ast.For):
                continue
            loop_names = ValidationEngine._loop_target_names(child.target)
            if dynamic_name not in loop_names:
                continue
            if ValidationEngine._is_range_len_iter(child.iter, root_name) or ValidationEngine._is_enumerate_iter(child.iter, root_name):
                return True
        return False

    @staticmethod
    def _is_text_open_without_encoding(node: ast.Call) -> bool:
        if not isinstance(node.func, ast.Name) or node.func.id != "open":
            return False
        if any(keyword.arg == "encoding" for keyword in node.keywords if keyword.arg):
            return False
        mode = "r"
        if len(node.args) >= 2 and isinstance(node.args[1], ast.Constant) and isinstance(node.args[1].value, str):
            mode = node.args[1].value
        for keyword in node.keywords:
            if keyword.arg == "mode" and isinstance(keyword.value, ast.Constant) and isinstance(keyword.value.value, str):
                mode = keyword.value.value
                break
        return "b" not in mode

    @staticmethod
    def _is_read_text_without_encoding(node: ast.Call) -> bool:
        return (
            isinstance(node.func, ast.Attribute)
            and node.func.attr == "read_text"
            and not any(keyword.arg == "encoding" for keyword in node.keywords if keyword.arg)
        )

    async def _rag_pattern_check(
        self,
        task: TaskState,
        repo_context: dict,
        repo_path: Path,
    ) -> ValidationCheckResult | None:
        """Check code against known bug-fix patterns from the RAG knowledge base."""
        if not self.rag_knowledge_base or not self.embedding_service:
            return None

        mentioned_python_files = [
            Path(str(path))
            for path in repo_context.get("mentioned_files", [])
            if str(path).endswith(".py")
        ]
        if not mentioned_python_files:
            return None

        all_issues: list[StructuredError] = []
        seen_examples: set[str] = set()

        for relative_path in mentioned_python_files:
            target_path = repo_path / relative_path
            try:
                content = target_path.read_text(encoding="utf-8")
            except (OSError, UnicodeDecodeError):
                continue

            for unit in self.rag_knowledge_base.extract_query_units(content, file_path=str(relative_path)):
                query_code = str(unit.get("code", "")).strip()
                if not query_code:
                    continue
                code_embedding = self.embedding_service.embed_code(query_code, context=task.goal)
                query = self.rag_knowledge_base.build_retrieval_query(
                    query_code=query_code,
                    bug_description=task.goal,
                    file_path=str(relative_path),
                    function_name=unit.get("function_name"),
                    max_results=self.settings.validation_rag_max_results_per_unit,
                )
                result = self.rag_knowledge_base.retrieve_similar(
                    query=query,
                    query_embedding=code_embedding,
                )
                if result.examples:
                    event_id = self.rag_knowledge_base.record_retrieval_event(
                        task_id=task.task_id,
                        stage="validation",
                        query=query,
                        examples=result.examples,
                        file_path=str(relative_path),
                        function_name=unit.get("function_name"),
                    )
                    task.rag_retrieval_events.append(
                        {
                            "event_id": event_id,
                            "stage": "validation",
                            "file_path": relative_path.as_posix(),
                            "function_name": unit.get("function_name"),
                            "example_ids": [item.example.id for item in result.examples],
                        }
                    )

                if not result.examples:
                    continue
                for retrieved in result.examples:
                    if retrieved.relevance_score <= self.settings.validation_rag_min_relevance_score or retrieved.example.id in seen_examples:
                        continue
                    seen_examples.add(retrieved.example.id)
                    example = retrieved.example
                    all_issues.append(
                        StructuredError(
                            error_type=f"RAGPattern:{example.bug_category}",
                            message=(
                                f"Similar to known bug pattern from {example.dataset} ({example.example_kind}): "
                                f"{example.bug_summary}. Suggested fix: {self._extract_fix_suggestion(example.diff)}"
                            ),
                            file=relative_path.as_posix(),
                            line=example.line_number,
                        )
                    )

        if not all_issues:
            return None

        return ValidationCheckResult(
            name="rag_pattern_match",
            success=False,
            command="rag knowledge base pattern matching",
            stderr=f"Found {len(all_issues)} potential issues matching known bug patterns",
            structured_errors=all_issues,
        )

    def _extract_fix_suggestion(self, diff: str) -> str:
        """Extract a brief fix suggestion from a diff."""
        if not diff:
            return "See diff for details"

        # Look for added lines (the fix)
        added_lines = [line[1:].strip() for line in diff.split('\n') if line.startswith('+') and not line.startswith('+++')]

        if added_lines:
            # Return first few added lines as suggestion
            return ' '.join(added_lines[:3])

        return "Check the fixed code pattern"
