"""Remediation Tracker for MCP Security Audit.

Tracks the lifecycle of security findings from discovery through
remediation to verification. Enables teams to manage their security
debt systematically.

Features:
- Track finding status: OPEN -> IN_PROGRESS -> FIXED -> VERIFIED -> WONTFIX
- Assign ownership and due dates
- Calculate mean-time-to-remediation (MTTR)
- SLA compliance tracking per severity
- Export remediation report as Markdown/JSON
- Trend analysis: open vs closed over time

CLI: mcpaudit remediation --status | --assign | --report

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

from __future__ import annotations

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


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

@unique
class RemediationStatus(Enum):
    """Lifecycle status of a finding remediation."""
    OPEN = "OPEN"
    IN_PROGRESS = "IN_PROGRESS"
    FIXED = "FIXED"
    VERIFIED = "VERIFIED"
    WONTFIX = "WONTFIX"
    DEFERRED = "DEFERRED"
    FALSE_POSITIVE = "FALSE_POSITIVE"


_TERMINAL_STATES = frozenset({
    RemediationStatus.VERIFIED,
    RemediationStatus.WONTFIX,
    RemediationStatus.FALSE_POSITIVE,
})

_VALID_TRANSITIONS: dict[RemediationStatus, frozenset[RemediationStatus]] = {
    RemediationStatus.OPEN: frozenset({
        RemediationStatus.IN_PROGRESS,
        RemediationStatus.WONTFIX,
        RemediationStatus.FALSE_POSITIVE,
        RemediationStatus.DEFERRED,
    }),
    RemediationStatus.IN_PROGRESS: frozenset({
        RemediationStatus.FIXED,
        RemediationStatus.OPEN,  # re-open
        RemediationStatus.DEFERRED,
    }),
    RemediationStatus.FIXED: frozenset({
        RemediationStatus.VERIFIED,
        RemediationStatus.OPEN,  # re-open if verification fails
    }),
    RemediationStatus.VERIFIED: frozenset(),  # terminal
    RemediationStatus.WONTFIX: frozenset({RemediationStatus.OPEN}),  # can reopen
    RemediationStatus.DEFERRED: frozenset({
        RemediationStatus.OPEN,
        RemediationStatus.IN_PROGRESS,
    }),
    RemediationStatus.FALSE_POSITIVE: frozenset({RemediationStatus.OPEN}),
}


# Default SLA per severity (hours)
DEFAULT_SLA_HOURS: dict[str, int] = {
    "CRITICAL": 24,
    "HIGH": 72,
    "MEDIUM": 168,  # 1 week
    "LOW": 720,     # 30 days
    "INFO": 0,      # no SLA
}


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

@dataclass
class StatusChange:
    """Record of a status transition."""
    from_status: RemediationStatus
    to_status: RemediationStatus
    timestamp: str  # ISO format
    comment: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "from": self.from_status.value,
            "to": self.to_status.value,
            "timestamp": self.timestamp,
            "comment": self.comment,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> StatusChange:
        return cls(
            from_status=RemediationStatus(data["from"]),
            to_status=RemediationStatus(data["to"]),
            timestamp=data["timestamp"],
            comment=data.get("comment", ""),
        )


@dataclass
class RemediationItem:
    """A tracked remediation for a specific finding."""
    finding_id: str  # fingerprint/hash of the finding
    rule_id: str
    severity: str
    file: str
    line: int
    title: str
    status: RemediationStatus = RemediationStatus.OPEN
    owner: str = ""
    due_date: str = ""  # ISO format
    created_at: str = ""
    updated_at: str = ""
    notes: str = ""
    history: list[StatusChange] = field(default_factory=list)

    def __post_init__(self) -> None:
        now = datetime.now(timezone.utc).isoformat()
        if not self.created_at:
            self.created_at = now
        if not self.updated_at:
            self.updated_at = now

    @property
    def is_terminal(self) -> bool:
        return self.status in _TERMINAL_STATES

    @property
    def is_overdue(self) -> bool:
        if not self.due_date or self.is_terminal:
            return False
        now = datetime.now(timezone.utc).isoformat()
        return now > self.due_date

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

    @property
    def time_to_remediation_hours(self) -> Optional[float]:
        """Hours from creation to FIXED/VERIFIED status. None if not resolved."""
        for change in self.history:
            if change.to_status in (RemediationStatus.FIXED, RemediationStatus.VERIFIED):
                created = datetime.fromisoformat(self.created_at)
                resolved = datetime.fromisoformat(change.timestamp)
                return (resolved - created).total_seconds() / 3600
        return None

    def transition(self, new_status: RemediationStatus, comment: str = "") -> None:
        """Transition to a new status with validation."""
        allowed = _VALID_TRANSITIONS.get(self.status, frozenset())
        if new_status not in allowed:
            raise ValueError(
                f"Cannot transition from {self.status.value} to {new_status.value}. "
                f"Allowed: {[s.value for s in allowed]}"
            )
        change = StatusChange(
            from_status=self.status,
            to_status=new_status,
            timestamp=datetime.now(timezone.utc).isoformat(),
            comment=comment,
        )
        self.history.append(change)
        self.status = new_status
        self.updated_at = change.timestamp

    def to_dict(self) -> dict[str, Any]:
        return {
            "finding_id": self.finding_id,
            "rule_id": self.rule_id,
            "severity": self.severity,
            "file": self.file,
            "line": self.line,
            "title": self.title,
            "status": self.status.value,
            "owner": self.owner,
            "due_date": self.due_date,
            "created_at": self.created_at,
            "updated_at": self.updated_at,
            "notes": self.notes,
            "history": [h.to_dict() for h in self.history],
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> RemediationItem:
        history = [StatusChange.from_dict(h) for h in data.get("history", [])]
        return cls(
            finding_id=data["finding_id"],
            rule_id=data["rule_id"],
            severity=data["severity"],
            file=data["file"],
            line=data["line"],
            title=data["title"],
            status=RemediationStatus(data["status"]),
            owner=data.get("owner", ""),
            due_date=data.get("due_date", ""),
            created_at=data.get("created_at", ""),
            updated_at=data.get("updated_at", ""),
            notes=data.get("notes", ""),
            history=history,
        )


# ---------------------------------------------------------------------------
# Tracker
# ---------------------------------------------------------------------------

class RemediationTracker:
    """Manages remediation items for a project."""

    def __init__(self, sla_hours: Optional[dict[str, int]] = None) -> None:
        self._items: dict[str, RemediationItem] = {}
        self._sla_hours = sla_hours or dict(DEFAULT_SLA_HOURS)

    @property
    def items(self) -> list[RemediationItem]:
        return list(self._items.values())

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

    def add(self, item: RemediationItem) -> None:
        """Add a remediation item."""
        if item.finding_id in self._items:
            raise ValueError(f"Finding {item.finding_id} already tracked")
        self._items[item.finding_id] = item

    def get(self, finding_id: str) -> Optional[RemediationItem]:
        return self._items.get(finding_id)

    def remove(self, finding_id: str) -> bool:
        """Remove an item. Returns True if found."""
        return self._items.pop(finding_id, None) is not None

    def transition(self, finding_id: str, new_status: RemediationStatus,
                   comment: str = "") -> None:
        item = self._items.get(finding_id)
        if item is None:
            raise KeyError(f"Finding {finding_id} not tracked")
        item.transition(new_status, comment)

    def assign(self, finding_id: str, owner: str) -> None:
        item = self._items.get(finding_id)
        if item is None:
            raise KeyError(f"Finding {finding_id} not tracked")
        item.owner = owner
        item.updated_at = datetime.now(timezone.utc).isoformat()

    def by_status(self, status: RemediationStatus) -> list[RemediationItem]:
        return [i for i in self._items.values() if i.status == status]

    def by_severity(self, severity: str) -> list[RemediationItem]:
        return [i for i in self._items.values() if i.severity == severity]

    def by_owner(self, owner: str) -> list[RemediationItem]:
        return [i for i in self._items.values() if i.owner == owner]

    def overdue(self) -> list[RemediationItem]:
        return [i for i in self._items.values() if i.is_overdue]

    def open_count(self) -> int:
        return sum(1 for i in self._items.values() if not i.is_terminal)

    def closed_count(self) -> int:
        return sum(1 for i in self._items.values() if i.is_terminal)

    # -- Metrics --

    def mttr_hours(self) -> Optional[float]:
        """Mean Time To Remediation in hours. None if no resolved items."""
        ttrs = [
            i.time_to_remediation_hours
            for i in self._items.values()
            if i.time_to_remediation_hours is not None
        ]
        if not ttrs:
            return None
        return sum(ttrs) / len(ttrs)

    def sla_compliance_rate(self) -> float:
        """Percentage of items within SLA. 1.0 = 100%."""
        total = 0
        compliant = 0
        for item in self._items.values():
            sla = self._sla_hours.get(item.severity, 0)
            if sla == 0:
                continue
            total += 1
            if item.is_terminal:
                ttr = item.time_to_remediation_hours
                if ttr is not None and ttr <= sla:
                    compliant += 1
            elif not item.is_overdue:
                compliant += 1
        return compliant / total if total else 1.0

    def severity_breakdown(self) -> dict[str, dict[str, int]]:
        """Breakdown of open/closed by severity."""
        result: dict[str, dict[str, int]] = {}
        for item in self._items.values():
            sev = item.severity
            if sev not in result:
                result[sev] = {"open": 0, "closed": 0, "total": 0}
            result[sev]["total"] += 1
            if item.is_terminal:
                result[sev]["closed"] += 1
            else:
                result[sev]["open"] += 1
        return result

    def status_counts(self) -> dict[str, int]:
        """Count items by status."""
        counts: dict[str, int] = {}
        for item in self._items.values():
            status = item.status.value
            counts[status] = counts.get(status, 0) + 1
        return counts

    # -- Persistence --

    def to_dict(self) -> dict[str, Any]:
        return {
            "version": "1.0",
            "sla_hours": self._sla_hours,
            "items": [i.to_dict() for i in self._items.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]) -> RemediationTracker:
        tracker = cls(sla_hours=data.get("sla_hours"))
        for item_data in data.get("items", []):
            item = RemediationItem.from_dict(item_data)
            tracker._items[item.finding_id] = item
        return tracker

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

    # -- Reports --

    def markdown_report(self) -> str:
        """Generate a Markdown remediation report."""
        lines = ["# Remediation Report", ""]
        # Summary
        lines.append("## Summary")
        lines.append(f"- **Total items**: {self.count}")
        lines.append(f"- **Open**: {self.open_count()}")
        lines.append(f"- **Closed**: {self.closed_count()}")
        mttr = self.mttr_hours()
        if mttr is not None:
            lines.append(f"- **MTTR**: {mttr:.1f} hours")
        lines.append(f"- **SLA compliance**: {self.sla_compliance_rate():.0%}")
        lines.append("")

        # By severity
        breakdown = self.severity_breakdown()
        if breakdown:
            lines.append("## By Severity")
            lines.append("| Severity | Open | Closed | Total |")
            lines.append("|----------|------|--------|-------|")
            for sev in ("CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"):
                if sev in breakdown:
                    b = breakdown[sev]
                    lines.append(f"| {sev} | {b['open']} | {b['closed']} | {b['total']} |")
            lines.append("")

        # Open items
        open_items = [i for i in self._items.values() if not i.is_terminal]
        if open_items:
            lines.append("## Open Items")
            lines.append("| ID | Severity | Status | Owner | File | Title |")
            lines.append("|----|----------|--------|-------|------|-------|")
            for item in sorted(open_items, key=lambda x: (
                {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3, "INFO": 4}.get(x.severity, 5)
            )):
                lines.append(
                    f"| {item.finding_id[:8]} | {item.severity} | {item.status.value} "
                    f"| {item.owner or '-'} | {item.file} | {item.title} |"
                )
            lines.append("")

        # Overdue
        overdue = self.overdue()
        if overdue:
            lines.append("## Overdue Items")
            for item in overdue:
                lines.append(f"- **{item.finding_id[:8]}** ({item.severity}): {item.title} — {item.file}")
            lines.append("")

        return "\n".join(lines)
