"""
AIPTX Beast Mode - WAF Detector
===============================

Fingerprint Web Application Firewalls from HTTP responses.
Enables targeted bypass selection.

Supported WAFs:
- Cloudflare
- AWS WAF
- Akamai
- Imperva/Incapsula
- ModSecurity
- F5 BIG-IP ASM
- Barracuda
- Fortinet FortiWeb
- Sucuri
- Wordfence (WordPress)
"""

from __future__ import annotations

import logging
import re
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Optional

logger = logging.getLogger(__name__)


class WAFType(str, Enum):
    """Known WAF types."""
    CLOUDFLARE = "cloudflare"
    AWS_WAF = "aws_waf"
    AKAMAI = "akamai"
    IMPERVA = "imperva"
    MODSECURITY = "modsecurity"
    F5_ASM = "f5_asm"
    BARRACUDA = "barracuda"
    FORTINET = "fortinet"
    SUCURI = "sucuri"
    WORDFENCE = "wordfence"
    NGINX = "nginx"
    APACHE = "apache"
    UNKNOWN = "unknown"
    NONE = "none"


@dataclass
class WAFSignature:
    """Signature for detecting a specific WAF."""
    waf_type: WAFType
    name: str
    headers: dict[str, str | re.Pattern] = field(default_factory=dict)
    cookies: dict[str, str | re.Pattern] = field(default_factory=dict)
    body_patterns: list[str | re.Pattern] = field(default_factory=list)
    status_codes: list[int] = field(default_factory=list)
    server_header: str | re.Pattern | None = None
    confidence_boost: float = 0.0  # Extra confidence if matched


@dataclass
class WAFDetectionResult:
    """Result of WAF detection."""
    detected: bool
    waf_type: WAFType
    waf_name: str
    confidence: float
    evidence: list[str]
    bypass_recommendations: list[str] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        """Convert to dictionary."""
        return {
            "detected": self.detected,
            "waf_type": self.waf_type.value,
            "waf_name": self.waf_name,
            "confidence": self.confidence,
            "evidence": self.evidence,
            "bypass_recommendations": self.bypass_recommendations,
        }


# =============================================================================
# WAF SIGNATURES
# =============================================================================

WAF_SIGNATURES: list[WAFSignature] = [
    # Cloudflare
    # NOTE: Cloudflare detection is critical - the Server header alone should be
    # sufficient evidence since it explicitly identifies Cloudflare.
    # confidence_boost ensures detection even when only server header is present.
    WAFSignature(
        waf_type=WAFType.CLOUDFLARE,
        name="Cloudflare",
        headers={
            "cf-ray": re.compile(r".*"),
            "cf-cache-status": re.compile(r".*"),
            "cf-request-id": re.compile(r".*"),
            "cf-edge": re.compile(r".*"),
            "server-timing": re.compile(r"cfCache|cfEdge|cfOrigin", re.I),
        },
        cookies={
            "__cfduid": re.compile(r".*"),
            "__cf_bm": re.compile(r".*"),
            "cf_clearance": re.compile(r".*"),
        },
        body_patterns=[
            re.compile(r"cloudflare", re.I),
            re.compile(r"ray id:", re.I),
            re.compile(r"attention required", re.I),
            re.compile(r"checking your browser", re.I),
        ],
        status_codes=[403, 503, 520, 521, 522, 523, 524],
        server_header=re.compile(r"cloudflare", re.I),
        confidence_boost=0.20,  # Cloudflare server header alone is strong evidence
    ),

    # AWS WAF
    WAFSignature(
        waf_type=WAFType.AWS_WAF,
        name="AWS WAF",
        headers={
            "x-amzn-requestid": re.compile(r".*"),
            "x-amz-cf-id": re.compile(r".*"),
            "x-amz-id-2": re.compile(r".*"),
        },
        body_patterns=[
            re.compile(r"aws", re.I),
            re.compile(r"amazon", re.I),
            re.compile(r"request blocked", re.I),
        ],
        status_codes=[403],
        server_header=re.compile(r"awselb|cloudfront", re.I),
    ),

    # Akamai
    WAFSignature(
        waf_type=WAFType.AKAMAI,
        name="Akamai Kona/Ghost",
        headers={
            "x-akamai-transformed": re.compile(r".*"),
            "akamai-grn": re.compile(r".*"),
            "x-akamai-session-info": re.compile(r".*"),
        },
        cookies={
            "akamai_generated": re.compile(r".*"),
            "ak_bmsc": re.compile(r".*"),
            "bm_sz": re.compile(r".*"),
        },
        body_patterns=[
            re.compile(r"akamai", re.I),
            re.compile(r"access denied", re.I),
            re.compile(r"reference#", re.I),
        ],
        status_codes=[403],
        server_header=re.compile(r"akamai|ghost", re.I),
    ),

    # Imperva/Incapsula
    WAFSignature(
        waf_type=WAFType.IMPERVA,
        name="Imperva Incapsula",
        headers={
            "x-iinfo": re.compile(r".*"),
            "x-cdn": re.compile(r"imperva|incapsula", re.I),
        },
        cookies={
            "incap_ses_": re.compile(r".*"),
            "visid_incap_": re.compile(r".*"),
            "nlbi_": re.compile(r".*"),
        },
        body_patterns=[
            re.compile(r"incapsula", re.I),
            re.compile(r"imperva", re.I),
            re.compile(r"incident id", re.I),
            re.compile(r"_incapsula_resource", re.I),
        ],
        status_codes=[403],
    ),

    # ModSecurity
    WAFSignature(
        waf_type=WAFType.MODSECURITY,
        name="ModSecurity",
        headers={
            "x-mod-security": re.compile(r".*"),
        },
        body_patterns=[
            re.compile(r"mod_security", re.I),
            re.compile(r"modsecurity", re.I),
            re.compile(r"owasp", re.I),
            re.compile(r"this error was generated by mod_security", re.I),
            re.compile(r"not acceptable", re.I),
        ],
        status_codes=[403, 406],
        server_header=re.compile(r"mod_security", re.I),
    ),

    # F5 BIG-IP ASM
    WAFSignature(
        waf_type=WAFType.F5_ASM,
        name="F5 BIG-IP ASM",
        headers={
            "x-wa-info": re.compile(r".*"),
        },
        cookies={
            "ts": re.compile(r"^[a-zA-Z0-9]{16,}"),
            "bigipserver": re.compile(r".*"),
            "f5": re.compile(r".*"),
        },
        body_patterns=[
            re.compile(r"f5 networks", re.I),
            re.compile(r"big-ip", re.I),
            re.compile(r"the requested url was rejected", re.I),
            re.compile(r"support id", re.I),
        ],
        status_codes=[403],
        server_header=re.compile(r"big-?ip|f5", re.I),
    ),

    # Barracuda
    WAFSignature(
        waf_type=WAFType.BARRACUDA,
        name="Barracuda WAF",
        headers={
            "barra_counter_session": re.compile(r".*"),
        },
        cookies={
            "barra_counter_session": re.compile(r".*"),
            "barracuda": re.compile(r".*"),
        },
        body_patterns=[
            re.compile(r"barracuda", re.I),
            re.compile(r"you have been blocked", re.I),
        ],
        status_codes=[403],
    ),

    # Fortinet FortiWeb
    WAFSignature(
        waf_type=WAFType.FORTINET,
        name="Fortinet FortiWeb",
        headers={
            "fortiwafsid": re.compile(r".*"),
        },
        cookies={
            "cookiesession1": re.compile(r".*"),
            "fortigate": re.compile(r".*"),
        },
        body_patterns=[
            re.compile(r"fortinet", re.I),
            re.compile(r"fortiweb", re.I),
            re.compile(r"fortigate", re.I),
        ],
        status_codes=[403],
        server_header=re.compile(r"forti", re.I),
    ),

    # Sucuri
    WAFSignature(
        waf_type=WAFType.SUCURI,
        name="Sucuri CloudProxy",
        headers={
            "x-sucuri-id": re.compile(r".*"),
            "x-sucuri-cache": re.compile(r".*"),
        },
        body_patterns=[
            re.compile(r"sucuri", re.I),
            re.compile(r"cloudproxy", re.I),
            re.compile(r"access denied", re.I),
        ],
        status_codes=[403],
        server_header=re.compile(r"sucuri", re.I),
    ),

    # Wordfence (WordPress)
    WAFSignature(
        waf_type=WAFType.WORDFENCE,
        name="Wordfence",
        body_patterns=[
            re.compile(r"wordfence", re.I),
            re.compile(r"blocked by wordfence", re.I),
            re.compile(r"your access to this site has been limited", re.I),
            re.compile(r"wfwaf-authcookie", re.I),
        ],
        cookies={
            "wfwaf-authcookie": re.compile(r".*"),
        },
        status_codes=[403, 503],
    ),

    # Generic NGINX rate limiting
    WAFSignature(
        waf_type=WAFType.NGINX,
        name="NGINX Rate Limiting",
        body_patterns=[
            re.compile(r"nginx", re.I),
            re.compile(r"rate limit exceeded", re.I),
        ],
        status_codes=[429, 503],
        server_header=re.compile(r"nginx", re.I),
    ),

    # Generic Apache mod_evasive
    WAFSignature(
        waf_type=WAFType.APACHE,
        name="Apache mod_evasive",
        body_patterns=[
            re.compile(r"apache", re.I),
            re.compile(r"mod_evasive", re.I),
        ],
        status_codes=[403, 429],
        server_header=re.compile(r"apache", re.I),
    ),
]


# Bypass recommendations per WAF
WAF_BYPASS_TIPS: dict[WAFType, list[str]] = {
    WAFType.CLOUDFLARE: [
        "Use Unicode/UTF-8 encoding for special characters",
        "Try double URL encoding",
        "Use comment injection (/**/) in SQL",
        "Bypass with IPv6 or origin IP if known",
        "Try case variation (sElEcT instead of SELECT)",
    ],
    WAFType.AWS_WAF: [
        "Use MySQL version comments (/*!50000SELECT*/)",
        "Try hex encoding for strings",
        "Use Unicode apostrophe (ʼ) instead of quote",
        "Experiment with HTTP parameter pollution",
    ],
    WAFType.AKAMAI: [
        "Use mixed case for keywords",
        "Try inline comments in SQL",
        "Replace OR with ||, AND with &&",
        "Use chunked transfer encoding",
    ],
    WAFType.IMPERVA: [
        "Try GBK encoding bypass (%bf%27)",
        "Use hex encoding for keywords",
        "Replace spaces with newlines (%0a)",
        "Experiment with HTTP verb tampering",
    ],
    WAFType.MODSECURITY: [
        "Check paranoia level - lower levels have more bypasses",
        "Use tab (%09) instead of space",
        "Try XOR instead of OR in boolean conditions",
        "Use comments between keywords",
    ],
    WAFType.F5_ASM: [
        "Try parameter pollution",
        "Use URL encoding variations",
        "Experiment with HTTP method override",
    ],
    WAFType.WORDFENCE: [
        "Check for plugin-specific bypasses",
        "Try longer payloads (may bypass regex)",
        "Use alternative event handlers for XSS",
    ],
}


class WAFDetector:
    """
    Detect and fingerprint Web Application Firewalls.

    Analyzes HTTP responses to identify WAF presence and type.
    """

    def __init__(self):
        """Initialize the WAF detector."""
        self._signatures = WAF_SIGNATURES
        self._last_detection: WAFDetectionResult | None = None

    def detect(
        self,
        status_code: int,
        headers: dict[str, str],
        body: str,
        cookies: dict[str, str] | None = None,
    ) -> WAFDetectionResult:
        """
        Detect WAF from HTTP response.

        Args:
            status_code: HTTP response status code
            headers: Response headers (case-insensitive)
            body: Response body
            cookies: Response cookies

        Returns:
            WAFDetectionResult with detection details
        """
        # Normalize headers to lowercase
        headers_lower = {k.lower(): v for k, v in headers.items()}
        cookies = cookies or {}

        best_match: tuple[WAFSignature, float, list[str]] | None = None

        for signature in self._signatures:
            confidence = 0.0
            evidence = []

            # Check headers
            for header_name, pattern in signature.headers.items():
                header_value = headers_lower.get(header_name.lower(), "")
                if self._matches(header_value, pattern):
                    confidence += 0.2
                    evidence.append(f"Header: {header_name}")

            # Check Server header specifically
            if signature.server_header:
                server_value = headers_lower.get("server", "")
                if self._matches(server_value, signature.server_header):
                    confidence += 0.15
                    evidence.append(f"Server: {server_value}")

            # Check cookies
            for cookie_name, pattern in signature.cookies.items():
                for actual_cookie_name, cookie_value in cookies.items():
                    if cookie_name.lower() in actual_cookie_name.lower():
                        if self._matches(cookie_value, pattern):
                            confidence += 0.15
                            evidence.append(f"Cookie: {actual_cookie_name}")

            # Check body patterns
            for pattern in signature.body_patterns:
                if self._matches(body, pattern):
                    confidence += 0.2
                    match_str = pattern.pattern if isinstance(pattern, re.Pattern) else pattern
                    evidence.append(f"Body pattern: {match_str[:30]}")

            # Check status code
            if status_code in signature.status_codes:
                confidence += 0.1
                evidence.append(f"Status code: {status_code}")

            # Apply confidence boost
            confidence += signature.confidence_boost

            # Normalize confidence
            confidence = min(1.0, confidence)

            # Update best match
            if confidence > 0 and (best_match is None or confidence > best_match[1]):
                best_match = (signature, confidence, evidence)

        if best_match and best_match[1] >= 0.3:  # Minimum confidence threshold
            signature, confidence, evidence = best_match
            bypass_tips = WAF_BYPASS_TIPS.get(signature.waf_type, [])

            result = WAFDetectionResult(
                detected=True,
                waf_type=signature.waf_type,
                waf_name=signature.name,
                confidence=confidence,
                evidence=evidence,
                bypass_recommendations=bypass_tips,
            )
        else:
            result = WAFDetectionResult(
                detected=False,
                waf_type=WAFType.NONE,
                waf_name="None detected",
                confidence=0.0,
                evidence=[],
            )

        self._last_detection = result
        logger.info(f"WAF detection: {result.waf_name} (confidence: {result.confidence:.2f})")

        return result

    def detect_from_blocked_response(
        self,
        response: dict[str, Any],
    ) -> WAFDetectionResult:
        """
        Detect WAF from a response that blocked a payload.

        Args:
            response: Dict with status_code, headers, body, cookies

        Returns:
            WAFDetectionResult
        """
        return self.detect(
            status_code=response.get("status_code", 403),
            headers=response.get("headers", {}),
            body=response.get("body", ""),
            cookies=response.get("cookies", {}),
        )

    def get_last_detection(self) -> WAFDetectionResult | None:
        """Get the most recent detection result."""
        return self._last_detection

    def _matches(self, value: str, pattern: str | re.Pattern) -> bool:
        """Check if value matches pattern."""
        if not value:
            return False

        if isinstance(pattern, re.Pattern):
            return bool(pattern.search(value))
        else:
            return pattern.lower() in value.lower()


def detect_waf(
    status_code: int,
    headers: dict[str, str],
    body: str,
    cookies: dict[str, str] | None = None,
) -> WAFDetectionResult:
    """Convenience function to detect WAF."""
    detector = WAFDetector()
    return detector.detect(status_code, headers, body, cookies)


__all__ = [
    "WAFType",
    "WAFSignature",
    "WAFDetectionResult",
    "WAF_SIGNATURES",
    "WAF_BYPASS_TIPS",
    "WAFDetector",
    "detect_waf",
]
