"""Compare desired state (IaC) against actual state (cloud) to detect drift."""

from __future__ import annotations

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

from ..store import (
    DriftCategory,
    DriftRecord,
    DriftSeverity,
    DriftStore,
    ScanRecord,
)


@dataclass
class AttributeDiff:
    """A single attribute difference between desired and actual state."""
    attribute: str
    desired: Any
    actual: Any
    diff_type: str  # "changed", "added", "removed"

    @property
    def is_security_relevant(self) -> bool:
        """Check if this attribute change is security-relevant."""
        security_keywords = {
            "public", "ingress", "egress", "firewall", "security", "acl",
            "encryption", "ssl", "tls", "kms", "iam", "policy", "role",
            "permission", "access", "auth", "logging", "network_rule",
            "ip_restriction", "allowed", "denied", "source_ranges",
        }
        attr_lower = self.attribute.lower()
        return any(kw in attr_lower for kw in security_keywords)

    @property
    def is_cost_relevant(self) -> bool:
        """Check if this attribute change is cost-relevant."""
        cost_keywords = {
            "instance_type", "machine_type", "sku", "tier", "size",
            "capacity", "replicas", "count", "storage", "iops",
        }
        attr_lower = self.attribute.lower()
        return any(kw in attr_lower for kw in cost_keywords)


@dataclass
class ResourceDiff:
    """Drift summary for a single resource."""
    resource_address: str
    resource_type: str
    resource_id: str
    resource_name: str
    provider: str
    diff_type: str  # "modified", "deleted", "unmanaged"
    attribute_diffs: list[AttributeDiff] = field(default_factory=list)

    @property
    def has_security_drift(self) -> bool:
        return any(d.is_security_relevant for d in self.attribute_diffs)

    @property
    def has_cost_drift(self) -> bool:
        return any(d.is_cost_relevant for d in self.attribute_diffs)

    @property
    def severity(self) -> DriftSeverity:
        """Calculate severity based on drift attributes."""
        if self.diff_type == "deleted":
            return DriftSeverity.HIGH
        if self.diff_type == "unmanaged":
            return DriftSeverity.MEDIUM
        if self.has_security_drift:
            return DriftSeverity.CRITICAL
        if self.has_cost_drift:
            return DriftSeverity.HIGH
        if len(self.attribute_diffs) > 5:
            return DriftSeverity.HIGH
        if len(self.attribute_diffs) > 2:
            return DriftSeverity.MEDIUM
        return DriftSeverity.LOW

    @property
    def category(self) -> DriftCategory:
        """Determine primary category of drift."""
        if self.has_security_drift:
            return DriftCategory.SECURITY
        if self.has_cost_drift:
            return DriftCategory.COST
        if self.diff_type in ("deleted", "unmanaged"):
            return DriftCategory.STRUCTURAL
        return DriftCategory.CONFIGURATION


@dataclass
class DriftResult:
    """Result of a drift scan across all resources."""
    scan_id: str
    provider: str
    iac_source: str
    state_path: str
    total_resources: int
    resource_diffs: list[ResourceDiff] = field(default_factory=list)
    started_at: float = field(default_factory=time.time)
    completed_at: float = 0.0

    @property
    def drifted_count(self) -> int:
        return sum(1 for d in self.resource_diffs if d.diff_type == "modified")

    @property
    def deleted_count(self) -> int:
        return sum(1 for d in self.resource_diffs if d.diff_type == "deleted")

    @property
    def unmanaged_count(self) -> int:
        return sum(1 for d in self.resource_diffs if d.diff_type == "unmanaged")

    @property
    def has_critical(self) -> bool:
        return any(d.severity == DriftSeverity.CRITICAL for d in self.resource_diffs)

    @property
    def drift_score(self) -> float:
        """Calculate a 0-100 drift score (100 = no drift, 0 = everything drifted)."""
        if self.total_resources == 0:
            return 100.0
        weights = {
            DriftSeverity.CRITICAL: 10,
            DriftSeverity.HIGH: 5,
            DriftSeverity.MEDIUM: 2,
            DriftSeverity.LOW: 1,
            DriftSeverity.INFO: 0.5,
        }
        total_weight = sum(weights.get(d.severity, 1) for d in self.resource_diffs)
        max_weight = self.total_resources * 10  # worst case all critical
        score = max(0.0, 100.0 - (total_weight / max_weight * 100)) if max_weight > 0 else 100.0
        return round(score, 1)

    def to_summary(self) -> dict[str, Any]:
        """Generate a summary dict."""
        return {
            "scan_id": self.scan_id,
            "provider": self.provider,
            "iac_source": self.iac_source,
            "total_resources": self.total_resources,
            "drifted": self.drifted_count,
            "deleted": self.deleted_count,
            "unmanaged": self.unmanaged_count,
            "drift_score": self.drift_score,
            "has_critical": self.has_critical,
            "duration_seconds": round(self.completed_at - self.started_at, 2),
        }


# Attributes to skip during comparison (volatile/computed)
_SKIP_ATTRIBUTES: set[str] = {
    "id", "arn", "self_link", "etag", "fingerprint",
    "creation_date", "created_at", "updated_at",
}


def _normalize_value(value: Any) -> Any:
    """Normalize a value for comparison."""
    if value is None:
        return None
    if isinstance(value, bool):
        return value
    if isinstance(value, (int, float)):
        return value
    if isinstance(value, str):
        return value.strip().lower()
    if isinstance(value, list):
        return sorted([_normalize_value(v) for v in value], key=str)
    if isinstance(value, dict):
        return {k: _normalize_value(v) for k, v in sorted(value.items())}
    return str(value).strip().lower()


def _values_equal(desired: Any, actual: Any) -> bool:
    """Compare two values with normalization."""
    # Handle None/empty equivalence
    if desired is None and actual is None:
        return True
    if desired is None and actual in ("", [], {}, 0, False):
        return True
    if actual is None and desired in ("", [], {}, 0, False):
        return True
    # Sensitive values — only check presence
    if desired == "<sensitive>" or actual == "<sensitive>":
        return True
    return _normalize_value(desired) == _normalize_value(actual)


class DriftDiffer:
    """Compare desired state (from IaC) against actual state (from cloud providers).

    Usage:
        differ = DriftDiffer(store=DriftStore())
        result = differ.compare(
            desired={"res1": {"attr1": "val1"}},
            actual={"res1": {"attr1": "val2"}},
            resource_metadata={"res1": {"type": "azurerm_storage_account", ...}},
            provider="azure",
            iac_source="terraform",
            state_path="terraform.tfstate",
        )
    """

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

    def compare(
        self,
        desired: dict[str, dict[str, Any]],
        actual: dict[str, dict[str, Any]],
        resource_metadata: dict[str, dict[str, Any]],
        provider: str = "unknown",
        iac_source: str = "terraform",
        state_path: str = "",
    ) -> DriftResult:
        """Compare desired vs actual state for all resources.

        Args:
            desired: Map of resource_address -> {attribute: value} from IaC state.
            actual: Map of resource_address -> {attribute: value} from live cloud.
            resource_metadata: Map of resource_address -> {type, id, name, provider}.
            provider: Cloud provider name.
            iac_source: IaC tool name (terraform, pulumi, cloudformation).
            state_path: Path to the IaC state file.

        Returns:
            DriftResult with all detected drifts.
        """
        scan_id = f"scan-{uuid.uuid4().hex[:12]}"
        result = DriftResult(
            scan_id=scan_id,
            provider=provider,
            iac_source=iac_source,
            state_path=state_path,
            total_resources=len(desired),
            started_at=time.time(),
        )

        all_addresses = set(desired.keys()) | set(actual.keys())

        for address in sorted(all_addresses):
            meta = resource_metadata.get(address, {})
            res_type = meta.get("type", "unknown")
            res_id = meta.get("id", address)
            res_name = meta.get("name", address)
            res_provider = meta.get("provider", provider)

            desired_attrs = desired.get(address, {})
            actual_attrs = actual.get(address, {})

            if address in desired and address not in actual:
                # Resource exists in IaC but not in cloud — deleted
                diff = ResourceDiff(
                    resource_address=address,
                    resource_type=res_type,
                    resource_id=res_id,
                    resource_name=res_name,
                    provider=res_provider,
                    diff_type="deleted",
                )
                result.resource_diffs.append(diff)
                continue

            if address not in desired and address in actual:
                # Resource exists in cloud but not in IaC — unmanaged
                diff = ResourceDiff(
                    resource_address=address,
                    resource_type=res_type,
                    resource_id=res_id,
                    resource_name=res_name,
                    provider=res_provider,
                    diff_type="unmanaged",
                )
                result.resource_diffs.append(diff)
                continue

            # Both exist — compare attributes
            attr_diffs = self._compare_attributes(desired_attrs, actual_attrs)
            if attr_diffs:
                diff = ResourceDiff(
                    resource_address=address,
                    resource_type=res_type,
                    resource_id=res_id,
                    resource_name=res_name,
                    provider=res_provider,
                    diff_type="modified",
                    attribute_diffs=attr_diffs,
                )
                result.resource_diffs.append(diff)

        result.completed_at = time.time()

        # Persist to store
        if self.store:
            self._persist_results(result)

        return result

    def _compare_attributes(
        self,
        desired: dict[str, Any],
        actual: dict[str, Any],
    ) -> list[AttributeDiff]:
        """Compare attribute maps and return differences."""
        diffs: list[AttributeDiff] = []
        all_keys = set(desired.keys()) | set(actual.keys())

        for key in sorted(all_keys):
            # Skip volatile attributes
            base_key = key.split(".")[-1]
            if base_key in _SKIP_ATTRIBUTES:
                continue

            desired_val = desired.get(key)
            actual_val = actual.get(key)

            if key in desired and key not in actual:
                diffs.append(AttributeDiff(
                    attribute=key, desired=desired_val, actual=None, diff_type="removed",
                ))
            elif key not in desired and key in actual:
                diffs.append(AttributeDiff(
                    attribute=key, desired=None, actual=actual_val, diff_type="added",
                ))
            elif not _values_equal(desired_val, actual_val):
                diffs.append(AttributeDiff(
                    attribute=key, desired=desired_val, actual=actual_val, diff_type="changed",
                ))

        return diffs

    def _persist_results(self, result: DriftResult) -> None:
        """Save scan results to the store."""
        if not self.store:
            return

        drift_ids: list[str] = []
        for diff in result.resource_diffs:
            for attr_diff in (diff.attribute_diffs or [{"attribute": "_resource", "desired": None, "actual": None}]):
                drift_id = f"drift-{uuid.uuid4().hex[:12]}"
                drift_ids.append(drift_id)
                attr = attr_diff if isinstance(attr_diff, AttributeDiff) else AttributeDiff(
                    attribute="_resource", desired=None, actual=None,
                    diff_type=diff.diff_type,
                )
                record = DriftRecord(
                    id=drift_id,
                    scan_id=result.scan_id,
                    provider=diff.provider,
                    resource_type=diff.resource_type,
                    resource_id=diff.resource_id,
                    resource_name=diff.resource_name,
                    attribute=attr.attribute,
                    desired_value=attr.desired,
                    actual_value=attr.actual,
                    severity=diff.severity,
                    category=diff.category,
                    detected_at=result.completed_at,
                    metadata={
                        "diff_type": diff.diff_type,
                        "resource_address": diff.resource_address,
                    },
                )
                self.store.save_drift(record)

        scan_record = ScanRecord(
            id=result.scan_id,
            provider=result.provider,
            iac_source=result.iac_source,
            state_path=result.state_path,
            total_resources=result.total_resources,
            drifted_resources=result.drifted_count,
            new_resources=result.unmanaged_count,
            deleted_resources=result.deleted_count,
            started_at=result.started_at,
            completed_at=result.completed_at,
            drift_ids=drift_ids,
        )
        self.store.save_scan(scan_record)
