"""Compliance Gap Analyzer for MCP servers.

Identifies compliance gaps against multiple security frameworks:
OWASP Agentic, NIST AI RMF, EU AI Act, SOC2, HIPAA, PCI-DSS.
Generates remediation roadmaps with prioritized actions.

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

from __future__ import annotations

import json
from dataclasses import dataclass, field
from enum import Enum
from typing import Any


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

class Framework(Enum):
    """Supported compliance frameworks."""

    OWASP_AGENTIC = "owasp"
    NIST_AI_RMF = "nist"
    EU_AI_ACT = "eu_ai_act"
    SOC2 = "soc2"
    HIPAA = "hipaa"
    PCI_DSS = "pci_dss"

    @classmethod
    def from_string(cls, value: str) -> Framework:
        """Parse a framework string (case-insensitive)."""
        mapping = {
            "owasp": cls.OWASP_AGENTIC,
            "owasp_agentic": cls.OWASP_AGENTIC,
            "owasp-agentic": cls.OWASP_AGENTIC,
            "nist": cls.NIST_AI_RMF,
            "nist_ai": cls.NIST_AI_RMF,
            "nist-ai": cls.NIST_AI_RMF,
            "nist_ai_rmf": cls.NIST_AI_RMF,
            "eu_ai_act": cls.EU_AI_ACT,
            "eu-ai-act": cls.EU_AI_ACT,
            "eu_ai": cls.EU_AI_ACT,
            "soc2": cls.SOC2,
            "soc-2": cls.SOC2,
            "hipaa": cls.HIPAA,
            "pci_dss": cls.PCI_DSS,
            "pci-dss": cls.PCI_DSS,
            "pci": cls.PCI_DSS,
        }
        key = value.lower().strip()
        if key in mapping:
            return mapping[key]
        valid = ", ".join(sorted(set(f.value for f in cls)))
        raise ValueError(f"Unknown framework '{value}'. Valid: {valid}")

    @classmethod
    def all_frameworks(cls) -> list[Framework]:
        """Return all supported frameworks."""
        return list(cls)


class ControlStatus(Enum):
    """Status of a compliance control."""

    COVERED = "Covered"
    PARTIAL = "Partial"
    GAP = "Gap"
    NOT_APPLICABLE = "N/A"


class RemediationPriority(Enum):
    """Priority levels for remediation actions."""

    CRITICAL = "Critical"
    HIGH = "High"
    MEDIUM = "Medium"
    LOW = "Low"

    @property
    def order(self) -> int:
        return {
            RemediationPriority.CRITICAL: 0,
            RemediationPriority.HIGH: 1,
            RemediationPriority.MEDIUM: 2,
            RemediationPriority.LOW: 3,
        }[self]


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

@dataclass
class Control:
    """A compliance framework control."""

    control_id: str
    name: str
    description: str
    framework: Framework
    required_rules: list[str] = field(default_factory=list)
    category: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "control_id": self.control_id,
            "name": self.name,
            "description": self.description,
            "framework": self.framework.value,
            "required_rules": self.required_rules,
            "category": self.category,
        }


@dataclass
class ControlResult:
    """Result of evaluating a single control."""

    control: Control
    status: ControlStatus
    matched_rules: list[str] = field(default_factory=list)
    missing_rules: list[str] = field(default_factory=list)
    coverage_pct: float = 0.0
    notes: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "control_id": self.control.control_id,
            "name": self.control.name,
            "framework": self.control.framework.value,
            "status": self.status.value,
            "matched_rules": self.matched_rules,
            "missing_rules": self.missing_rules,
            "coverage_pct": round(self.coverage_pct, 1),
            "notes": self.notes,
        }


@dataclass
class FrameworkResult:
    """Result for a single framework."""

    framework: Framework
    controls: list[ControlResult] = field(default_factory=list)

    @property
    def total_controls(self) -> int:
        return len(self.controls)

    @property
    def covered_controls(self) -> int:
        return sum(
            1 for c in self.controls if c.status == ControlStatus.COVERED
        )

    @property
    def partial_controls(self) -> int:
        return sum(
            1 for c in self.controls if c.status == ControlStatus.PARTIAL
        )

    @property
    def gap_controls(self) -> int:
        return sum(1 for c in self.controls if c.status == ControlStatus.GAP)

    @property
    def coverage_pct(self) -> float:
        if not self.controls:
            return 0.0
        applicable = [
            c for c in self.controls if c.status != ControlStatus.NOT_APPLICABLE
        ]
        if not applicable:
            return 100.0
        covered = sum(1 for c in applicable if c.status == ControlStatus.COVERED)
        partial = sum(1 for c in applicable if c.status == ControlStatus.PARTIAL)
        return round((covered + partial * 0.5) / len(applicable) * 100, 1)

    def to_dict(self) -> dict[str, Any]:
        return {
            "framework": self.framework.value,
            "total_controls": self.total_controls,
            "covered": self.covered_controls,
            "partial": self.partial_controls,
            "gaps": self.gap_controls,
            "coverage_pct": self.coverage_pct,
            "controls": [c.to_dict() for c in self.controls],
        }


@dataclass
class RemediationAction:
    """A remediation action to close a gap."""

    action_id: str
    description: str
    priority: RemediationPriority
    framework: Framework
    control_ids: list[str] = field(default_factory=list)
    effort: str = "medium"  # low, medium, high
    estimated_days: int = 5

    def to_dict(self) -> dict[str, Any]:
        return {
            "action_id": self.action_id,
            "description": self.description,
            "priority": self.priority.value,
            "framework": self.framework.value,
            "control_ids": self.control_ids,
            "effort": self.effort,
            "estimated_days": self.estimated_days,
        }


@dataclass
class Roadmap:
    """Remediation roadmap."""

    actions: list[RemediationAction] = field(default_factory=list)
    total_estimated_days: int = 0

    @property
    def critical_actions(self) -> list[RemediationAction]:
        return [a for a in self.actions if a.priority == RemediationPriority.CRITICAL]

    @property
    def high_actions(self) -> list[RemediationAction]:
        return [a for a in self.actions if a.priority == RemediationPriority.HIGH]

    def to_dict(self) -> dict[str, Any]:
        return {
            "total_actions": len(self.actions),
            "total_estimated_days": self.total_estimated_days,
            "critical_count": len(self.critical_actions),
            "high_count": len(self.high_actions),
            "actions": [a.to_dict() for a in self.actions],
        }


@dataclass
class GapAnalysis:
    """Complete gap analysis result."""

    frameworks: list[FrameworkResult] = field(default_factory=list)
    roadmap: Roadmap = field(default_factory=Roadmap)
    scan_grade: str = "?"
    total_findings: int = 0

    @property
    def overall_coverage(self) -> float:
        """Average coverage across all frameworks."""
        if not self.frameworks:
            return 0.0
        return round(
            sum(f.coverage_pct for f in self.frameworks) / len(self.frameworks), 1
        )

    @property
    def total_gaps(self) -> int:
        return sum(f.gap_controls for f in self.frameworks)

    def to_dict(self) -> dict[str, Any]:
        return {
            "scan_grade": self.scan_grade,
            "total_findings": self.total_findings,
            "overall_coverage": self.overall_coverage,
            "total_gaps": self.total_gaps,
            "frameworks": [f.to_dict() for f in self.frameworks],
            "roadmap": self.roadmap.to_dict(),
        }


# ---------------------------------------------------------------------------
# Framework control definitions
# ---------------------------------------------------------------------------

def _get_owasp_controls() -> list[Control]:
    """OWASP Agentic Top 10 controls."""
    return [
        Control("OWASP-A01", "Excessive Agency",
                "Ensure tools don't have more permissions than needed",
                Framework.OWASP_AGENTIC, ["TOOL", "PRIV", "SCOPE", "AGENT"],
                "Access Control"),
        Control("OWASP-A02", "Prompt Injection",
                "Protect against direct and indirect prompt injection",
                Framework.OWASP_AGENTIC, ["PROMPT", "INJ"],
                "Input Validation"),
        Control("OWASP-A03", "Insecure Output Handling",
                "Validate and sanitize all tool outputs",
                Framework.OWASP_AGENTIC, ["XSS", "INJ", "VALID"],
                "Output Handling"),
        Control("OWASP-A04", "Tool Poisoning",
                "Verify tool integrity and provenance",
                Framework.OWASP_AGENTIC, ["TOOL", "MCP"],
                "Supply Chain"),
        Control("OWASP-A05", "Inadequate Sandboxing",
                "Run tools in sandboxed environments",
                Framework.OWASP_AGENTIC, ["CMD", "PATH", "DESER"],
                "Isolation"),
        Control("OWASP-A06", "Insufficient Monitoring",
                "Log and monitor all tool invocations",
                Framework.OWASP_AGENTIC, ["LOG", "ERR"],
                "Observability"),
        Control("OWASP-A07", "Confused Deputy",
                "Prevent confused deputy attacks on MCP tools",
                Framework.OWASP_AGENTIC, ["CONFUSED", "DEPUTY", "AUTH"],
                "Authorization"),
        Control("OWASP-A08", "Data Leakage",
                "Prevent sensitive data from leaking through tools",
                Framework.OWASP_AGENTIC, ["LEAK", "SEC", "CRYPTO"],
                "Data Protection"),
        Control("OWASP-A09", "Rate Limiting",
                "Implement rate limits on tool invocations",
                Framework.OWASP_AGENTIC, ["RATE"],
                "Availability"),
        Control("OWASP-A10", "SSRF via Tools",
                "Prevent SSRF attacks through MCP tools",
                Framework.OWASP_AGENTIC, ["SSRF", "PATH"],
                "Network Security"),
    ]


def _get_nist_controls() -> list[Control]:
    """NIST AI RMF controls."""
    return [
        Control("NIST-MAP-1", "AI Risk Framing",
                "Identify and document AI system risks",
                Framework.NIST_AI_RMF, ["SEC", "TOOL"],
                "Govern"),
        Control("NIST-MAP-2", "Categorize AI System",
                "Categorize the AI system and its components",
                Framework.NIST_AI_RMF, ["MCP", "RESOURCE"],
                "Map"),
        Control("NIST-MEASURE-1", "AI Risk Metrics",
                "Define and track AI risk metrics",
                Framework.NIST_AI_RMF, ["LOG", "ERR"],
                "Measure"),
        Control("NIST-MEASURE-2", "AI Testing",
                "Test AI system for security vulnerabilities",
                Framework.NIST_AI_RMF, ["PROMPT", "INJ", "CMD"],
                "Measure"),
        Control("NIST-MANAGE-1", "Risk Treatment",
                "Implement risk treatment strategies",
                Framework.NIST_AI_RMF, ["AUTH", "PRIV", "RATE"],
                "Manage"),
        Control("NIST-MANAGE-2", "Incident Response",
                "AI-specific incident response procedures",
                Framework.NIST_AI_RMF, ["LOG", "ERR"],
                "Manage"),
        Control("NIST-GOVERN-1", "AI Governance",
                "Establish AI governance framework",
                Framework.NIST_AI_RMF, ["SEC"],
                "Govern"),
        Control("NIST-GOVERN-2", "Accountability",
                "Assign accountability for AI system decisions",
                Framework.NIST_AI_RMF, ["LOG"],
                "Govern"),
    ]


def _get_eu_ai_act_controls() -> list[Control]:
    """EU AI Act controls."""
    return [
        Control("EU-AI-01", "Risk Assessment",
                "Perform risk assessment for AI system",
                Framework.EU_AI_ACT, ["SEC", "TOOL", "AGENT"],
                "Risk Management"),
        Control("EU-AI-02", "Data Governance",
                "Ensure proper governance of training and runtime data",
                Framework.EU_AI_ACT, ["LEAK", "SEC", "CRYPTO"],
                "Data Quality"),
        Control("EU-AI-03", "Transparency",
                "Ensure transparency of AI system operations",
                Framework.EU_AI_ACT, ["LOG"],
                "Transparency"),
        Control("EU-AI-04", "Human Oversight",
                "Enable human oversight of AI decisions",
                Framework.EU_AI_ACT, ["TOOL", "PRIV", "SCOPE"],
                "Oversight"),
        Control("EU-AI-05", "Accuracy & Robustness",
                "Ensure accuracy and robustness against adversarial inputs",
                Framework.EU_AI_ACT, ["PROMPT", "INJ", "VALID"],
                "Robustness"),
        Control("EU-AI-06", "Cybersecurity",
                "Implement cybersecurity measures for AI systems",
                Framework.EU_AI_ACT, ["AUTH", "CRYPTO", "CMD"],
                "Security"),
        Control("EU-AI-07", "Record Keeping",
                "Maintain records of AI system operations",
                Framework.EU_AI_ACT, ["LOG", "ERR"],
                "Documentation"),
    ]


def _get_soc2_controls() -> list[Control]:
    """SOC2 AI-relevant controls."""
    return [
        Control("SOC2-CC6.1", "Logical Access",
                "Implement logical access controls",
                Framework.SOC2, ["AUTH", "PRIV"],
                "Security"),
        Control("SOC2-CC6.2", "Authentication",
                "Require authentication before system access",
                Framework.SOC2, ["AUTH"],
                "Security"),
        Control("SOC2-CC6.3", "Authorization",
                "Restrict access based on authorization",
                Framework.SOC2, ["PRIV", "SCOPE", "TOOL"],
                "Security"),
        Control("SOC2-CC7.1", "Monitoring",
                "Monitor system components for anomalies",
                Framework.SOC2, ["LOG", "ERR"],
                "Monitoring"),
        Control("SOC2-CC7.2", "Incident Detection",
                "Detect security incidents",
                Framework.SOC2, ["LOG"],
                "Monitoring"),
        Control("SOC2-CC8.1", "Change Management",
                "Manage changes to system components",
                Framework.SOC2, ["MCP"],
                "Operations"),
        Control("SOC2-A1.1", "Availability",
                "Maintain system availability",
                Framework.SOC2, ["RATE"],
                "Availability"),
    ]


def _get_hipaa_controls() -> list[Control]:
    """HIPAA security-relevant controls."""
    return [
        Control("HIPAA-164.312a", "Access Control",
                "Implement access controls for ePHI",
                Framework.HIPAA, ["AUTH", "PRIV"],
                "Access Control"),
        Control("HIPAA-164.312b", "Audit Controls",
                "Implement audit controls for ePHI access",
                Framework.HIPAA, ["LOG"],
                "Audit"),
        Control("HIPAA-164.312c", "Integrity",
                "Protect ePHI from improper alteration",
                Framework.HIPAA, ["VALID", "INJ"],
                "Integrity"),
        Control("HIPAA-164.312d", "Authentication",
                "Verify identity before ePHI access",
                Framework.HIPAA, ["AUTH"],
                "Authentication"),
        Control("HIPAA-164.312e", "Transmission Security",
                "Protect ePHI during transmission",
                Framework.HIPAA, ["CRYPTO", "SEC"],
                "Encryption"),
        Control("HIPAA-164.308a5", "Security Awareness",
                "Security awareness and training",
                Framework.HIPAA, ["SEC"],
                "Training"),
    ]


def _get_pci_dss_controls() -> list[Control]:
    """PCI-DSS relevant controls."""
    return [
        Control("PCI-1.1", "Network Controls",
                "Install and maintain network security controls",
                Framework.PCI_DSS, ["SSRF", "CORS"],
                "Network"),
        Control("PCI-2.1", "Secure Configurations",
                "Apply secure configurations to all components",
                Framework.PCI_DSS, ["SEC", "MCP"],
                "Configuration"),
        Control("PCI-3.1", "Protect Stored Data",
                "Protect stored account data",
                Framework.PCI_DSS, ["CRYPTO", "LEAK"],
                "Data Protection"),
        Control("PCI-6.1", "Secure Development",
                "Develop and maintain secure systems",
                Framework.PCI_DSS, ["CMD", "INJ", "XSS", "DESER"],
                "Development"),
        Control("PCI-7.1", "Restrict Access",
                "Restrict access by business need to know",
                Framework.PCI_DSS, ["AUTH", "PRIV"],
                "Access Control"),
        Control("PCI-8.1", "Identify Users",
                "Identify and authenticate users",
                Framework.PCI_DSS, ["AUTH"],
                "Authentication"),
        Control("PCI-10.1", "Logging",
                "Log and monitor all access to system components",
                Framework.PCI_DSS, ["LOG"],
                "Logging"),
    ]


_FRAMEWORK_CONTROLS: dict[Framework, Any] = {
    Framework.OWASP_AGENTIC: _get_owasp_controls,
    Framework.NIST_AI_RMF: _get_nist_controls,
    Framework.EU_AI_ACT: _get_eu_ai_act_controls,
    Framework.SOC2: _get_soc2_controls,
    Framework.HIPAA: _get_hipaa_controls,
    Framework.PCI_DSS: _get_pci_dss_controls,
}


def get_framework_controls(framework: Framework) -> list[Control]:
    """Get controls for a specific framework.

    Args:
        framework: The compliance framework.

    Returns:
        List of Control definitions.
    """
    getter = _FRAMEWORK_CONTROLS.get(framework)
    if getter:
        return getter()
    return []


def list_supported_frameworks() -> list[str]:
    """List all supported framework identifiers."""
    return [f.value for f in Framework]


# ---------------------------------------------------------------------------
# Core Analysis
# ---------------------------------------------------------------------------

def _extract_rule_prefixes(scan_data: dict[str, Any]) -> set[str]:
    """Extract all rule ID prefixes from scan results."""
    prefixes: set[str] = set()
    results = scan_data.get("results", [])
    for r in results:
        for f in r.get("findings", []):
            rule_id = f.get("rule_id", "")
            if rule_id:
                prefixes.add(rule_id)
                # Also add prefix (e.g., "CMD" from "CMD-001")
                parts = rule_id.split("-")
                if parts:
                    prefixes.add(parts[0])
    return prefixes


def _evaluate_control(
    control: Control, active_rules: set[str]
) -> ControlResult:
    """Evaluate a single control against active scan rules."""
    if not control.required_rules:
        return ControlResult(
            control=control,
            status=ControlStatus.NOT_APPLICABLE,
            coverage_pct=100.0,
            notes="No specific rules mapped",
        )

    matched: list[str] = []
    missing: list[str] = []
    for req in control.required_rules:
        if any(rule.startswith(req) or rule == req for rule in active_rules):
            matched.append(req)
        else:
            missing.append(req)

    total = len(control.required_rules)
    covered = len(matched)
    pct = (covered / total * 100) if total > 0 else 0.0

    if covered == total:
        status = ControlStatus.COVERED
    elif covered > 0:
        status = ControlStatus.PARTIAL
    else:
        status = ControlStatus.GAP

    return ControlResult(
        control=control,
        status=status,
        matched_rules=matched,
        missing_rules=missing,
        coverage_pct=round(pct, 1),
    )


def analyze_gaps(
    scan_result: dict[str, Any],
    frameworks: list[str] | None = None,
) -> GapAnalysis:
    """Analyze compliance gaps against specified frameworks.

    Args:
        scan_result: ScanSummary.to_dict() output or compatible dict.
        frameworks: List of framework identifiers. None = all frameworks.

    Returns:
        Complete GapAnalysis with framework results and roadmap.
    """
    summary = scan_result.get("summary", {})
    active_rules = _extract_rule_prefixes(scan_result)

    # Determine which frameworks to evaluate
    if frameworks:
        fw_list = [Framework.from_string(f) for f in frameworks]
    else:
        fw_list = Framework.all_frameworks()

    analysis = GapAnalysis(
        scan_grade=summary.get("overall_grade", "?"),
        total_findings=summary.get("total_findings", 0),
    )

    for fw in fw_list:
        controls = get_framework_controls(fw)
        fw_result = FrameworkResult(framework=fw)
        for ctrl in controls:
            result = _evaluate_control(ctrl, active_rules)
            fw_result.controls.append(result)
        analysis.frameworks.append(fw_result)

    # Generate remediation roadmap
    analysis.roadmap = generate_remediation_roadmap(analysis)

    return analysis


def generate_remediation_roadmap(analysis: GapAnalysis) -> Roadmap:
    """Generate a prioritized remediation roadmap from gap analysis.

    Args:
        analysis: GapAnalysis result (or GapAnalysis with frameworks set).

    Returns:
        Roadmap with prioritized actions.
    """
    roadmap = Roadmap()
    action_idx = 0

    # Collect all gaps and partials
    gap_controls: list[tuple[ControlResult, Framework]] = []
    for fw_result in analysis.frameworks:
        for ctrl_result in fw_result.controls:
            if ctrl_result.status in (ControlStatus.GAP, ControlStatus.PARTIAL):
                gap_controls.append((ctrl_result, fw_result.framework))

    # Group by missing rules to avoid duplicates
    missing_to_controls: dict[str, list[tuple[str, Framework]]] = {}
    for ctrl_result, fw in gap_controls:
        for missing in ctrl_result.missing_rules:
            if missing not in missing_to_controls:
                missing_to_controls[missing] = []
            missing_to_controls[missing].append(
                (ctrl_result.control.control_id, fw)
            )

    # Priority based on how many controls a missing rule affects
    for missing_rule, affected in sorted(
        missing_to_controls.items(),
        key=lambda x: len(x[1]),
        reverse=True,
    ):
        action_idx += 1
        count = len(affected)

        if count >= 4:
            priority = RemediationPriority.CRITICAL
            effort = "medium"
            days = 3
        elif count >= 2:
            priority = RemediationPriority.HIGH
            effort = "medium"
            days = 5
        else:
            priority = RemediationPriority.MEDIUM
            effort = "low"
            days = 3

        # Use framework from first affected control
        fw = affected[0][1]
        control_ids = [cid for cid, _ in affected]

        roadmap.actions.append(RemediationAction(
            action_id=f"REM-{action_idx:03d}",
            description=f"Implement {missing_rule} rules to cover {count} control(s)",
            priority=priority,
            framework=fw,
            control_ids=control_ids,
            effort=effort,
            estimated_days=days,
        ))

    # Sort by priority
    roadmap.actions.sort(key=lambda a: a.priority.order)
    roadmap.total_estimated_days = sum(a.estimated_days for a in roadmap.actions)

    return roadmap


# ---------------------------------------------------------------------------
# Formatters
# ---------------------------------------------------------------------------

def format_gaps_text(analysis: GapAnalysis) -> str:
    """Format gap analysis as human-readable text.

    Args:
        analysis: GapAnalysis result.

    Returns:
        Formatted text string.
    """
    lines: list[str] = []
    lines.append(f"\n{'=' * 70}")
    lines.append("  Compliance Gap Analysis")
    lines.append(f"{'=' * 70}")
    lines.append(f"  Scan Grade: {analysis.scan_grade}")
    lines.append(f"  Overall Coverage: {analysis.overall_coverage}%")
    lines.append(f"  Total Gaps: {analysis.total_gaps}")
    lines.append("")

    for fw_result in analysis.frameworks:
        lines.append(f"  --- {fw_result.framework.value.upper()} ---")
        lines.append(f"  Coverage: {fw_result.coverage_pct}%")
        lines.append(
            f"  Covered: {fw_result.covered_controls} | "
            f"Partial: {fw_result.partial_controls} | "
            f"Gap: {fw_result.gap_controls}"
        )

        for cr in fw_result.controls:
            status_sym = {
                ControlStatus.COVERED: "[+]",
                ControlStatus.PARTIAL: "[~]",
                ControlStatus.GAP: "[-]",
                ControlStatus.NOT_APPLICABLE: "[.]",
            }.get(cr.status, "[?]")
            lines.append(
                f"    {status_sym} {cr.control.control_id}: "
                f"{cr.control.name} ({cr.coverage_pct}%)"
            )
        lines.append("")

    # Roadmap
    if analysis.roadmap.actions:
        lines.append("  Remediation Roadmap:")
        lines.append(
            f"  Total Actions: {len(analysis.roadmap.actions)} | "
            f"Estimated Days: {analysis.roadmap.total_estimated_days}"
        )
        for action in analysis.roadmap.actions:
            lines.append(
                f"    [{action.priority.value}] {action.action_id}: "
                f"{action.description} ({action.estimated_days}d)"
            )
    lines.append(f"\n{'=' * 70}\n")
    return "\n".join(lines)


def format_gaps_json(analysis: GapAnalysis) -> str:
    """Format gap analysis as JSON.

    Args:
        analysis: GapAnalysis result.

    Returns:
        JSON string.
    """
    return json.dumps(analysis.to_dict(), indent=2)


def format_gaps_html(analysis: GapAnalysis) -> str:
    """Format gap analysis as HTML report.

    Args:
        analysis: GapAnalysis result.

    Returns:
        Complete HTML document string.
    """
    # Framework summary cards
    fw_cards = ""
    for fw_result in analysis.frameworks:
        cov_color = _coverage_color(fw_result.coverage_pct)
        fw_cards += (
            f'<div class="fw-card">'
            f'<div class="fw-name">{fw_result.framework.value.upper()}</div>'
            f'<div class="fw-cov" style="color:{cov_color}">'
            f'{fw_result.coverage_pct}%</div>'
            f'<div class="fw-detail">'
            f'Covered: {fw_result.covered_controls} | '
            f'Partial: {fw_result.partial_controls} | '
            f'Gaps: {fw_result.gap_controls}'
            f'</div></div>'
        )

    # Control checklists
    checklist_sections = ""
    for fw_result in analysis.frameworks:
        rows = ""
        for cr in fw_result.controls:
            status_color = {
                ControlStatus.COVERED: "#22c55e",
                ControlStatus.PARTIAL: "#eab308",
                ControlStatus.GAP: "#ef4444",
                ControlStatus.NOT_APPLICABLE: "#6b7280",
            }.get(cr.status, "#6b7280")
            status_icon = {
                ControlStatus.COVERED: "&#10003;",
                ControlStatus.PARTIAL: "&#9679;",
                ControlStatus.GAP: "&#10007;",
                ControlStatus.NOT_APPLICABLE: "&#8212;",
            }.get(cr.status, "?")
            missing = ", ".join(cr.missing_rules) if cr.missing_rules else "-"
            rows += (
                f'<tr>'
                f'<td style="color:{status_color};font-weight:bold">'
                f'{status_icon}</td>'
                f'<td>{_escape_html(cr.control.control_id)}</td>'
                f'<td>{_escape_html(cr.control.name)}</td>'
                f'<td>{cr.coverage_pct}%</td>'
                f'<td>{missing}</td>'
                f'</tr>'
            )

        checklist_sections += f"""
        <h3>{fw_result.framework.value.upper()}</h3>
        <table>
        <thead><tr>
            <th>Status</th><th>ID</th><th>Control</th>
            <th>Coverage</th><th>Missing Rules</th>
        </tr></thead>
        <tbody>{rows}</tbody>
        </table>"""

    # Roadmap table
    roadmap_rows = ""
    for action in analysis.roadmap.actions:
        prio_color = {
            "Critical": "#ef4444",
            "High": "#f97316",
            "Medium": "#eab308",
            "Low": "#22c55e",
        }.get(action.priority.value, "#6b7280")
        cids = ", ".join(action.control_ids[:3])
        if len(action.control_ids) > 3:
            cids += f" (+{len(action.control_ids) - 3})"
        roadmap_rows += (
            f'<tr>'
            f'<td>{action.action_id}</td>'
            f'<td style="color:{prio_color};font-weight:bold">'
            f'{action.priority.value}</td>'
            f'<td>{_escape_html(action.description)}</td>'
            f'<td>{action.effort}</td>'
            f'<td>{action.estimated_days}d</td>'
            f'<td>{cids}</td>'
            f'</tr>'
        )

    overall_color = _coverage_color(analysis.overall_coverage)

    return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Compliance Gap Analysis</title>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
       background: #0f172a; color: #e2e8f0; padding: 24px; line-height: 1.6; }}
h1 {{ font-size: 1.5rem; margin-bottom: 4px; }}
h2 {{ font-size: 1.2rem; margin: 24px 0 12px; color: #94a3b8; }}
h3 {{ font-size: 1rem; margin: 16px 0 8px; }}
.subtitle {{ color: #94a3b8; margin-bottom: 24px; }}
.summary-cards {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
                  gap: 12px; margin-bottom: 24px; }}
.card {{ background: #1e293b; border-radius: 10px; padding: 16px; text-align: center; }}
.card-label {{ font-size: 0.7rem; text-transform: uppercase; color: #94a3b8; }}
.card-value {{ font-size: 1.8rem; font-weight: 700; margin-top: 4px; }}
.fw-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 12px; margin-bottom: 24px; }}
.fw-card {{ background: #1e293b; border-radius: 10px; padding: 16px; }}
.fw-name {{ font-size: 0.8rem; text-transform: uppercase; color: #94a3b8;
            font-weight: 600; }}
.fw-cov {{ font-size: 1.5rem; font-weight: 700; margin: 4px 0; }}
.fw-detail {{ font-size: 0.8rem; color: #94a3b8; }}
table {{ width: 100%; border-collapse: collapse; background: #1e293b;
         border-radius: 10px; overflow: hidden; margin-bottom: 16px; }}
th {{ text-align: left; padding: 10px 14px; background: #334155;
      font-size: 0.75rem; text-transform: uppercase; color: #94a3b8; }}
td {{ padding: 8px 14px; border-bottom: 1px solid #334155; font-size: 0.85rem; }}
tr:hover {{ background: #1e3a5f; }}
.footer {{ text-align: center; color: #475569; margin-top: 32px; font-size: 0.8rem; }}
</style>
</head>
<body>
<h1>Compliance Gap Analysis</h1>
<p class="subtitle">Gap analysis across security compliance frameworks</p>

<div class="summary-cards">
    <div class="card">
        <div class="card-label">Overall Coverage</div>
        <div class="card-value" style="color:{overall_color}">{analysis.overall_coverage}%</div>
    </div>
    <div class="card">
        <div class="card-label">Total Gaps</div>
        <div class="card-value" style="color:#ef4444">{analysis.total_gaps}</div>
    </div>
    <div class="card">
        <div class="card-label">Frameworks</div>
        <div class="card-value">{len(analysis.frameworks)}</div>
    </div>
    <div class="card">
        <div class="card-label">Remediation Actions</div>
        <div class="card-value">{len(analysis.roadmap.actions)}</div>
    </div>
</div>

<h2>Framework Coverage</h2>
<div class="fw-grid">{fw_cards}</div>

<h2>Control Checklists</h2>
{checklist_sections}

<h2>Remediation Roadmap</h2>
<p style="color:#94a3b8;margin-bottom:12px">
    Total: {len(analysis.roadmap.actions)} actions |
    Estimated: {analysis.roadmap.total_estimated_days} days
</p>
<table>
<thead><tr>
    <th>ID</th><th>Priority</th><th>Action</th>
    <th>Effort</th><th>Days</th><th>Controls</th>
</tr></thead>
<tbody>{roadmap_rows}</tbody>
</table>

<p class="footer">Generated by mcpaudit compliance gap analyzer</p>
</body>
</html>"""


def _coverage_color(pct: float) -> str:
    """Return color for a coverage percentage."""
    if pct >= 80:
        return "#22c55e"
    if pct >= 60:
        return "#eab308"
    if pct >= 40:
        return "#f97316"
    return "#ef4444"


def _escape_html(text: str) -> str:
    """Escape HTML special characters."""
    return (
        text.replace("&", "&amp;")
        .replace("<", "&lt;")
        .replace(">", "&gt;")
        .replace('"', "&quot;")
    )
