"""
Composite threat scorer for scandar-guard.

Weighs all detection signals — pattern matches, encoding evasion,
multi-turn fragments, behavioral anomalies, profile deviations,
and pre-deployment scan results — into a single 0-100 threat score.

This is the intelligence layer that transforms individual findings
into an actionable risk assessment.
"""
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Optional

from .findings import RuntimeFinding


@dataclass
class ThreatScore:
    """Composite threat assessment for a single LLM call."""
    score: int  # 0-100 (higher = more dangerous)
    classification: str  # "safe", "elevated", "high", "critical"
    signals: dict = field(default_factory=dict)
    findings_count: int = 0
    should_block: bool = False

    @property
    def is_safe(self) -> bool:
        return self.classification == "safe"


# Severity weights for different finding types
_WEIGHTS = {
    # Pattern-matched findings
    "pattern_critical": 25,
    "pattern_high": 12,
    "pattern_medium": 5,
    "pattern_low": 2,

    # Encoding evasion is a STRONG adversarial signal
    "encoding_evasion": 30,

    # Multi-turn splits indicate sophisticated attack
    "multi_turn": 25,

    # Social engineering escalation
    "social_engineering": 15,

    # Behavioral anomalies
    "anomaly_critical": 20,
    "anomaly_high": 10,
    "anomaly_medium": 5,

    # Profile deviation
    "profile_deviation": 12,

    # Pre-deployment context
    "low_scan_score_penalty": 10,
}

# Classification thresholds
_THRESHOLDS = {
    "critical": 50,
    "high": 25,
    "elevated": 10,
}


class ThreatScorer:
    """
    Produces a composite threat score from all detection signals.

    The score is not a simple sum — signals interact:
    - Encoding evasion + pattern match = much higher score (adversarial intent confirmed)
    - Multi-turn + escalation = compounded (sophisticated multi-step attack)
    - Profile deviation alone = moderate (could be legitimate new feature)
    - Profile deviation + pattern match = high (new tool AND injection = likely compromise)
    """

    def score(
        self,
        pattern_findings: list[RuntimeFinding],
        encoding_findings: list[RuntimeFinding],
        multi_turn_findings: list[RuntimeFinding],
        anomaly_findings: list[RuntimeFinding],
        profile_findings: list[RuntimeFinding],
        scan_trust_score: Optional[int] = None,
        block_on: Optional[list[str]] = None,
    ) -> ThreatScore:
        raw = 0
        signals: dict = {}
        total_findings = 0

        # ── Pattern findings ──────────────────────────────────────────
        for f in pattern_findings:
            total_findings += 1
            key = f"pattern_{f.severity}"
            raw += _WEIGHTS.get(key, 3)
        if pattern_findings:
            signals["patterns"] = {
                "count": len(pattern_findings),
                "severities": _count_severities(pattern_findings),
            }

        # ── Encoding evasion ──────────────────────────────────────────
        for f in encoding_findings:
            total_findings += 1
            raw += _WEIGHTS["encoding_evasion"]
        if encoding_findings:
            signals["encoding_evasion"] = {
                "count": len(encoding_findings),
                "encodings": list({f.context.get("encoding", "unknown") for f in encoding_findings}),
            }

        # ── Interaction bonus: encoding + pattern = confirmed adversarial ──
        if encoding_findings and pattern_findings:
            raw += 15  # Compound signal
            signals["compound_encoding_pattern"] = True

        # ── Multi-turn injection ──────────────────────────────────────
        for f in multi_turn_findings:
            total_findings += 1
            if f.category == "SOCIAL_ENGINEERING_ESCALATION":
                raw += _WEIGHTS["social_engineering"]
            else:
                raw += _WEIGHTS["multi_turn"]
        if multi_turn_findings:
            signals["multi_turn"] = {"count": len(multi_turn_findings)}

        # ── Interaction bonus: multi-turn + escalation = sophisticated attack ──
        has_split = any(f.category == "SPLIT_INJECTION" for f in multi_turn_findings)
        has_escalation = any(f.category == "SOCIAL_ENGINEERING_ESCALATION" for f in multi_turn_findings)
        if has_split and has_escalation:
            raw += 10
            signals["compound_split_escalation"] = True

        # ── Behavioral anomalies ──────────────────────────────────────
        for f in anomaly_findings:
            total_findings += 1
            key = f"anomaly_{f.severity}"
            raw += _WEIGHTS.get(key, 3)
        if anomaly_findings:
            signals["anomalies"] = {"count": len(anomaly_findings)}

        # ── Profile deviation ─────────────────────────────────────────
        for f in profile_findings:
            total_findings += 1
            raw += _WEIGHTS["profile_deviation"]
        if profile_findings:
            signals["profile_deviation"] = {"count": len(profile_findings)}

        # ── Interaction bonus: profile deviation + pattern match = likely compromise ──
        if profile_findings and pattern_findings:
            raw += 15
            signals["compound_profile_pattern"] = True

        # ── Pre-deployment context ────────────────────────────────────
        if scan_trust_score is not None and scan_trust_score < 70:
            raw += _WEIGHTS["low_scan_score_penalty"]
            signals["pre_deployment_risk"] = {
                "scan_trust_score": scan_trust_score,
                "penalty_applied": True,
            }

        # ── Cap and classify ──────────────────────────────────────────
        score = min(100, max(0, raw))

        if score >= _THRESHOLDS["critical"]:
            classification = "critical"
        elif score >= _THRESHOLDS["high"]:
            classification = "high"
        elif score >= _THRESHOLDS["elevated"]:
            classification = "elevated"
        else:
            classification = "safe"

        # Determine if this should be blocked
        should_block = False
        if block_on:
            all_findings = pattern_findings + encoding_findings + multi_turn_findings + anomaly_findings + profile_findings
            should_block = any(f.severity in block_on for f in all_findings)

        return ThreatScore(
            score=score,
            classification=classification,
            signals=signals,
            findings_count=total_findings,
            should_block=should_block,
        )


def _count_severities(findings: list[RuntimeFinding]) -> dict[str, int]:
    counts: dict[str, int] = {}
    for f in findings:
        counts[f.severity] = counts.get(f.severity, 0) + 1
    return counts
