"""Specialist: resource leaks — open() without context manager, unclosed connections."""

from __future__ import annotations

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


class ResourceLeakSpecialist(Specialist):
    name = "resource_leak"
    trigger_features = ["has_file_ops"]

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

        parent_map: dict[int, ast.AST] = {}
        for node in ast.walk(ast_data.tree):
            for child in ast.iter_child_nodes(node):
                parent_map[id(child)] = node

        for node in ast.walk(ast_data.tree):
            if not isinstance(node, ast.Call):
                continue

            # Match bare open() calls
            if isinstance(node.func, ast.Name) and node.func.id == "open":
                pass
            elif (isinstance(node.func, ast.Attribute) and node.func.attr == "open"
                  and isinstance(node.func.value, ast.Name)
                  and node.func.value.id in ("io", "codecs", "builtins")):
                pass
            else:
                continue

            # Check if inside a `with` statement — that's safe
            if _is_inside_with(node, parent_map):
                continue

            # Check if assigned to a variable that's later closed
            parent = parent_map.get(id(node))
            if isinstance(parent, ast.Assign):
                # open() in assignment — check if .close() is called later
                # This is hard to check perfectly, so flag as LIKELY
                findings.append(Finding(
                    line=node.lineno,
                    severity="MEDIUM",
                    message="`open()` without context manager (`with`) — file handle may leak if an exception occurs",
                    specialist=self.name,
                    confidence="LIKELY",
                ))
            elif isinstance(parent, ast.Expr):
                # open() as bare expression — result discarded
                findings.append(Finding(
                    line=node.lineno,
                    severity="HIGH",
                    message="`open()` result discarded — file handle leaked",
                    specialist=self.name,
                    confidence="DEFINITE",
                ))
            elif isinstance(parent, ast.Call):
                # open() passed directly to another function like json.load(open(...))
                # Common pattern, technically leaks but often harmless
                pass
            elif isinstance(parent, ast.Return):
                # Returning open() — caller is responsible
                pass
            else:
                # open() in some other context without with — flag it
                findings.append(Finding(
                    line=node.lineno,
                    severity="LOW",
                    message="`open()` without context manager — consider using `with` for safety",
                    specialist=self.name,
                    confidence="LIKELY",
                ))

        return findings


def _is_inside_with(node: ast.AST, parent_map: dict[int, ast.AST]) -> bool:
    """Check if node is inside a `with` statement's context expression."""
    current = node
    depth = 0
    while depth < 10:
        parent = parent_map.get(id(current))
        if parent is None:
            return False
        if isinstance(parent, ast.With) or isinstance(parent, ast.AsyncWith):
            # Check if our node is in the context_expr, not in the body
            for item in parent.items:
                if _contains(item.context_expr, node):
                    return True
            # We're in the body of a with, but not the context_expr itself
            # That's fine too — the with block is managing some resource
            return False
        current = parent
        depth += 1
    return False


def _contains(tree: ast.AST, target: ast.AST) -> bool:
    """Check if target node is anywhere inside tree."""
    if tree is target:
        return True
    for child in ast.iter_child_nodes(tree):
        if _contains(child, target):
            return True
    return False
