"""Vulture dead-code detection runner.

Class-body findings (fields, methods inside a class) are filtered out here
and handled separately by checks/classes.py which understands Pydantic.
"""

from __future__ import annotations

import ast
import re
import shutil
import subprocess
from functools import lru_cache
from pathlib import Path

from . import is_excepted, is_lintignored

CONFIG_DIR = Path(__file__).resolve().parent.parent / "config"


@lru_cache(maxsize=64)
def _class_line_ranges(filepath: str) -> list[tuple[int, int]]:
    """Return (start, end) line ranges for all class bodies in *filepath*."""
    try:
        tree = ast.parse(Path(filepath).read_text())
    except (OSError, SyntaxError):
        return []
    ranges: list[tuple[int, int]] = []
    for node in ast.walk(tree):
        if isinstance(node, ast.ClassDef):
            end = max(getattr(n, "lineno", node.lineno) for n in ast.walk(node))
            ranges.append((node.lineno, end))
    return ranges


def _is_inside_class(filepath: str, lineno: int) -> bool:
    """True when *lineno* is strictly inside a class body.

    The class declaration line itself (``class Foo:``) is *not* considered
    inside, so vulture's "unused class" findings still pass through.
    """
    return any(start < lineno <= end for start, end in _class_line_ranges(filepath))


def run_vulture(
    root: Path, min_confidence: int, error_threshold: int,
    exceptions: dict[str, list[str]],
    ignores: dict[Path, set[str]] | None = None,
) -> list[str]:
    """Run vulture on the Python backend and return errors."""
    vulture_bin = root / "backend" / ".venv" / "bin" / "vulture"
    if not vulture_bin.exists():
        found = shutil.which("vulture")
        if not found:
            return []
        vulture_bin = Path(found)

    whitelist = CONFIG_DIR / "vulture_whitelist.py"
    targets = ["backend"]
    if (root / "debug.py").exists():
        targets.append("debug.py")
    cmd = [str(vulture_bin), *targets]
    if whitelist.exists():
        cmd.append(str(whitelist))
    cmd.extend([
        "--min-confidence", str(min_confidence),
        "--exclude", ".venv,__pycache__,data,uv-bin",
        "--ignore-decorators", "@*.router.*,@*.websocket,@app.*,@pytest.fixture,@pytest.fixture*",
        "--ignore-names", "cls",
    ])

    try:
        result = subprocess.run(
            cmd, capture_output=True, text=True, cwd=str(root), timeout=30,
        )
    except (OSError, subprocess.TimeoutExpired):
        return []

    errors: list[str] = []
    for line in result.stdout.strip().splitlines():
        m = re.match(r"^(.+):(\d+): (.+)$", line)
        if not m:
            continue
        filepath, lineno, message = m.groups()
        if is_excepted(filepath, "vulture", exceptions):
            continue
        if ignores and is_lintignored(root / filepath, root, "vulture", ignores):
            continue
        if _is_inside_class(str(root / filepath), int(lineno)):
            continue
        conf = re.search(r"\((\d+)% confidence\)", message)
        confidence = int(conf.group(1)) if conf else 0
        severity = "error" if confidence >= error_threshold else "warning"
        errors.append(f"{filepath}:{lineno}:1: {severity}: [vulture] {message}")
    return errors
