"""Specialist: command injection via subprocess with shell=True."""

from __future__ import annotations

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


_SUBPROCESS_FUNCS = {"run", "Popen", "call", "check_output", "check_call"}


class CommandInjectionSpecialist(Specialist):
    name = "command_injection"
    trigger_features = ["has_subprocess"]

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

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

            # Match subprocess.run/Popen/call/etc.
            if not (isinstance(node.func, ast.Attribute)
                    and node.func.attr in _SUBPROCESS_FUNCS):
                continue

            # Check if the receiver is 'subprocess'
            if isinstance(node.func.value, ast.Name) and node.func.value.id != "subprocess":
                continue

            # Check for shell=True
            has_shell = False
            for kw in node.keywords:
                if kw.arg == "shell" and isinstance(kw.value, ast.Constant) and kw.value.value is True:
                    has_shell = True
                    break

            if not has_shell:
                continue  # list args without shell=True — safe

            # Analyze the first argument
            if not node.args:
                continue
            first_arg = node.args[0]

            # shell=True + f-string → DEFINITE injection
            if isinstance(first_arg, ast.JoinedStr):
                findings.append(Finding(
                    line=node.lineno,
                    severity="HIGH",
                    message=f"`{node.func.attr}(shell=True)` with f-string — command injection",
                    specialist=self.name,
                    confidence="DEFINITE",
                ))
                continue

            # shell=True + string concatenation → DEFINITE injection
            if isinstance(first_arg, ast.BinOp) and isinstance(first_arg.op, ast.Add):
                findings.append(Finding(
                    line=node.lineno,
                    severity="HIGH",
                    message=f"`{node.func.attr}(shell=True)` with string concatenation — command injection",
                    specialist=self.name,
                    confidence="DEFINITE",
                ))
                continue

            # shell=True + format() call → DEFINITE injection
            if (isinstance(first_arg, ast.Call)
                    and isinstance(first_arg.func, ast.Attribute)
                    and first_arg.func.attr == "format"):
                findings.append(Finding(
                    line=node.lineno,
                    severity="HIGH",
                    message=f"`{node.func.attr}(shell=True)` with .format() — command injection",
                    specialist=self.name,
                    confidence="DEFINITE",
                ))
                continue

            # shell=True + constant string → safe (no user input)
            if isinstance(first_arg, ast.Constant) and isinstance(first_arg.value, str):
                continue

            # shell=True + variable → ambiguous, needs LLM
            ctx_start = max(0, node.lineno - 4)
            ctx_end = min(len(ast_data.source_lines), node.lineno + 3)
            context = "\n".join(ast_data.source_lines[ctx_start:ctx_end])

            verdict = await self.llm_ask(
                question="Does the variable passed to shell=True come from user input or external data?",
                context=context,
                examples=[
                    (
                        "cmd = f'git log {branch}'\nsubprocess.run(cmd, shell=True)",
                        "FLAG: branch could be user-controlled",
                    ),
                    (
                        "cmd = 'ls -la'\nsubprocess.run(cmd, shell=True)",
                        "SKIP: hardcoded command string",
                    ),
                ],
            )

            if verdict.flag:
                findings.append(Finding(
                    line=node.lineno,
                    severity="MEDIUM",
                    message=f"`{node.func.attr}(shell=True)` with variable: {verdict.reason}",
                    specialist=self.name,
                    confidence="LIKELY",
                ))

        return findings
