"""MCP Ecosystem Analyzer — Holistic analysis of MCP server ecosystems.

Analyzes the complete MCP ecosystem of an organization, mapping
relationships, trust boundaries, data flows, and detecting architectural
issues like single points of failure and over-connected servers.

Features:
- Ecosystem map with server relationships and trust zones
- Trust zones: internal, external, privileged, sandbox
- Detection: single points of failure, over-connected, orphan servers
- Data flow analysis between servers
- HTML ecosystem report generation

CLI: mcpaudit ecosystem --analyze | --report | --html

Zero external dependencies. Python 3.11+ stdlib only.
Part of mcpaudit v3.0.0.
"""

from __future__ import annotations

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


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

@unique
class TrustZone(Enum):
    """Trust zone classification for MCP servers."""
    INTERNAL = "internal"
    EXTERNAL = "external"
    PRIVILEGED = "privileged"
    SANDBOX = "sandbox"

    @classmethod
    def from_string(cls, value: str) -> TrustZone:
        normalized = value.lower().strip()
        for member in cls:
            if member.value == normalized:
                return member
        return cls.INTERNAL


@unique
class ConnectionType(Enum):
    """Type of connection between MCP servers."""
    DATA_FLOW = "data_flow"
    CONTROL = "control"
    DELEGATION = "delegation"
    SHARED_RESOURCE = "shared_resource"


@unique
class IssueSeverity(Enum):
    """Severity of an ecosystem issue."""
    CRITICAL = "CRITICAL"
    HIGH = "HIGH"
    MEDIUM = "MEDIUM"
    LOW = "LOW"
    INFO = "INFO"

    @property
    def weight(self) -> int:
        return {
            IssueSeverity.CRITICAL: 10,
            IssueSeverity.HIGH: 5,
            IssueSeverity.MEDIUM: 2,
            IssueSeverity.LOW: 1,
            IssueSeverity.INFO: 0,
        }[self]


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

@dataclass
class MCPServerConfig:
    """Configuration of a single MCP server in the ecosystem."""
    name: str
    path: str = ""
    transport: str = "stdio"
    trust_zone: TrustZone = TrustZone.INTERNAL
    tools: list[str] = field(default_factory=list)
    resources: list[str] = field(default_factory=list)
    capabilities: list[str] = field(default_factory=list)
    dependencies: list[str] = field(default_factory=list)  # names of other servers
    tags: list[str] = field(default_factory=list)
    metadata: dict[str, Any] = field(default_factory=dict)

    @property
    def tool_count(self) -> int:
        return len(self.tools)

    @property
    def connection_count(self) -> int:
        return len(self.dependencies)

    def to_dict(self) -> dict[str, Any]:
        return {
            "name": self.name,
            "path": self.path,
            "transport": self.transport,
            "trust_zone": self.trust_zone.value,
            "tools": list(self.tools),
            "resources": list(self.resources),
            "capabilities": list(self.capabilities),
            "dependencies": list(self.dependencies),
            "tags": list(self.tags),
            "metadata": dict(self.metadata),
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> MCPServerConfig:
        return cls(
            name=data.get("name", ""),
            path=data.get("path", ""),
            transport=data.get("transport", "stdio"),
            trust_zone=TrustZone.from_string(data.get("trust_zone", "internal")),
            tools=data.get("tools", []),
            resources=data.get("resources", []),
            capabilities=data.get("capabilities", []),
            dependencies=data.get("dependencies", []),
            tags=data.get("tags", []),
            metadata=data.get("metadata", {}),
        )


@dataclass
class ServerConnection:
    """A connection between two servers in the ecosystem."""
    source: str
    target: str
    connection_type: ConnectionType = ConnectionType.DATA_FLOW
    description: str = ""
    bidirectional: bool = False

    def to_dict(self) -> dict[str, Any]:
        return {
            "source": self.source,
            "target": self.target,
            "connection_type": self.connection_type.value,
            "description": self.description,
            "bidirectional": self.bidirectional,
        }


@dataclass
class EcosystemIssue:
    """An issue detected in the ecosystem."""
    issue_id: str
    title: str
    description: str
    severity: IssueSeverity
    affected_servers: list[str] = field(default_factory=list)
    recommendation: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "issue_id": self.issue_id,
            "title": self.title,
            "description": self.description,
            "severity": self.severity.value,
            "affected_servers": list(self.affected_servers),
            "recommendation": self.recommendation,
        }


@dataclass
class TrustBoundary:
    """A trust boundary between zones in the ecosystem."""
    zone_a: TrustZone
    zone_b: TrustZone
    crossing_connections: list[ServerConnection] = field(default_factory=list)

    @property
    def risk_level(self) -> str:
        """Risk level of this trust boundary crossing."""
        high_risk = {
            (TrustZone.SANDBOX, TrustZone.PRIVILEGED),
            (TrustZone.EXTERNAL, TrustZone.PRIVILEGED),
            (TrustZone.EXTERNAL, TrustZone.INTERNAL),
        }
        pair = frozenset([self.zone_a, self.zone_b])
        for a, b in high_risk:
            if frozenset([a, b]) == pair:
                return "HIGH"
        return "MEDIUM"

    def to_dict(self) -> dict[str, Any]:
        return {
            "zone_a": self.zone_a.value,
            "zone_b": self.zone_b.value,
            "risk_level": self.risk_level,
            "crossing_count": len(self.crossing_connections),
            "connections": [c.to_dict() for c in self.crossing_connections],
        }


@dataclass
class EcosystemMetrics:
    """Aggregate metrics for the ecosystem."""
    total_servers: int = 0
    total_connections: int = 0
    total_tools: int = 0
    total_resources: int = 0
    zone_distribution: dict[str, int] = field(default_factory=dict)
    transport_distribution: dict[str, int] = field(default_factory=dict)
    avg_tools_per_server: float = 0.0
    avg_connections_per_server: float = 0.0
    max_connections: int = 0
    max_connections_server: str = ""
    orphan_count: int = 0
    issue_count: int = 0
    health_score: int = 100

    def to_dict(self) -> dict[str, Any]:
        return {
            "total_servers": self.total_servers,
            "total_connections": self.total_connections,
            "total_tools": self.total_tools,
            "total_resources": self.total_resources,
            "zone_distribution": dict(self.zone_distribution),
            "transport_distribution": dict(self.transport_distribution),
            "avg_tools_per_server": round(self.avg_tools_per_server, 1),
            "avg_connections_per_server": round(self.avg_connections_per_server, 1),
            "max_connections": self.max_connections,
            "max_connections_server": self.max_connections_server,
            "orphan_count": self.orphan_count,
            "issue_count": self.issue_count,
            "health_score": self.health_score,
        }


@dataclass
class EcosystemAnalysis:
    """Complete analysis of an MCP ecosystem."""
    servers: list[MCPServerConfig] = field(default_factory=list)
    connections: list[ServerConnection] = field(default_factory=list)
    trust_boundaries: list[TrustBoundary] = field(default_factory=list)
    issues: list[EcosystemIssue] = field(default_factory=list)
    metrics: EcosystemMetrics = field(default_factory=EcosystemMetrics)
    timestamp: str = ""

    def __post_init__(self):
        if not self.timestamp:
            self.timestamp = datetime.now(timezone.utc).isoformat()

    @property
    def server_count(self) -> int:
        return len(self.servers)

    @property
    def issue_count(self) -> int:
        return len(self.issues)

    @property
    def critical_issues(self) -> list[EcosystemIssue]:
        return [i for i in self.issues if i.severity == IssueSeverity.CRITICAL]

    @property
    def high_issues(self) -> list[EcosystemIssue]:
        return [i for i in self.issues if i.severity == IssueSeverity.HIGH]

    def get_server(self, name: str) -> MCPServerConfig | None:
        for s in self.servers:
            if s.name == name:
                return s
        return None

    def to_dict(self) -> dict[str, Any]:
        return {
            "servers": [s.to_dict() for s in self.servers],
            "connections": [c.to_dict() for c in self.connections],
            "trust_boundaries": [tb.to_dict() for tb in self.trust_boundaries],
            "issues": [i.to_dict() for i in self.issues],
            "metrics": self.metrics.to_dict(),
            "timestamp": self.timestamp,
        }


# ---------------------------------------------------------------------------
# Analysis engine
# ---------------------------------------------------------------------------

def _build_connections(servers: list[MCPServerConfig]) -> list[ServerConnection]:
    """Build connection list from server dependency declarations."""
    server_names = {s.name for s in servers}
    connections: list[ServerConnection] = []

    for server in servers:
        for dep in server.dependencies:
            if dep in server_names:
                connections.append(ServerConnection(
                    source=server.name,
                    target=dep,
                    connection_type=ConnectionType.DATA_FLOW,
                    description=f"{server.name} depends on {dep}",
                ))

    # Detect shared resources
    resource_owners: dict[str, list[str]] = {}
    for server in servers:
        for res in server.resources:
            resource_owners.setdefault(res, []).append(server.name)

    for resource, owners in resource_owners.items():
        if len(owners) > 1:
            for i in range(len(owners)):
                for j in range(i + 1, len(owners)):
                    connections.append(ServerConnection(
                        source=owners[i],
                        target=owners[j],
                        connection_type=ConnectionType.SHARED_RESOURCE,
                        description=f"Shared resource: {resource}",
                        bidirectional=True,
                    ))

    return connections


def _build_trust_boundaries(
    servers: list[MCPServerConfig],
    connections: list[ServerConnection],
) -> list[TrustBoundary]:
    """Identify trust boundaries where connections cross zones."""
    server_zones: dict[str, TrustZone] = {s.name: s.trust_zone for s in servers}
    boundary_map: dict[tuple[TrustZone, TrustZone], list[ServerConnection]] = {}

    for conn in connections:
        zone_a = server_zones.get(conn.source, TrustZone.INTERNAL)
        zone_b = server_zones.get(conn.target, TrustZone.INTERNAL)
        if zone_a != zone_b:
            key = (min(zone_a, zone_b, key=lambda z: z.value),
                   max(zone_a, zone_b, key=lambda z: z.value))
            boundary_map.setdefault(key, []).append(conn)

    return [
        TrustBoundary(zone_a=z_a, zone_b=z_b, crossing_connections=conns)
        for (z_a, z_b), conns in boundary_map.items()
    ]


def _detect_orphans(
    servers: list[MCPServerConfig],
    connections: list[ServerConnection],
) -> list[str]:
    """Detect orphan servers with no connections."""
    connected: set[str] = set()
    for conn in connections:
        connected.add(conn.source)
        connected.add(conn.target)

    orphans = []
    for server in servers:
        if server.name not in connected and not server.dependencies:
            orphans.append(server.name)
    return orphans


def _detect_single_points_of_failure(
    servers: list[MCPServerConfig],
    connections: list[ServerConnection],
) -> list[str]:
    """Detect servers that are single points of failure.

    A server is a SPOF if removing it would disconnect other servers
    that depend only on it.
    """
    # Count how many servers depend on each target
    dependents: dict[str, list[str]] = {}
    for conn in connections:
        if conn.connection_type in (ConnectionType.DATA_FLOW, ConnectionType.CONTROL):
            dependents.setdefault(conn.target, []).append(conn.source)

    spofs = []
    for target, deps in dependents.items():
        if len(deps) >= 2:
            spofs.append(target)

    return spofs


def _detect_over_connected(
    servers: list[MCPServerConfig],
    connections: list[ServerConnection],
    threshold: int = 5,
) -> list[str]:
    """Detect servers with too many connections."""
    conn_counts: Counter[str] = Counter()
    for conn in connections:
        conn_counts[conn.source] += 1
        conn_counts[conn.target] += 1

    return [name for name, count in conn_counts.items() if count >= threshold]


def _detect_issues(
    servers: list[MCPServerConfig],
    connections: list[ServerConnection],
    trust_boundaries: list[TrustBoundary],
) -> list[EcosystemIssue]:
    """Detect all ecosystem issues."""
    issues: list[EcosystemIssue] = []
    issue_counter = 0

    # 1. Orphan servers
    orphans = _detect_orphans(servers, connections)
    for orphan in orphans:
        issue_counter += 1
        issues.append(EcosystemIssue(
            issue_id=f"ECO-{issue_counter:03d}",
            title="Orphan server detected",
            description=f"Server '{orphan}' has no connections to other servers",
            severity=IssueSeverity.LOW,
            affected_servers=[orphan],
            recommendation="Review if this server should be connected or removed",
        ))

    # 2. Single points of failure
    spofs = _detect_single_points_of_failure(servers, connections)
    for spof in spofs:
        issue_counter += 1
        issues.append(EcosystemIssue(
            issue_id=f"ECO-{issue_counter:03d}",
            title="Single point of failure",
            description=f"Server '{spof}' is a dependency for multiple servers",
            severity=IssueSeverity.HIGH,
            affected_servers=[spof],
            recommendation="Add redundancy or fallback mechanisms",
        ))

    # 3. Over-connected servers
    over_connected = _detect_over_connected(servers, connections)
    for oc in over_connected:
        issue_counter += 1
        issues.append(EcosystemIssue(
            issue_id=f"ECO-{issue_counter:03d}",
            title="Over-connected server",
            description=f"Server '{oc}' has many connections — attack surface risk",
            severity=IssueSeverity.MEDIUM,
            affected_servers=[oc],
            recommendation="Consider splitting into smaller, focused servers",
        ))

    # 4. High-risk trust boundary crossings
    for tb in trust_boundaries:
        if tb.risk_level == "HIGH":
            affected = set()
            for conn in tb.crossing_connections:
                affected.add(conn.source)
                affected.add(conn.target)
            issue_counter += 1
            issues.append(EcosystemIssue(
                issue_id=f"ECO-{issue_counter:03d}",
                title="High-risk trust boundary crossing",
                description=(
                    f"Connections cross from {tb.zone_a.value} to {tb.zone_b.value} "
                    f"({len(tb.crossing_connections)} connections)"
                ),
                severity=IssueSeverity.HIGH,
                affected_servers=sorted(affected),
                recommendation="Add authentication and authorization at trust boundaries",
            ))

    # 5. Privileged servers with many tools
    for server in servers:
        if server.trust_zone == TrustZone.PRIVILEGED and server.tool_count > 10:
            issue_counter += 1
            issues.append(EcosystemIssue(
                issue_id=f"ECO-{issue_counter:03d}",
                title="Privileged server with excessive tools",
                description=(
                    f"Server '{server.name}' is in privileged zone with "
                    f"{server.tool_count} tools"
                ),
                severity=IssueSeverity.CRITICAL,
                affected_servers=[server.name],
                recommendation="Reduce tool count or move to a less privileged zone",
            ))

    # 6. External servers with privileged dependencies
    server_zones = {s.name: s.trust_zone for s in servers}
    for conn in connections:
        src_zone = server_zones.get(conn.source, TrustZone.INTERNAL)
        tgt_zone = server_zones.get(conn.target, TrustZone.INTERNAL)
        if src_zone == TrustZone.EXTERNAL and tgt_zone == TrustZone.PRIVILEGED:
            issue_counter += 1
            issues.append(EcosystemIssue(
                issue_id=f"ECO-{issue_counter:03d}",
                title="External server accessing privileged zone",
                description=(
                    f"External server '{conn.source}' has direct access "
                    f"to privileged server '{conn.target}'"
                ),
                severity=IssueSeverity.CRITICAL,
                affected_servers=[conn.source, conn.target],
                recommendation="Route through an internal proxy with auth checks",
            ))

    return issues


def _compute_metrics(
    servers: list[MCPServerConfig],
    connections: list[ServerConnection],
    orphans: list[str],
    issues: list[EcosystemIssue],
) -> EcosystemMetrics:
    """Compute aggregate ecosystem metrics."""
    total_tools = sum(s.tool_count for s in servers)
    total_resources = sum(len(s.resources) for s in servers)

    zone_dist: Counter[str] = Counter()
    transport_dist: Counter[str] = Counter()
    for s in servers:
        zone_dist[s.trust_zone.value] += 1
        transport_dist[s.transport] += 1

    conn_counts: Counter[str] = Counter()
    for c in connections:
        conn_counts[c.source] += 1
        conn_counts[c.target] += 1

    max_conn_server = ""
    max_conn = 0
    if conn_counts:
        max_conn_server = conn_counts.most_common(1)[0][0]
        max_conn = conn_counts.most_common(1)[0][1]

    n = len(servers) or 1
    avg_tools = total_tools / n
    avg_conns = len(connections) / n

    # Health score: start at 100, deduct per issue
    health = 100
    for issue in issues:
        health -= issue.severity.weight
    health = max(0, min(100, health))

    return EcosystemMetrics(
        total_servers=len(servers),
        total_connections=len(connections),
        total_tools=total_tools,
        total_resources=total_resources,
        zone_distribution=dict(zone_dist),
        transport_distribution=dict(transport_dist),
        avg_tools_per_server=avg_tools,
        avg_connections_per_server=avg_conns,
        max_connections=max_conn,
        max_connections_server=max_conn_server,
        orphan_count=len(orphans),
        issue_count=len(issues),
        health_score=health,
    )


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def analyze_ecosystem(
    servers: list[MCPServerConfig | dict[str, Any]],
) -> EcosystemAnalysis:
    """Analyze an MCP server ecosystem.

    Args:
        servers: List of MCPServerConfig objects or dicts describing servers.

    Returns:
        EcosystemAnalysis with complete ecosystem analysis.
    """
    # Normalize inputs
    normalized: list[MCPServerConfig] = []
    for s in servers:
        if isinstance(s, dict):
            normalized.append(MCPServerConfig.from_dict(s))
        else:
            normalized.append(s)

    # Build connections from declared dependencies
    connections = _build_connections(normalized)

    # Identify trust boundaries
    trust_boundaries = _build_trust_boundaries(normalized, connections)

    # Detect issues
    issues = _detect_issues(normalized, connections, trust_boundaries)

    # Orphans
    orphans = _detect_orphans(normalized, connections)

    # Compute metrics
    metrics = _compute_metrics(normalized, connections, orphans, issues)

    return EcosystemAnalysis(
        servers=normalized,
        connections=connections,
        trust_boundaries=trust_boundaries,
        issues=issues,
        metrics=metrics,
    )


def analyze_ecosystem_from_dicts(
    server_dicts: list[dict[str, Any]],
) -> EcosystemAnalysis:
    """Convenience: analyze from a list of server dicts."""
    return analyze_ecosystem(server_dicts)


# ---------------------------------------------------------------------------
# HTML Report
# ---------------------------------------------------------------------------

def generate_ecosystem_html(analysis: EcosystemAnalysis) -> str:
    """Generate a self-contained HTML report for the ecosystem analysis.

    Args:
        analysis: EcosystemAnalysis to render.

    Returns:
        HTML string.
    """
    m = analysis.metrics

    # Zone colors
    zone_colors = {
        "internal": "#3b82f6",
        "external": "#ef4444",
        "privileged": "#f59e0b",
        "sandbox": "#22c55e",
    }

    severity_colors = {
        "CRITICAL": "#dc2626",
        "HIGH": "#f97316",
        "MEDIUM": "#eab308",
        "LOW": "#22c55e",
        "INFO": "#6b7280",
    }

    # Server cards
    server_html = ""
    for s in analysis.servers:
        zc = zone_colors.get(s.trust_zone.value, "#6b7280")
        tools_list = ", ".join(html_mod.escape(t) for t in s.tools[:10])
        if len(s.tools) > 10:
            tools_list += f" ... (+{len(s.tools) - 10} more)"
        deps_list = ", ".join(html_mod.escape(d) for d in s.dependencies) or "none"
        server_html += (
            f"<div class='card'>"
            f"<div class='card-header'>"
            f"<span class='zone-badge' style='background:{zc}'>"
            f"{html_mod.escape(s.trust_zone.value)}</span>"
            f"<strong>{html_mod.escape(s.name)}</strong>"
            f"<span class='transport'>{html_mod.escape(s.transport)}</span>"
            f"</div>"
            f"<div class='card-body'>"
            f"<div>Tools ({s.tool_count}): {tools_list}</div>"
            f"<div>Dependencies: {deps_list}</div>"
            f"</div></div>"
        )

    # Issue cards
    issue_html = ""
    for issue in analysis.issues:
        sc = severity_colors.get(issue.severity.value, "#6b7280")
        affected = ", ".join(html_mod.escape(s) for s in issue.affected_servers)
        issue_html += (
            f"<div class='card'>"
            f"<div class='card-header'>"
            f"<span class='zone-badge' style='background:{sc}'>"
            f"{html_mod.escape(issue.severity.value)}</span>"
            f"<strong>{html_mod.escape(issue.issue_id)}: "
            f"{html_mod.escape(issue.title)}</strong>"
            f"</div>"
            f"<div class='card-body'>"
            f"<p>{html_mod.escape(issue.description)}</p>"
            f"<div class='affected'>Affected: {affected}</div>"
            f"<div class='recommendation'>"
            f"Recommendation: {html_mod.escape(issue.recommendation)}</div>"
            f"</div></div>"
        )

    # Trust boundary rows
    tb_html = ""
    for tb in analysis.trust_boundaries:
        risk_color = "#f97316" if tb.risk_level == "HIGH" else "#eab308"
        tb_html += (
            f"<tr>"
            f"<td>{html_mod.escape(tb.zone_a.value)}</td>"
            f"<td>{html_mod.escape(tb.zone_b.value)}</td>"
            f"<td style='color:{risk_color}'>{tb.risk_level}</td>"
            f"<td>{len(tb.crossing_connections)}</td>"
            f"</tr>"
        )

    css = """
:root{--bg:#0f172a;--surface:#1e293b;--text:#e2e8f0;--text-dim:#94a3b8;--border:#475569}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:var(--bg);
color:var(--text);padding:2rem;max-width:1000px;margin:0 auto;line-height:1.6}
h1{font-size:1.5rem;margin-bottom:.25rem}
h2{font-size:1.2rem;margin:1.5rem 0 .75rem;border-bottom:1px solid var(--border);padding-bottom:.25rem}
.subtitle{color:var(--text-dim);font-size:.85rem;margin-bottom:1.5rem}
.metrics{background:var(--surface);border-radius:8px;padding:1.2rem;margin-bottom:1.5rem;
display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:1rem}
.metric{text-align:center}
.metric .label{font-size:.7rem;color:var(--text-dim);text-transform:uppercase}
.metric .value{font-size:1.4rem;font-weight:700}
.card{background:var(--surface);border-radius:8px;padding:1rem;margin-bottom:.75rem}
.card-header{display:flex;gap:.5rem;align-items:center;margin-bottom:.5rem}
.zone-badge{padding:2px 8px;border-radius:4px;color:#fff;font-size:.7rem;font-weight:600}
.transport{color:var(--text-dim);font-size:.75rem;margin-left:auto}
.card-body{font-size:.85rem;color:var(--text-dim)}
.card-body div{margin:.25rem 0}
.affected{font-family:monospace;font-size:.8rem}
.recommendation{font-style:italic;color:#94a3b8}
table{width:100%;border-collapse:collapse;font-size:.85rem}
th,td{text-align:left;padding:.5rem;border-bottom:1px solid var(--border)}
th{color:var(--text-dim);font-size:.75rem;text-transform:uppercase}
.health-good{color:#22c55e}.health-warn{color:#eab308}.health-bad{color:#ef4444}
.footer{text-align:center;color:var(--text-dim);font-size:.75rem;margin-top:2rem;
padding-top:1rem;border-top:1px solid var(--border)}"""

    health_class = "health-good" if m.health_score >= 80 else (
        "health-warn" if m.health_score >= 50 else "health-bad"
    )

    return (
        f"<!DOCTYPE html><html lang='en'><head><meta charset='UTF-8'>"
        f"<meta name='viewport' content='width=device-width,initial-scale=1.0'>"
        f"<title>MCP Ecosystem Analysis</title>"
        f"<style>{css}</style></head><body>"
        f"<h1>MCP Ecosystem Analysis</h1>"
        f"<div class='subtitle'>Generated: {html_mod.escape(analysis.timestamp)}"
        f" | mcpaudit v3.0.0</div>"
        # Metrics
        f"<div class='metrics'>"
        f"<div class='metric'><div class='label'>Servers</div>"
        f"<div class='value'>{m.total_servers}</div></div>"
        f"<div class='metric'><div class='label'>Connections</div>"
        f"<div class='value'>{m.total_connections}</div></div>"
        f"<div class='metric'><div class='label'>Tools</div>"
        f"<div class='value'>{m.total_tools}</div></div>"
        f"<div class='metric'><div class='label'>Issues</div>"
        f"<div class='value'>{m.issue_count}</div></div>"
        f"<div class='metric'><div class='label'>Health</div>"
        f"<div class='value {health_class}'>{m.health_score}</div></div>"
        f"<div class='metric'><div class='label'>Orphans</div>"
        f"<div class='value'>{m.orphan_count}</div></div>"
        f"</div>"
        # Servers
        f"<h2>Servers ({m.total_servers})</h2>"
        f"{server_html}"
        # Issues
        f"<h2>Issues ({len(analysis.issues)})</h2>"
        f"{issue_html if issue_html else '<p>No issues detected.</p>'}"
        # Trust boundaries
        f"<h2>Trust Boundaries</h2>"
        f"<table><thead><tr><th>Zone A</th><th>Zone B</th>"
        f"<th>Risk</th><th>Connections</th></tr></thead>"
        f"<tbody>{tb_html if tb_html else '<tr><td colspan=4>No boundary crossings</td></tr>'}"
        f"</tbody></table>"
        # Footer
        f"<div class='footer'>Generated by mcpaudit v3.0.0 — MCP Ecosystem Analyzer</div>"
        f"</body></html>"
    )


def format_ecosystem_text(analysis: EcosystemAnalysis) -> str:
    """Format ecosystem analysis as plain text."""
    m = analysis.metrics
    lines: list[str] = []
    lines.append(f"{'=' * 60}")
    lines.append("  MCP Ecosystem Analysis")
    lines.append(f"{'=' * 60}")
    lines.append(f"  Servers: {m.total_servers}")
    lines.append(f"  Connections: {m.total_connections}")
    lines.append(f"  Total tools: {m.total_tools}")
    lines.append(f"  Health score: {m.health_score}/100")
    lines.append(f"  Issues: {m.issue_count}")
    lines.append(f"  Orphans: {m.orphan_count}")

    if analysis.servers:
        lines.append(f"\n  {'─' * 50}")
        lines.append("  SERVERS")
        for s in analysis.servers:
            lines.append(
                f"    [{s.trust_zone.value}] {s.name} "
                f"({s.transport}, {s.tool_count} tools)"
            )

    if analysis.issues:
        lines.append(f"\n  {'─' * 50}")
        lines.append("  ISSUES")
        for issue in analysis.issues:
            lines.append(
                f"    [{issue.severity.value}] {issue.issue_id}: {issue.title}"
            )
            lines.append(f"      {issue.description}")

    if analysis.trust_boundaries:
        lines.append(f"\n  {'─' * 50}")
        lines.append("  TRUST BOUNDARIES")
        for tb in analysis.trust_boundaries:
            lines.append(
                f"    {tb.zone_a.value} <-> {tb.zone_b.value} "
                f"(risk: {tb.risk_level}, {len(tb.crossing_connections)} connections)"
            )

    lines.append(f"\n{'=' * 60}")
    return "\n".join(lines)


def format_ecosystem_json(analysis: EcosystemAnalysis) -> str:
    """Format ecosystem analysis as JSON."""
    return json.dumps(analysis.to_dict(), indent=2)
