"""Specialist: missing None checks — .get() result used without guard, unchecked optional returns."""

from __future__ import annotations

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

# Names that are very likely dicts (common Python patterns)
_DICT_NAMES = {
    "env", "environ", "config", "settings", "params", "kwargs", "headers",
    "metadata", "attrs", "options", "context", "data", "payload", "body",
    "result", "response", "cache", "registry", "mapping", "lookup",
}


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

    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

            # Pattern: x.get("key").something — .get() can return None
            if (isinstance(node.func, ast.Attribute)
                    and node.func.attr == "get"
                    and len(node.args) >= 1
                    and len(node.args) <= 1):  # no default provided

                # Only flag if the receiver looks like a dict, not an arbitrary object.
                # client.get("/url").data is an HTTP call, not dict.get().
                receiver = node.func.value
                if not _looks_like_dict(receiver, node.args[0]):
                    continue

                parent = parent_map.get(id(node))
                # Check if the .get() result is immediately accessed
                if isinstance(parent, ast.Attribute):
                    # x.get("key").attr — will crash if None
                    findings.append(Finding(
                        line=node.lineno,
                        severity="MEDIUM",
                        message=f"`.get()` without default chained with `.{parent.attr}` — crashes with `AttributeError` if key missing",
                        specialist=self.name,
                        confidence="LIKELY",
                    ))
                elif isinstance(parent, ast.Subscript):
                    # x.get("key")[0] — will crash if None
                    findings.append(Finding(
                        line=node.lineno,
                        severity="MEDIUM",
                        message="`.get()` without default used with subscript — crashes with `TypeError` if key missing",
                        specialist=self.name,
                        confidence="LIKELY",
                    ))

        # == None / != None checks
        for node in ast.walk(ast_data.tree):
            if not isinstance(node, ast.Compare):
                continue
            for op, comparator in zip(node.ops, node.comparators):
                if isinstance(comparator, ast.Constant) and comparator.value is None:
                    if isinstance(op, ast.Eq):
                        findings.append(Finding(
                            line=node.lineno,
                            severity="LOW",
                            message="`== None` should be `is None` (identity check, not equality)",
                            specialist=self.name,
                            confidence="DEFINITE",
                        ))
                    elif isinstance(op, ast.NotEq):
                        findings.append(Finding(
                            line=node.lineno,
                            severity="LOW",
                            message="`!= None` should be `is not None` (identity check, not equality)",
                            specialist=self.name,
                            confidence="DEFINITE",
                        ))

        return findings


def _looks_like_dict(receiver: ast.AST, first_arg: ast.AST) -> bool:
    """Heuristic: does this .get() call look like dict.get() vs object.get()?

    Dict.get() typically has a string key: d.get("key")
    HTTP .get() typically has a URL path: client.get("/path")
    """
    # If the first arg is a string starting with "/" or "http", it's likely HTTP
    if isinstance(first_arg, ast.Constant) and isinstance(first_arg.value, str):
        val = first_arg.value
        if val.startswith("/") or val.startswith("http"):
            return False

    # If the receiver is a known dict-like name, flag it
    if isinstance(receiver, ast.Name) and receiver.id.lower() in _DICT_NAMES:
        return True

    # If receiver is a subscript (d["section"].get("key")), likely dict
    if isinstance(receiver, ast.Subscript):
        return True

    # If receiver is itself a .get() call or dict literal, likely dict
    if isinstance(receiver, (ast.Dict, ast.DictComp)):
        return True

    # If receiver is an attribute access on a known dict name
    if isinstance(receiver, ast.Attribute):
        if isinstance(receiver.value, ast.Name) and receiver.value.id.lower() in (
            "self", "os", "request", "session",
        ):
            if receiver.attr.lower() in _DICT_NAMES or receiver.attr == "environ":
                return True

    # If the first arg looks like a dict key (short identifier-like string), likely dict
    if isinstance(first_arg, ast.Constant) and isinstance(first_arg.value, str):
        val = first_arg.value
        # Short, identifier-like strings are dict keys
        if val.isidentifier() and len(val) < 30:
            return True

    # Default: don't flag (avoid false positives)
    return False
