"""Specialist: common Python bug patterns caught via AST analysis."""

from __future__ import annotations

import ast
from autodebug.agent.specialists.base import ASTData, Finding, Specialist


class CommonBugsSpecialist(Specialist):
    name = "common_bugs"
    trigger_features: list[str] = []  # always run

    async def run(self, source: str, ast_data: ASTData) -> list[Finding]:
        findings: list[Finding] = []

        findings.extend(_check_except_pass(ast_data))
        findings.extend(_check_is_literal(ast_data))
        findings.extend(_check_return_in_init(ast_data))
        findings.extend(_check_mutable_class_var(ast_data))

        return findings


# ---------------------------------------------------------------------------
# (a) except Exception: pass  /  except: pass
# ---------------------------------------------------------------------------

def _check_except_pass(ast_data: ASTData) -> list[Finding]:
    """Flag bare 'except: pass' and 'except Exception: pass'."""
    findings: list[Finding] = []
    for handler in ast_data.except_handlers:
        if handler.body_type != "pass":
            continue
        # Only flag broad catches: bare except or Exception/BaseException
        if handler.type_name is not None and handler.type_name not in ("Exception", "BaseException"):
            continue
        kind = "bare except" if handler.type_name is None else f"except {handler.type_name}"
        findings.append(Finding(
            line=handler.line,
            severity="MEDIUM",
            message=f"Silent error swallowing: `{kind}: pass` hides all errors",
            specialist="common_bugs",
            confidence="DEFINITE",
        ))
    return findings


# ---------------------------------------------------------------------------
# (b) `is` comparison with string/int literal
# ---------------------------------------------------------------------------

def _check_is_literal(ast_data: ASTData) -> list[Finding]:
    """Flag `x is "hello"` or `x is 0` — should use == instead."""
    findings: list[Finding] = []
    for node in ast.walk(ast_data.tree):
        if not isinstance(node, ast.Compare):
            continue
        # Check each comparator pair
        for op, right in zip(node.ops, node.comparators):
            if not isinstance(op, (ast.Is, ast.IsNot)):
                continue
            if _is_str_or_int_literal(right):
                op_str = "is" if isinstance(op, ast.Is) else "is not"
                findings.append(Finding(
                    line=node.lineno,
                    severity="HIGH",
                    message=f"Identity check `{op_str}` used with literal; use `==`/`!=` instead (identity != equality for str/int)",
                    specialist="common_bugs",
                    confidence="DEFINITE",
                ))
            # Also check left side for reversed comparisons like `"hello" is x`
        # Check if left is a literal compared with is
        for op, right in zip(node.ops, node.comparators):
            if not isinstance(op, (ast.Is, ast.IsNot)):
                continue
            if _is_str_or_int_literal(node.left):
                # Avoid double-reporting if right was also a literal
                if not _is_str_or_int_literal(right):
                    op_str = "is" if isinstance(op, ast.Is) else "is not"
                    findings.append(Finding(
                        line=node.lineno,
                        severity="HIGH",
                        message=f"Identity check `{op_str}` used with literal; use `==`/`!=` instead (identity != equality for str/int)",
                        specialist="common_bugs",
                        confidence="DEFINITE",
                    ))
    return findings


def _is_str_or_int_literal(node: ast.expr) -> bool:
    """Check if an AST node is a string or int literal."""
    if isinstance(node, ast.Constant):
        return isinstance(node.value, (str, int)) and not isinstance(node.value, bool)
    return False


# ---------------------------------------------------------------------------
# (c) Return value in __init__
# ---------------------------------------------------------------------------

def _check_return_in_init(ast_data: ASTData) -> list[Finding]:
    """Flag `return <value>` inside __init__ (only bare return/return None is OK)."""
    findings: list[Finding] = []
    for node in ast.walk(ast_data.tree):
        if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
            continue
        if node.name != "__init__":
            continue
        for child in ast.walk(node):
            if isinstance(child, ast.Return) and child.value is not None:
                # Allow `return None` explicitly
                if isinstance(child.value, ast.Constant) and child.value.value is None:
                    continue
                findings.append(Finding(
                    line=child.lineno,
                    severity="HIGH",
                    message="`__init__` must not return a value (TypeError at runtime)",
                    specialist="common_bugs",
                    confidence="DEFINITE",
                ))
    return findings


# ---------------------------------------------------------------------------
# (d) Mutable default in class variable
# ---------------------------------------------------------------------------

def _check_mutable_class_var(ast_data: ASTData) -> list[Finding]:
    """Flag `class Foo: items = []` — mutable shared across all instances."""
    findings: list[Finding] = []
    for node in ast.walk(ast_data.tree):
        if not isinstance(node, ast.ClassDef):
            continue
        for stmt in node.body:
            if not isinstance(stmt, ast.Assign):
                continue
            if _is_mutable_literal(stmt.value):
                # Get variable name for message
                names = []
                for target in stmt.targets:
                    if isinstance(target, ast.Name):
                        names.append(target.id)
                name_str = ", ".join(names) if names else "variable"
                # Skip intentional class-level declarations
                if all(_is_intentional_class_var(n) for n in names):
                    continue
                findings.append(Finding(
                    line=stmt.lineno,
                    severity="MEDIUM",
                    message=f"Mutable default `{name_str}` as class variable is shared across all instances — use `__init__` instead",
                    specialist="common_bugs",
                    confidence="LIKELY",
                ))
    return findings


def _is_intentional_class_var(name: str) -> bool:
    """Skip class vars that are intentionally mutable class-level declarations."""
    # Dunder attrs (e.g., __attrs__, __slots__, __all__)
    if name.startswith("__") and name.endswith("__"):
        return True
    # Pydantic/dataclass config patterns
    if name in ("model_config", "Config", "Meta", "default_tags", "REGISTRY"):
        return True
    # Type annotation helpers, class registries
    if name.isupper():  # ALL_CAPS constants are intentional
        return True
    return False


def _is_mutable_literal(node: ast.expr) -> bool:
    """Check if node is a mutable literal: [], {}, set()."""
    if isinstance(node, ast.List):
        return True
    if isinstance(node, ast.Dict):
        return True
    if isinstance(node, ast.Set):
        return True
    if isinstance(node, ast.Call):
        # set(), list(), dict() calls with no args
        if isinstance(node.func, ast.Name) and node.func.id in ("set", "list", "dict"):
            if not node.args and not node.keywords:
                return True
    return False
