from __future__ import annotations

import ast
import json
import re
import shutil
import subprocess
import time
import tomllib
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List

from vibegate.config import (
    ConfigSanityCheck,
    DependencyHygieneCheck,
    ErrorHandlingCheck,
    DefensiveProgrammingCheck,
    ComplexityCheck,
    DeadCodeCheck,
    GitleaksCheck,
    RuffFormatCheck,
    RuffLintCheck,
    PyrightCheck,
    PytestCheck,
    BanditCheck,
    VulnerabilityCheck,
    RuntimeSmokeCheck,
    VibeGateConfig,
)
from vibegate.findings import Finding, FindingLocation
from vibegate.workspace import filter_workspace_files


@dataclass(frozen=True)
class ToolResult:
    argv: List[str]
    stdout: str
    stderr: str
    exit_code: int
    duration_ms: int
    tool_version: str
    artifacts: List[Path]


@dataclass(frozen=True)
class CheckOutcome:
    check_id: str
    findings: List[Finding]
    skipped_reason: str | None
    tool_result: ToolResult | None


def tool_exists(tool: str) -> bool:
    return shutil.which(tool) is not None


def tool_version(tool: str) -> str:
    try:
        output = subprocess.check_output([tool, "--version"], stderr=subprocess.STDOUT)
        return output.decode("utf-8").strip()
    except (subprocess.CalledProcessError, FileNotFoundError):
        return "unknown"


def run_tool(
    tool: str, args: List[str], cwd: Path, timeout: int, env: Dict[str, str]
) -> ToolResult:
    argv = [tool] + args
    started = time.monotonic()
    completed = subprocess.run(
        argv,
        cwd=cwd,
        env=env,
        capture_output=True,
        text=True,
        timeout=timeout,
    )
    duration_ms = int((time.monotonic() - started) * 1000)
    return ToolResult(
        argv=argv,
        stdout=completed.stdout,
        stderr=completed.stderr,
        exit_code=completed.returncode,
        duration_ms=duration_ms,
        tool_version=tool_version(tool),
        artifacts=[],
    )


def _severity_from_pyright(level: str) -> str:
    mapping = {
        "error": "high",
        "warning": "medium",
        "information": "low",
        "hint": "info",
    }
    return mapping.get(level.lower(), "medium")


def run_ruff_format(
    check: RuffFormatCheck, repo_root: Path, env: Dict[str, str]
) -> CheckOutcome:
    if not tool_exists("ruff"):
        return CheckOutcome(check.id, [], "ruff not found", None)

    result = run_tool("ruff", check.args, repo_root, check.timeout_sec, env)
    findings: List[Finding] = []
    if result.exit_code != 0:
        findings.append(
            Finding(
                check_id=check.id,
                finding_type="formatting",
                rule_id="RUFF_FORMAT",
                severity="low",
                message="Ruff formatter reported formatting differences.",
                fingerprint="",
                tool="ruff",
                remediation_hint="Run ruff format to apply formatting.",
            )
        )
    return CheckOutcome(check.id, findings, None, result)


def run_ruff_lint(
    check: RuffLintCheck, repo_root: Path, env: Dict[str, str]
) -> CheckOutcome:
    if not tool_exists("ruff"):
        return CheckOutcome(check.id, [], "ruff not found", None)

    result = run_tool("ruff", check.args, repo_root, check.timeout_sec, env)
    findings: List[Finding] = []
    if result.stdout.strip():
        try:
            payload = json.loads(result.stdout)
        except json.JSONDecodeError:
            payload = []
        for item in payload:
            location = FindingLocation(
                path=item.get("filename"),
                line=item.get("location", {}).get("row"),
                col=item.get("location", {}).get("column"),
                end_line=item.get("end_location", {}).get("row"),
                end_col=item.get("end_location", {}).get("column"),
            )
            findings.append(
                Finding(
                    check_id=check.id,
                    finding_type="lint",
                    rule_id=item.get("code", "RUFF"),
                    severity="medium",
                    message=item.get("message", "Ruff lint finding."),
                    fingerprint="",
                    tool="ruff",
                    remediation_hint=item.get("fix", {}).get("message")
                    if isinstance(item.get("fix"), dict)
                    else None,
                    location=location,
                )
            )
    elif result.exit_code != 0:
        findings.append(
            Finding(
                check_id=check.id,
                finding_type="lint",
                rule_id="RUFF",
                severity="medium",
                message="Ruff reported lint issues.",
                fingerprint="",
                tool="ruff",
            )
        )
    return CheckOutcome(check.id, findings, None, result)


def run_pyright(
    check: PyrightCheck, repo_root: Path, env: Dict[str, str]
) -> CheckOutcome:
    if not tool_exists("pyright"):
        return CheckOutcome(check.id, [], "pyright not found", None)

    result = run_tool("pyright", check.args, repo_root, check.timeout_sec, env)
    findings: List[Finding] = []
    if result.stdout.strip():
        try:
            payload = json.loads(result.stdout)
        except json.JSONDecodeError:
            payload = {}
        diagnostics = payload.get("generalDiagnostics", [])
        for item in diagnostics:
            severity = _severity_from_pyright(item.get("severity", "error"))
            location = None
            if "file" in item and "range" in item:
                range_data = item.get("range", {})
                start = range_data.get("start", {})
                end = range_data.get("end", {})
                location = FindingLocation(
                    path=item.get("file"),
                    line=start.get("line", 0) + 1,
                    col=start.get("character", 0) + 1,
                    end_line=end.get("line", 0) + 1,
                    end_col=end.get("character", 0) + 1,
                )
            findings.append(
                Finding(
                    check_id=check.id,
                    finding_type="typecheck",
                    rule_id=item.get("rule") or item.get("ruleId") or "PYRIGHT",
                    severity=severity,
                    message=item.get("message", "Pyright diagnostic."),
                    fingerprint="",
                    tool="pyright",
                    location=location,
                )
            )
    elif result.exit_code != 0:
        findings.append(
            Finding(
                check_id=check.id,
                finding_type="typecheck",
                rule_id="PYRIGHT",
                severity="high",
                message="Pyright reported typecheck errors.",
                fingerprint="",
                tool="pyright",
            )
        )
    return CheckOutcome(check.id, findings, None, result)


def run_pytest(
    check: PytestCheck, repo_root: Path, env: Dict[str, str]
) -> CheckOutcome:
    if not tool_exists("pytest"):
        return CheckOutcome(check.id, [], "pytest not found", None)

    result = run_tool("pytest", check.args, repo_root, check.timeout_sec, env)
    findings: List[Finding] = []
    if result.exit_code != 0:
        message = result.stdout.strip().splitlines()[:1]
        summary = message[0] if message else "Pytest reported failures."
        findings.append(
            Finding(
                check_id=check.id,
                finding_type="tests",
                rule_id="PYTEST_FAILED",
                severity="high",
                message=summary,
                fingerprint="",
                tool="pytest",
            )
        )
    return CheckOutcome(check.id, findings, None, result)


def _default_lockfiles(tool: str) -> List[str]:
    mapping = {
        "uv": ["uv.lock"],
        "poetry": ["poetry.lock"],
        "pdm": ["pdm.lock"],
        "pip-tools": ["requirements.txt"],
        "pip": ["requirements.txt"],
    }
    return mapping.get(tool, [])


def detect_packaging_tool(config: VibeGateConfig) -> str:
    if config.packaging.tool != "auto":
        return config.packaging.tool
    for tool in config.packaging.tool_detection_order:
        if tool_exists(tool):
            return tool
    return "pip"


def _script_mentions_uv(value: object) -> bool:
    if isinstance(value, str):
        return "uv" in value.lower()
    if isinstance(value, list):
        return any(_script_mentions_uv(item) for item in value)
    if isinstance(value, dict):
        return any(_script_mentions_uv(item) for item in value.values())
    return False


def _uv_detected(repo_root: Path, workspace_files: List[Path]) -> bool:
    if (repo_root / "uv.lock").exists():
        return True
    pyproject_path = repo_root / "pyproject.toml"
    if not pyproject_path.exists():
        return False
    if pyproject_path not in workspace_files:
        return False
    try:
        payload = tomllib.loads(pyproject_path.read_text(encoding="utf-8"))
    except (OSError, tomllib.TOMLDecodeError):
        return False
    tool_payload = payload.get("tool", {})
    if isinstance(tool_payload, dict) and "uv" in tool_payload:
        return True
    project_scripts = payload.get("project", {}).get("scripts", {})
    poetry_scripts = tool_payload.get("poetry", {}).get("scripts", {})
    pdm_scripts = tool_payload.get("pdm", {}).get("scripts", {})
    return any(
        _script_mentions_uv(scripts)
        for scripts in (project_scripts, poetry_scripts, pdm_scripts)
    )


def run_dependency_hygiene(
    check: DependencyHygieneCheck,
    repo_root: Path,
    env: Dict[str, str],
    config: VibeGateConfig,
    workspace_files: List[Path],
) -> CheckOutcome:
    findings: List[Finding] = []
    tool = detect_packaging_tool(config)
    lockfiles = config.packaging.lockfiles or _default_lockfiles(tool)

    if tool == "uv" and not _uv_detected(repo_root, workspace_files):
        return CheckOutcome(check.id, [], "uv not detected in project", None)

    if "lockfile_required" in check.rules:
        if lockfiles and not any((repo_root / path).exists() for path in lockfiles):
            findings.append(
                Finding(
                    check_id=check.id,
                    finding_type="dependency_hygiene",
                    rule_id="LOCKFILE_MISSING",
                    severity="high",
                    message=f"Missing lockfile for packaging tool {tool}.",
                    fingerprint="",
                )
            )

    tool_result = None
    if tool == "uv" and "lockfile_fresh" in check.rules:
        if not tool_exists("uv"):
            return CheckOutcome(check.id, findings, "uv not found", None)
        tool_result = run_tool(
            "uv", ["lock", "--check"], repo_root, check.timeout_sec, env
        )
        if tool_result.exit_code != 0:
            findings.append(
                Finding(
                    check_id=check.id,
                    finding_type="dependency_hygiene",
                    rule_id="LOCKFILE_STALE",
                    severity="medium",
                    message="uv lockfile is out of date.",
                    fingerprint="",
                    tool="uv",
                )
            )

    return CheckOutcome(check.id, findings, None, tool_result)


def run_bandit(
    check: BanditCheck, repo_root: Path, env: Dict[str, str]
) -> CheckOutcome:
    if not tool_exists("bandit"):
        return CheckOutcome(check.id, [], "bandit not found", None)

    result = run_tool("bandit", check.args, repo_root, check.timeout_sec, env)
    artifacts = [
        repo_root / Path(path) for path in check.args if path.endswith(".json")
    ]
    result = ToolResult(
        argv=result.argv,
        stdout=result.stdout,
        stderr=result.stderr,
        exit_code=result.exit_code,
        duration_ms=result.duration_ms,
        tool_version=result.tool_version,
        artifacts=artifacts,
    )
    findings: List[Finding] = []
    for artifact in artifacts:
        if not artifact.exists():
            continue
        payload = json.loads(artifact.read_text(encoding="utf-8"))
        for item in payload.get("results", []):
            severity = str(item.get("issue_severity", "medium")).lower()
            findings.append(
                Finding(
                    check_id=check.id,
                    finding_type="sast",
                    rule_id=item.get("test_id", "BANDIT"),
                    severity=severity,
                    message=item.get("issue_text", "Bandit finding."),
                    fingerprint="",
                    tool="bandit",
                    location=FindingLocation(
                        path=item.get("filename"), line=item.get("line_number")
                    ),
                )
            )
    return CheckOutcome(check.id, findings, None, result)


def run_gitleaks(
    check: GitleaksCheck, repo_root: Path, env: Dict[str, str]
) -> CheckOutcome:
    if not tool_exists("gitleaks"):
        return CheckOutcome(check.id, [], "gitleaks not found", None)

    result = run_tool("gitleaks", check.args, repo_root, check.timeout_sec, env)
    artifacts = [
        repo_root / Path(path) for path in check.args if path.endswith(".json")
    ]
    result = ToolResult(
        argv=result.argv,
        stdout=result.stdout,
        stderr=result.stderr,
        exit_code=result.exit_code,
        duration_ms=result.duration_ms,
        tool_version=result.tool_version,
        artifacts=artifacts,
    )
    findings: List[Finding] = []
    for artifact in artifacts:
        if not artifact.exists():
            continue
        payload = json.loads(artifact.read_text(encoding="utf-8"))
        for item in payload:
            findings.append(
                Finding(
                    check_id=check.id,
                    finding_type="secrets",
                    rule_id=item.get("RuleID", "GITLEAKS"),
                    severity="high",
                    message=item.get("Description", "Secret detected."),
                    fingerprint="",
                    tool="gitleaks",
                    location=FindingLocation(
                        path=item.get("File"), line=item.get("StartLine")
                    ),
                )
            )
    return CheckOutcome(check.id, findings, None, result)


def run_osv(
    check: VulnerabilityCheck, repo_root: Path, env: Dict[str, str]
) -> CheckOutcome:
    if check.mode == "skip":
        return CheckOutcome(check.id, [], "vulnerability check disabled", None)
    if check.local_db is None or not (repo_root / check.local_db.path).exists():
        return CheckOutcome(check.id, [], "offline DB snapshot not configured", None)
    if not tool_exists("osv-scanner"):
        return CheckOutcome(check.id, [], "osv-scanner not found", None)

    result = run_tool("osv-scanner", check.args, repo_root, check.timeout_sec, env)
    findings: List[Finding] = []
    if result.exit_code != 0:
        message = result.stdout.strip().splitlines()[:1]
        summary = message[0] if message else "osv-scanner reported vulnerabilities."
        findings.append(
            Finding(
                check_id=check.id,
                finding_type="vulnerability",
                rule_id="OSV",
                severity="high",
                message=summary,
                fingerprint="",
                tool="osv-scanner",
            )
        )
    return CheckOutcome(check.id, findings, None, result)


def run_config_sanity(
    check: ConfigSanityCheck, repo_root: Path, workspace_files: List[Path]
) -> CheckOutcome:
    findings: List[Finding] = []
    files = filter_workspace_files(
        workspace_files, repo_root, check.include_globs, check.exclude_globs
    )

    debug_pattern = re.compile(r"debug=True")
    debug_env_pattern = re.compile(r"DEBUG\s*=\s*True")
    app_debug_pattern = re.compile(r"app\.debug\s*=\s*True")
    reload_pattern = re.compile(r"--reload|reload=True")
    cors_pattern = re.compile(
        r"allow_origins\s*=\s*\[\"\*\"\]|allow_origin_regex\s*=\s*\".*\*"
    )
    secret_pattern = re.compile(r"(SECRET|TOKEN|API_KEY|PASSWORD)\s*[:=]\s*['\"]")
    placeholder_comment_pattern = re.compile(
        r"#\s*(TODO|FIXME|XXX|HACK|WIP)\b", re.IGNORECASE
    )
    not_implemented_pattern = re.compile(r"raise\s+NotImplementedError")
    placeholder_docstring_pattern = re.compile(
        r'""".*(?:TODO|placeholder|implement this).*"""', re.IGNORECASE | re.DOTALL
    )

    for path in files:
        try:
            content = path.read_text(encoding="utf-8", errors="ignore")
        except FileNotFoundError:
            continue

        # Check for placeholder docstrings in the entire file content
        if "no_placeholder_docstrings" in check.rules and path.suffix == ".py":
            if placeholder_docstring_pattern.search(content):
                # Try to find the line number
                lines = content.splitlines()
                for idx, line in enumerate(lines, start=1):
                    if '"""' in line and (
                        re.search(
                            r"TODO|placeholder|implement this", line, re.IGNORECASE
                        )
                    ):
                        findings.append(
                            Finding(
                                check_id=check.id,
                                finding_type="config_sanity",
                                rule_id="no_placeholder_docstrings",
                                severity="medium",
                                message="Placeholder content detected in docstring.",
                                fingerprint="",
                                location=FindingLocation(
                                    path=str(path.relative_to(repo_root)), line=idx
                                ),
                            )
                        )
                        break

        for idx, line in enumerate(content.splitlines(), start=1):
            if "no_debug_true" in check.rules and path.suffix == ".py":
                if (
                    debug_pattern.search(line)
                    or debug_env_pattern.search(line)
                    or app_debug_pattern.search(line)
                ):
                    findings.append(
                        Finding(
                            check_id=check.id,
                            finding_type="config_sanity",
                            rule_id="no_debug_true",
                            severity="high",
                            message="Debug mode appears to be enabled.",
                            fingerprint="",
                            location=FindingLocation(
                                path=str(path.relative_to(repo_root)), line=idx
                            ),
                        )
                    )
            if "no_uvicorn_reload_in_prod" in check.rules:
                if reload_pattern.search(line):
                    findings.append(
                        Finding(
                            check_id=check.id,
                            finding_type="config_sanity",
                            rule_id="no_uvicorn_reload_in_prod",
                            severity="medium",
                            message="Uvicorn reload appears enabled.",
                            fingerprint="",
                            location=FindingLocation(
                                path=str(path.relative_to(repo_root)), line=idx
                            ),
                        )
                    )
            if "no_allow_all_cors" in check.rules:
                if cors_pattern.search(line):
                    findings.append(
                        Finding(
                            check_id=check.id,
                            finding_type="config_sanity",
                            rule_id="no_allow_all_cors",
                            severity="medium",
                            message="CORS allow-all configuration detected.",
                            fingerprint="",
                            location=FindingLocation(
                                path=str(path.relative_to(repo_root)), line=idx
                            ),
                        )
                    )
            if "no_hardcoded_secrets_like_patterns" in check.rules:
                if secret_pattern.search(line):
                    findings.append(
                        Finding(
                            check_id=check.id,
                            finding_type="config_sanity",
                            rule_id="no_hardcoded_secrets_like_patterns",
                            severity="high",
                            message="Potential hardcoded secret detected.",
                            fingerprint="",
                            location=FindingLocation(
                                path=str(path.relative_to(repo_root)), line=idx
                            ),
                        )
                    )
            if "no_placeholder_comments" in check.rules and path.suffix == ".py":
                if placeholder_comment_pattern.search(line):
                    findings.append(
                        Finding(
                            check_id=check.id,
                            finding_type="config_sanity",
                            rule_id="no_placeholder_comments",
                            severity="medium",
                            message="Placeholder comment detected (TODO/FIXME/XXX/HACK/WIP).",
                            fingerprint="",
                            location=FindingLocation(
                                path=str(path.relative_to(repo_root)), line=idx
                            ),
                        )
                    )
            if "no_not_implemented" in check.rules and path.suffix == ".py":
                if not_implemented_pattern.search(line):
                    findings.append(
                        Finding(
                            check_id=check.id,
                            finding_type="config_sanity",
                            rule_id="no_not_implemented",
                            severity="high",
                            message="NotImplementedError detected in code.",
                            fingerprint="",
                            location=FindingLocation(
                                path=str(path.relative_to(repo_root)), line=idx
                            ),
                        )
                    )
    return CheckOutcome(check.id, findings, None, None)


def _scan_python_files(
    repo_root: Path, workspace_files: List[Path], exclude_globs: List[str] | None = None
) -> List[Path]:
    """Scan for Python files in workspace."""
    if exclude_globs is None:
        exclude_globs = []
    return filter_workspace_files(
        workspace_files, repo_root, ["**/*.py"], exclude_globs
    )


class ErrorHandlingVisitor(ast.NodeVisitor):
    """AST visitor to detect error handling issues."""

    def __init__(self, file_path: Path, repo_root: Path, check: ErrorHandlingCheck):
        self.file_path = file_path
        self.repo_root = repo_root
        self.check = check
        self.findings: List[Finding] = []
        self.has_logging_import = False

    def visit_Import(self, node: ast.Import) -> None:
        """Check for logging import."""
        for alias in node.names:
            if alias.name == "logging":
                self.has_logging_import = True
        self.generic_visit(node)

    def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
        """Check for logging import from."""
        if node.module == "logging":
            self.has_logging_import = True
        self.generic_visit(node)

    def visit_Try(self, node: ast.Try) -> None:
        """Visit try/except blocks to detect error handling issues."""
        for handler in node.handlers:
            # Check for bare except
            if "bare_except" in self.check.rules:
                if handler.type is None:
                    self.findings.append(
                        Finding(
                            check_id=self.check.id,
                            finding_type="error_handling",
                            rule_id="bare_except",
                            severity="high",
                            message="Bare except clause without exception type",
                            fingerprint="",
                            tool="vibegate",
                            remediation_hint="Specify exception type: except ValueError:",
                            location=FindingLocation(
                                path=str(self.file_path.relative_to(self.repo_root)),
                                line=handler.lineno,
                            ),
                        )
                    )

            # Check for empty except body
            if "empty_except" in self.check.rules:
                if len(handler.body) == 1 and isinstance(handler.body[0], ast.Pass):
                    self.findings.append(
                        Finding(
                            check_id=self.check.id,
                            finding_type="error_handling",
                            rule_id="empty_except",
                            severity="high",
                            message="Empty except block swallows errors silently",
                            fingerprint="",
                            tool="vibegate",
                            remediation_hint="Add logging or re-raise exception",
                            location=FindingLocation(
                                path=str(self.file_path.relative_to(self.repo_root)),
                                line=handler.lineno,
                            ),
                        )
                    )

            # Check for generic Exception catch
            if "generic_exceptions" in self.check.rules:
                if (
                    isinstance(handler.type, ast.Name)
                    and handler.type.id == "Exception"
                ):
                    self.findings.append(
                        Finding(
                            check_id=self.check.id,
                            finding_type="error_handling",
                            rule_id="generic_exceptions",
                            severity="medium",
                            message="Catching generic Exception - too broad",
                            fingerprint="",
                            tool="vibegate",
                            remediation_hint="Catch specific exception types",
                            location=FindingLocation(
                                path=str(self.file_path.relative_to(self.repo_root)),
                                line=handler.lineno,
                            ),
                        )
                    )

            # Check for missing logging (heuristic)
            if "missing_logging" in self.check.rules:
                if self.has_logging_import:
                    # Check if handler body contains logging calls
                    has_logging = self._has_logging_call(handler.body)
                    if not has_logging and len(handler.body) > 0:
                        # Not empty and no logging - could be an issue
                        self.findings.append(
                            Finding(
                                check_id=self.check.id,
                                finding_type="error_handling",
                                rule_id="missing_logging",
                                severity="low",
                                message="Exception handler may benefit from logging",
                                fingerprint="",
                                tool="vibegate",
                                remediation_hint="Consider adding logging.error() or logging.exception()",
                                location=FindingLocation(
                                    path=str(
                                        self.file_path.relative_to(self.repo_root)
                                    ),
                                    line=handler.lineno,
                                ),
                            )
                        )

        self.generic_visit(node)

    def _has_logging_call(self, body: List[ast.stmt]) -> bool:
        """Check if body contains logging calls."""
        for stmt in body:
            if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Call):
                if isinstance(stmt.value.func, ast.Attribute):
                    if isinstance(stmt.value.func.value, ast.Name):
                        if stmt.value.func.value.id == "logging":
                            return True
                    elif isinstance(stmt.value.func.value, ast.Attribute):
                        # Handle cases like logger.error()
                        if isinstance(stmt.value.func.value.value, ast.Name):
                            if stmt.value.func.value.value.id in (
                                "logging",
                                "logger",
                                "log",
                            ):
                                return True
            elif isinstance(stmt, (ast.If, ast.For, ast.While, ast.Try)):
                # Recursively check nested structures
                if hasattr(stmt, "body") and self._has_logging_call(stmt.body):
                    return True
        return False


def run_error_handling_check(
    check: ErrorHandlingCheck, repo_root: Path, workspace_files: List[Path]
) -> CheckOutcome:
    """Run error handling checks using AST analysis."""
    findings: List[Finding] = []
    python_files = _scan_python_files(repo_root, workspace_files, check.exclude_globs)

    for py_file in python_files:
        try:
            content = py_file.read_text(encoding="utf-8")
            tree = ast.parse(content, filename=str(py_file))
            visitor = ErrorHandlingVisitor(py_file, repo_root, check)
            visitor.visit(tree)
            findings.extend(visitor.findings)
        except SyntaxError:
            # Skip files with syntax errors (caught by other checks)
            continue
        except Exception:
            # Skip files that can't be parsed
            continue

    return CheckOutcome(check.id, findings, None, None)


class DefensiveProgrammingVisitor(ast.NodeVisitor):
    """AST visitor to detect defensive programming issues."""

    def __init__(
        self, file_path: Path, repo_root: Path, check: DefensiveProgrammingCheck
    ):
        self.file_path = file_path
        self.repo_root = repo_root
        self.check = check
        self.findings: List[Finding] = []
        self.parent_nodes: List[ast.AST] = []
        self.guarded_attributes: Dict[str, List[int]] = {}
        self.guarded_dicts: Dict[str, List[int]] = {}
        self.guarded_lists: Dict[str, List[int]] = {}

    def visit(self, node: ast.AST) -> None:
        """Visit node and maintain parent stack."""
        self.parent_nodes.append(node)
        super().visit(node)
        self.parent_nodes.pop()

    def visit_Attribute(self, node: ast.Attribute) -> None:
        """Check attribute access for None guards."""
        if self.check.require_none_checks:
            # Check if this attribute access is guarded
            if not self._has_none_guard(node.value):
                # Only report if it's not a method call on a known safe object
                if not isinstance(node.value, (ast.Constant, ast.Name, ast.Call)):
                    self.findings.append(
                        Finding(
                            check_id=self.check.id,
                            finding_type="defensive_coding",
                            rule_id="missing_none_check",
                            severity="medium",
                            message=f"Attribute access '{node.attr}' without None check",
                            fingerprint="",
                            tool="vibegate",
                            remediation_hint="Add 'if obj is not None:' guard before attribute access",
                            location=FindingLocation(
                                path=str(self.file_path.relative_to(self.repo_root)),
                                line=node.lineno,
                            ),
                        )
                    )
        self.generic_visit(node)

    def _is_type_annotation(self, node: ast.Subscript) -> bool:
        """Check if this subscript is part of a type annotation (safe, false positive)."""
        # Type annotations appear in function signatures, variable annotations, dataclass fields.
        # Strategy: Check all ancestors in parent_nodes to see if any indicate annotation context.
        # This handles nested subscripts like List[dict[str, str]] automatically since any
        # ancestor being in an annotation context means this subscript is also a type annotation.

        # parent_nodes contains all ancestors including current node (current is last)
        # Check all ancestors (skip current node itself which is at index -1)
        ancestors = self.parent_nodes[:-1] if len(self.parent_nodes) > 1 else []

        for ancestor in ancestors:
            # Function parameter annotations (func(arg: List[str]))
            if isinstance(ancestor, ast.arg):
                return True
            # Variable annotations (x: List[str] = ...) or dataclass field annotations
            if isinstance(ancestor, ast.AnnAssign):
                # If we're inside an AnnAssign, any subscript is part of the type annotation
                return True
            # Function return type annotations
            if isinstance(ancestor, (ast.FunctionDef, ast.AsyncFunctionDef)):
                if ancestor.returns and self._is_nested_in(node, ancestor.returns):
                    return True
        return False

    def _is_nested_in(self, needle: ast.AST, haystack: ast.AST) -> bool:
        """Check if needle AST node is nested within haystack."""
        for child in ast.walk(haystack):
            if child == needle:
                return True
        return False

    def _is_ast_node_access(self, node: ast.Subscript) -> bool:
        """Check if this subscript access is actually AST node attribute access (safe)."""
        # AST node attributes like node.attr, node.value, node.id are accessed via attributes,
        # not subscripts. But if the value is a known AST node variable, the subscript
        # is likely accessing a container property that's guaranteed to exist.
        # However, we can't reliably detect this without type information.
        # Instead, check if we're in an AST visitor context (visiting AST nodes)
        # This is a heuristic: if the parent is an AST visitor method, skip
        for parent in reversed(self.parent_nodes):
            if isinstance(parent, (ast.FunctionDef, ast.AsyncFunctionDef)):
                # Check if function name starts with 'visit_' (AST visitor pattern)
                if parent.name.startswith("visit_"):
                    return True
                # Also check if it's in a visitor class context
                if any(
                    isinstance(p, ast.ClassDef) and "Visitor" in p.name
                    for p in self.parent_nodes
                ):
                    return True
        return False

    def visit_Subscript(self, node: ast.Subscript) -> None:
        """Check dictionary/list access for bounds/keys checks."""
        # Skip type annotations (false positives)
        if self._is_type_annotation(node):
            self.generic_visit(node)
            return

        # Skip AST node attribute accesses (these are safe, not dict access)
        if self._is_ast_node_access(node):
            self.generic_visit(node)
            return

        # Skip string slicing operations (they use Slice nodes, not Index/Constant)
        # String slicing like base[:77] has a Slice node as the slice, not a dict key
        if isinstance(node.slice, ast.Slice):
            self.generic_visit(node)
            return

        if isinstance(node.value, ast.Name) or isinstance(node.value, ast.Attribute):
            var_name = self._get_variable_name(node.value)
            if var_name:
                # Check for dictionary access
                if self.check.require_bounds_checks:
                    # Heuristic: if we can't determine it's a dict, check anyway
                    if not self._has_dict_key_check(
                        var_name, node
                    ) and not self._has_dict_get_pattern(node):
                        self.findings.append(
                            Finding(
                                check_id=self.check.id,
                                finding_type="defensive_coding",
                                rule_id="missing_dict_key_check",
                                severity="medium",
                                message="Dictionary access without key check or .get()",
                                fingerprint="",
                                tool="vibegate",
                                remediation_hint="Use .get() method or check 'key in dict' before access",
                                location=FindingLocation(
                                    path=str(
                                        self.file_path.relative_to(self.repo_root)
                                    ),
                                    line=node.lineno,
                                ),
                            )
                        )
                    # Check for list indexing
                    elif not self._has_list_length_check(var_name, node):
                        self.findings.append(
                            Finding(
                                check_id=self.check.id,
                                finding_type="defensive_coding",
                                rule_id="missing_bounds_check",
                                severity="medium",
                                message="List indexing without length validation",
                                fingerprint="",
                                tool="vibegate",
                                remediation_hint="Add length check: 'if idx < len(list):' before indexing",
                                location=FindingLocation(
                                    path=str(
                                        self.file_path.relative_to(self.repo_root)
                                    ),
                                    line=node.lineno,
                                ),
                            )
                        )
        self.generic_visit(node)

    def visit_BinOp(self, node: ast.BinOp) -> None:
        """Check division operations for zero divisor."""
        if isinstance(node.op, (ast.Div, ast.FloorDiv, ast.Mod)):
            # Skip Path division operations (Path / str)
            if self._is_path_division(node):
                self.generic_visit(node)
                return

            if self.check.require_bounds_checks:
                if not self._has_zero_check(node.right):
                    self.findings.append(
                        Finding(
                            check_id=self.check.id,
                            finding_type="defensive_coding",
                            rule_id="missing_zero_check",
                            severity="high",
                            message="Division operation without zero divisor check",
                            fingerprint="",
                            tool="vibegate",
                            remediation_hint="Add check: 'if divisor != 0:' before division",
                            location=FindingLocation(
                                path=str(self.file_path.relative_to(self.repo_root)),
                                line=node.lineno,
                            ),
                        )
                    )
        self.generic_visit(node)

    def _has_none_guard(self, node: ast.AST) -> bool:
        """Check if attribute access is guarded by None check."""
        if isinstance(node, ast.Name):
            var_name = node.id
            # Check parent if statements
            for parent in reversed(self.parent_nodes):
                if isinstance(parent, ast.If):
                    test = parent.test
                    if isinstance(test, ast.Compare):
                        if (
                            len(test.ops) == 1
                            and isinstance(test.ops[0], ast.IsNot)
                            and isinstance(test.left, ast.Name)
                            and test.left.id == var_name
                            and isinstance(test.comparators[0], ast.Constant)
                            and test.comparators[0].value is None
                        ):
                            return True
        return False

    def _get_variable_name(self, node: ast.AST) -> str | None:
        """Get variable name from node."""
        if isinstance(node, ast.Name):
            return node.id
        elif isinstance(node, ast.Attribute):
            return node.attr
        return None

    def _has_dict_key_check(self, var_name: str, node: ast.Subscript) -> bool:
        """Check if dictionary access has key check."""
        # Look for 'key in dict' or 'dict.get(key)' patterns
        for parent in reversed(self.parent_nodes):
            if isinstance(parent, ast.If):
                test = parent.test
                if isinstance(test, ast.Compare):
                    if (
                        len(test.ops) == 1
                        and isinstance(test.ops[0], ast.In)
                        and isinstance(test.left, ast.Name)
                    ):
                        # Check if comparing against the dict variable
                        if (
                            isinstance(test.comparators[0], ast.Name)
                            and test.comparators[0].id == var_name
                        ):
                            return True
        return False

    def _has_dict_get_pattern(self, node: ast.Subscript) -> bool:
        """Check if this is actually a .get() call (which is safe)."""
        # This would be caught at a different level, but we can check parent
        for parent in reversed(self.parent_nodes):
            if isinstance(parent, ast.Call):
                if isinstance(parent.func, ast.Attribute) and parent.func.attr == "get":
                    return True
        return False

    def _has_list_length_check(self, var_name: str, node: ast.Subscript) -> bool:
        """Check if list indexing has length check."""
        for parent in reversed(self.parent_nodes):
            if isinstance(parent, ast.If):
                test = parent.test
                if isinstance(test, ast.Compare):
                    # Check for 'idx < len(list)' or 'idx < list.__len__()'
                    if len(test.ops) == 1 and isinstance(
                        test.ops[0], (ast.Lt, ast.LtE, ast.Is, ast.IsNot)
                    ):
                        # Check if comparing index with len(var_name)
                        if len(test.comparators) == 1 and isinstance(
                            test.comparators[0], ast.Call
                        ):
                            call = test.comparators[0]
                            if (
                                isinstance(call.func, ast.Name)
                                and call.func.id == "len"
                                and len(call.args) == 1
                                and isinstance(call.args[0], ast.Name)
                                and call.args[0].id == var_name
                            ):
                                return True
        return False

    def _is_path_division(self, node: ast.BinOp) -> bool:
        """Check if this is a Path division operation (Path / str) rather than math division."""
        # Path division typically: Path / str or Path-like variable / str
        # Check if right operand is a string constant or variable (in file/dir context)
        right_is_str = isinstance(node.right, ast.Constant) and isinstance(
            node.right.value, str
        )

        # Also check if right is a variable name that's likely a string (in os.walk context)
        # In os.walk, dirs and filenames are always strings
        right_is_likely_str = False
        if isinstance(node.right, ast.Name):
            # Common string variable names in file operations
            str_names = {"name", "filename", "dirname", "entry", "rel_path", "rel_root"}
            if node.right.id.lower() in str_names or node.right.id.endswith("name"):
                right_is_likely_str = True

        # Check if left operand looks like a Path
        left = node.left
        left_is_path = False

        # Handle nested Path operations: (Path.cwd() / "schema") / filename
        # If left is itself a BinOp with /, it's likely a Path operation
        if isinstance(left, ast.BinOp) and isinstance(left.op, ast.Div):
            # Recursively check if the nested operation is Path division
            left_is_path = self._is_path_division(left)

        if isinstance(left, ast.Name):
            # Common Path variable names
            path_names = {
                "path",
                "repo_root",
                "root",
                "base",
                "dir",
                "directory",
                "cwd",
                "rel_root",
                "git_dir",
                "artifacts_root",
                "cwd",
            }
            if (
                left.id.lower() in path_names
                or left.id.endswith("_path")
                or left.id.endswith("_dir")
                or left.id.endswith("_root")
            ):
                left_is_path = True
        elif isinstance(left, ast.Attribute):
            # Check if it's Path.xxx or pathlib.Path
            if left.attr in ["parent", "stem", "suffix", "name", "cwd"]:
                left_is_path = True
            # Check for common Path construction patterns (Path.cwd(), pathlib.Path)
            if isinstance(left.value, ast.Name) and left.value.id == "Path":
                left_is_path = True
        elif isinstance(left, ast.Call):
            # Check if it's Path(...) constructor
            if isinstance(left.func, ast.Name) and left.func.id == "Path":
                left_is_path = True
            if isinstance(left.func, ast.Attribute) and left.func.attr == "Path":
                left_is_path = True
            # Path.cwd() / "schema"
            if isinstance(left.func, ast.Attribute) and left.func.attr == "cwd":
                if (
                    isinstance(left.func.value, ast.Name)
                    and left.func.value.id == "Path"
                ):
                    left_is_path = True

        # If left is clearly a Path, and right is string or likely string, it's Path division
        if left_is_path and (right_is_str or right_is_likely_str):
            return True

        # If left is a Path constructor/attribute, assume right is string (Path operations)
        if left_is_path:
            return True

        # Also check if result is used in Path context (assigned to path variable)
        if right_is_str and isinstance(left, (ast.Name, ast.Attribute, ast.Call)):
            # If right is string constant and left looks Path-like, it's likely Path division
            return True

        return False

    def _has_zero_check(self, node: ast.AST) -> bool:
        """Check if divisor has zero check."""
        if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
            return node.value != 0
        if isinstance(node, ast.Name):
            var_name = node.id
            # Check parent if statements for zero checks
            for parent in reversed(self.parent_nodes):
                if isinstance(parent, ast.If):
                    test = parent.test
                    if isinstance(test, ast.Compare):
                        if (
                            len(test.ops) == 1
                            and len(test.comparators) == 1
                            and isinstance(test.ops[0], ast.NotEq)
                            and isinstance(test.left, ast.Name)
                            and test.left.id == var_name
                            and isinstance(test.comparators[0], ast.Constant)
                            and test.comparators[0].value == 0
                        ):
                            return True
        return False


def run_defensive_programming_check(
    check: DefensiveProgrammingCheck,
    repo_root: Path,
    workspace_files: List[Path],
) -> CheckOutcome:
    """Run defensive programming checks using AST analysis."""
    findings: List[Finding] = []
    python_files = _scan_python_files(repo_root, workspace_files, check.exclude_globs)

    for py_file in python_files:
        try:
            content = py_file.read_text(encoding="utf-8")
            tree = ast.parse(content, filename=str(py_file))
            visitor = DefensiveProgrammingVisitor(py_file, repo_root, check)
            visitor.visit(tree)
            findings.extend(visitor.findings)
        except SyntaxError:
            continue
        except Exception:
            continue

    return CheckOutcome(check.id, findings, None, None)


class ComplexityVisitor(ast.NodeVisitor):
    """AST visitor to detect complexity issues."""

    def __init__(
        self, file_path: Path, repo_root: Path, check: ComplexityCheck, content: str
    ):
        self.file_path = file_path
        self.repo_root = repo_root
        self.check = check
        self.content = content
        self.findings: List[Finding] = []
        self.nesting_depth = 0
        self.max_nesting = 0

    def visit_FunctionDef(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
        """Analyze function complexity."""
        # Calculate function length
        func_lines = self._count_function_lines(node)
        if func_lines > self.check.max_function_lines:
            self.findings.append(
                Finding(
                    check_id=self.check.id,
                    finding_type="complexity",
                    rule_id="function_too_long",
                    severity="medium",
                    message=f"Function '{node.name}' is {func_lines} lines (max: {self.check.max_function_lines})",
                    fingerprint="",
                    tool="vibegate",
                    remediation_hint=f"Split function into smaller functions (max {self.check.max_function_lines} lines)",
                    location=FindingLocation(
                        path=str(self.file_path.relative_to(self.repo_root)),
                        line=node.lineno,
                    ),
                )
            )

        # Reset nesting for this function
        old_nesting = self.nesting_depth
        self.nesting_depth = 0
        self.max_nesting = 0

        # Visit function body to calculate nesting and complexity
        for stmt in node.body:
            self.visit(stmt)

        # Check nesting depth
        if self.max_nesting > self.check.max_nesting_depth:
            self.findings.append(
                Finding(
                    check_id=self.check.id,
                    finding_type="complexity",
                    rule_id="nesting_too_deep",
                    severity="medium",
                    message=f"Function '{node.name}' has nesting depth {self.max_nesting} (max: {self.check.max_nesting_depth})",
                    fingerprint="",
                    tool="vibegate",
                    remediation_hint=f"Refactor to reduce nesting (max depth {self.check.max_nesting_depth})",
                    location=FindingLocation(
                        path=str(self.file_path.relative_to(self.repo_root)),
                        line=node.lineno,
                    ),
                )
            )

        # Calculate cyclomatic complexity
        complexity = self._calculate_complexity(node)
        if complexity > self.check.max_complexity:
            self.findings.append(
                Finding(
                    check_id=self.check.id,
                    finding_type="complexity",
                    rule_id="complexity_too_high",
                    severity="medium",
                    message=f"Function '{node.name}' has cyclomatic complexity {complexity} (max: {self.check.max_complexity})",
                    fingerprint="",
                    tool="vibegate",
                    remediation_hint=f"Simplify function logic to reduce complexity (max {self.check.max_complexity})",
                    location=FindingLocation(
                        path=str(self.file_path.relative_to(self.repo_root)),
                        line=node.lineno,
                    ),
                )
            )

        # Restore nesting
        self.nesting_depth = old_nesting

    def visit_If(self, node: ast.If) -> None:
        """Track nesting for if statements."""
        self.nesting_depth += 1
        self.max_nesting = max(self.max_nesting, self.nesting_depth)
        self.generic_visit(node)
        self.nesting_depth -= 1

    def visit_For(self, node: ast.For | ast.AsyncFor) -> None:
        """Track nesting for for loops."""
        self.nesting_depth += 1
        self.max_nesting = max(self.max_nesting, self.nesting_depth)
        self.generic_visit(node)
        self.nesting_depth -= 1

    def visit_While(self, node: ast.While) -> None:
        """Track nesting for while loops."""
        self.nesting_depth += 1
        self.max_nesting = max(self.max_nesting, self.nesting_depth)
        self.generic_visit(node)
        self.nesting_depth -= 1

    def visit_Try(self, node: ast.Try) -> None:
        """Track nesting for try blocks."""
        self.nesting_depth += 1
        self.max_nesting = max(self.max_nesting, self.nesting_depth)
        self.generic_visit(node)
        self.nesting_depth -= 1

    def visit_With(self, node: ast.With | ast.AsyncWith) -> None:
        """Track nesting for with statements."""
        self.nesting_depth += 1
        self.max_nesting = max(self.max_nesting, self.nesting_depth)
        self.generic_visit(node)
        self.nesting_depth -= 1

    def _count_function_lines(
        self, node: ast.FunctionDef | ast.AsyncFunctionDef
    ) -> int:
        """Count lines in function body."""
        if not node.body:
            return 0
        start_line = node.lineno
        end_line = node.body[-1].end_lineno or start_line
        return end_line - start_line + 1

    def _calculate_complexity(
        self, node: ast.FunctionDef | ast.AsyncFunctionDef
    ) -> int:
        """Calculate cyclomatic complexity."""
        complexity = 1  # Base complexity
        for child in ast.walk(node):
            if isinstance(child, (ast.If, ast.While, ast.For, ast.AsyncFor)):
                complexity += 1
            elif isinstance(child, ast.BoolOp):
                complexity += len(child.values) - 1
            elif isinstance(child, ast.ExceptHandler):
                complexity += 1
        return complexity


def run_complexity_check(
    check: ComplexityCheck, repo_root: Path, workspace_files: List[Path]
) -> CheckOutcome:
    """Run complexity checks using AST analysis."""
    findings: List[Finding] = []
    python_files = _scan_python_files(repo_root, workspace_files, check.exclude_globs)

    for py_file in python_files:
        try:
            content = py_file.read_text(encoding="utf-8")
            tree = ast.parse(content, filename=str(py_file))
            visitor = ComplexityVisitor(py_file, repo_root, check, content)
            visitor.visit(tree)
            findings.extend(visitor.findings)
        except SyntaxError:
            continue
        except Exception:
            continue

    return CheckOutcome(check.id, findings, None, None)


class DeadCodeVisitor(ast.NodeVisitor):
    """AST visitor to detect unreachable code."""

    def __init__(self, file_path: Path, repo_root: Path, check: DeadCodeCheck):
        self.file_path = file_path
        self.repo_root = repo_root
        self.check = check
        self.findings: List[Finding] = []

    def visit_FunctionDef(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
        """Check for unreachable code after return/raise."""
        if not node.body:
            return

        for i, stmt in enumerate(node.body):
            # Check if this statement is a return or raise
            if isinstance(stmt, (ast.Return, ast.Raise)):
                # Check if there are more statements after this
                if i + 1 < len(node.body):
                    next_stmt = node.body[i + 1]
                    # Skip docstrings and comments (they're OK)
                    if not (
                        isinstance(next_stmt, ast.Expr)
                        and isinstance(next_stmt.value, ast.Constant)
                        and isinstance(next_stmt.value.value, str)
                    ):
                        self.findings.append(
                            Finding(
                                check_id=self.check.id,
                                finding_type="dead_code",
                                rule_id="unreachable_code",
                                severity="low",
                                message=f"Unreachable code after {type(stmt).__name__.lower()} statement",
                                fingerprint="",
                                tool="vibegate",
                                remediation_hint="Remove unreachable code or refactor control flow",
                                location=FindingLocation(
                                    path=str(
                                        self.file_path.relative_to(self.repo_root)
                                    ),
                                    line=next_stmt.lineno,
                                ),
                            )
                        )
                        break  # Only report first unreachable block

        self.generic_visit(node)


def run_dead_code_check(
    check: DeadCodeCheck,
    repo_root: Path,
    workspace_files: List[Path],
    env: Dict[str, str],
) -> CheckOutcome:
    """Run dead code checks using tool integration and AST fallback."""
    findings: List[Finding] = []

    # Try vulture tool integration
    if tool_exists("vulture"):
        python_files = _scan_python_files(
            repo_root, workspace_files, check.exclude_globs
        )
        if python_files:
            # Build paths string
            paths = [str(f.relative_to(repo_root)) for f in python_files]
            try:
                # vulture doesn't have great JSON output, so we'll use AST fallback primarily
                # But we can still try to run it
                result = run_tool(
                    "vulture",
                    ["--min-confidence", str(check.min_confidence)]
                    + paths[:10],  # Limit for performance
                    repo_root,
                    check.timeout_sec,
                    env,
                )
                # Parse vulture output (text format)
                if result.stdout:
                    for line in result.stdout.splitlines():
                        # Vulture format: path:line: unused function/class/variable 'name'
                        if ":" in line and "unused" in line.lower():
                            parts = line.split(":", 2)
                            if len(parts) >= 3:
                                file_path = parts[0]
                                try:
                                    line_no = int(parts[1])
                                    message = parts[2].strip()
                                    findings.append(
                                        Finding(
                                            check_id=check.id,
                                            finding_type="dead_code",
                                            rule_id="unused_code",
                                            severity="low",
                                            message=message,
                                            fingerprint="",
                                            tool="vulture",
                                            remediation_hint="Remove unused code or mark as used",
                                            location=FindingLocation(
                                                path=file_path,
                                                line=line_no,
                                            ),
                                        )
                                    )
                                except ValueError:
                                    continue
            except Exception:
                # If vulture fails, continue with AST fallback
                pass

    # AST fallback for unreachable code
    if check.detect_commented_code:
        python_files = _scan_python_files(
            repo_root, workspace_files, check.exclude_globs
        )
        for py_file in python_files:
            try:
                content = py_file.read_text(encoding="utf-8")
                tree = ast.parse(content, filename=str(py_file))
                visitor = DeadCodeVisitor(py_file, repo_root, check)
                visitor.visit(tree)
                findings.extend(visitor.findings)

                # Detect commented-out code blocks (heuristic)
                lines = content.splitlines()
                commented_block_start = None
                for idx, line in enumerate(lines, start=1):
                    stripped = line.strip()
                    # Look for consecutive comment lines that look like code
                    if stripped.startswith("#") and len(stripped) > 2:
                        # Check if it looks like code (has common code patterns)
                        comment_content = stripped[1:].strip()
                        if any(
                            pattern in comment_content
                            for pattern in [
                                "def ",
                                "if ",
                                "for ",
                                "while ",
                                "=",
                                "(",
                                ")",
                                "return",
                                "import",
                            ]
                        ):
                            if commented_block_start is None:
                                commented_block_start = idx
                        else:
                            if (
                                commented_block_start is not None
                                and (idx - commented_block_start) >= 3
                            ):
                                # Found a block of 3+ consecutive code-like comments
                                findings.append(
                                    Finding(
                                        check_id=check.id,
                                        finding_type="dead_code",
                                        rule_id="commented_code",
                                        severity="low",
                                        message="Commented-out code block detected",
                                        fingerprint="",
                                        tool="vibegate",
                                        remediation_hint="Remove commented code or convert to documentation",
                                        location=FindingLocation(
                                            path=str(py_file.relative_to(repo_root)),
                                            line=commented_block_start,
                                        ),
                                    )
                                )
                            commented_block_start = None
                    else:
                        if (
                            commented_block_start is not None
                            and (idx - commented_block_start) >= 3
                        ):
                            findings.append(
                                Finding(
                                    check_id=check.id,
                                    finding_type="dead_code",
                                    rule_id="commented_code",
                                    severity="low",
                                    message="Commented-out code block detected",
                                    fingerprint="",
                                    tool="vibegate",
                                    remediation_hint="Remove commented code or convert to documentation",
                                    location=FindingLocation(
                                        path=str(py_file.relative_to(repo_root)),
                                        line=commented_block_start,
                                    ),
                                )
                            )
                        commented_block_start = None

            except SyntaxError:
                continue
            except Exception:
                continue

    return CheckOutcome(check.id, findings, None, None)


def run_runtime_smoke(
    check: RuntimeSmokeCheck, repo_root: Path, env: Dict[str, str]
) -> CheckOutcome:
    return CheckOutcome(check.id, [], "runtime smoke check not implemented", None)
