"""Specialist: except → return None where caller doesn't check for None."""

from __future__ import annotations

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


class NoneReturnSpecialist(Specialist):
    name = "none_return"
    trigger_features: list[str] = []

    def should_run(self, ast_data: ASTData, focus: str) -> bool:
        return any(h.body_type == "return_none" for h in ast_data.except_handlers)

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

        # Find functions that return None on exception
        none_return_funcs: list[tuple[str, int]] = []
        for handler in ast_data.except_handlers:
            if handler.body_type != "return_none":
                continue
            if handler.enclosing_function:
                none_return_funcs.append((handler.enclosing_function, handler.line))

        if not none_return_funcs:
            return findings

        lines = ast_data.source_lines

        for func_name, handler_line in none_return_funcs:
            # Check return type annotation — Optional[...] or ... | None is intentional
            func_range = ast_data.function_ranges.get(func_name)
            if func_range:
                def_line = lines[func_range[0] - 1] if func_range[0] <= len(lines) else ""
                if "Optional" in def_line or "| None" in def_line or "-> None" in def_line:
                    continue  # type-annotated as nullable — callers should handle it

            # Find call sites of this function
            call_lines = ast_data.call_sites.get(func_name, [])
            if not call_lines:
                continue  # no callers in this file

            for call_line in call_lines:
                if call_line < 1 or call_line > len(lines):
                    continue

                # Check if caller handles None
                # Look at the line and the next 2 lines for None checks
                ctx_start = call_line - 1
                ctx_end = min(len(lines), call_line + 3)
                caller_ctx = "\n".join(lines[ctx_start:ctx_end])

                if _has_none_check(caller_ctx, func_name):
                    continue  # caller handles None

                # Check if result is used with attribute access (crash risk)
                call_line_text = lines[call_line - 1]
                assign_match = re.match(r'\s*(\w+)\s*=\s*' + re.escape(func_name), call_line_text)
                if not assign_match:
                    continue  # result not assigned — fire-and-forget call

                var_name = assign_match.group(1)

                # Check subsequent lines for .attr access on the variable
                for i in range(call_line, min(len(lines), call_line + 10)):
                    subsequent = lines[i]
                    if re.search(r'\b' + re.escape(var_name) + r'\.\w+', subsequent):
                        # Attribute access on potentially-None value
                        findings.append(Finding(
                            line=i + 1,
                            severity="HIGH",
                            message=f"`{func_name}()` returns None on error (line {handler_line}), "
                                    f"but `{var_name}.` is accessed without None check",
                            specialist=self.name,
                            confidence="LIKELY",
                        ))
                        break

        return findings


def _has_none_check(context: str, func_name: str) -> bool:
    """Check if the caller context handles None returns."""
    checks = [
        "is None", "is not None",
        "if not ", "if " + func_name,
        " or {}", " or []", " or ''", ' or ""',
        " or default", " or fallback",
        "try:", "except",
    ]
    ctx_lower = context.lower()
    return any(chk.lower() in ctx_lower for chk in checks)
