"""Specialist: SQL injection via string concatenation/formatting in queries."""

from __future__ import annotations

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


_SQL_KEYWORDS = re.compile(
    r'\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|GRANT|EXECUTE|EXEC)\b',
    re.IGNORECASE,
)

_SQL_FUNCS = {"execute", "executemany", "executescript", "mogrify", "raw", "text"}


class SQLInjectionSpecialist(Specialist):
    name = "sql_injection"
    trigger_features = ["has_sql"]

    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 cursor.execute(), conn.execute(), db.raw(), etc.
            if not (isinstance(node.func, ast.Attribute)
                    and node.func.attr in _SQL_FUNCS):
                continue

            if not node.args:
                continue

            first_arg = node.args[0]
            func_name = node.func.attr

            # f-string with SQL keywords → DEFINITE injection
            if isinstance(first_arg, ast.JoinedStr):
                # Check if the f-string contains SQL keywords
                try:
                    src_line = ast_data.source_lines[node.lineno - 1] if node.lineno <= len(ast_data.source_lines) else ""
                except IndexError:
                    src_line = ""
                if _SQL_KEYWORDS.search(src_line):
                    findings.append(Finding(
                        line=node.lineno,
                        severity="HIGH",
                        message=f"SQL injection: `{func_name}()` with f-string containing SQL. Use parameterized queries",
                        specialist=self.name,
                        confidence="DEFINITE",
                    ))
                    continue

            # String concatenation with SQL → DEFINITE injection
            if isinstance(first_arg, ast.BinOp) and isinstance(first_arg.op, (ast.Add, ast.Mod)):
                # Check left side for SQL keywords
                if isinstance(first_arg.left, ast.Constant) and isinstance(first_arg.left.value, str):
                    if _SQL_KEYWORDS.search(first_arg.left.value):
                        findings.append(Finding(
                            line=node.lineno,
                            severity="HIGH",
                            message=f"SQL injection: `{func_name}()` with string concatenation. Use parameterized queries",
                            specialist=self.name,
                            confidence="DEFINITE",
                        ))
                        continue

            # .format() on SQL string → DEFINITE injection
            if isinstance(first_arg, ast.Call) and isinstance(first_arg.func, ast.Attribute):
                if first_arg.func.attr == "format":
                    if (isinstance(first_arg.func.value, ast.Constant)
                            and isinstance(first_arg.func.value.value, str)
                            and _SQL_KEYWORDS.search(first_arg.func.value.value)):
                        findings.append(Finding(
                            line=node.lineno,
                            severity="HIGH",
                            message=f"SQL injection: `{func_name}()` with `.format()`. Use parameterized queries",
                            specialist=self.name,
                            confidence="DEFINITE",
                        ))

        return findings
