"""Multi-Project Aggregation for MCP Security Audit.

Scan multiple projects/directories with a unified report, cross-project
deduplication, fleet statistics, and an executive HTML dashboard.

Usage:
    from mcpaudit.multi_project import scan_projects, generate_fleet_html
    report = scan_projects(["/project1", "/project2"])
    html = generate_fleet_html(report)

CLI: mcpaudit fleet scan /project1 /project2 /project3

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

from __future__ import annotations

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

from .models import Finding, ScanResult, ScanSummary, Severity
from .scanner import MCPSecurityScanner
from .trust_score import compute_summary_trust_score, TrustScoreResult


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

_SEVERITY_ORDER: dict[str, int] = {
    "CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3, "INFO": 4,
}

_GRADE_ORDER: list[str] = ["A+", "A", "B", "C", "D", "F"]

_FILE_EXTENSIONS = frozenset({
    ".py", ".pyw", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".go",
})

_SKIP_DIRS = frozenset({
    "node_modules", ".git", "__pycache__", ".venv", "venv",
    "dist", "build", ".tox", ".mypy_cache", ".pytest_cache",
    ".ruff_cache", "egg-info", ".eggs",
})


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

@dataclass
class ProjectFinding:
    """A finding associated with a specific project."""

    rule_id: str
    severity: str
    title: str
    description: str
    file: str
    line: int
    project: str
    code_snippet: str = ""
    cwe: str = ""

    @property
    def dedup_key(self) -> tuple[str, str, int]:
        """Key for cross-project deduplication: (rule_id, file, line)."""
        return (self.rule_id, self.file, self.line)

    def to_dict(self) -> dict[str, Any]:
        return {
            "rule_id": self.rule_id,
            "severity": self.severity,
            "title": self.title,
            "description": self.description,
            "file": self.file,
            "line": self.line,
            "project": self.project,
            "code_snippet": self.code_snippet,
            "cwe": self.cwe,
        }


@dataclass
class ProjectResult:
    """Scan result for a single project."""

    name: str
    path: str
    files_scanned: int = 0
    total_findings: int = 0
    severity_counts: dict[str, int] = field(default_factory=dict)
    grade: str = "A+"
    trust_score: float = 100.0
    scan_time_ms: float = 0.0
    findings: list[ProjectFinding] = field(default_factory=list)

    def to_dict(self) -> dict[str, Any]:
        return {
            "name": self.name,
            "path": self.path,
            "files_scanned": self.files_scanned,
            "total_findings": self.total_findings,
            "severity_counts": self.severity_counts,
            "grade": self.grade,
            "trust_score": round(self.trust_score, 1),
            "scan_time_ms": round(self.scan_time_ms, 2),
        }


@dataclass
class FleetStats:
    """Aggregate statistics across all projects."""

    total_projects: int = 0
    total_files: int = 0
    total_findings: int = 0
    unique_findings: int = 0
    severity_counts: dict[str, int] = field(default_factory=dict)
    worst_project: str = ""
    best_project: str = ""
    avg_trust_score: float = 0.0

    def to_dict(self) -> dict[str, Any]:
        return {
            "total_projects": self.total_projects,
            "total_files": self.total_files,
            "total_findings": self.total_findings,
            "unique_findings": self.unique_findings,
            "severity_counts": self.severity_counts,
            "worst_project": self.worst_project,
            "best_project": self.best_project,
            "avg_trust_score": round(self.avg_trust_score, 1),
        }


@dataclass
class AggregatedReport:
    """Consolidated report from scanning multiple projects."""

    projects: list[ProjectResult] = field(default_factory=list)
    stats: FleetStats = field(default_factory=FleetStats)
    findings: list[ProjectFinding] = field(default_factory=list)
    fleet_grade: str = "A+"
    scan_time_ms: float = 0.0

    def to_dict(self) -> dict[str, Any]:
        return {
            "fleet_grade": self.fleet_grade,
            "scan_time_ms": round(self.scan_time_ms, 2),
            "stats": self.stats.to_dict(),
            "projects": [p.to_dict() for p in self.projects],
            "findings": [f.to_dict() for f in self.findings],
        }


# ---------------------------------------------------------------------------
# Scanning
# ---------------------------------------------------------------------------

def _collect_files(directory: Path) -> list[Path]:
    """Recursively collect scannable files from a directory.

    Args:
        directory: Root directory.

    Returns:
        List of file paths.
    """
    files: list[Path] = []
    if not directory.exists() or not directory.is_dir():
        return files

    for item in sorted(directory.iterdir()):
        if item.is_dir():
            if item.name in _SKIP_DIRS or item.name.endswith(".egg-info"):
                continue
            files.extend(_collect_files(item))
        elif item.is_file() and item.suffix.lower() in _FILE_EXTENSIONS:
            files.append(item)
    return files


def _scan_project(
    path: str,
    scanner: MCPSecurityScanner,
) -> ProjectResult:
    """Scan a single project directory.

    Args:
        path: Path to the project directory.
        scanner: Scanner instance.

    Returns:
        ProjectResult with findings and stats.
    """
    start = time.monotonic()
    project_path = Path(path)
    project_name = project_path.name

    if not project_path.exists():
        return ProjectResult(
            name=project_name,
            path=str(project_path),
        )

    # If path is a file, scan just that file
    if project_path.is_file():
        files = [project_path]
    else:
        files = _collect_files(project_path)

    results: list[ScanResult] = []
    project_findings: list[ProjectFinding] = []

    for fp in files:
        scan_result = scanner.scan_file(fp)
        results.append(scan_result)
        for f in scan_result.findings:
            project_findings.append(ProjectFinding(
                rule_id=f.rule_id,
                severity=f.severity.value,
                title=f.title,
                description=f.description,
                file=f.file,
                line=f.line,
                project=project_name,
                code_snippet=f.code_snippet,
                cwe=f.cwe,
            ))

    summary = ScanSummary(results=results)
    elapsed = (time.monotonic() - start) * 1000

    # Compute trust score
    trust = compute_summary_trust_score(summary)

    return ProjectResult(
        name=project_name,
        path=str(project_path),
        files_scanned=len(files),
        total_findings=len(project_findings),
        severity_counts=dict(summary.severity_counts),
        grade=summary.overall_grade,
        trust_score=trust.score,
        scan_time_ms=elapsed,
        findings=project_findings,
    )


def _dedup_findings(
    findings: list[ProjectFinding],
) -> list[ProjectFinding]:
    """Deduplicate findings across projects by (rule_id, file, line).

    Args:
        findings: All findings from all projects.

    Returns:
        Deduplicated list sorted by severity.
    """
    seen: dict[tuple[str, str, int], ProjectFinding] = {}
    for f in findings:
        key = f.dedup_key
        if key not in seen:
            seen[key] = f
    result = list(seen.values())
    result.sort(key=lambda x: (
        _SEVERITY_ORDER.get(x.severity, 99),
        x.project,
        x.file,
    ))
    return result


def _compute_fleet_grade(projects: list[ProjectResult]) -> str:
    """Compute fleet grade as the worst grade across all projects."""
    if not projects:
        return "A+"
    worst_idx = 0
    for p in projects:
        idx = _GRADE_ORDER.index(p.grade) if p.grade in _GRADE_ORDER else 5
        worst_idx = max(worst_idx, idx)
    return _GRADE_ORDER[worst_idx]


def _compute_stats(
    projects: list[ProjectResult],
    deduped: list[ProjectFinding],
) -> FleetStats:
    """Compute aggregate fleet statistics.

    Args:
        projects: All project results.
        deduped: Deduplicated findings.

    Returns:
        FleetStats.
    """
    sev_counts: dict[str, int] = {
        "CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "INFO": 0,
    }
    for f in deduped:
        sev_counts[f.severity] = sev_counts.get(f.severity, 0) + 1

    total_findings = sum(p.total_findings for p in projects)
    total_files = sum(p.files_scanned for p in projects)

    worst = ""
    best = ""
    if projects:
        sorted_by_score = sorted(projects, key=lambda p: p.trust_score)
        worst = sorted_by_score[0].name
        best = sorted_by_score[-1].name

    avg_score = 0.0
    if projects:
        avg_score = sum(p.trust_score for p in projects) / len(projects)

    return FleetStats(
        total_projects=len(projects),
        total_files=total_files,
        total_findings=total_findings,
        unique_findings=len(deduped),
        severity_counts=sev_counts,
        worst_project=worst,
        best_project=best,
        avg_trust_score=avg_score,
    )


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

def scan_projects(
    paths: list[str],
    scanner: MCPSecurityScanner | None = None,
) -> AggregatedReport:
    """Scan multiple projects and produce a unified report.

    Args:
        paths: List of project directory paths.
        scanner: Optional scanner instance.

    Returns:
        AggregatedReport with fleet-level statistics.
    """
    start = time.monotonic()
    sc = scanner or MCPSecurityScanner()

    projects: list[ProjectResult] = []
    all_findings: list[ProjectFinding] = []

    for path in paths:
        result = _scan_project(path, sc)
        projects.append(result)
        all_findings.extend(result.findings)

    deduped = _dedup_findings(all_findings)
    stats = _compute_stats(projects, deduped)
    fleet_grade = _compute_fleet_grade(projects)
    elapsed = (time.monotonic() - start) * 1000

    return AggregatedReport(
        projects=projects,
        stats=stats,
        findings=deduped,
        fleet_grade=fleet_grade,
        scan_time_ms=elapsed,
    )


def scan_projects_from_parent(
    parent: str,
    scanner: MCPSecurityScanner | None = None,
) -> AggregatedReport:
    """Scan all subdirectories of a parent directory as projects.

    Args:
        parent: Parent directory containing project subdirs.
        scanner: Optional scanner instance.

    Returns:
        AggregatedReport.
    """
    parent_path = Path(parent)
    if not parent_path.is_dir():
        return AggregatedReport()

    paths = [
        str(d) for d in sorted(parent_path.iterdir())
        if d.is_dir() and d.name not in _SKIP_DIRS
    ]
    return scan_projects(paths, scanner)


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

def format_fleet_text(report: AggregatedReport) -> str:
    """Format aggregated report as human-readable text.

    Args:
        report: AggregatedReport to format.

    Returns:
        Formatted text.
    """
    lines: list[str] = []
    lines.append("=" * 65)
    lines.append("  mcpaudit — Multi-Project Fleet Report")
    lines.append("=" * 65)

    s = report.stats
    lines.append(f"  Fleet Grade:     {report.fleet_grade}")
    lines.append(f"  Projects:        {s.total_projects}")
    lines.append(f"  Files scanned:   {s.total_files}")
    lines.append(f"  Total findings:  {s.total_findings}")
    lines.append(f"  Unique findings: {s.unique_findings}")
    lines.append(f"  Avg trust score: {s.avg_trust_score:.1f}/100")
    lines.append(f"  Best project:    {s.best_project}")
    lines.append(f"  Worst project:   {s.worst_project}")
    lines.append("")

    # Severity breakdown
    lines.append("  Severity Breakdown:")
    for sev in ("CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"):
        count = s.severity_counts.get(sev, 0)
        if count > 0:
            lines.append(f"    {sev:<10} {count}")
    lines.append("")

    # Per-project table
    lines.append(
        f"  {'Project':<30} {'Grade':<8} {'Trust':<8} "
        f"{'Findings':<10} {'Files'}"
    )
    lines.append(f"  {'_' * 30} {'_' * 8} {'_' * 8} {'_' * 10} {'_' * 6}")
    for p in report.projects:
        lines.append(
            f"  {p.name:<30} {p.grade:<8} "
            f"{p.trust_score:>5.1f}   "
            f"{p.total_findings:<10} {p.files_scanned}"
        )

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


def format_fleet_json(report: AggregatedReport) -> str:
    """Format aggregated report as JSON.

    Args:
        report: AggregatedReport to format.

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


def generate_fleet_html(report: AggregatedReport) -> str:
    """Generate executive HTML report with project table and CSS charts.

    Args:
        report: AggregatedReport to format.

    Returns:
        Self-contained HTML string.
    """
    def esc(text: str) -> str:
        return (
            text.replace("&", "&amp;")
            .replace("<", "&lt;")
            .replace(">", "&gt;")
            .replace('"', "&quot;")
        )

    s = report.stats
    grade_color = _grade_color(report.fleet_grade)

    # Project rows
    project_rows = ""
    for p in report.projects:
        gc = _grade_color(p.grade)
        project_rows += (
            f"<tr>"
            f"<td>{esc(p.name)}</td>"
            f"<td style='color:{gc};font-weight:bold'>{esc(p.grade)}</td>"
            f"<td>{p.trust_score:.1f}</td>"
            f"<td>{p.total_findings}</td>"
            f"<td>{p.files_scanned}</td>"
            f"<td>{p.scan_time_ms:.0f}ms</td>"
            f"</tr>\n"
        )

    # Severity bar chart (CSS-only)
    sev_bars = ""
    max_count = max(
        (s.severity_counts.get(sv, 0) for sv in _SEVERITY_ORDER),
        default=1,
    ) or 1
    for sev, color in _SEV_COLORS.items():
        count = s.severity_counts.get(sev, 0)
        pct = int((count / max_count) * 100) if max_count else 0
        sev_bars += (
            f"<div class='bar-row'>"
            f"<span class='bar-label'>{sev}</span>"
            f"<div class='bar-track'>"
            f"<div class='bar-fill' style='width:{pct}%;"
            f"background:{color}'></div>"
            f"</div>"
            f"<span class='bar-count'>{count}</span>"
            f"</div>\n"
        )

    # Finding rows (top 50)
    finding_rows = ""
    for f in report.findings[:50]:
        fc = _SEV_COLORS.get(f.severity, "#666")
        finding_rows += (
            f"<tr>"
            f"<td style='color:{fc};font-weight:bold'>{esc(f.severity)}</td>"
            f"<td>{esc(f.rule_id)}</td>"
            f"<td>{esc(f.title)}</td>"
            f"<td>{esc(f.project)}</td>"
            f"<td>{esc(f.file)}:{f.line}</td>"
            f"</tr>\n"
        )

    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:4px}}
h2{{font-size:1.1rem;margin:24px 0 12px;color:#94a3b8}}
.sub{{color:#64748b;margin-bottom:24px}}
.cards{{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));
  gap:12px;margin-bottom:28px}}
.card{{background:#1e293b;border-radius:10px;padding:16px}}
.card-label{{font-size:.7rem;text-transform:uppercase;color:#64748b;
  letter-spacing:.05em}}
.card-value{{font-size:1.8rem;font-weight:700;margin-top:2px}}
table{{width:100%;border-collapse:collapse;background:#1e293b;
  border-radius:10px;overflow:hidden;margin-bottom:24px}}
th{{text-align:left;padding:10px 14px;background:#334155;font-size:.75rem;
  text-transform:uppercase;color:#94a3b8}}
td{{padding:8px 14px;border-bottom:1px solid #334155;font-size:.85rem}}
tr:hover{{background:#1e3a5f}}
.bar-row{{display:flex;align-items:center;gap:8px;margin:4px 0}}
.bar-label{{width:70px;font-size:.8rem;text-align:right;color:#94a3b8}}
.bar-track{{flex:1;height:18px;background:#1e293b;border-radius:4px;
  overflow:hidden}}
.bar-fill{{height:100%;border-radius:4px;transition:width .3s}}
.bar-count{{width:30px;font-size:.8rem;color:#94a3b8}}
.footer{{text-align:center;color:#475569;margin-top:32px;font-size:.75rem}}
</style></head><body>
<h1>mcpaudit Multi-Project Fleet Report</h1>
<p class="sub">Aggregate security analysis across {s.total_projects} projects</p>

<div class="cards">
  <div class="card"><div class="card-label">Fleet Grade</div>
    <div class="card-value" style="color:{grade_color}">{esc(report.fleet_grade)}</div></div>
  <div class="card"><div class="card-label">Projects</div>
    <div class="card-value">{s.total_projects}</div></div>
  <div class="card"><div class="card-label">Files</div>
    <div class="card-value">{s.total_files}</div></div>
  <div class="card"><div class="card-label">Findings</div>
    <div class="card-value">{s.total_findings}</div></div>
  <div class="card"><div class="card-label">Unique</div>
    <div class="card-value">{s.unique_findings}</div></div>
  <div class="card"><div class="card-label">Avg Trust</div>
    <div class="card-value">{s.avg_trust_score:.0f}</div></div>
</div>

<h2>Severity Distribution</h2>
{sev_bars}

<h2>Project Breakdown</h2>
<table><thead><tr>
  <th>Project</th><th>Grade</th><th>Trust</th><th>Findings</th>
  <th>Files</th><th>Time</th>
</tr></thead><tbody>{project_rows}</tbody></table>

<h2>Top Findings ({s.unique_findings} unique)</h2>
<table><thead><tr>
  <th>Severity</th><th>Rule</th><th>Title</th><th>Project</th><th>Location</th>
</tr></thead><tbody>{finding_rows}</tbody></table>

<p class="footer">Generated by mcpaudit v2.2.0</p>
</body></html>"""


# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------

_SEV_COLORS: dict[str, str] = {
    "CRITICAL": "#dc2626",
    "HIGH": "#f97316",
    "MEDIUM": "#eab308",
    "LOW": "#22c55e",
    "INFO": "#64748b",
}


def _grade_color(grade: str) -> str:
    """Map a grade to a CSS color."""
    return {
        "A+": "#22c55e", "A": "#22c55e", "B": "#eab308",
        "C": "#f97316", "D": "#ef4444", "F": "#dc2626",
    }.get(grade, "#64748b")
