"""CMD-001/CMD-002: Command Injection detection rules.

NOTE: This module contains string references to unsafe subprocess functions
(like os.system) because it is a SECURITY SCANNER that detects these patterns
in target code. These are detection signatures, not actual usage.
"""

from __future__ import annotations

import ast

from . import Rule
from ..models import Finding, Severity

# Detection signatures for subprocess functions in scanned code.
# These are pattern names the scanner looks for — not executed.
UNSAFE_SUBPROCESS_FUNCS = {
    "subprocess.run", "subprocess.call", "subprocess.Popen",
    "subprocess.check_output", "subprocess.check_call",
    "os.system", "os.popen",
}


class CommandInjectionShellRule(Rule):
    """Detects subprocess calls with shell=True."""

    @property
    def rule_id(self) -> str:
        return "CMD-001"

    @property
    def name(self) -> str:
        return "Command Injection (shell=True)"

    @property
    def severity(self) -> Severity:
        return Severity.CRITICAL

    @property
    def cwe(self) -> str:
        return "CWE-78"

    @property
    def description(self) -> str:
        return "Subprocess call with shell=True enables command injection."

    @property
    def detailed_description(self) -> str:
        return (
            "Subprocess call with shell=True enables command injection.\n\n"
            "When shell=True is passed to subprocess functions, the command string\n"
            "is interpreted by the system shell. If any part of that string comes\n"
            "from user input, an attacker can inject arbitrary commands.\n\n"
            "Examples of vulnerable code:\n"
            "  subprocess.run(f'ls {user_dir}', shell=True)\n"
            "  subprocess.call('grep ' + pattern, shell=True)\n"
            "  os.system('ping ' + host)\n\n"
            "How to fix:\n"
            "  - Pass commands as a list: subprocess.run(['ls', user_dir])\n"
            "  - Remove shell=True and use argument lists\n"
            "  - Use shlex.quote() if shell=True is unavoidable"
        )

    def check(self, tree: ast.Module, source: str) -> list[Finding]:
        """Check for subprocess calls with shell=True."""
        findings: list[Finding] = []
        for node in ast.walk(tree):
            if not isinstance(node, ast.Call):
                continue
            func_name = self._get_func_name(node)
            if func_name not in UNSAFE_SUBPROCESS_FUNCS:
                continue

            shell_true = any(
                isinstance(kw.value, ast.Constant) and kw.value.value is True
                for kw in node.keywords
                if kw.arg == "shell"
            )
            if shell_true:
                findings.append(Finding(
                    rule_id=self.rule_id,
                    severity=self.severity,
                    title="Command Injection (shell=True)",
                    description=f"{func_name}() with shell=True.",
                    file="",
                    line=node.lineno,
                    code_snippet=self._get_snippet(source, node.lineno),
                    recommendation="Use argument list without shell=True.",
                    cwe="CWE-78",
                ))
        return findings


class CommandInjectionDynamicRule(Rule):
    """Detects subprocess calls with dynamically constructed arguments."""

    @property
    def rule_id(self) -> str:
        return "CMD-002"

    @property
    def name(self) -> str:
        return "Command Injection (dynamic args)"

    @property
    def severity(self) -> Severity:
        return Severity.HIGH

    @property
    def description(self) -> str:
        return "Subprocess call with dynamically constructed command string."

    def check(self, tree: ast.Module, source: str) -> list[Finding]:
        """Check for subprocess calls with f-strings or string concatenation."""
        findings: list[Finding] = []
        for node in ast.walk(tree):
            if not isinstance(node, ast.Call):
                continue
            func_name = self._get_func_name(node)
            if func_name not in UNSAFE_SUBPROCESS_FUNCS:
                continue
            if node.args and isinstance(node.args[0], (ast.JoinedStr, ast.BinOp)):
                findings.append(Finding(
                    rule_id=self.rule_id,
                    severity=self.severity,
                    title="Potential Command Injection (dynamic string)",
                    description=f"{func_name}() with dynamically constructed arguments.",
                    file="",
                    line=node.lineno,
                    code_snippet=self._get_snippet(source, node.lineno),
                    recommendation="Use shlex.quote() to sanitize input.",
                    cwe="CWE-78",
                ))
        return findings
