"""Classify detected drift as unauthorized, intentional, or emergency.

Uses heuristics based on:
- Time of change (business hours vs off-hours)
- Change patterns (gradual vs sudden)
- Attribute type (security vs config)
- Historical baselines
- Git commit correlation
"""

from __future__ import annotations

import time
from dataclasses import dataclass, field
from typing import Any, Optional

from ..store import DriftClassification, DriftSeverity, DriftStore
from .differ import AttributeDiff, ResourceDiff


@dataclass
class ClassificationResult:
    """Result of classifying a drift."""
    classification: DriftClassification
    confidence: float  # 0.0 to 1.0
    reasons: list[str] = field(default_factory=list)
    risk_score: float = 0.0  # 0.0 to 10.0
    recommended_action: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "classification": self.classification.value,
            "confidence": round(self.confidence, 2),
            "reasons": self.reasons,
            "risk_score": round(self.risk_score, 1),
            "recommended_action": self.recommended_action,
        }


# Patterns that suggest emergency changes
_EMERGENCY_PATTERNS: set[str] = {
    "firewall", "security_group", "network_acl", "waf",
    "block", "deny", "restrict", "disable",
}

# Patterns that suggest intentional changes (scaling, maintenance)
_INTENTIONAL_PATTERNS: set[str] = {
    "instance_type", "machine_type", "sku", "tier",
    "capacity", "replicas", "count", "size",
    "tags", "labels", "description",
}

# High-risk attributes that are rarely changed intentionally without IaC
_HIGH_RISK_ATTRIBUTES: set[str] = {
    "public_access", "public_ip", "publicly_accessible",
    "encryption", "kms_key", "ssl_enforcement",
    "iam_policy", "role_policy", "assume_role_policy",
    "security_group_ids", "network_acls",
    "logging_enabled", "monitoring",
    "deletion_protection", "termination_protection",
    "backup_retention", "multi_az",
}


class DriftClassifier:
    """Classify drift based on heuristics and historical patterns.

    Classification categories:
    - UNAUTHORIZED: Unexpected changes, likely manual, potentially malicious
    - INTENTIONAL: Planned changes made outside IaC (e.g., scaling, tagging)
    - EMERGENCY: Urgent security/incident response changes
    - UNKNOWN: Cannot determine with confidence
    """

    def __init__(self, store: Optional[DriftStore] = None) -> None:
        self.store = store

    def classify(
        self,
        resource_diff: ResourceDiff,
        context: Optional[dict[str, Any]] = None,
    ) -> ClassificationResult:
        """Classify a single resource diff.

        Args:
            resource_diff: The detected resource drift.
            context: Optional context (e.g., time of change, git correlation).

        Returns:
            ClassificationResult with classification and reasoning.
        """
        context = context or {}
        signals: list[tuple[DriftClassification, float, str]] = []

        # Signal 1: Deleted resources in IaC but not cloud
        if resource_diff.diff_type == "deleted":
            signals.append((
                DriftClassification.UNAUTHORIZED,
                0.7,
                "Resource deleted from cloud but exists in IaC state",
            ))

        # Signal 2: Unmanaged resources
        if resource_diff.diff_type == "unmanaged":
            signals.append((
                DriftClassification.UNAUTHORIZED,
                0.5,
                "Resource exists in cloud but not in IaC — possibly manually created",
            ))

        # Analyze attribute-level signals
        for attr_diff in resource_diff.attribute_diffs:
            signals.extend(self._classify_attribute(attr_diff))

        # Signal 3: Time-based heuristics
        change_time = context.get("change_time", time.time())
        hour = time.gmtime(change_time).tm_hour
        weekday = time.gmtime(change_time).tm_wday
        if hour < 6 or hour > 22 or weekday >= 5:
            signals.append((
                DriftClassification.EMERGENCY,
                0.3,
                f"Change detected during off-hours (hour={hour}, weekday={weekday})",
            ))

        # Signal 4: Git correlation
        if context.get("has_recent_commit"):
            signals.append((
                DriftClassification.INTENTIONAL,
                0.6,
                "Recent git commit found correlating with this change",
            ))

        # Signal 5: Historical baseline
        if self.store:
            baseline = self.store.get_baseline(
                resource_diff.resource_id,
                resource_diff.attribute_diffs[0].attribute if resource_diff.attribute_diffs else "_",
            )
            if baseline:
                signals.append((
                    DriftClassification.INTENTIONAL,
                    0.8,
                    f"Change matches accepted baseline: {baseline.get('reason', '')}",
                ))

        # Signal 6: Number of simultaneous changes
        if len(resource_diff.attribute_diffs) > 10:
            signals.append((
                DriftClassification.UNAUTHORIZED,
                0.4,
                f"Unusually large number of attribute changes ({len(resource_diff.attribute_diffs)})",
            ))

        # Aggregate signals
        return self._aggregate_signals(signals, resource_diff)

    def _classify_attribute(
        self,
        attr_diff: AttributeDiff,
    ) -> list[tuple[DriftClassification, float, str]]:
        """Generate classification signals from a single attribute diff."""
        signals: list[tuple[DriftClassification, float, str]] = []
        attr_lower = attr_diff.attribute.lower()

        # Emergency patterns
        for pattern in _EMERGENCY_PATTERNS:
            if pattern in attr_lower:
                signals.append((
                    DriftClassification.EMERGENCY,
                    0.5,
                    f"Security-related attribute changed: {attr_diff.attribute}",
                ))
                break

        # Intentional patterns
        for pattern in _INTENTIONAL_PATTERNS:
            if pattern in attr_lower:
                signals.append((
                    DriftClassification.INTENTIONAL,
                    0.4,
                    f"Common operational attribute changed: {attr_diff.attribute}",
                ))
                break

        # High-risk attributes
        for pattern in _HIGH_RISK_ATTRIBUTES:
            if pattern in attr_lower:
                signals.append((
                    DriftClassification.UNAUTHORIZED,
                    0.6,
                    f"High-risk attribute changed: {attr_diff.attribute}",
                ))
                break

        # Value-based signals
        if attr_diff.actual is not None and attr_diff.desired is not None:
            actual_str = str(attr_diff.actual).lower()
            desired_str = str(attr_diff.desired).lower()
            # Public access enabled
            if "public" in attr_lower and actual_str in ("true", "enabled", "yes"):
                signals.append((
                    DriftClassification.UNAUTHORIZED,
                    0.8,
                    f"Public access enabled on {attr_diff.attribute}",
                ))
            # Encryption disabled
            if "encrypt" in attr_lower and actual_str in ("false", "disabled", "none"):
                signals.append((
                    DriftClassification.UNAUTHORIZED,
                    0.9,
                    f"Encryption disabled on {attr_diff.attribute}",
                ))
            # Open CIDR
            if "cidr" in attr_lower and "0.0.0.0/0" in actual_str:
                signals.append((
                    DriftClassification.EMERGENCY,
                    0.7,
                    f"Wide-open CIDR (0.0.0.0/0) on {attr_diff.attribute}",
                ))

        return signals

    def _aggregate_signals(
        self,
        signals: list[tuple[DriftClassification, float, str]],
        resource_diff: ResourceDiff,
    ) -> ClassificationResult:
        """Aggregate classification signals into a final result."""
        if not signals:
            return ClassificationResult(
                classification=DriftClassification.UNKNOWN,
                confidence=0.0,
                reasons=["No classification signals available"],
                risk_score=2.0,
                recommended_action="Manual review required",
            )

        # Weighted voting
        scores: dict[DriftClassification, float] = {
            DriftClassification.UNAUTHORIZED: 0.0,
            DriftClassification.INTENTIONAL: 0.0,
            DriftClassification.EMERGENCY: 0.0,
            DriftClassification.UNKNOWN: 0.0,
        }
        reasons: list[str] = []
        for classification, weight, reason in signals:
            scores[classification] += weight
            reasons.append(f"[{classification.value}:{weight:.1f}] {reason}")

        # Pick highest score
        best = max(scores, key=scores.get)  # type: ignore[arg-type]
        total_weight = sum(scores.values())
        confidence = scores[best] / total_weight if total_weight > 0 else 0.0

        # Risk score (0-10)
        risk_base = {
            DriftClassification.UNAUTHORIZED: 7.0,
            DriftClassification.EMERGENCY: 8.0,
            DriftClassification.INTENTIONAL: 2.0,
            DriftClassification.UNKNOWN: 5.0,
        }
        severity_multiplier = {
            DriftSeverity.CRITICAL: 1.4,
            DriftSeverity.HIGH: 1.2,
            DriftSeverity.MEDIUM: 1.0,
            DriftSeverity.LOW: 0.7,
            DriftSeverity.INFO: 0.5,
        }
        risk = risk_base[best] * severity_multiplier.get(resource_diff.severity, 1.0)
        risk = min(10.0, risk)

        # Recommended action
        actions = {
            DriftClassification.UNAUTHORIZED: "Investigate immediately. Revert via IaC if no justification found.",
            DriftClassification.EMERGENCY: "Verify with on-call team. Update IaC to match if change is valid.",
            DriftClassification.INTENTIONAL: "Update IaC to reflect the intentional change.",
            DriftClassification.UNKNOWN: "Manual review required to determine change origin.",
        }

        return ClassificationResult(
            classification=best,
            confidence=round(confidence, 2),
            reasons=reasons,
            risk_score=round(risk, 1),
            recommended_action=actions[best],
        )

    def classify_batch(
        self,
        diffs: list[ResourceDiff],
        context: Optional[dict[str, Any]] = None,
    ) -> list[ClassificationResult]:
        """Classify multiple drifts."""
        return [self.classify(d, context) for d in diffs]
