"""Checker for SQL injection vulnerabilities.

Supports: Python, JavaScript/TypeScript, Go, Rust, Ruby
"""

import re
from pathlib import Path
from typing import List

from stablestack.checkers.base import BaseChecker
from stablestack.models import Finding, Severity, Tier


class SqlInjectionChecker(BaseChecker):
    """Detects SQL string concatenation that could lead to injection.

    Supports: Python, JavaScript/TypeScript, Go, Rust, Ruby
    """

    rule_id = "SEC002"
    rule_name = "sql-injection"
    description = "SQL queries built with string concatenation are vulnerable to injection"
    why = (
        "Building SQL queries by concatenating strings allows attackers to inject "
        "malicious SQL code. If a user enters \"'; DROP TABLE users; --\" as their name, "
        "your database could be destroyed. Always use parameterized queries."
    )
    severity = Severity.ERROR
    tier = Tier.CRITICAL  # Security issues are always critical
    file_extensions = {".py", ".js", ".ts", ".jsx", ".tsx", ".go", ".rs", ".rb"}

    # SQL keywords that indicate a query (case-insensitive matching done at compile time)
    # Word boundaries prevent matching words that contain SQL keywords (e.g., "Updated" matching UPDATE)
    _SQL_KEYWORDS = r'\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE|GRANT|EXECUTE|UNION)\b'

    # Patterns for dangerous string building
    _PATTERNS = [
        # Python/JS: f"SELECT ... {var}" - f-strings with SQL keywords and interpolation
        re.compile(rf'''f['"].*{_SQL_KEYWORDS}.*\{{[^}}]+\}}.*['"]''', re.IGNORECASE),
        # Python/JS/Go: "SELECT ... " + var
        re.compile(rf'''['"]{_SQL_KEYWORDS}[^'"]*['"]\s*\+\s*\w+''', re.IGNORECASE),
        # Python: "SELECT ... %s" % var
        re.compile(rf'''['"]{_SQL_KEYWORDS}[^'"]*%s[^'"]*['"]\s*%\s*''', re.IGNORECASE),
        # Python: "SELECT ... " + str(var)
        re.compile(rf'''['"]{_SQL_KEYWORDS}[^'"]*['"]\s*\+\s*str\s*\(''', re.IGNORECASE),
        # Python: "SELECT ... {}".format(var)
        re.compile(rf'''['"]{_SQL_KEYWORDS}[^'"]*\{{\}}[^'"]*['"]\.format\s*\(''', re.IGNORECASE),
        # JS: `SELECT ... ${var}` (template literals)
        re.compile(rf'''`{_SQL_KEYWORDS}[^`]*\$\{{[^}}]+\}}[^`]*`''', re.IGNORECASE),
        # Go: fmt.Sprintf("SELECT ... %s", var)
        re.compile(rf'''fmt\.Sprintf\s*\(\s*["`]{_SQL_KEYWORDS}[^"`]*%[svdq][^"`]*["`]''', re.IGNORECASE),
        # Rust: format!("SELECT ... {}", var)
        re.compile(rf'''format!\s*\(\s*["`]{_SQL_KEYWORDS}[^"`]*\{{\}}[^"`]*["`]''', re.IGNORECASE),
        # Ruby: "SELECT ... #{var}"
        re.compile(rf'''["`]{_SQL_KEYWORDS}[^"`]*\#\{{[^}}]+\}}[^"`]*["`]''', re.IGNORECASE),
    ]

    def check_file(self, path: Path, content: str) -> List[Finding]:
        findings = []
        lines = content.splitlines()

        for pattern in self._PATTERNS:
            for match in pattern.finditer(content):
                line_num = content[:match.start()].count('\n') + 1
                line_content = lines[line_num - 1] if line_num <= len(lines) else ""

                # Skip if using parameterized query markers
                if '?' in line_content or ':param' in line_content.lower():
                    continue

                # Skip tagged template literals — these are parameterized by the tag function.
                # Prisma.sql`...${var}...`, sql`...`, and Prisma.$queryRaw`...` are safe.
                stripped_line = line_content.strip()
                if re.search(r'(?:Prisma\.sql|Prisma\.\$queryRaw|sql)\s*`', stripped_line):
                    continue

                findings.append(
                    self.create_finding(
                        path=path,
                        line=line_num,
                        column=match.start() - content.rfind('\n', 0, match.start()),
                        code=line_content.strip()[:60] + ("..." if len(line_content.strip()) > 60 else ""),
                        message=(
                            "SQL query built with string concatenation. "
                            "This is vulnerable to SQL injection attacks!"
                        ),
                        fix_hint=self._get_fix_hint(path),
                    )
                )

        return findings

    def _get_fix_hint(self, path: Path) -> str:
        suffix = path.suffix
        if suffix == '.go':
            return (
                "Use parameterized queries:\n\n"
                "// database/sql:\n"
                "db.Query(\"SELECT * FROM users WHERE id = $1\", userID)\n\n"
                "// sqlx:\n"
                "db.Get(&user, \"SELECT * FROM users WHERE id = $1\", userID)"
            )
        elif suffix == '.rs':
            return (
                "Use parameterized queries:\n\n"
                "// sqlx:\n"
                "sqlx::query(\"SELECT * FROM users WHERE id = $1\")\n"
                "    .bind(user_id)\n"
                "    .fetch_one(&pool).await\n\n"
                "// diesel:\n"
                "users.filter(id.eq(user_id)).first::<User>(&conn)"
            )
        elif suffix == '.rb':
            return (
                "Use parameterized queries:\n\n"
                "# ActiveRecord:\n"
                "User.where(id: user_id)\n"
                "User.where('id = ?', user_id)\n\n"
                "# Raw SQL:\n"
                "ActiveRecord::Base.connection.exec_query(\n"
                "  'SELECT * FROM users WHERE id = $1', 'SQL', [[nil, user_id]])"
            )
        else:
            return (
                "Use parameterized queries:\n\n"
                "Python (sqlite3):\n"
                "cursor.execute('SELECT * FROM users WHERE id = ?', (user_id,))\n\n"
                "Python (psycopg2):\n"
                "cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,))\n\n"
                "JavaScript (pg):\n"
                "client.query('SELECT * FROM users WHERE id = $1', [userId])"
            )
