"""Specialist: missing await on async function calls.

Fully deterministic — no LLM needed.
"""

from __future__ import annotations

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


class MissingAwaitSpecialist(Specialist):
    name = "missing_await"
    trigger_features = ["has_async"]

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

        if not async_funcs:
            return findings

        # Build parent map to check context
        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):
            # We want bare expression statements: func() on its own line
            if not isinstance(node, ast.Expr):
                continue
            if not isinstance(node.value, ast.Call):
                continue

            call = node.value
            call_name = None
            if isinstance(call.func, ast.Name):
                call_name = call.func.id
            elif isinstance(call.func, ast.Attribute):
                call_name = call.func.attr

            if call_name not in async_funcs:
                continue

            # Check: is this call inside an async function?
            enclosing = _find_enclosing_async(node, parent_map)
            if not enclosing:
                continue  # not in async context — calling async from sync is a different issue

            # Check: is the call wrapped in await? (it shouldn't be if we got here —
            # Expr(Call) means no await, Expr(Await(Call)) would be different)
            # But let's verify: the parent of the Call should be Expr, not Await
            parent_of_call = parent_map.get(id(call))
            if isinstance(parent_of_call, ast.Await):
                continue  # properly awaited

            # Also check if it's passed to asyncio.gather, create_task, etc.
            # That would look like: asyncio.gather(func()) — the Call would be inside
            # another Call, not a bare Expr. So bare Expr → definitely not gathered.

            findings.append(Finding(
                line=node.lineno,
                severity="HIGH",
                message=f"Async function `{call_name}()` called without `await` — coroutine never executes",
                specialist=self.name,
                confidence="DEFINITE",
            ))

        return findings


def _find_enclosing_async(node: ast.AST, parent_map: dict[int, ast.AST]) -> bool:
    """Check if this node is inside an async function."""
    current = node
    while True:
        parent = parent_map.get(id(current))
        if parent is None:
            return False
        if isinstance(parent, ast.AsyncFunctionDef):
            return True
        if isinstance(parent, ast.FunctionDef):
            return False  # sync function — stop here
        current = parent
