#!/usr/bin/env python3
from typing import Dict, Any, Optional, Iterable, Protocol, Sequence

from ..utils.logger import Logger
from .config_manager import ConfigManager

logger = Logger("core.exploitability_calculator")


class ReflectionPointProtocol(Protocol):
    reflection_type: Any
    accuracy: float
    completeness: float
    special_chars_preserved: Optional[Iterable[str]]
    filters_detected: Optional[Sequence[str]]


class ReflectionResultProtocol(Protocol):
    reflection_points: Sequence[ReflectionPointProtocol]


class ExploitabilityCalculator:
    """Calculates the exploitability of a potential XSS vulnerability"""

    def __init__(self, config: Optional[ConfigManager] = None):
        self.config = config or ConfigManager()
        self.reflection_scores: Dict[str, float] = self.config.get(
            "scoring.reflection_scores",
            {
                "exact": 1.0,
                "partial": 0.8, # Increased from 0.7
                "modified": 0.6,
                "encoded": 0.5,
                "obfuscated": 0.4,
                "filtered": 0.2, # Decreased from 0.3
                "not_reflected": 0.0,
            },
        )

    def _safe_reflection_key(self, value: Any) -> str:
        if value is None:
            return ""
        if hasattr(value, "value"):
            value = value.value
        return str(value).lower()

    def _clamp01(self, v: float) -> float:
        if v != v:  # NaN guard
            return 0.0
        return max(0.0, min(1.0, float(v)))

    def calculate_exploitability_score(self, reflection_result: Optional[ReflectionResultProtocol]) -> float:
        """
        Calculate exploitability score (0-1.0) based on reflection quality.
        Expects reflection_result.reflection_points to be iterable of objects complying with ReflectionPointProtocol.
        """
        if not reflection_result or not getattr(reflection_result, "reflection_points", None):
            return 0.0

        best_score = 0.0
        total_filters = set()

        critical_chars = ("<", ">", '"', "'")
        for point in reflection_result.reflection_points:
            key = self._safe_reflection_key(getattr(point, "reflection_type", None))
            base_score = float(self.reflection_scores.get(key, 0.1))

            accuracy = self._clamp01(getattr(point, "accuracy", 0.0))
            completeness = self._clamp01(getattr(point, "completeness", 0.0))
            quality = (accuracy + completeness) / 2.0

            point_score = base_score * quality

            preserved = tuple(point.special_chars_preserved or ())
            preserved_critical = sum(1 for c in critical_chars if c in preserved)
            if preserved_critical > 0:
                point_score += (preserved_critical / len(critical_chars)) * 0.2 # Bonus

            # accumulate filters
            filters = getattr(point, "filters_detected", None) or ()
            for f in filters:
                total_filters.add(str(f))

            best_score = max(best_score, point_score)

        # apply penalty based on total unique filters detected across all points
        if total_filters:
            penalty = len(total_filters) * 0.2 # Increased penalty
            best_score -= penalty

        final_score = self._clamp01(best_score)
        
        # Perfect reflection → guaranteed full exploitability
        try:
            first_point = reflection_result.reflection_points[0]
            ref_type = getattr(first_point.reflection_type, "value", str(first_point.reflection_type)).lower()
            ctx = getattr(first_point, "context", "").lower()
            quality = (getattr(first_point, "accuracy", 0) + getattr(first_point, "completeness", 0)) / 2.0
            if ref_type == "exact" and "html" in ctx and quality >= 0.9:
                final_score = 1.0
        except Exception:
            pass

        # --- global calibration multiplier ---
        final_score = min(1.0, final_score * 1.25)
        # -------------------------------------

        logger.debug(f"Exploitability score: {final_score:.2f}")
        return final_score