"""
AIPTX Beast Mode - Payload Encoders
===================================

Encoding and obfuscation techniques for WAF bypass.

Supported Encodings:
- URL encoding (single and double)
- Base64
- Hex
- Unicode (various forms)
- HTML entities
- Mixed case
- Comment injection
- Encoding chains (stacked encodings)
"""

from __future__ import annotations

import base64
import logging
import urllib.parse
from dataclasses import dataclass
from enum import Enum
from typing import Callable

logger = logging.getLogger(__name__)


class EncodingType(str, Enum):
    """Types of encoding available."""
    URL = "url"
    DOUBLE_URL = "double_url"
    BASE64 = "base64"
    HEX = "hex"
    UNICODE = "unicode"
    UNICODE_FULL = "unicode_full"
    HTML_ENTITY = "html_entity"
    HTML_HEX = "html_hex"
    MIXED_CASE = "mixed_case"
    CHAR_CODE = "char_code"  # JavaScript String.fromCharCode
    CONCAT = "concat"  # SQL string concatenation


# =============================================================================
# BASIC ENCODERS
# =============================================================================

def url_encode(payload: str, safe: str = "") -> str:
    """
    URL encode a payload.

    Args:
        payload: String to encode
        safe: Characters to not encode

    Returns:
        URL encoded string
    """
    return urllib.parse.quote(payload, safe=safe)


def double_url_encode(payload: str) -> str:
    """
    Double URL encode a payload.

    Useful for bypassing filters that decode once.

    Args:
        payload: String to encode

    Returns:
        Double URL encoded string
    """
    return url_encode(url_encode(payload))


def base64_encode(payload: str) -> str:
    """
    Base64 encode a payload.

    Args:
        payload: String to encode

    Returns:
        Base64 encoded string
    """
    return base64.b64encode(payload.encode()).decode()


def base64_decode(payload: str) -> str:
    """
    Base64 decode a payload.

    Args:
        payload: Base64 string to decode

    Returns:
        Decoded string
    """
    try:
        return base64.b64decode(payload).decode()
    except Exception:
        return payload


def hex_encode(payload: str, prefix: str = "0x", separator: str = "") -> str:
    """
    Hex encode a payload.

    Args:
        payload: String to encode
        prefix: Prefix for each hex byte (e.g., "0x", "\\x", "")
        separator: Separator between bytes

    Returns:
        Hex encoded string
    """
    hex_chars = [f"{prefix}{ord(c):02x}" for c in payload]
    return separator.join(hex_chars)


def unicode_encode(payload: str, style: str = "basic") -> str:
    """
    Unicode encode a payload.

    Args:
        payload: String to encode
        style: Encoding style:
            - "basic": %uXXXX
            - "full": \\uXXXX
            - "html": &#xXXXX;

    Returns:
        Unicode encoded string
    """
    result = []
    for char in payload:
        code = ord(char)
        if style == "basic":
            result.append(f"%u{code:04x}")
        elif style == "full":
            result.append(f"\\u{code:04x}")
        elif style == "html":
            result.append(f"&#x{code:04x};")
        else:
            result.append(char)
    return "".join(result)


def html_entity_encode(payload: str, numeric: bool = False) -> str:
    """
    HTML entity encode a payload.

    Args:
        payload: String to encode
        numeric: Use numeric entities (&#XX;) instead of named

    Returns:
        HTML entity encoded string
    """
    if numeric:
        return "".join(f"&#{ord(c)};" for c in payload)

    # Named entities for common chars
    entities = {
        "<": "&lt;",
        ">": "&gt;",
        "&": "&amp;",
        '"': "&quot;",
        "'": "&#39;",
        "/": "&#47;",
    }

    result = []
    for char in payload:
        result.append(entities.get(char, char))
    return "".join(result)


def html_hex_encode(payload: str) -> str:
    """
    HTML hex entity encode a payload.

    Args:
        payload: String to encode

    Returns:
        HTML hex encoded string (&#xXX;)
    """
    return "".join(f"&#x{ord(c):02x};" for c in payload)


def mixed_case(payload: str) -> str:
    """
    Apply mixed case to bypass case-sensitive filters.

    Args:
        payload: String to transform

    Returns:
        Mixed case string (e.g., "SeLeCt")
    """
    result = []
    for i, char in enumerate(payload):
        if i % 2 == 0:
            result.append(char.upper())
        else:
            result.append(char.lower())
    return "".join(result)


def reverse_string(payload: str) -> str:
    """
    Reverse a string (for filters that detect keywords).

    Args:
        payload: String to reverse

    Returns:
        Reversed string
    """
    return payload[::-1]


# =============================================================================
# SQL-SPECIFIC ENCODERS
# =============================================================================

def sql_char_encode(payload: str, dbms: str = "mysql") -> str:
    """
    Encode string using SQL CHAR() function.

    Args:
        payload: String to encode
        dbms: Database type (mysql, mssql, oracle, postgres)

    Returns:
        SQL CHAR encoded string
    """
    char_codes = [str(ord(c)) for c in payload]

    if dbms in ("mysql", "mariadb"):
        return f"CHAR({','.join(char_codes)})"
    elif dbms in ("mssql", "sqlserver"):
        return f"CHAR({')+ CHAR('.join(char_codes)})"
    elif dbms == "oracle":
        return " || ".join(f"CHR({code})" for code in char_codes)
    elif dbms in ("postgres", "postgresql"):
        return " || ".join(f"CHR({code})" for code in char_codes)
    else:
        return f"CHAR({','.join(char_codes)})"


def sql_concat_encode(payload: str, dbms: str = "mysql") -> str:
    """
    Encode string using SQL concatenation.

    Args:
        payload: String to encode
        dbms: Database type

    Returns:
        Concatenated SQL string
    """
    parts = list(payload)

    if dbms in ("mysql", "mariadb"):
        return "CONCAT(" + ",".join(f"'{p}'" for p in parts) + ")"
    elif dbms in ("mssql", "sqlserver"):
        return "+".join(f"'{p}'" for p in parts)
    elif dbms in ("oracle", "postgres", "postgresql"):
        return "||".join(f"'{p}'" for p in parts)
    else:
        return "CONCAT(" + ",".join(f"'{p}'" for p in parts) + ")"


def sql_hex_encode(payload: str, dbms: str = "mysql") -> str:
    """
    Encode string as SQL hex literal.

    Args:
        payload: String to encode
        dbms: Database type

    Returns:
        SQL hex literal
    """
    hex_str = payload.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}'"
    else:
        return f"0x{hex_str}"


# =============================================================================
# JAVASCRIPT-SPECIFIC ENCODERS
# =============================================================================

def js_char_code(payload: str) -> str:
    """
    Encode string using JavaScript String.fromCharCode.

    Args:
        payload: String to encode

    Returns:
        JavaScript String.fromCharCode expression
    """
    char_codes = [str(ord(c)) for c in payload]
    return f"String.fromCharCode({','.join(char_codes)})"


def js_unicode(payload: str) -> str:
    """
    Encode string using JavaScript unicode escapes.

    Args:
        payload: String to encode

    Returns:
        JavaScript unicode escaped string
    """
    return "".join(f"\\u{ord(c):04x}" for c in payload)


def js_hex(payload: str) -> str:
    """
    Encode string using JavaScript hex escapes.

    Args:
        payload: String to encode

    Returns:
        JavaScript hex escaped string
    """
    return "".join(f"\\x{ord(c):02x}" for c in payload)


def js_octal(payload: str) -> str:
    """
    Encode string using JavaScript octal escapes.

    Args:
        payload: String to encode

    Returns:
        JavaScript octal escaped string
    """
    return "".join(f"\\{ord(c):03o}" for c in payload)


def js_atob(payload: str) -> str:
    """
    Encode string for JavaScript atob() decoding.

    Args:
        payload: String to encode

    Returns:
        JavaScript atob expression
    """
    b64 = base64_encode(payload)
    return f"atob('{b64}')"


# =============================================================================
# COMMAND INJECTION ENCODERS
# =============================================================================

def cmd_variable_expansion(payload: str) -> str:
    """
    Encode using shell variable expansion.

    Example: cat → $'c'$'a'$'t'

    Args:
        payload: Command to encode

    Returns:
        Variable expansion encoded string
    """
    return "".join(f"$'{c}'" for c in payload)


def cmd_hex_escape(payload: str) -> str:
    """
    Encode using bash hex escapes.

    Example: cat → $'\\x63\\x61\\x74'

    Args:
        payload: Command to encode

    Returns:
        Hex escaped bash string
    """
    hex_chars = "".join(f"\\x{ord(c):02x}" for c in payload)
    return f"$'{hex_chars}'"


def cmd_brace_expansion(command: str) -> str:
    """
    Use brace expansion to obfuscate command.

    Example: cat /etc/passwd → {cat,/etc/passwd}

    Args:
        command: Full command to encode

    Returns:
        Brace expansion command
    """
    parts = command.split()
    if len(parts) > 1:
        return "{" + ",".join(parts) + "}"
    return command


def cmd_base64_inline(payload: str) -> str:
    """
    Encode command for inline base64 execution.

    Example: cat → echo Y2F0 | base64 -d

    Args:
        payload: Command to encode

    Returns:
        Base64 inline execution string
    """
    b64 = base64_encode(payload)
    return f"echo {b64}|base64 -d"


def cmd_rev_inline(payload: str) -> str:
    """
    Encode command using rev for execution.

    Example: cat → echo tac | rev

    Args:
        payload: Command to encode

    Returns:
        Rev inline execution string
    """
    return f"echo {payload[::-1]}|rev"


# =============================================================================
# ENCODING CHAINS
# =============================================================================

@dataclass
class Encoder:
    """Wrapper for encoding functions with metadata."""
    name: str
    encode_func: Callable[[str], str]
    encoding_type: EncodingType
    description: str = ""
    applicable_to: list[str] = None  # ["sqli", "xss", "cmdi", "all"]

    def __post_init__(self):
        if self.applicable_to is None:
            self.applicable_to = ["all"]

    def encode(self, payload: str) -> str:
        """Encode a payload using this encoder."""
        return self.encode_func(payload)


# Registry of all encoders
ENCODERS: dict[str, Encoder] = {
    "url": Encoder("url", url_encode, EncodingType.URL, "URL encoding"),
    "double_url": Encoder("double_url", double_url_encode, EncodingType.DOUBLE_URL, "Double URL encoding"),
    "base64": Encoder("base64", base64_encode, EncodingType.BASE64, "Base64 encoding"),
    "hex": Encoder("hex", hex_encode, EncodingType.HEX, "Hex encoding"),
    "unicode": Encoder("unicode", unicode_encode, EncodingType.UNICODE, "Unicode encoding"),
    "html_entity": Encoder("html_entity", html_entity_encode, EncodingType.HTML_ENTITY, "HTML entity encoding"),
    "html_hex": Encoder("html_hex", html_hex_encode, EncodingType.HTML_HEX, "HTML hex encoding"),
    "mixed_case": Encoder("mixed_case", mixed_case, EncodingType.MIXED_CASE, "Mixed case", ["sqli"]),
    "js_charcode": Encoder("js_charcode", js_char_code, EncodingType.CHAR_CODE, "JS String.fromCharCode", ["xss"]),
    "js_unicode": Encoder("js_unicode", js_unicode, EncodingType.UNICODE, "JS unicode escapes", ["xss"]),
    "js_atob": Encoder("js_atob", js_atob, EncodingType.BASE64, "JS atob() decoding", ["xss"]),
    "cmd_hex": Encoder("cmd_hex", cmd_hex_escape, EncodingType.HEX, "Bash hex escape", ["cmdi"]),
    "cmd_base64": Encoder("cmd_base64", cmd_base64_inline, EncodingType.BASE64, "Bash base64 inline", ["cmdi"]),
}


def chain_encode(payload: str, encoding_chain: list[str]) -> str:
    """
    Apply multiple encodings in sequence.

    Args:
        payload: Original payload
        encoding_chain: List of encoder names to apply in order

    Returns:
        Encoded payload

    Example:
        chain_encode("' OR 1=1--", ["url", "base64"])
    """
    result = payload
    for encoder_name in encoding_chain:
        if encoder_name in ENCODERS:
            result = ENCODERS[encoder_name].encode(result)
            logger.debug(f"Applied {encoder_name}: {result[:50]}...")
        else:
            logger.warning(f"Unknown encoder: {encoder_name}")
    return result


def get_encoders_for_context(context: str) -> list[Encoder]:
    """
    Get encoders applicable to a specific context.

    Args:
        context: Attack context (sqli, xss, cmdi)

    Returns:
        List of applicable encoders
    """
    return [
        enc for enc in ENCODERS.values()
        if "all" in enc.applicable_to or context in enc.applicable_to
    ]


def generate_encoding_variants(payload: str, max_depth: int = 2) -> list[tuple[str, list[str]]]:
    """
    Generate multiple encoding variants of a payload.

    Args:
        payload: Original payload
        max_depth: Maximum encoding chain depth

    Returns:
        List of (encoded_payload, encoding_chain) tuples
    """
    variants = [(payload, [])]  # Original

    # Single encodings
    for name, encoder in ENCODERS.items():
        encoded = encoder.encode(payload)
        variants.append((encoded, [name]))

    # Double encodings (if max_depth >= 2)
    if max_depth >= 2:
        common_chains = [
            ["url", "url"],  # Double URL
            ["base64", "url"],  # Base64 then URL
            ["hex", "url"],  # Hex then URL
            ["unicode", "url"],  # Unicode then URL
        ]
        for chain in common_chains:
            try:
                encoded = chain_encode(payload, chain)
                variants.append((encoded, chain))
            except Exception as e:
                logger.debug(f"Chain {chain} failed: {e}")

    return variants


__all__ = [
    # Basic encoders
    "url_encode",
    "double_url_encode",
    "base64_encode",
    "base64_decode",
    "hex_encode",
    "unicode_encode",
    "html_entity_encode",
    "html_hex_encode",
    "mixed_case",
    "reverse_string",
    # SQL encoders
    "sql_char_encode",
    "sql_concat_encode",
    "sql_hex_encode",
    # JavaScript encoders
    "js_char_code",
    "js_unicode",
    "js_hex",
    "js_octal",
    "js_atob",
    # Command injection encoders
    "cmd_variable_expansion",
    "cmd_hex_escape",
    "cmd_brace_expansion",
    "cmd_base64_inline",
    "cmd_rev_inline",
    # Encoder class and registry
    "Encoder",
    "EncodingType",
    "ENCODERS",
    "chain_encode",
    "get_encoders_for_context",
    "generate_encoding_variants",
]
