"""Multi-target scanning — scan-all across directories.

Recursively discovers MCP servers in a directory tree by detecting
known patterns (mcp.server, McpServer, FastMCP, createServer, etc.)
and runs the security scanner on each detected server file.

Produces a FleetReport with aggregate statistics and a fleet-level
security grade.
"""

from __future__ import annotations

import json
import re
import time
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Any

from .models import ScanResult, ScanSummary, Severity
from .scanner import MCPSecurityScanner, detect_language

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

# Directories to skip during recursive discovery
EXCLUDE_DIRS: frozenset[str] = frozenset({
    "node_modules", ".git", "__pycache__", ".venv", "venv",
    "dist", "build", ".tox", ".mypy_cache", ".pytest_cache",
    ".ruff_cache", "egg-info",
})

# Patterns that indicate a file is an MCP server
_PY_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
    ("mcp.server", re.compile(r"from\s+mcp\.server")),
    ("FastMCP", re.compile(r"(?:from\s+fastmcp|FastMCP)")),
    ("stdio_server", re.compile(r"stdio_server")),
]

_TS_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
    ("McpServer", re.compile(r"McpServer")),
    ("createServer", re.compile(r"createServer")),
    ("@modelcontextprotocol", re.compile(r"@modelcontextprotocol")),
]

# File extensions to scan
_PY_EXTS: frozenset[str] = frozenset({".py", ".pyw"})
_TS_EXTS: frozenset[str] = frozenset({".ts", ".tsx", ".js", ".jsx", ".mjs"})


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

@dataclass
class ServerDetection:
    """A detected MCP server file."""

    path: Path
    language: str       # "python" or "typescript"
    pattern: str        # which pattern matched


class FleetGrade(Enum):
    """Fleet-level security grade."""

    A_PLUS = "A+"
    A = "A"
    B = "B"
    C = "C"
    D = "D"
    F = "F"
    NO_SERVERS = "N/A"


@dataclass
class FleetReport:
    """Aggregate report across multiple MCP servers."""

    servers: list[dict[str, Any]] = field(default_factory=list)
    detections: list[ServerDetection] = field(default_factory=list)
    scan_time_ms: float = 0.0

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

    @property
    def total_findings(self) -> int:
        return sum(
            s.get("findings_count", 0) for s in self.servers
        )

    @property
    def severity_counts(self) -> dict[str, int]:
        counts = {sev.value: 0 for sev in Severity}
        for srv in self.servers:
            sr: ScanResult | None = srv.get("scan_result")
            if sr is None:
                continue
            for f in sr.findings:
                counts[f.severity.value] += 1
        return counts

    @property
    def fleet_grade(self) -> FleetGrade:
        if not self.servers:
            return FleetGrade.NO_SERVERS
        return _compute_fleet_grade(self.servers)

    def to_dict(self) -> dict[str, Any]:
        servers_out = []
        for srv in self.servers:
            sr: ScanResult | None = srv.get("scan_result")
            servers_out.append({
                "file": srv["file"],
                "grade": srv["grade"],
                "findings_count": srv["findings_count"],
                "scan_result": sr.to_dict() if sr else {},
            })
        return {
            "total_servers": self.total_servers,
            "total_findings": self.total_findings,
            "fleet_grade": self.fleet_grade.value,
            "severity_counts": self.severity_counts,
            "scan_time_ms": round(self.scan_time_ms, 2),
            "servers": servers_out,
        }


# ---------------------------------------------------------------------------
# Detection logic
# ---------------------------------------------------------------------------

def _should_skip_dir(name: str) -> bool:
    """Check if a directory name should be skipped."""
    if name in EXCLUDE_DIRS:
        return True
    if name.endswith(".egg-info"):
        return True
    return False


def _detect_in_file(filepath: Path) -> ServerDetection | None:
    """Check if a file contains MCP server patterns.

    Args:
        filepath: Path to the source file.

    Returns:
        ServerDetection if patterns found, None otherwise.
    """
    ext = filepath.suffix.lower()
    if ext in _PY_EXTS:
        patterns = _PY_PATTERNS
        language = "python"
    elif ext in _TS_EXTS:
        patterns = _TS_PATTERNS
        language = "typescript"
    else:
        return None

    try:
        content = filepath.read_text(encoding="utf-8", errors="replace")
    except (OSError, PermissionError):
        return None

    for name, regex in patterns:
        if regex.search(content):
            return ServerDetection(
                path=filepath,
                language=language,
                pattern=name,
            )
    return None


def detect_mcp_servers(directory: Path) -> list[ServerDetection]:
    """Recursively detect MCP server files in a directory.

    Auto-detects Python and TypeScript/JavaScript MCP servers by
    looking for known import patterns and SDK usage.

    Args:
        directory: Root directory to scan.

    Returns:
        List of ServerDetection objects for each detected server.
    """
    if not directory.exists() or not directory.is_dir():
        return []

    detections: list[ServerDetection] = []

    for item in sorted(directory.iterdir()):
        if item.is_dir():
            if _should_skip_dir(item.name):
                continue
            detections.extend(detect_mcp_servers(item))
        elif item.is_file():
            detection = _detect_in_file(item)
            if detection is not None:
                detections.append(detection)

    return detections


# ---------------------------------------------------------------------------
# Fleet grade calculation
# ---------------------------------------------------------------------------

_GRADE_ORDER: list[str] = ["A+", "A", "B", "C", "D", "F"]
_GRADE_MAP: dict[str, FleetGrade] = {
    "A+": FleetGrade.A_PLUS,
    "A": FleetGrade.A,
    "B": FleetGrade.B,
    "C": FleetGrade.C,
    "D": FleetGrade.D,
    "F": FleetGrade.F,
}


def _compute_fleet_grade(servers: list[dict[str, Any]]) -> FleetGrade:
    """Compute fleet grade as the worst grade across all servers."""
    worst_idx = 0
    for srv in servers:
        grade = srv.get("grade", "F")
        idx = _GRADE_ORDER.index(grade) if grade in _GRADE_ORDER else 5
        worst_idx = max(worst_idx, idx)
    grade_str = _GRADE_ORDER[worst_idx]
    return _GRADE_MAP[grade_str]


# ---------------------------------------------------------------------------
# MultiScanner
# ---------------------------------------------------------------------------

class MultiScanner:
    """Scan multiple MCP servers across a directory tree."""

    def __init__(self, scanner: MCPSecurityScanner | None = None) -> None:
        self._scanner = scanner or MCPSecurityScanner()

    def scan_all(self, directory: Path) -> FleetReport:
        """Scan all detected MCP servers in a directory tree.

        Args:
            directory: Root directory to scan recursively.

        Returns:
            FleetReport with aggregate results.
        """
        start = time.monotonic()
        detections = detect_mcp_servers(directory)
        servers: list[dict[str, Any]] = []

        for detection in detections:
            scan_result = self._scanner.scan_file(detection.path)
            servers.append({
                "file": str(detection.path),
                "grade": scan_result.grade,
                "findings_count": len(scan_result.findings),
                "scan_result": scan_result,
            })

        elapsed = (time.monotonic() - start) * 1000
        return FleetReport(
            servers=servers,
            detections=detections,
            scan_time_ms=elapsed,
        )


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

def format_fleet_text(report: FleetReport) -> str:
    """Format a fleet report as human-readable terminal text.

    Args:
        report: FleetReport to format.

    Returns:
        Formatted text string.
    """
    lines: list[str] = []
    lines.append(f"\n{'=' * 60}")
    lines.append("  mcpaudit — Fleet Security Report")
    lines.append(f"{'=' * 60}")

    if report.total_servers == 0:
        lines.append("  No MCP servers detected in the target directory.")
        lines.append(f"{'=' * 60}\n")
        return "\n".join(lines)

    lines.append(f"  Servers scanned: {report.total_servers}")
    lines.append(f"  Total findings:  {report.total_findings}")
    lines.append(f"  Fleet Grade:     {report.fleet_grade.value}")
    lines.append(f"  Scan time:       {report.scan_time_ms:.1f}ms")
    lines.append(f"{'=' * 60}")

    # Severity breakdown
    counts = report.severity_counts
    lines.append("  Severity breakdown:")
    for sev in Severity:
        count = counts.get(sev.value, 0)
        if count > 0:
            lines.append(f"    {sev.value:<10} {count}")

    lines.append(f"\n  {'File':<50} {'Grade':<8} {'Findings'}")
    lines.append(f"  {'─' * 50} {'─' * 8} {'─' * 10}")

    for srv in report.servers:
        path = srv["file"]
        # Truncate long paths
        display = path if len(path) <= 48 else "..." + path[-45:]
        lines.append(
            f"  {display:<50} {srv['grade']:<8} {srv['findings_count']}"
        )

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


def format_fleet_json(report: FleetReport) -> str:
    """Format a fleet report as JSON.

    Args:
        report: FleetReport to format.

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


def format_fleet_html(report: FleetReport) -> str:
    """Format a fleet report as an HTML dashboard.

    Args:
        report: FleetReport to format.

    Returns:
        Complete HTML document string.
    """
    counts = report.severity_counts
    grade_color = {
        "A+": "#22c55e", "A": "#22c55e", "B": "#eab308",
        "C": "#f97316", "D": "#ef4444", "F": "#dc2626", "N/A": "#6b7280",
    }.get(report.fleet_grade.value, "#6b7280")

    table_rows = ""
    for srv in report.servers:
        gc = {
            "A+": "#22c55e", "A": "#22c55e", "B": "#eab308",
            "C": "#f97316", "D": "#ef4444", "F": "#dc2626",
        }.get(srv["grade"], "#6b7280")
        table_rows += f"""<tr>
            <td>{srv['file']}</td>
            <td style="color:{gc};font-weight:bold">{srv['grade']}</td>
            <td>{srv['findings_count']}</td>
        </tr>"""

    return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>mcpaudit Fleet Report</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; }}
h1 {{ font-size: 1.5rem; margin-bottom: 8px; }}
.subtitle {{ color: #94a3b8; margin-bottom: 24px; }}
.cards {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
          gap: 16px; margin-bottom: 32px; }}
.card {{ background: #1e293b; border-radius: 12px; padding: 20px; }}
.card-label {{ font-size: 0.75rem; text-transform: uppercase; color: #94a3b8;
              letter-spacing: 0.05em; }}
.card-value {{ font-size: 2rem; font-weight: 700; margin-top: 4px; }}
table {{ width: 100%; border-collapse: collapse; background: #1e293b;
         border-radius: 12px; overflow: hidden; }}
th {{ text-align: left; padding: 12px 16px; background: #334155;
      font-size: 0.8rem; text-transform: uppercase; color: #94a3b8; }}
td {{ padding: 10px 16px; border-bottom: 1px solid #334155; font-size: 0.9rem; }}
tr:hover {{ background: #1e3a5f; }}
.footer {{ text-align: center; color: #475569; margin-top: 32px; font-size: 0.8rem; }}
</style>
</head>
<body>
<h1>mcpaudit Fleet Security Report</h1>
<p class="subtitle">Aggregate security analysis across all MCP servers</p>

<div class="cards">
    <div class="card">
        <div class="card-label">Fleet Grade</div>
        <div class="card-value" style="color:{grade_color}">{report.fleet_grade.value}</div>
    </div>
    <div class="card">
        <div class="card-label">Servers</div>
        <div class="card-value">{report.total_servers}</div>
    </div>
    <div class="card">
        <div class="card-label">Findings</div>
        <div class="card-value">{report.total_findings}</div>
    </div>
    <div class="card">
        <div class="card-label">Critical</div>
        <div class="card-value" style="color:#ef4444">{counts.get('CRITICAL', 0)}</div>
    </div>
    <div class="card">
        <div class="card-label">High</div>
        <div class="card-value" style="color:#f97316">{counts.get('HIGH', 0)}</div>
    </div>
    <div class="card">
        <div class="card-label">Medium</div>
        <div class="card-value" style="color:#eab308">{counts.get('MEDIUM', 0)}</div>
    </div>
</div>

<h3 style="margin-bottom:12px">Server Details</h3>
<table>
<thead><tr>
    <th>File</th><th>Grade</th><th>Findings</th>
</tr></thead>
<tbody>{table_rows}</tbody>
</table>

<p class="footer">Generated by mcpaudit v1.0.0 | Carlos Miret</p>
</body>
</html>"""
