"""MCP Security Dashboard Generator — Unified HTML dashboard.

Generates a single-page, self-contained HTML dashboard with:
- Executive Summary with overall grade and risk score
- Trust Score breakdown with deductions/bonuses
- Findings by severity (CSS pie chart)
- OWASP Agentic Top 10 coverage matrix
- Top findings ranked by frequency
- Compliance status against standard frameworks
- Dark theme, responsive, zero external dependencies

CLI: mcpaudit dashboard <path> --output dashboard.html
"""

from __future__ import annotations

import html
import json
from collections import Counter
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any

from .models import Finding, ScanResult, ScanSummary, Severity
from .trust_score import TrustScoreResult, compute_trust_score


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

_SEVERITY_COLORS: dict[str, str] = {
    "CRITICAL": "#dc2626",
    "HIGH": "#f97316",
    "MEDIUM": "#eab308",
    "LOW": "#22c55e",
    "INFO": "#6b7280",
}

_GRADE_COLORS: dict[str, str] = {
    "A+": "#22c55e",
    "A": "#4ade80",
    "B": "#eab308",
    "C": "#f97316",
    "D": "#ef4444",
    "F": "#dc2626",
}

_OWASP_TOP10: list[dict[str, str]] = [
    {"id": "A01", "name": "Prompt Injection", "rules": "MCP-004"},
    {"id": "A02", "name": "Insecure Tool Implementation",
     "rules": "CMD-001,CMD-002,SQL-001,PATH-001,DESER-001"},
    {"id": "A03", "name": "Excessive Agency", "rules": "PERM-001,PRIV-001"},
    {"id": "A04", "name": "Insecure Output Handling",
     "rules": "INFO-001,ERR-001"},
    {"id": "A05", "name": "Insufficient Access Control",
     "rules": "AUTH-001,AUTHZ-001"},
    {"id": "A06", "name": "Improper Input Validation",
     "rules": "VAL-001,IDOR-001"},
    {"id": "A07", "name": "SSRF", "rules": "SSRF-001"},
    {"id": "A08", "name": "Data Exfiltration",
     "rules": "SEC-001,MCP-003,FILE-001,LOG-001"},
    {"id": "A09", "name": "Denial of Service",
     "rules": "RATE-001,RES-001,REDOS-001"},
    {"id": "A10", "name": "Insecure Configuration",
     "rules": "PATH-002,TEMP-001,CORS-001,MCP-001,MCP-002"},
]


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

@dataclass
class DashboardConfig:
    """Configuration options for the dashboard generator."""

    title: str = "MCP Security Dashboard"
    show_trust_score: bool = True
    show_owasp_coverage: bool = True
    show_compliance: bool = True
    show_top_findings: bool = True
    top_findings_limit: int = 10
    show_severity_chart: bool = True
    show_file_breakdown: bool = True
    timestamp: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "title": self.title,
            "show_trust_score": self.show_trust_score,
            "show_owasp_coverage": self.show_owasp_coverage,
            "show_compliance": self.show_compliance,
            "show_top_findings": self.show_top_findings,
            "top_findings_limit": self.top_findings_limit,
            "show_severity_chart": self.show_severity_chart,
            "show_file_breakdown": self.show_file_breakdown,
        }


@dataclass
class DashboardData:
    """Aggregated data for rendering the dashboard."""

    grade: str = "A+"
    risk_score: int = 0
    trust_score: int = 100
    trust_label: str = "EXCELLENT"
    total_files: int = 0
    total_findings: int = 0
    total_lines: int = 0
    scan_time_ms: float = 0.0
    severity_counts: dict[str, int] = field(default_factory=dict)
    findings_by_rule: dict[str, int] = field(default_factory=dict)
    findings_by_file: dict[str, int] = field(default_factory=dict)
    owasp_coverage: dict[str, bool] = field(default_factory=dict)
    top_findings: list[dict[str, Any]] = field(default_factory=list)
    compliance_status: dict[str, str] = field(default_factory=dict)
    trust_deductions: dict[str, int] = field(default_factory=dict)
    trust_bonuses: dict[str, int] = field(default_factory=dict)
    timestamp: str = ""

    def to_dict(self) -> dict[str, Any]:
        return {
            "grade": self.grade,
            "risk_score": self.risk_score,
            "trust_score": self.trust_score,
            "trust_label": self.trust_label,
            "total_files": self.total_files,
            "total_findings": self.total_findings,
            "total_lines": self.total_lines,
            "scan_time_ms": round(self.scan_time_ms, 2),
            "severity_counts": dict(self.severity_counts),
            "owasp_coverage": dict(self.owasp_coverage),
            "compliance_status": dict(self.compliance_status),
            "timestamp": self.timestamp,
        }


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _severity_color(severity: str) -> str:
    return _SEVERITY_COLORS.get(severity, "#6b7280")


def _grade_color(grade: str) -> str:
    return _GRADE_COLORS.get(grade, "#6b7280")


def _compute_risk_score(severity_counts: dict[str, int]) -> int:
    weights = {"CRITICAL": 25, "HIGH": 15, "MEDIUM": 5, "LOW": 2, "INFO": 0}
    raw = sum(weights.get(s, 0) * c for s, c in severity_counts.items())
    return min(100, raw)


def _risk_label(score: int) -> str:
    if score == 0:
        return "CLEAN"
    if score <= 15:
        return "LOW"
    if score <= 40:
        return "MODERATE"
    if score <= 70:
        return "HIGH"
    return "CRITICAL"


def _extract_category(rule_id: str) -> str:
    parts = rule_id.rsplit("-", 1)
    return parts[0] if len(parts) == 2 else rule_id


# ---------------------------------------------------------------------------
# Data aggregation
# ---------------------------------------------------------------------------

def build_dashboard_data(
    summary: ScanSummary,
    config: DashboardConfig | None = None,
) -> DashboardData:
    """Build aggregated dashboard data from scan results.

    Args:
        summary: ScanSummary from scanner.
        config: Optional dashboard configuration.

    Returns:
        DashboardData with all computed values.
    """
    cfg = config or DashboardConfig()
    now = cfg.timestamp or datetime.now(
        timezone.utc
    ).strftime("%Y-%m-%d %H:%M UTC")

    all_findings: list[Finding] = []
    for r in summary.results:
        all_findings.extend(r.findings)

    severity_counts = summary.severity_counts
    risk_score = _compute_risk_score(severity_counts)

    # Trust score
    ts_result = compute_trust_score(all_findings)

    # Findings by rule
    rule_counter: Counter[str] = Counter()
    first_finding: dict[str, Finding] = {}
    for f in all_findings:
        rule_counter[f.rule_id] += 1
        if f.rule_id not in first_finding:
            first_finding[f.rule_id] = f

    # Findings by file
    file_counter: Counter[str] = Counter()
    for r in summary.results:
        if r.findings:
            file_counter[r.file] = len(r.findings)

    # OWASP coverage
    found_rules = set(f.rule_id for f in all_findings)
    owasp_cov: dict[str, bool] = {}
    for item in _OWASP_TOP10:
        rules = [r.strip() for r in item["rules"].split(",")]
        # Coverage = at least one rule from this category was checked
        # For dashboard: mark covered if NO findings in that category
        has_findings = any(r in found_rules for r in rules)
        owasp_cov[item["id"]] = not has_findings

    # Top findings
    top = rule_counter.most_common(cfg.top_findings_limit)
    top_findings: list[dict[str, Any]] = []
    for rule_id, count in top:
        f = first_finding[rule_id]
        top_findings.append({
            "rule_id": rule_id,
            "severity": f.severity.value,
            "title": f.title,
            "count": count,
            "recommendation": f.recommendation or "",
        })

    # Compliance
    compliance: dict[str, str] = {}
    if severity_counts.get("CRITICAL", 0) > 0:
        compliance["overall"] = "FAIL"
    elif severity_counts.get("HIGH", 0) > 0:
        compliance["overall"] = "WARNING"
    else:
        compliance["overall"] = "PASS"

    owasp_pass = sum(1 for v in owasp_cov.values() if v)
    compliance["owasp_coverage"] = (
        f"{owasp_pass}/{len(owasp_cov)}"
    )
    compliance["grade"] = summary.overall_grade

    return DashboardData(
        grade=summary.overall_grade,
        risk_score=risk_score,
        trust_score=ts_result.score,
        trust_label=ts_result.label,
        total_files=summary.total_files,
        total_findings=summary.total_findings,
        total_lines=summary.total_lines,
        scan_time_ms=summary.total_scan_time_ms,
        severity_counts=dict(severity_counts),
        findings_by_rule=dict(rule_counter),
        findings_by_file=dict(file_counter),
        owasp_coverage=owasp_cov,
        top_findings=top_findings,
        compliance_status=compliance,
        trust_deductions=dict(ts_result.deductions),
        trust_bonuses=dict(ts_result.bonuses),
        timestamp=now,
    )


# ---------------------------------------------------------------------------
# HTML sections
# ---------------------------------------------------------------------------

def _section_executive_summary(data: DashboardData) -> str:
    gc = _grade_color(data.grade)
    rl = _risk_label(data.risk_score)
    rc = _severity_color("HIGH") if data.risk_score > 40 else (
        _severity_color("MEDIUM") if data.risk_score > 15 else "#22c55e"
    )

    return f"""
    <section class="section">
        <h2>Executive Summary</h2>
        <div class="cards">
            <div class="card">
                <div class="card-label">Security Grade</div>
                <div class="card-value" style="color:{gc};font-size:3rem">
                    {html.escape(data.grade)}</div>
            </div>
            <div class="card">
                <div class="card-label">Risk Score</div>
                <div class="card-value" style="color:{rc}">
                    {data.risk_score}/100</div>
                <div class="card-sub">{rl}</div>
            </div>
            <div class="card">
                <div class="card-label">Files Scanned</div>
                <div class="card-value">{data.total_files}</div>
            </div>
            <div class="card">
                <div class="card-label">Total Findings</div>
                <div class="card-value">{data.total_findings}</div>
            </div>
            <div class="card">
                <div class="card-label">Lines Analyzed</div>
                <div class="card-value">{data.total_lines:,}</div>
            </div>
            <div class="card">
                <div class="card-label">Scan Time</div>
                <div class="card-value">{data.scan_time_ms:.0f}ms</div>
            </div>
        </div>
        <div class="meta">Generated: {html.escape(data.timestamp)}</div>
    </section>"""


def _section_trust_score(data: DashboardData) -> str:
    tc = "#22c55e" if data.trust_score >= 75 else (
        "#eab308" if data.trust_score >= 50 else "#dc2626"
    )

    deductions_html = ""
    if data.trust_deductions:
        parts = [
            f'<span class="deduction">{s}: -{p}</span>'
            for s, p in data.trust_deductions.items()
        ]
        deductions_html = f'<div class="trust-details">Deductions: {" ".join(parts)}</div>'

    bonuses_html = ""
    if data.trust_bonuses:
        parts = [
            f'<span class="bonus">{n}: +{p}</span>'
            for n, p in data.trust_bonuses.items()
        ]
        bonuses_html = f'<div class="trust-details">Bonuses: {" ".join(parts)}</div>'

    return f"""
    <section class="section">
        <h2>Trust Score</h2>
        <div class="trust-container">
            <div class="trust-score" style="color:{tc}">
                {data.trust_score}/100
            </div>
            <div class="trust-label">{html.escape(data.trust_label)}</div>
            {deductions_html}
            {bonuses_html}
        </div>
    </section>"""


def _section_severity_chart(data: DashboardData) -> str:
    total = data.total_findings
    if total == 0:
        return """
    <section class="section">
        <h2>Findings by Severity</h2>
        <p class="empty-msg">No findings detected. Clean scan.</p>
    </section>"""

    stops: list[str] = []
    cumulative = 0.0
    legend_items: list[str] = []
    order = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]

    for sev in order:
        count = data.severity_counts.get(sev, 0)
        if count == 0:
            continue
        pct = count / total * 100
        color = _severity_color(sev)
        stops.append(f"{color} {cumulative:.1f}% {cumulative + pct:.1f}%")
        cumulative += pct
        legend_items.append(
            f'<div class="legend-item">'
            f'<span class="legend-dot" style="background:{color}"></span>'
            f'{sev}: {count} ({pct:.0f}%)</div>'
        )

    gradient = ", ".join(stops)

    return f"""
    <section class="section">
        <h2>Findings by Severity</h2>
        <div class="chart-container">
            <div class="pie" style="background:conic-gradient({gradient})">
            </div>
            <div class="legend">
                {''.join(legend_items)}
            </div>
        </div>
    </section>"""


def _section_owasp_coverage(data: DashboardData) -> str:
    rows = ""
    for item in _OWASP_TOP10:
        covered = data.owasp_coverage.get(item["id"], True)
        status = "PASS" if covered else "FAIL"
        color = "#22c55e" if covered else "#dc2626"
        rows += f"""
            <tr>
                <td class="owasp-id">{html.escape(item["id"])}</td>
                <td>{html.escape(item["name"])}</td>
                <td class="owasp-rules">{html.escape(item["rules"])}</td>
                <td><span class="status-badge" style="background:{color}">
                    {status}</span></td>
            </tr>"""

    return f"""
    <section class="section">
        <h2>OWASP Agentic Top 10 Coverage</h2>
        <table class="findings-table">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>Category</th>
                    <th>Rules</th>
                    <th>Status</th>
                </tr>
            </thead>
            <tbody>
                {rows}
            </tbody>
        </table>
    </section>"""


def _section_top_findings(data: DashboardData) -> str:
    if not data.top_findings:
        return ""

    rows = ""
    for rank, item in enumerate(data.top_findings, 1):
        sc = _severity_color(item["severity"])
        rows += f"""
            <tr>
                <td class="rank">{rank}</td>
                <td><span class="sev-badge" style="background:{sc}">
                    {html.escape(item["severity"])}</span></td>
                <td class="rule-id">{html.escape(item["rule_id"])}</td>
                <td>{html.escape(item["title"])}</td>
                <td class="count">{item["count"]}</td>
            </tr>"""

    return f"""
    <section class="section">
        <h2>Top Findings</h2>
        <table class="findings-table">
            <thead>
                <tr>
                    <th>#</th>
                    <th>Severity</th>
                    <th>Rule</th>
                    <th>Title</th>
                    <th>Count</th>
                </tr>
            </thead>
            <tbody>
                {rows}
            </tbody>
        </table>
    </section>"""


def _section_compliance(data: DashboardData) -> str:
    if not data.compliance_status:
        return ""

    overall = data.compliance_status.get("overall", "UNKNOWN")
    oc = "#22c55e" if overall == "PASS" else (
        "#eab308" if overall == "WARNING" else "#dc2626"
    )
    owasp = data.compliance_status.get("owasp_coverage", "N/A")
    grade = data.compliance_status.get("grade", "?")

    return f"""
    <section class="section">
        <h2>Compliance Status</h2>
        <div class="cards">
            <div class="card">
                <div class="card-label">Overall</div>
                <div class="card-value" style="color:{oc}">{overall}</div>
            </div>
            <div class="card">
                <div class="card-label">OWASP Coverage</div>
                <div class="card-value">{html.escape(owasp)}</div>
            </div>
            <div class="card">
                <div class="card-label">Grade</div>
                <div class="card-value" style="color:{_grade_color(grade)}">
                    {html.escape(grade)}</div>
            </div>
        </div>
    </section>"""


# ---------------------------------------------------------------------------
# CSS
# ---------------------------------------------------------------------------

_CSS = """:root {
    --bg: #0f172a;
    --surface: #1e293b;
    --surface2: #334155;
    --text: #e2e8f0;
    --text-dim: #94a3b8;
    --border: #475569;
    --accent: #3b82f6;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background: var(--bg);
    color: var(--text);
    line-height: 1.6;
    padding: 2rem;
    max-width: 1200px;
    margin: 0 auto;
}
h1 { font-size: 1.8rem; margin-bottom: 0.5rem; }
h2 {
    font-size: 1.3rem;
    margin-bottom: 1rem;
    border-bottom: 1px solid var(--border);
    padding-bottom: 0.5rem;
}
.header { text-align: center; margin-bottom: 2rem; }
.header .subtitle { color: var(--text-dim); font-size: 0.9rem; }
.section {
    background: var(--surface);
    border-radius: 12px;
    padding: 1.5rem;
    margin-bottom: 1.5rem;
}
.cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 1rem;
    margin-bottom: 1rem;
}
.card {
    background: var(--surface2);
    border-radius: 8px;
    padding: 1rem;
    text-align: center;
}
.card-label {
    font-size: 0.75rem;
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.05em;
}
.card-value { font-size: 1.8rem; font-weight: 700; margin: 0.25rem 0; }
.card-sub { font-size: 0.8rem; color: var(--text-dim); }
.meta { font-size: 0.75rem; color: var(--text-dim); text-align: right; }
.empty-msg {
    color: #22c55e;
    font-size: 1.1rem;
    text-align: center;
    padding: 2rem;
}
.chart-container {
    display: flex;
    align-items: center;
    gap: 2rem;
    flex-wrap: wrap;
    justify-content: center;
}
.pie { width: 200px; height: 200px; border-radius: 50%; flex-shrink: 0; }
.legend { display: flex; flex-direction: column; gap: 0.5rem; }
.legend-item { display: flex; align-items: center; gap: 0.5rem; font-size: 0.9rem; }
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
.findings-table { width: 100%; border-collapse: collapse; }
.findings-table th, .findings-table td {
    padding: 0.5rem 0.75rem;
    text-align: left;
    border-bottom: 1px solid var(--border);
    font-size: 0.85rem;
}
.findings-table th {
    color: var(--text-dim);
    font-weight: 600;
    text-transform: uppercase;
    font-size: 0.7rem;
}
.rank { width: 30px; text-align: center; }
.count { text-align: center; font-weight: 600; }
.rule-id { font-family: monospace; color: var(--accent); }
.owasp-id { font-family: monospace; font-weight: 600; }
.owasp-rules { font-family: monospace; font-size: 0.75rem; color: var(--text-dim); }
.sev-badge, .status-badge {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 4px;
    color: #fff;
    font-size: 0.7rem;
    font-weight: 600;
}
.trust-container { text-align: center; padding: 1rem; }
.trust-score { font-size: 3rem; font-weight: 700; }
.trust-label { font-size: 1.2rem; color: var(--text-dim); margin-bottom: 0.5rem; }
.trust-details { font-size: 0.8rem; color: var(--text-dim); margin-top: 0.5rem; }
.deduction { color: #ef4444; margin-right: 1rem; }
.bonus { color: #22c55e; margin-right: 1rem; }
.footer {
    text-align: center;
    color: var(--text-dim);
    font-size: 0.75rem;
    margin-top: 2rem;
    padding-top: 1rem;
    border-top: 1px solid var(--border);
}"""


# ---------------------------------------------------------------------------
# HTML assembly
# ---------------------------------------------------------------------------

def _wrap_html(title: str, body: str) -> str:
    return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{html.escape(title)}</title>
<style>
{_CSS}
</style>
</head>
<body>
<div class="header">
    <h1>{html.escape(title)}</h1>
    <div class="subtitle">mcpaudit v2.0.0 — MCP Security Dashboard</div>
</div>
{body}
<div class="footer">
    Generated by mcpaudit — MCP Security Audit Scanner
</div>
</body>
</html>"""


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

def generate_security_dashboard(
    summary: ScanSummary,
    config: DashboardConfig | None = None,
) -> str:
    """Generate a self-contained HTML security dashboard.

    Args:
        summary: ScanSummary from scanner.
        config: Optional dashboard configuration.

    Returns:
        Complete HTML document string.
    """
    cfg = config or DashboardConfig()
    data = build_dashboard_data(summary, cfg)

    sections: list[str] = [
        _section_executive_summary(data),
    ]

    if cfg.show_trust_score:
        sections.append(_section_trust_score(data))

    if cfg.show_severity_chart:
        sections.append(_section_severity_chart(data))

    if cfg.show_owasp_coverage:
        sections.append(_section_owasp_coverage(data))

    if cfg.show_top_findings:
        s = _section_top_findings(data)
        if s:
            sections.append(s)

    if cfg.show_compliance:
        s = _section_compliance(data)
        if s:
            sections.append(s)

    body = "\n".join(sections)
    return _wrap_html(cfg.title, body)


def generate_dashboard_from_scan_data(
    data: dict[str, Any],
    config: DashboardConfig | None = None,
) -> str:
    """Generate dashboard from a scan result dictionary.

    Useful when loading from JSON files.

    Args:
        data: Scan result dictionary (as from ScanSummary.to_dict()).
        config: Optional dashboard configuration.

    Returns:
        Complete HTML document string.
    """
    summary_data = data.get("summary", data)
    results_data = data.get("results", [])

    results: list[ScanResult] = []
    for rd in results_data:
        findings: list[Finding] = []
        for fd in rd.get("findings", []):
            findings.append(Finding(
                rule_id=fd.get("rule_id", ""),
                severity=Severity.from_string(fd.get("severity", "INFO")),
                title=fd.get("title", ""),
                description=fd.get("description", ""),
                file=fd.get("file", ""),
                line=fd.get("line", 0),
                recommendation=fd.get("recommendation", ""),
                cwe=fd.get("cwe", ""),
            ))
        results.append(ScanResult(
            file=rd.get("file", ""),
            findings=findings,
            lines_scanned=rd.get("lines_scanned", 0),
            scan_time_ms=rd.get("scan_time_ms", 0.0),
        ))

    summary = ScanSummary(results=results)
    summary.total_scan_time_ms = summary_data.get("scan_time_ms", 0.0)

    return generate_security_dashboard(summary, config=config)


def format_dashboard_json(data: DashboardData) -> str:
    """Format dashboard data as JSON.

    Args:
        data: DashboardData from build_dashboard_data.

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


def format_dashboard_text(data: DashboardData) -> str:
    """Format dashboard data as plain text.

    Args:
        data: DashboardData from build_dashboard_data.

    Returns:
        Formatted text string.
    """
    lines: list[str] = []
    lines.append(f"\n{'=' * 60}")
    lines.append("  MCP Security Dashboard")
    lines.append(f"{'=' * 60}")
    lines.append(f"  Grade: {data.grade}")
    lines.append(f"  Risk Score: {data.risk_score}/100")
    lines.append(f"  Trust Score: {data.trust_score}/100 ({data.trust_label})")
    lines.append(f"  Files: {data.total_files}")
    lines.append(f"  Findings: {data.total_findings}")
    lines.append(f"  Lines: {data.total_lines:,}")
    lines.append(f"  Time: {data.scan_time_ms:.0f}ms")
    lines.append(f"{'=' * 60}")

    if data.severity_counts:
        lines.append("  Severity breakdown:")
        for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]:
            count = data.severity_counts.get(sev, 0)
            if count > 0:
                lines.append(f"    {sev}: {count}")

    if data.compliance_status:
        lines.append(f"\n  Compliance: {data.compliance_status.get('overall', '?')}")
        lines.append(
            f"  OWASP Coverage: {data.compliance_status.get('owasp_coverage', '?')}"
        )

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