"""SLA Monitor for MCP Security Audit.

Monitors remediation SLA compliance and generates alerts when
findings are approaching or have exceeded their SLA deadlines.

Features:
- Define SLA policies per severity level
- Track SLA status: ON_TRACK, AT_RISK (>75%), BREACHED
- Escalation rules: auto-assign, notify, block deploy
- SLA dashboard with compliance metrics
- Historical SLA performance tracking

CLI: mcpaudit sla --status | --policy | --breaches

Zero external dependencies. Part of mcpaudit v2.1.0.
"""

from __future__ import annotations

import json
from dataclasses import dataclass, field
from datetime import datetime, timedelta, timezone
from enum import Enum, unique
from typing import Any, Optional


# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------

@unique
class SLAStatus(Enum):
    """SLA compliance status."""
    ON_TRACK = "ON_TRACK"       # < 50% of SLA elapsed
    AT_RISK = "AT_RISK"         # 50-99% of SLA elapsed
    BREACHED = "BREACHED"       # > 100% of SLA elapsed
    RESOLVED = "RESOLVED"       # Fixed within SLA
    WAIVED = "WAIVED"           # SLA explicitly waived


@unique
class EscalationLevel(Enum):
    """Escalation levels for SLA breaches."""
    NONE = "NONE"
    NOTIFY = "NOTIFY"           # send notification
    ASSIGN = "ASSIGN"           # auto-assign to team lead
    BLOCK_DEPLOY = "BLOCK_DEPLOY"  # block deployments


# ---------------------------------------------------------------------------
# Data models
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class SLAPolicy:
    """SLA policy for a severity level."""
    severity: str
    max_hours: int
    warning_threshold: float = 0.75  # warn at 75% elapsed
    escalation: EscalationLevel = EscalationLevel.NOTIFY

    def to_dict(self) -> dict[str, Any]:
        return {
            "severity": self.severity,
            "max_hours": self.max_hours,
            "warning_threshold": self.warning_threshold,
            "escalation": self.escalation.value,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> SLAPolicy:
        return cls(
            severity=data["severity"],
            max_hours=data["max_hours"],
            warning_threshold=data.get("warning_threshold", 0.75),
            escalation=EscalationLevel(data.get("escalation", "NOTIFY")),
        )


@dataclass
class SLAEntry:
    """Tracked SLA for a specific finding."""
    finding_id: str
    severity: str
    title: str
    created_at: str  # ISO format
    sla_deadline: str  # ISO format
    resolved_at: str = ""  # ISO format, empty if unresolved
    status: SLAStatus = SLAStatus.ON_TRACK
    owner: str = ""
    waived: bool = False
    waive_reason: str = ""

    @property
    def hours_elapsed(self) -> float:
        """Hours since finding was created."""
        created = datetime.fromisoformat(self.created_at)
        now = datetime.now(timezone.utc)
        return (now - created).total_seconds() / 3600

    @property
    def hours_remaining(self) -> float:
        """Hours until SLA deadline. Negative if breached."""
        deadline = datetime.fromisoformat(self.sla_deadline)
        now = datetime.now(timezone.utc)
        return (deadline - now).total_seconds() / 3600

    @property
    def elapsed_percentage(self) -> float:
        """Percentage of SLA time elapsed."""
        created = datetime.fromisoformat(self.created_at)
        deadline = datetime.fromisoformat(self.sla_deadline)
        total = (deadline - created).total_seconds()
        if total <= 0:
            return 1.0
        now = datetime.now(timezone.utc)
        elapsed = (now - created).total_seconds()
        return min(elapsed / total, 2.0)  # cap at 200%

    def compute_status(self, warning_threshold: float = 0.75) -> SLAStatus:
        """Compute current SLA status."""
        if self.waived:
            return SLAStatus.WAIVED
        if self.resolved_at:
            return SLAStatus.RESOLVED
        pct = self.elapsed_percentage
        if pct >= 1.0:
            return SLAStatus.BREACHED
        if pct >= warning_threshold:
            return SLAStatus.AT_RISK
        return SLAStatus.ON_TRACK

    def to_dict(self) -> dict[str, Any]:
        return {
            "finding_id": self.finding_id,
            "severity": self.severity,
            "title": self.title,
            "created_at": self.created_at,
            "sla_deadline": self.sla_deadline,
            "resolved_at": self.resolved_at,
            "status": self.status.value,
            "owner": self.owner,
            "waived": self.waived,
            "waive_reason": self.waive_reason,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> SLAEntry:
        return cls(
            finding_id=data["finding_id"],
            severity=data["severity"],
            title=data["title"],
            created_at=data["created_at"],
            sla_deadline=data["sla_deadline"],
            resolved_at=data.get("resolved_at", ""),
            status=SLAStatus(data.get("status", "ON_TRACK")),
            owner=data.get("owner", ""),
            waived=data.get("waived", False),
            waive_reason=data.get("waive_reason", ""),
        )


# ---------------------------------------------------------------------------
# Monitor
# ---------------------------------------------------------------------------

DEFAULT_POLICIES: list[SLAPolicy] = [
    SLAPolicy("CRITICAL", 24, 0.5, EscalationLevel.BLOCK_DEPLOY),
    SLAPolicy("HIGH", 72, 0.75, EscalationLevel.ASSIGN),
    SLAPolicy("MEDIUM", 168, 0.75, EscalationLevel.NOTIFY),
    SLAPolicy("LOW", 720, 0.9, EscalationLevel.NONE),
]


class SLAMonitor:
    """Monitors SLA compliance across findings."""

    def __init__(self, policies: Optional[list[SLAPolicy]] = None) -> None:
        self._policies: dict[str, SLAPolicy] = {}
        for p in (policies or DEFAULT_POLICIES):
            self._policies[p.severity] = p
        self._entries: dict[str, SLAEntry] = {}

    @property
    def policies(self) -> list[SLAPolicy]:
        return list(self._policies.values())

    @property
    def entries(self) -> list[SLAEntry]:
        return list(self._entries.values())

    @property
    def count(self) -> int:
        return len(self._entries)

    def get_policy(self, severity: str) -> Optional[SLAPolicy]:
        return self._policies.get(severity)

    def set_policy(self, policy: SLAPolicy) -> None:
        self._policies[policy.severity] = policy

    def add_finding(self, finding_id: str, severity: str, title: str,
                    created_at: Optional[str] = None, owner: str = "") -> SLAEntry:
        """Track a new finding for SLA monitoring."""
        if finding_id in self._entries:
            raise ValueError(f"Finding {finding_id} already tracked")

        now = datetime.now(timezone.utc)
        created = created_at or now.isoformat()
        policy = self._policies.get(severity)
        max_hours = policy.max_hours if policy else 720

        created_dt = datetime.fromisoformat(created)
        deadline = (created_dt + timedelta(hours=max_hours)).isoformat()

        entry = SLAEntry(
            finding_id=finding_id,
            severity=severity,
            title=title,
            created_at=created,
            sla_deadline=deadline,
            owner=owner,
        )
        self._entries[finding_id] = entry
        return entry

    def resolve(self, finding_id: str) -> None:
        """Mark a finding as resolved."""
        entry = self._entries.get(finding_id)
        if entry is None:
            raise KeyError(f"Finding {finding_id} not tracked")
        entry.resolved_at = datetime.now(timezone.utc).isoformat()
        entry.status = SLAStatus.RESOLVED

    def waive(self, finding_id: str, reason: str) -> None:
        """Waive SLA for a finding."""
        entry = self._entries.get(finding_id)
        if entry is None:
            raise KeyError(f"Finding {finding_id} not tracked")
        entry.waived = True
        entry.waive_reason = reason
        entry.status = SLAStatus.WAIVED

    def refresh_statuses(self) -> None:
        """Recompute all SLA statuses based on current time."""
        for entry in self._entries.values():
            policy = self._policies.get(entry.severity)
            threshold = policy.warning_threshold if policy else 0.75
            entry.status = entry.compute_status(threshold)

    # -- Queries --

    def breached(self) -> list[SLAEntry]:
        """Return entries with breached SLAs."""
        self.refresh_statuses()
        return [e for e in self._entries.values() if e.status == SLAStatus.BREACHED]

    def at_risk(self) -> list[SLAEntry]:
        """Return entries at risk of breaching."""
        self.refresh_statuses()
        return [e for e in self._entries.values() if e.status == SLAStatus.AT_RISK]

    def on_track(self) -> list[SLAEntry]:
        """Return entries on track."""
        self.refresh_statuses()
        return [e for e in self._entries.values() if e.status == SLAStatus.ON_TRACK]

    def by_severity(self, severity: str) -> list[SLAEntry]:
        return [e for e in self._entries.values() if e.severity == severity]

    # -- Metrics --

    def compliance_rate(self) -> float:
        """Percentage of entries within SLA. 1.0 = 100%."""
        if not self._entries:
            return 1.0
        self.refresh_statuses()
        compliant = sum(
            1 for e in self._entries.values()
            if e.status in (SLAStatus.ON_TRACK, SLAStatus.RESOLVED, SLAStatus.WAIVED)
        )
        return compliant / len(self._entries)

    def breach_count(self) -> int:
        self.refresh_statuses()
        return sum(1 for e in self._entries.values() if e.status == SLAStatus.BREACHED)

    def severity_compliance(self) -> dict[str, float]:
        """Compliance rate per severity."""
        self.refresh_statuses()
        by_sev: dict[str, list[SLAEntry]] = {}
        for e in self._entries.values():
            by_sev.setdefault(e.severity, []).append(e)
        result = {}
        for sev, entries in by_sev.items():
            compliant = sum(
                1 for e in entries
                if e.status in (SLAStatus.ON_TRACK, SLAStatus.RESOLVED, SLAStatus.WAIVED)
            )
            result[sev] = compliant / len(entries) if entries else 1.0
        return result

    def needs_escalation(self) -> list[tuple[SLAEntry, EscalationLevel]]:
        """Return entries needing escalation with their escalation level."""
        self.refresh_statuses()
        result = []
        for e in self._entries.values():
            if e.status in (SLAStatus.BREACHED, SLAStatus.AT_RISK):
                policy = self._policies.get(e.severity)
                level = policy.escalation if policy else EscalationLevel.NOTIFY
                if level != EscalationLevel.NONE:
                    result.append((e, level))
        return result

    # -- Persistence --

    def to_dict(self) -> dict[str, Any]:
        return {
            "version": "1.0",
            "policies": [p.to_dict() for p in self._policies.values()],
            "entries": [e.to_dict() for e in self._entries.values()],
        }

    def to_json(self, indent: int = 2) -> str:
        return json.dumps(self.to_dict(), indent=indent)

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> SLAMonitor:
        policies = [SLAPolicy.from_dict(p) for p in data.get("policies", [])]
        monitor = cls(policies=policies if policies else None)
        for e in data.get("entries", []):
            entry = SLAEntry.from_dict(e)
            monitor._entries[entry.finding_id] = entry
        return monitor

    @classmethod
    def from_json(cls, json_str: str) -> SLAMonitor:
        return cls.from_dict(json.loads(json_str))

    # -- Report --

    def markdown_report(self) -> str:
        """Generate SLA compliance report."""
        self.refresh_statuses()
        lines = ["# SLA Compliance Report", ""]

        # Summary
        lines.append("## Summary")
        lines.append(f"- **Tracked findings**: {self.count}")
        lines.append(f"- **Compliance rate**: {self.compliance_rate():.0%}")
        lines.append(f"- **Breached**: {self.breach_count()}")
        lines.append(f"- **At risk**: {len(self.at_risk())}")
        lines.append("")

        # By severity
        sev_comp = self.severity_compliance()
        if sev_comp:
            lines.append("## Compliance by Severity")
            lines.append("| Severity | Compliance | Count |")
            lines.append("|----------|-----------|-------|")
            for sev in ("CRITICAL", "HIGH", "MEDIUM", "LOW"):
                if sev in sev_comp:
                    count = len(self.by_severity(sev))
                    lines.append(f"| {sev} | {sev_comp[sev]:.0%} | {count} |")
            lines.append("")

        # Breaches
        breaches = self.breached()
        if breaches:
            lines.append("## SLA Breaches")
            lines.append("| ID | Severity | Title | Owner | Hours Over |")
            lines.append("|----|----------|-------|-------|-----------|")
            for e in breaches:
                hours_over = abs(e.hours_remaining)
                lines.append(
                    f"| {e.finding_id[:8]} | {e.severity} | {e.title} "
                    f"| {e.owner or '-'} | {hours_over:.0f}h |"
                )
            lines.append("")

        # Escalations
        escalations = self.needs_escalation()
        if escalations:
            lines.append("## Pending Escalations")
            for entry, level in escalations:
                lines.append(f"- **{entry.finding_id[:8]}** ({entry.severity}): {level.value}")
            lines.append("")

        return "\n".join(lines)
