"""Incident Response Playbook Generator for MCP Security Findings.

Generates structured incident response playbooks based on scan findings.
Classifies incidents by severity and type, provides step-by-step response
procedures following NIST SP 800-61 phases: Detect, Contain, Eradicate,
Recover, Lessons Learned.

Includes playbook templates for: data breach, privilege escalation,
injection attacks, and denial of service.

CLI: mcpaudit incident-response <path> [--format text|json|html]
     mcpaudit incident-response <path> --type data_breach
     mcpaudit incident-response <path> --severity critical

Zero external dependencies. Python 3.11+ stdlib only.
"""

from __future__ import annotations

import html as html_mod
import json
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any

from .models import Finding, ScanResult, ScanSummary, Severity


__all__ = [
    "ChecklistItem",
    "IncidentClassification",
    "IncidentPhase",
    "IncidentPlaybook",
    "IncidentType",
    "PlaybookStep",
    "ResponseAction",
    "RunbookEntry",
    "classify_incident",
    "format_playbook_html",
    "format_playbook_json",
    "format_playbook_text",
    "generate_playbook",
    "generate_playbooks_from_findings",
    "generate_playbooks_from_scan",
    "generate_runbook",
    "get_incident_templates",
    "get_template_for_type",
]


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

class IncidentType(Enum):
    """Types of security incidents."""

    DATA_BREACH = "data_breach"
    PRIVILEGE_ESCALATION = "privilege_escalation"
    INJECTION = "injection"
    DOS = "denial_of_service"
    UNAUTHORIZED_ACCESS = "unauthorized_access"
    SUPPLY_CHAIN = "supply_chain"
    CONFIGURATION_DRIFT = "configuration_drift"
    TOOL_ABUSE = "tool_abuse"

    @classmethod
    def from_string(cls, value: str) -> IncidentType:
        normalized = value.lower().strip().replace("-", "_").replace(" ", "_")
        for member in cls:
            if member.value == normalized:
                return member
        # Try partial matches
        for member in cls:
            if normalized in member.value or member.value in normalized:
                return member
        valid = ", ".join(m.value for m in cls)
        raise ValueError(f"Unknown incident type '{value}'. Valid: {valid}")


class IncidentPhase(Enum):
    """NIST SP 800-61 incident response phases."""

    DETECT = "detect"
    CONTAIN = "contain"
    ERADICATE = "eradicate"
    RECOVER = "recover"
    LESSONS_LEARNED = "lessons_learned"

    @property
    def label(self) -> str:
        return {
            IncidentPhase.DETECT: "Detection & Analysis",
            IncidentPhase.CONTAIN: "Containment",
            IncidentPhase.ERADICATE: "Eradication",
            IncidentPhase.RECOVER: "Recovery",
            IncidentPhase.LESSONS_LEARNED: "Lessons Learned",
        }[self]

    @property
    def order(self) -> int:
        return {
            IncidentPhase.DETECT: 1,
            IncidentPhase.CONTAIN: 2,
            IncidentPhase.ERADICATE: 3,
            IncidentPhase.RECOVER: 4,
            IncidentPhase.LESSONS_LEARNED: 5,
        }[self]


class ResponseAction(Enum):
    """Types of response actions."""

    ISOLATE = "isolate"
    BLOCK = "block"
    PATCH = "patch"
    ROLLBACK = "rollback"
    MONITOR = "monitor"
    NOTIFY = "notify"
    INVESTIGATE = "investigate"
    DOCUMENT = "document"
    REVIEW = "review"
    HARDEN = "harden"

    @property
    def urgency(self) -> int:
        """Lower = more urgent."""
        return {
            ResponseAction.ISOLATE: 1,
            ResponseAction.BLOCK: 2,
            ResponseAction.PATCH: 3,
            ResponseAction.ROLLBACK: 4,
            ResponseAction.NOTIFY: 5,
            ResponseAction.INVESTIGATE: 6,
            ResponseAction.MONITOR: 7,
            ResponseAction.HARDEN: 8,
            ResponseAction.REVIEW: 9,
            ResponseAction.DOCUMENT: 10,
        }[self]


# ---------------------------------------------------------------------------
# Data Models
# ---------------------------------------------------------------------------

@dataclass
class PlaybookStep:
    """A single step in an incident response playbook."""

    phase: IncidentPhase
    step_number: int
    action: ResponseAction
    title: str
    description: str
    commands: list[str] = field(default_factory=list)
    responsible: str = "Security Team"
    estimated_minutes: int = 15
    is_critical: bool = False

    def to_dict(self) -> dict[str, Any]:
        return {
            "phase": self.phase.value,
            "step_number": self.step_number,
            "action": self.action.value,
            "title": self.title,
            "description": self.description,
            "commands": self.commands,
            "responsible": self.responsible,
            "estimated_minutes": self.estimated_minutes,
            "is_critical": self.is_critical,
        }


@dataclass
class ChecklistItem:
    """A checklist item for a runbook."""

    item_id: str
    description: str
    phase: IncidentPhase
    is_required: bool = True
    is_completed: bool = False

    def to_dict(self) -> dict[str, Any]:
        return {
            "item_id": self.item_id,
            "description": self.description,
            "phase": self.phase.value,
            "is_required": self.is_required,
            "is_completed": self.is_completed,
        }


@dataclass
class RunbookEntry:
    """An executable runbook with checklist items."""

    title: str
    incident_type: IncidentType
    checklist: list[ChecklistItem] = field(default_factory=list)
    estimated_total_minutes: int = 0
    priority: str = "HIGH"

    def to_dict(self) -> dict[str, Any]:
        return {
            "title": self.title,
            "incident_type": self.incident_type.value,
            "checklist": [c.to_dict() for c in self.checklist],
            "estimated_total_minutes": self.estimated_total_minutes,
            "priority": self.priority,
            "total_items": len(self.checklist),
            "required_items": sum(1 for c in self.checklist if c.is_required),
        }


@dataclass
class IncidentClassification:
    """Classification of an incident based on findings."""

    incident_type: IncidentType
    severity: str  # CRITICAL, HIGH, MEDIUM, LOW
    confidence: float  # 0.0 - 1.0
    affected_findings: list[str] = field(default_factory=list)
    description: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "incident_type": self.incident_type.value,
            "severity": self.severity,
            "confidence": round(self.confidence, 2),
            "affected_findings": self.affected_findings,
            "description": self.description,
        }


@dataclass
class IncidentPlaybook:
    """A complete incident response playbook."""

    title: str
    incident_type: IncidentType
    classification: IncidentClassification
    steps: list[PlaybookStep] = field(default_factory=list)
    runbook: RunbookEntry | None = None
    created_at: str = ""
    estimated_total_minutes: int = 0
    affected_files: list[str] = field(default_factory=list)
    references: list[str] = field(default_factory=list)

    def __post_init__(self) -> None:
        if not self.created_at:
            self.created_at = datetime.now(timezone.utc).isoformat()
        if not self.estimated_total_minutes and self.steps:
            self.estimated_total_minutes = sum(
                s.estimated_minutes for s in self.steps
            )

    @property
    def phases(self) -> list[IncidentPhase]:
        """Get unique phases in order."""
        seen = set()
        result = []
        for s in self.steps:
            if s.phase not in seen:
                seen.add(s.phase)
                result.append(s.phase)
        result.sort(key=lambda p: p.order)
        return result

    @property
    def critical_steps(self) -> list[PlaybookStep]:
        return [s for s in self.steps if s.is_critical]

    def steps_for_phase(self, phase: IncidentPhase) -> list[PlaybookStep]:
        return [s for s in self.steps if s.phase == phase]

    def to_dict(self) -> dict[str, Any]:
        return {
            "title": self.title,
            "incident_type": self.incident_type.value,
            "classification": self.classification.to_dict(),
            "steps": [s.to_dict() for s in self.steps],
            "runbook": self.runbook.to_dict() if self.runbook else None,
            "created_at": self.created_at,
            "estimated_total_minutes": self.estimated_total_minutes,
            "affected_files": self.affected_files,
            "references": self.references,
            "phases": [p.value for p in self.phases],
            "total_steps": len(self.steps),
            "critical_steps_count": len(self.critical_steps),
        }


# ---------------------------------------------------------------------------
# Incident Classification
# ---------------------------------------------------------------------------

# Rule ID prefixes -> incident type mapping
_RULE_TYPE_MAP: dict[str, IncidentType] = {
    "CMD": IncidentType.INJECTION,
    "INJ": IncidentType.INJECTION,
    "SSRF": IncidentType.INJECTION,
    "XSS": IncidentType.INJECTION,
    "SQL": IncidentType.INJECTION,
    "PRIV": IncidentType.PRIVILEGE_ESCALATION,
    "AUTH": IncidentType.UNAUTHORIZED_ACCESS,
    "PERM": IncidentType.PRIVILEGE_ESCALATION,
    "LEAK": IncidentType.DATA_BREACH,
    "DATA": IncidentType.DATA_BREACH,
    "CRED": IncidentType.DATA_BREACH,
    "SEC": IncidentType.DATA_BREACH,
    "KEY": IncidentType.DATA_BREACH,
    "DOS": IncidentType.DOS,
    "RATE": IncidentType.DOS,
    "RESOURCE": IncidentType.DOS,
    "SUPPLY": IncidentType.SUPPLY_CHAIN,
    "DEP": IncidentType.SUPPLY_CHAIN,
    "DRIFT": IncidentType.CONFIGURATION_DRIFT,
    "CONFIG": IncidentType.CONFIGURATION_DRIFT,
    "TOOL": IncidentType.TOOL_ABUSE,
    "MCP": IncidentType.TOOL_ABUSE,
}


def _classify_rule(rule_id: str) -> IncidentType:
    """Classify a single rule ID to an incident type."""
    prefix = rule_id.split("-")[0].upper() if "-" in rule_id else rule_id.upper()
    if prefix in _RULE_TYPE_MAP:
        return _RULE_TYPE_MAP[prefix]
    # Try first 3 chars
    for key, val in _RULE_TYPE_MAP.items():
        if prefix.startswith(key):
            return val
    return IncidentType.UNAUTHORIZED_ACCESS


def _severity_to_confidence(severity: str) -> float:
    """Map severity to confidence score."""
    return {
        "CRITICAL": 0.95,
        "HIGH": 0.85,
        "MEDIUM": 0.70,
        "LOW": 0.55,
        "INFO": 0.40,
    }.get(severity.upper(), 0.50)


def classify_incident(findings: list[dict[str, Any]]) -> IncidentClassification:
    """Classify findings into an incident type."""
    if not findings:
        return IncidentClassification(
            incident_type=IncidentType.UNAUTHORIZED_ACCESS,
            severity="LOW",
            confidence=0.0,
            description="No findings to classify.",
        )

    # Count incident types from findings
    type_counts: dict[IncidentType, int] = {}
    severities: list[str] = []
    rule_ids: list[str] = []

    for f in findings:
        rule_id = f.get("rule_id", "UNKNOWN-001")
        severity = f.get("severity", "MEDIUM")
        itype = _classify_rule(rule_id)
        type_counts[itype] = type_counts.get(itype, 0) + 1
        severities.append(severity)
        rule_ids.append(rule_id)

    # Dominant type
    dominant = max(type_counts, key=type_counts.get)  # type: ignore[arg-type]

    # Worst severity
    sev_order = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]
    worst_sev = "LOW"
    for s in severities:
        su = s.upper()
        if su in sev_order and sev_order.index(su) < sev_order.index(worst_sev):
            worst_sev = su

    # Confidence from severity and count
    base_conf = _severity_to_confidence(worst_sev)
    count_bonus = min(0.1, len(findings) * 0.02)
    confidence = min(1.0, base_conf + count_bonus)

    type_labels = {
        IncidentType.DATA_BREACH: "Data breach detected",
        IncidentType.PRIVILEGE_ESCALATION: "Privilege escalation vulnerability",
        IncidentType.INJECTION: "Injection attack vector identified",
        IncidentType.DOS: "Denial of service risk",
        IncidentType.UNAUTHORIZED_ACCESS: "Unauthorized access risk",
        IncidentType.SUPPLY_CHAIN: "Supply chain compromise risk",
        IncidentType.CONFIGURATION_DRIFT: "Configuration drift detected",
        IncidentType.TOOL_ABUSE: "MCP tool abuse vector",
    }

    return IncidentClassification(
        incident_type=dominant,
        severity=worst_sev,
        confidence=confidence,
        affected_findings=rule_ids,
        description=type_labels.get(
            dominant, f"Security incident: {dominant.value}"
        ),
    )


# ---------------------------------------------------------------------------
# Playbook Templates
# ---------------------------------------------------------------------------

def _build_template_steps(
    incident_type: IncidentType,
) -> list[PlaybookStep]:
    """Build playbook steps for a given incident type."""
    templates: dict[IncidentType, list[PlaybookStep]] = {
        IncidentType.DATA_BREACH: _data_breach_steps(),
        IncidentType.PRIVILEGE_ESCALATION: _privilege_escalation_steps(),
        IncidentType.INJECTION: _injection_steps(),
        IncidentType.DOS: _dos_steps(),
        IncidentType.UNAUTHORIZED_ACCESS: _unauthorized_access_steps(),
        IncidentType.SUPPLY_CHAIN: _supply_chain_steps(),
        IncidentType.CONFIGURATION_DRIFT: _config_drift_steps(),
        IncidentType.TOOL_ABUSE: _tool_abuse_steps(),
    }
    return templates.get(incident_type, _generic_steps())


def _data_breach_steps() -> list[PlaybookStep]:
    return [
        PlaybookStep(
            phase=IncidentPhase.DETECT,
            step_number=1,
            action=ResponseAction.INVESTIGATE,
            title="Identify data exposure scope",
            description="Determine what data was exposed, how, and to whom.",
            commands=["mcpaudit scan --severity critical,high"],
            estimated_minutes=30,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.DETECT,
            step_number=2,
            action=ResponseAction.MONITOR,
            title="Check access logs",
            description="Review server access logs for unauthorized data access patterns.",
            estimated_minutes=20,
        ),
        PlaybookStep(
            phase=IncidentPhase.CONTAIN,
            step_number=3,
            action=ResponseAction.ISOLATE,
            title="Isolate affected MCP server",
            description="Disconnect the affected server from the network to prevent further exposure.",
            commands=["systemctl stop mcp-server"],
            estimated_minutes=10,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.CONTAIN,
            step_number=4,
            action=ResponseAction.BLOCK,
            title="Revoke compromised credentials",
            description="Rotate all API keys, tokens, and credentials that may have been exposed.",
            estimated_minutes=15,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.ERADICATE,
            step_number=5,
            action=ResponseAction.PATCH,
            title="Fix data exposure vulnerabilities",
            description="Apply patches to fix the root cause of data exposure.",
            commands=["mcpaudit remediate --fix-type code_change"],
            estimated_minutes=60,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.ERADICATE,
            step_number=6,
            action=ResponseAction.HARDEN,
            title="Apply data protection hardening",
            description="Enable encryption, access controls, and data masking.",
            estimated_minutes=45,
        ),
        PlaybookStep(
            phase=IncidentPhase.RECOVER,
            step_number=7,
            action=ResponseAction.ROLLBACK,
            title="Restore from clean backup",
            description="Restore server configuration from last known good state.",
            estimated_minutes=30,
        ),
        PlaybookStep(
            phase=IncidentPhase.RECOVER,
            step_number=8,
            action=ResponseAction.MONITOR,
            title="Enable enhanced monitoring",
            description="Deploy additional monitoring to detect recurrence.",
            estimated_minutes=20,
        ),
        PlaybookStep(
            phase=IncidentPhase.LESSONS_LEARNED,
            step_number=9,
            action=ResponseAction.DOCUMENT,
            title="Document incident timeline",
            description="Record full incident timeline, actions taken, and impact assessment.",
            estimated_minutes=60,
        ),
        PlaybookStep(
            phase=IncidentPhase.LESSONS_LEARNED,
            step_number=10,
            action=ResponseAction.REVIEW,
            title="Conduct post-incident review",
            description="Hold team review to identify prevention measures.",
            estimated_minutes=90,
        ),
    ]


def _privilege_escalation_steps() -> list[PlaybookStep]:
    return [
        PlaybookStep(
            phase=IncidentPhase.DETECT,
            step_number=1,
            action=ResponseAction.INVESTIGATE,
            title="Identify privilege escalation vector",
            description="Determine how elevated privileges were obtained.",
            commands=["mcpaudit scan --category authorization"],
            estimated_minutes=25,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.CONTAIN,
            step_number=2,
            action=ResponseAction.BLOCK,
            title="Revoke escalated permissions",
            description="Remove unauthorized elevated permissions immediately.",
            estimated_minutes=10,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.CONTAIN,
            step_number=3,
            action=ResponseAction.ISOLATE,
            title="Isolate affected components",
            description="Isolate MCP tools and resources with escalated access.",
            estimated_minutes=15,
        ),
        PlaybookStep(
            phase=IncidentPhase.ERADICATE,
            step_number=4,
            action=ResponseAction.PATCH,
            title="Fix authorization checks",
            description="Implement proper authorization validation at all entry points.",
            commands=["mcpaudit remediate --priority critical"],
            estimated_minutes=45,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.ERADICATE,
            step_number=5,
            action=ResponseAction.HARDEN,
            title="Apply least-privilege principle",
            description="Review and restrict all tool permissions to minimum required.",
            estimated_minutes=30,
        ),
        PlaybookStep(
            phase=IncidentPhase.RECOVER,
            step_number=6,
            action=ResponseAction.MONITOR,
            title="Deploy privilege monitoring",
            description="Set up alerts for unusual privilege usage patterns.",
            estimated_minutes=20,
        ),
        PlaybookStep(
            phase=IncidentPhase.LESSONS_LEARNED,
            step_number=7,
            action=ResponseAction.DOCUMENT,
            title="Document authorization gaps",
            description="Record all authorization gaps found and fixes applied.",
            estimated_minutes=30,
        ),
        PlaybookStep(
            phase=IncidentPhase.LESSONS_LEARNED,
            step_number=8,
            action=ResponseAction.REVIEW,
            title="Review access control model",
            description="Comprehensive review of the MCP access control architecture.",
            estimated_minutes=60,
        ),
    ]


def _injection_steps() -> list[PlaybookStep]:
    return [
        PlaybookStep(
            phase=IncidentPhase.DETECT,
            step_number=1,
            action=ResponseAction.INVESTIGATE,
            title="Identify injection vectors",
            description="Scan for command injection, prompt injection, and SSRF vectors.",
            commands=["mcpaudit scan --severity critical,high"],
            estimated_minutes=20,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.DETECT,
            step_number=2,
            action=ResponseAction.MONITOR,
            title="Analyze input patterns",
            description="Review recent inputs for malicious patterns.",
            estimated_minutes=15,
        ),
        PlaybookStep(
            phase=IncidentPhase.CONTAIN,
            step_number=3,
            action=ResponseAction.BLOCK,
            title="Block malicious inputs",
            description="Deploy input validation and WAF rules to block injection attempts.",
            estimated_minutes=15,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.CONTAIN,
            step_number=4,
            action=ResponseAction.ISOLATE,
            title="Isolate vulnerable endpoints",
            description="Disable or restrict access to vulnerable MCP tools.",
            estimated_minutes=10,
        ),
        PlaybookStep(
            phase=IncidentPhase.ERADICATE,
            step_number=5,
            action=ResponseAction.PATCH,
            title="Implement input sanitization",
            description="Add proper input validation, parameterized queries, shell escaping.",
            commands=[
                "mcpaudit remediate --fix-type code_change",
                "mcpaudit scan --verify",
            ],
            estimated_minutes=60,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.ERADICATE,
            step_number=6,
            action=ResponseAction.HARDEN,
            title="Enable Content Security Policy",
            description="Apply CSP headers and output encoding to prevent XSS.",
            estimated_minutes=30,
        ),
        PlaybookStep(
            phase=IncidentPhase.RECOVER,
            step_number=7,
            action=ResponseAction.MONITOR,
            title="Deploy injection detection",
            description="Set up WAF rules and anomaly detection for injection attempts.",
            estimated_minutes=25,
        ),
        PlaybookStep(
            phase=IncidentPhase.LESSONS_LEARNED,
            step_number=8,
            action=ResponseAction.DOCUMENT,
            title="Update secure coding guidelines",
            description="Document injection patterns found and update team guidelines.",
            estimated_minutes=45,
        ),
        PlaybookStep(
            phase=IncidentPhase.LESSONS_LEARNED,
            step_number=9,
            action=ResponseAction.REVIEW,
            title="Conduct code review",
            description="Review all input handling code paths for similar vulnerabilities.",
            estimated_minutes=60,
        ),
    ]


def _dos_steps() -> list[PlaybookStep]:
    return [
        PlaybookStep(
            phase=IncidentPhase.DETECT,
            step_number=1,
            action=ResponseAction.INVESTIGATE,
            title="Identify DoS attack vector",
            description="Determine the type and source of denial of service attack.",
            estimated_minutes=15,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.CONTAIN,
            step_number=2,
            action=ResponseAction.BLOCK,
            title="Apply rate limiting",
            description="Enable rate limiting on all MCP endpoints.",
            estimated_minutes=10,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.CONTAIN,
            step_number=3,
            action=ResponseAction.ISOLATE,
            title="Scale or redirect traffic",
            description="Scale up or redirect traffic away from affected server.",
            estimated_minutes=15,
        ),
        PlaybookStep(
            phase=IncidentPhase.ERADICATE,
            step_number=4,
            action=ResponseAction.PATCH,
            title="Fix resource exhaustion vulnerabilities",
            description="Add timeouts, connection limits, and resource quotas.",
            commands=["mcpaudit remediate --category dos"],
            estimated_minutes=45,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.ERADICATE,
            step_number=5,
            action=ResponseAction.HARDEN,
            title="Implement circuit breakers",
            description="Add circuit breakers and graceful degradation patterns.",
            estimated_minutes=30,
        ),
        PlaybookStep(
            phase=IncidentPhase.RECOVER,
            step_number=6,
            action=ResponseAction.MONITOR,
            title="Deploy traffic monitoring",
            description="Set up traffic analysis and anomaly detection.",
            estimated_minutes=20,
        ),
        PlaybookStep(
            phase=IncidentPhase.LESSONS_LEARNED,
            step_number=7,
            action=ResponseAction.DOCUMENT,
            title="Document capacity limits",
            description="Record server capacity limits and scaling thresholds.",
            estimated_minutes=30,
        ),
    ]


def _unauthorized_access_steps() -> list[PlaybookStep]:
    return [
        PlaybookStep(
            phase=IncidentPhase.DETECT,
            step_number=1,
            action=ResponseAction.INVESTIGATE,
            title="Identify unauthorized access",
            description="Determine scope and method of unauthorized access.",
            estimated_minutes=25,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.CONTAIN,
            step_number=2,
            action=ResponseAction.BLOCK,
            title="Block unauthorized access",
            description="Revoke compromised credentials and block access.",
            estimated_minutes=10,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.ERADICATE,
            step_number=3,
            action=ResponseAction.PATCH,
            title="Fix authentication issues",
            description="Implement proper authentication and session management.",
            estimated_minutes=45,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.RECOVER,
            step_number=4,
            action=ResponseAction.MONITOR,
            title="Enable access auditing",
            description="Deploy comprehensive access logging and monitoring.",
            estimated_minutes=20,
        ),
        PlaybookStep(
            phase=IncidentPhase.LESSONS_LEARNED,
            step_number=5,
            action=ResponseAction.DOCUMENT,
            title="Document access control improvements",
            description="Record findings and improvements to access controls.",
            estimated_minutes=30,
        ),
    ]


def _supply_chain_steps() -> list[PlaybookStep]:
    return [
        PlaybookStep(
            phase=IncidentPhase.DETECT,
            step_number=1,
            action=ResponseAction.INVESTIGATE,
            title="Identify compromised dependencies",
            description="Scan all dependencies for known vulnerabilities and tampering.",
            commands=["mcpaudit deps --check-vuln"],
            estimated_minutes=20,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.CONTAIN,
            step_number=2,
            action=ResponseAction.ISOLATE,
            title="Pin and lock dependencies",
            description="Pin all dependencies to known-good versions.",
            estimated_minutes=15,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.ERADICATE,
            step_number=3,
            action=ResponseAction.PATCH,
            title="Update to safe versions",
            description="Update all vulnerable dependencies to patched versions.",
            estimated_minutes=30,
        ),
        PlaybookStep(
            phase=IncidentPhase.RECOVER,
            step_number=4,
            action=ResponseAction.MONITOR,
            title="Enable dependency monitoring",
            description="Set up automated dependency vulnerability scanning.",
            estimated_minutes=20,
        ),
        PlaybookStep(
            phase=IncidentPhase.LESSONS_LEARNED,
            step_number=5,
            action=ResponseAction.REVIEW,
            title="Review supply chain security",
            description="Review dependency management practices and SBOM process.",
            estimated_minutes=45,
        ),
    ]


def _config_drift_steps() -> list[PlaybookStep]:
    return [
        PlaybookStep(
            phase=IncidentPhase.DETECT,
            step_number=1,
            action=ResponseAction.INVESTIGATE,
            title="Identify configuration changes",
            description="Compare current config against approved baseline.",
            commands=["mcpaudit drift --check"],
            estimated_minutes=15,
        ),
        PlaybookStep(
            phase=IncidentPhase.CONTAIN,
            step_number=2,
            action=ResponseAction.ROLLBACK,
            title="Revert unauthorized changes",
            description="Roll back configuration to last approved state.",
            estimated_minutes=10,
        ),
        PlaybookStep(
            phase=IncidentPhase.ERADICATE,
            step_number=3,
            action=ResponseAction.HARDEN,
            title="Enforce config-as-code",
            description="Implement infrastructure-as-code with approval workflows.",
            estimated_minutes=30,
        ),
        PlaybookStep(
            phase=IncidentPhase.RECOVER,
            step_number=4,
            action=ResponseAction.MONITOR,
            title="Enable drift detection",
            description="Set up automated configuration drift monitoring.",
            estimated_minutes=20,
        ),
        PlaybookStep(
            phase=IncidentPhase.LESSONS_LEARNED,
            step_number=5,
            action=ResponseAction.DOCUMENT,
            title="Document config management",
            description="Record configuration management procedures and policies.",
            estimated_minutes=30,
        ),
    ]


def _tool_abuse_steps() -> list[PlaybookStep]:
    return [
        PlaybookStep(
            phase=IncidentPhase.DETECT,
            step_number=1,
            action=ResponseAction.INVESTIGATE,
            title="Identify tool abuse patterns",
            description="Analyze MCP tool usage for unauthorized or malicious patterns.",
            estimated_minutes=20,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.CONTAIN,
            step_number=2,
            action=ResponseAction.BLOCK,
            title="Restrict tool access",
            description="Apply tool-level access controls and rate limits.",
            estimated_minutes=10,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.ERADICATE,
            step_number=3,
            action=ResponseAction.PATCH,
            title="Fix tool permission model",
            description="Implement least-privilege tool permissions.",
            estimated_minutes=40,
        ),
        PlaybookStep(
            phase=IncidentPhase.RECOVER,
            step_number=4,
            action=ResponseAction.MONITOR,
            title="Deploy tool usage monitoring",
            description="Set up alerts for abnormal tool usage patterns.",
            estimated_minutes=20,
        ),
        PlaybookStep(
            phase=IncidentPhase.LESSONS_LEARNED,
            step_number=5,
            action=ResponseAction.REVIEW,
            title="Review tool security model",
            description="Comprehensive review of MCP tool security architecture.",
            estimated_minutes=45,
        ),
    ]


def _generic_steps() -> list[PlaybookStep]:
    """Fallback generic response steps."""
    return [
        PlaybookStep(
            phase=IncidentPhase.DETECT,
            step_number=1,
            action=ResponseAction.INVESTIGATE,
            title="Investigate security incident",
            description="Determine scope and impact of the security incident.",
            estimated_minutes=30,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.CONTAIN,
            step_number=2,
            action=ResponseAction.ISOLATE,
            title="Contain the incident",
            description="Isolate affected components to prevent further damage.",
            estimated_minutes=15,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.ERADICATE,
            step_number=3,
            action=ResponseAction.PATCH,
            title="Fix root cause",
            description="Apply fixes to address the root cause of the incident.",
            estimated_minutes=45,
            is_critical=True,
        ),
        PlaybookStep(
            phase=IncidentPhase.RECOVER,
            step_number=4,
            action=ResponseAction.MONITOR,
            title="Monitor for recurrence",
            description="Deploy monitoring to detect if the issue recurs.",
            estimated_minutes=20,
        ),
        PlaybookStep(
            phase=IncidentPhase.LESSONS_LEARNED,
            step_number=5,
            action=ResponseAction.DOCUMENT,
            title="Document lessons learned",
            description="Record findings and improvements for future reference.",
            estimated_minutes=30,
        ),
    ]


def get_incident_templates() -> dict[str, IncidentType]:
    """Get all available incident type templates."""
    return {t.value: t for t in IncidentType}


def get_template_for_type(incident_type: IncidentType) -> list[PlaybookStep]:
    """Get the template steps for a given incident type."""
    return _build_template_steps(incident_type)


# ---------------------------------------------------------------------------
# Runbook Generation
# ---------------------------------------------------------------------------

def _build_checklist(
    steps: list[PlaybookStep],
    incident_type: IncidentType,
) -> list[ChecklistItem]:
    """Build a runbook checklist from playbook steps."""
    items: list[ChecklistItem] = []
    for step in steps:
        item_id = f"{incident_type.value}_{step.phase.value}_{step.step_number}"
        items.append(ChecklistItem(
            item_id=item_id,
            description=f"[{step.action.value.upper()}] {step.title}: {step.description}",
            phase=step.phase,
            is_required=step.is_critical,
        ))
    return items


def generate_runbook(
    incident_type: IncidentType,
    steps: list[PlaybookStep] | None = None,
    severity: str = "HIGH",
) -> RunbookEntry:
    """Generate an executable runbook for an incident type."""
    if steps is None:
        steps = _build_template_steps(incident_type)

    checklist = _build_checklist(steps, incident_type)
    total_minutes = sum(s.estimated_minutes for s in steps)

    label = incident_type.value.replace("_", " ").title()

    return RunbookEntry(
        title=f"Runbook: {label} Response",
        incident_type=incident_type,
        checklist=checklist,
        estimated_total_minutes=total_minutes,
        priority=severity,
    )


# ---------------------------------------------------------------------------
# Playbook Generation (main API)
# ---------------------------------------------------------------------------

def generate_playbook(
    findings: list[dict[str, Any]],
    incident_type: IncidentType | None = None,
) -> IncidentPlaybook:
    """Generate an incident response playbook from findings.

    Parameters
    ----------
    findings : list[dict]
        List of finding dicts with rule_id, severity, file, etc.
    incident_type : IncidentType | None
        Force a specific incident type; if None, auto-classify.

    Returns
    -------
    IncidentPlaybook
        Complete playbook with steps, runbook, and classification.
    """
    classification = classify_incident(findings)

    if incident_type is not None:
        classification = IncidentClassification(
            incident_type=incident_type,
            severity=classification.severity,
            confidence=classification.confidence,
            affected_findings=classification.affected_findings,
            description=f"Forced type: {incident_type.value}",
        )

    itype = classification.incident_type
    steps = _build_template_steps(itype)
    runbook = generate_runbook(itype, steps, classification.severity)

    # Collect affected files
    files = list({f.get("file", "") for f in findings if f.get("file")})

    # References
    refs = [
        "NIST SP 800-61 Rev 2: Computer Security Incident Handling Guide",
        "OWASP Incident Response Cheat Sheet",
    ]
    if itype == IncidentType.DATA_BREACH:
        refs.append("GDPR Article 33: Notification of a data breach")
    elif itype == IncidentType.SUPPLY_CHAIN:
        refs.append("NIST SP 800-161: Supply Chain Risk Management")

    label = itype.value.replace("_", " ").title()
    return IncidentPlaybook(
        title=f"Incident Response: {label}",
        incident_type=itype,
        classification=classification,
        steps=steps,
        runbook=runbook,
        affected_files=files,
        references=refs,
    )


def generate_playbooks_from_findings(
    findings: list[dict[str, Any]],
) -> list[IncidentPlaybook]:
    """Generate playbooks for each incident type found in findings."""
    if not findings:
        return []

    # Group findings by incident type
    groups: dict[IncidentType, list[dict[str, Any]]] = {}
    for f in findings:
        itype = _classify_rule(f.get("rule_id", "UNKNOWN-001"))
        groups.setdefault(itype, []).append(f)

    playbooks = []
    for itype, group_findings in sorted(groups.items(), key=lambda x: x[0].value):
        pb = generate_playbook(group_findings, incident_type=itype)
        playbooks.append(pb)

    return playbooks


def generate_playbooks_from_scan(
    scan_data: dict[str, Any],
) -> list[IncidentPlaybook]:
    """Generate playbooks from scan result dict."""
    findings = []
    for result in scan_data.get("results", []):
        for f in result.get("findings", []):
            findings.append(f)
    return generate_playbooks_from_findings(findings)


# ---------------------------------------------------------------------------
# Format: JSON
# ---------------------------------------------------------------------------

def format_playbook_json(playbook: IncidentPlaybook) -> str:
    """Format a playbook as JSON string."""
    return json.dumps(playbook.to_dict(), indent=2)


# ---------------------------------------------------------------------------
# Format: Text
# ---------------------------------------------------------------------------

def format_playbook_text(playbook: IncidentPlaybook) -> str:
    """Format a playbook as human-readable text."""
    lines: list[str] = []
    lines.append(f"{'=' * 70}")
    lines.append(f"  INCIDENT RESPONSE PLAYBOOK")
    lines.append(f"  {playbook.title}")
    lines.append(f"{'=' * 70}")
    lines.append("")

    # Classification
    c = playbook.classification
    lines.append(f"  Type:       {c.incident_type.value}")
    lines.append(f"  Severity:   {c.severity}")
    lines.append(f"  Confidence: {c.confidence:.0%}")
    lines.append(f"  Findings:   {len(c.affected_findings)}")
    if c.description:
        lines.append(f"  Summary:    {c.description}")
    lines.append("")

    # Steps by phase
    for phase in playbook.phases:
        lines.append(f"--- {phase.label} ---")
        for step in playbook.steps_for_phase(phase):
            marker = "[!]" if step.is_critical else "[ ]"
            lines.append(
                f"  {marker} Step {step.step_number}: {step.title}"
            )
            lines.append(f"      Action: {step.action.value}")
            lines.append(f"      {step.description}")
            if step.commands:
                lines.append(f"      Commands: {'; '.join(step.commands)}")
            lines.append(
                f"      Responsible: {step.responsible} | "
                f"Est: {step.estimated_minutes} min"
            )
            lines.append("")

    # Affected files
    if playbook.affected_files:
        lines.append("Affected Files:")
        for af in playbook.affected_files:
            lines.append(f"  - {af}")
        lines.append("")

    # Runbook summary
    if playbook.runbook:
        rb = playbook.runbook
        lines.append(f"Runbook: {rb.title}")
        lines.append(
            f"  Total items: {len(rb.checklist)} | "
            f"Required: {sum(1 for c in rb.checklist if c.is_required)} | "
            f"Est: {rb.estimated_total_minutes} min"
        )
        lines.append("")

    # References
    if playbook.references:
        lines.append("References:")
        for ref in playbook.references:
            lines.append(f"  - {ref}")

    lines.append(f"\nEstimated total time: {playbook.estimated_total_minutes} min")
    lines.append(f"Generated: {playbook.created_at}")
    return "\n".join(lines)


# ---------------------------------------------------------------------------
# Format: HTML
# ---------------------------------------------------------------------------

def format_playbook_html(playbook: IncidentPlaybook) -> str:
    """Format a playbook as an HTML report."""
    e = html_mod.escape
    c = playbook.classification

    sev_colors = {
        "CRITICAL": "#dc3545",
        "HIGH": "#fd7e14",
        "MEDIUM": "#ffc107",
        "LOW": "#28a745",
        "INFO": "#17a2b8",
    }
    sev_color = sev_colors.get(c.severity, "#6c757d")

    html_parts: list[str] = []
    html_parts.append("<!DOCTYPE html>")
    html_parts.append("<html lang='en'><head>")
    html_parts.append("<meta charset='UTF-8'>")
    html_parts.append(f"<title>{e(playbook.title)}</title>")
    html_parts.append("<style>")
    html_parts.append("""
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif;
       max-width: 900px; margin: 0 auto; padding: 20px;
       background: #f8f9fa; color: #212529; }
h1 { border-bottom: 3px solid #212529; padding-bottom: 10px; }
h2 { color: #495057; border-bottom: 1px solid #dee2e6; padding-bottom: 5px; }
.classification { background: #fff; border-radius: 8px;
                   padding: 15px; margin: 15px 0; border-left: 4px solid; }
.phase { background: #fff; border-radius: 8px; padding: 15px;
         margin: 15px 0; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.step { padding: 10px; margin: 8px 0; border-radius: 4px; }
.step.critical { background: #fff3cd; border-left: 3px solid #ffc107; }
.step.normal { background: #f8f9fa; border-left: 3px solid #6c757d; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px;
         color: #fff; font-size: 0.85em; font-weight: bold; }
.commands { background: #212529; color: #00ff41; padding: 8px;
            border-radius: 4px; font-family: monospace; margin: 5px 0; }
.runbook { background: #fff; padding: 15px; border-radius: 8px;
           margin: 15px 0; }
.checklist-item { padding: 5px; margin: 3px 0; }
.required { font-weight: bold; }
table { width: 100%; border-collapse: collapse; margin: 10px 0; }
td, th { padding: 8px; text-align: left; border-bottom: 1px solid #dee2e6; }
""")
    html_parts.append("</style></head><body>")

    # Title
    html_parts.append(f"<h1>{e(playbook.title)}</h1>")

    # Classification
    html_parts.append(
        f"<div class='classification' style='border-color:{sev_color}'>"
    )
    html_parts.append("<h2>Incident Classification</h2>")
    html_parts.append("<table>")
    html_parts.append(
        f"<tr><td><strong>Type</strong></td>"
        f"<td>{e(c.incident_type.value)}</td></tr>"
    )
    html_parts.append(
        f"<tr><td><strong>Severity</strong></td>"
        f"<td><span class='badge' style='background:{sev_color}'>"
        f"{e(c.severity)}</span></td></tr>"
    )
    html_parts.append(
        f"<tr><td><strong>Confidence</strong></td>"
        f"<td>{c.confidence:.0%}</td></tr>"
    )
    html_parts.append(
        f"<tr><td><strong>Findings</strong></td>"
        f"<td>{len(c.affected_findings)}</td></tr>"
    )
    if c.description:
        html_parts.append(
            f"<tr><td><strong>Summary</strong></td>"
            f"<td>{e(c.description)}</td></tr>"
        )
    html_parts.append("</table></div>")

    # Steps by phase
    for phase in playbook.phases:
        phase_steps = playbook.steps_for_phase(phase)
        html_parts.append(f"<div class='phase'>")
        html_parts.append(f"<h2>{e(phase.label)}</h2>")
        for step in phase_steps:
            cls = "critical" if step.is_critical else "normal"
            html_parts.append(f"<div class='step {cls}'>")
            html_parts.append(
                f"<strong>Step {step.step_number}: {e(step.title)}</strong>"
            )
            if step.is_critical:
                html_parts.append(
                    " <span class='badge' style='background:#dc3545'>"
                    "CRITICAL</span>"
                )
            html_parts.append(f"<p>{e(step.description)}</p>")
            html_parts.append(
                f"<p><em>Action: {e(step.action.value)} | "
                f"Responsible: {e(step.responsible)} | "
                f"Est: {step.estimated_minutes} min</em></p>"
            )
            if step.commands:
                html_parts.append("<div class='commands'>")
                for cmd in step.commands:
                    html_parts.append(f"$ {e(cmd)}<br>")
                html_parts.append("</div>")
            html_parts.append("</div>")
        html_parts.append("</div>")

    # Runbook
    if playbook.runbook:
        rb = playbook.runbook
        html_parts.append("<div class='runbook'>")
        html_parts.append(f"<h2>{e(rb.title)}</h2>")
        html_parts.append(
            f"<p>Total items: {len(rb.checklist)} | "
            f"Required: {sum(1 for c2 in rb.checklist if c2.is_required)} | "
            f"Est: {rb.estimated_total_minutes} min</p>"
        )
        for item in rb.checklist:
            req = " class='required'" if item.is_required else ""
            box = "&#9745;" if item.is_completed else "&#9744;"
            html_parts.append(
                f"<div class='checklist-item'{req}>"
                f"{box} {e(item.description)}</div>"
            )
        html_parts.append("</div>")

    # Affected files
    if playbook.affected_files:
        html_parts.append("<h2>Affected Files</h2><ul>")
        for af in playbook.affected_files:
            html_parts.append(f"<li><code>{e(af)}</code></li>")
        html_parts.append("</ul>")

    # References
    if playbook.references:
        html_parts.append("<h2>References</h2><ul>")
        for ref in playbook.references:
            html_parts.append(f"<li>{e(ref)}</li>")
        html_parts.append("</ul>")

    # Footer
    html_parts.append(
        f"<p><em>Estimated total time: "
        f"{playbook.estimated_total_minutes} min | "
        f"Generated: {e(playbook.created_at)}</em></p>"
    )
    html_parts.append("</body></html>")

    return "\n".join(html_parts)
