import enum
import re
from typing import Optional


class ChallengeType(str, enum.Enum):
    NONE = "none"
    CF_IUAM = "cf_iuam"
    CF_CAPTCHA = "cf_captcha"
    CF_TURNSTILE = "cf_turnstile"
    CF_MANAGED = "cf_managed"
    CF_BLOCK_1020 = "cf_block_1020"
    DATADOME = "datadome"
    PERIMETERX = "perimeterx"
    KASADA = "kasada"
    AKAMAI = "akamai"
    IMPERVA = "imperva"
    SHAPE = "shape"
    JS_REQUIRED = "js_required"
    RATE_LIMITED = "rate_limited"
    REDIRECT_LOOP = "redirect_loop"
    UNKNOWN = "unknown"


class WAFVendor(str, enum.Enum):
    NONE = "none"
    CLOUDFLARE = "cloudflare"
    AKAMAI = "akamai"
    IMPERVA = "imperva"
    DATADOME = "datadome"
    PERIMETERX = "perimeterx"
    KASADA = "kasada"
    SHAPE = "shape"
    UNKNOWN = "unknown"


class IntelResult:
    def __init__(self):
        self.challenge_type: ChallengeType = ChallengeType.NONE
        self.waf_vendor: WAFVendor = WAFVendor.NONE
        self.risk_score: int = 0
        self.retry_recommended: bool = False
        self.rotate_session: bool = False
        self.switch_transport: bool = False
        self.details: dict = {}

    def to_meta(self) -> dict:
        return {
            "challenge_type": self.challenge_type.value,
            "waf_vendor": self.waf_vendor.value,
            "risk_score": self.risk_score,
            "retry_recommended": self.retry_recommended,
            "rotate_session": self.rotate_session,
            "switch_transport": self.switch_transport,
            "details": self.details,
        }


_CF_SERVER_PATTERN = re.compile(r'^cloudflare', re.I)
_CF_IUAM_PATTERN = re.compile(r'/cdn-cgi/images/trace/jsch/', re.M | re.S)
_CF_CHALLENGE_FORM = re.compile(r'<form .*?="challenge-form"', re.M | re.S)
_CF_TURNSTILE = re.compile(r'cf-turnstile|challenges\.cloudflare\.com/turnstile', re.M | re.S | re.I)
_CF_MANAGED = re.compile(r'orchestrate/(captcha|managed)/v1', re.M | re.S)
_CF_JSCH_V1 = re.compile(r'cpo\.src\s*=\s*[\'\"]/cdn-cgi/challenge-platform/\S+orchestrate/jsch/v1', re.M | re.S)
_CF_1020 = re.compile(r'<span class="cf-error-code">1020</span>', re.M | re.DOTALL)
_DATADOME = re.compile(r'datadome\.co|ddg-set-cookie|dd_sitekey', re.I)
_PERIMETERX = re.compile(r'_pxdk|PerimeterX|px\.js|px-captcha|__utmz=px', re.I)
_KASADA = re.compile(r'kasada|kpsdk|k\.js', re.I)
_AKAMAI = re.compile(r'_abck=|ak_bmsc=|akamai-bm-telemetry|sensor_data', re.I)
_IMPERVA = re.compile(r'incap_ses_|visid_incap_|incapsula', re.I)
_SHAPE = re.compile(r'shape\.io|x-shape-|f5\.com/shape', re.I)
_JS_REQUIRED = re.compile(r'<noscript|enable javascript|javascript is required|javascript must be enabled', re.I)
_RETRY_AFTER = re.compile(r'^Retry-After$', re.I)


class SessionIntelligence:
    def __init__(self):
        self._seen_urls: list = []

    def _detect_waf(self, resp) -> WAFVendor:
        server = resp.headers.get('Server', '')
        headers_str = str(resp.headers)
        body = ''
        try:
            body = resp.text[:8192]
        except Exception:
            pass

        if _CF_SERVER_PATTERN.match(server):
            return WAFVendor.CLOUDFLARE
        if _DATADOME.search(headers_str) or _DATADOME.search(body):
            return WAFVendor.DATADOME
        if _PERIMETERX.search(headers_str) or _PERIMETERX.search(body):
            return WAFVendor.PERIMETERX
        if _KASADA.search(headers_str) or _KASADA.search(body):
            return WAFVendor.KASADA
        if _AKAMAI.search(headers_str) or _AKAMAI.search(body):
            return WAFVendor.AKAMAI
        if _IMPERVA.search(headers_str) or _IMPERVA.search(body):
            return WAFVendor.IMPERVA
        if _SHAPE.search(headers_str) or _SHAPE.search(body):
            return WAFVendor.SHAPE
        return WAFVendor.NONE

    def _detect_challenge(self, resp, waf: WAFVendor) -> ChallengeType:
        status = resp.status_code
        body = ''
        try:
            body = resp.text[:16384]
        except Exception:
            pass

        if waf == WAFVendor.CLOUDFLARE:
            if _CF_1020.search(body):
                return ChallengeType.CF_BLOCK_1020
            if _CF_TURNSTILE.search(body):
                return ChallengeType.CF_TURNSTILE
            if _CF_MANAGED.search(body):
                return ChallengeType.CF_MANAGED
            if _CF_JSCH_V1.search(body):
                return ChallengeType.CF_IUAM
            if status in (429, 503) and _CF_IUAM_PATTERN.search(body) and _CF_CHALLENGE_FORM.search(body):
                return ChallengeType.CF_IUAM
            if status == 403 and _CF_CHALLENGE_FORM.search(body):
                return ChallengeType.CF_CAPTCHA

        if waf == WAFVendor.DATADOME and status in (403, 429):
            return ChallengeType.DATADOME
        if waf == WAFVendor.PERIMETERX and status in (403, 429):
            return ChallengeType.PERIMETERX
        if waf == WAFVendor.KASADA and status in (429, 403):
            return ChallengeType.KASADA
        if waf == WAFVendor.AKAMAI and status in (403, 429):
            return ChallengeType.AKAMAI

        if status == 429 or resp.headers.get('Retry-After'):
            return ChallengeType.RATE_LIMITED

        if _JS_REQUIRED.search(body):
            return ChallengeType.JS_REQUIRED

        return ChallengeType.NONE

    def _compute_risk_score(self, status: int, challenge: ChallengeType, waf: WAFVendor) -> int:
        score = 0
        risk_map = {
            ChallengeType.CF_BLOCK_1020: 95,
            ChallengeType.CF_TURNSTILE: 85,
            ChallengeType.CF_MANAGED: 80,
            ChallengeType.CF_CAPTCHA: 75,
            ChallengeType.CF_IUAM: 65,
            ChallengeType.KASADA: 90,
            ChallengeType.PERIMETERX: 80,
            ChallengeType.DATADOME: 75,
            ChallengeType.AKAMAI: 70,
            ChallengeType.IMPERVA: 65,
            ChallengeType.SHAPE: 80,
            ChallengeType.RATE_LIMITED: 40,
            ChallengeType.JS_REQUIRED: 50,
            ChallengeType.NONE: 0,
        }
        score = risk_map.get(challenge, 0)
        if status in (403, 429, 503):
            score = min(score + 10, 100)
        if waf != WAFVendor.NONE and challenge == ChallengeType.NONE:
            score = max(score, 20)
        return score

    def is_redirect_loop(self, resp) -> bool:
        if not hasattr(resp, 'history'):
            return False
        seen = set()
        for r in resp.history:
            if r.url in seen:
                return True
            seen.add(r.url)
        return len(resp.history) > 10

    def analyze(self, resp) -> IntelResult:
        result = IntelResult()

        if self.is_redirect_loop(resp):
            result.challenge_type = ChallengeType.REDIRECT_LOOP
            result.risk_score = 60
            result.retry_recommended = False
            return result

        result.waf_vendor = self._detect_waf(resp)
        result.challenge_type = self._detect_challenge(resp, result.waf_vendor)
        result.risk_score = self._compute_risk_score(resp.status_code, result.challenge_type, result.waf_vendor)

        resolvable = {
            ChallengeType.CF_IUAM, ChallengeType.RATE_LIMITED,
            ChallengeType.DATADOME, ChallengeType.AKAMAI,
        }
        escalate = {
            ChallengeType.CF_TURNSTILE, ChallengeType.CF_MANAGED,
            ChallengeType.CF_CAPTCHA, ChallengeType.KASADA,
            ChallengeType.PERIMETERX, ChallengeType.SHAPE,
        }

        if result.challenge_type in resolvable:
            result.retry_recommended = True
            result.rotate_session = True
        elif result.challenge_type in escalate:
            result.retry_recommended = True
            result.rotate_session = True
            result.switch_transport = True

        result.details["retry_after"] = resp.headers.get("Retry-After")
        result.details["cf_ray"] = resp.headers.get("CF-RAY")
        result.details["status_code"] = resp.status_code

        return result
