"""Executive Security Report Generator for MCP Security Audit.

Generates a professional, print-ready HTML report with executive summary,
findings by severity, OWASP coverage, trust score evolution, recommendations,
and appendix. Styled for A4 printing with CSS @media print.

Usage:
    from mcpaudit.report_generator import generate_executive_report
    html = generate_executive_report(scan_result)

CLI: mcpaudit report /path --output report.html --title "Security Audit Q1 2026"

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

from __future__ import annotations

import html as html_mod
import json
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any


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

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

_SEVERITY_ORDER: list[str] = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]

_OWASP_AGENTIC_TOP10: dict[str, str] = {
    "A01": "Tool Poisoning",
    "A02": "Insecure Tool Implementation",
    "A03": "Excessive Agency",
    "A04": "Insecure Output Handling",
    "A05": "Insufficient Access Control",
    "A06": "Improper Input Validation",
    "A07": "Server-Side Request Forgery",
    "A08": "Data Exfiltration",
    "A09": "Denial of Service",
    "A10": "Insecure Configuration",
}

_RULE_PREFIX_TO_OWASP: dict[str, str] = {
    "CMD": "A02",
    "SQL": "A02",
    "PATH": "A02",
    "DESER": "A02",
    "MCP": "A10",
    "AUTH": "A05",
    "AUTHZ": "A05",
    "VAL": "A06",
    "IDOR": "A06",
    "SSRF": "A07",
    "SEC": "A08",
    "FILE": "A08",
    "LOG": "A08",
    "RATE": "A09",
    "RES": "A09",
    "REDOS": "A09",
    "TEMP": "A10",
    "CORS": "A10",
    "PERM": "A03",
    "PRIV": "A03",
    "INFO": "A04",
    "ERR": "A04",
    "INJ": "A01",
    "TOOL": "A01",
}

DEFAULT_TITLE = "MCP Security Audit — Executive Report"
DEFAULT_LOGO_TEXT = "mcpaudit"


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

@dataclass
class ReportOptions:
    """Options for report generation."""

    title: str = DEFAULT_TITLE
    subtitle: str = ""
    author: str = ""
    logo_text: str = DEFAULT_LOGO_TEXT
    logo_url: str = ""
    include_appendix: bool = True
    include_recommendations: bool = True
    include_toc: bool = True
    include_owasp: bool = True
    include_trust_score: bool = True
    generated_at: str = ""
    custom_css: str = ""

    def __post_init__(self) -> None:
        if not self.generated_at:
            self.generated_at = _now_iso()

    def to_dict(self) -> dict[str, Any]:
        return {
            "title": self.title,
            "subtitle": self.subtitle,
            "author": self.author,
            "logo_text": self.logo_text,
            "logo_url": self.logo_url,
            "include_appendix": self.include_appendix,
            "include_recommendations": self.include_recommendations,
            "include_toc": self.include_toc,
            "include_owasp": self.include_owasp,
            "include_trust_score": self.include_trust_score,
            "generated_at": self.generated_at,
            "custom_css": self.custom_css,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> ReportOptions:
        return cls(
            title=data.get("title", DEFAULT_TITLE),
            subtitle=data.get("subtitle", ""),
            author=data.get("author", ""),
            logo_text=data.get("logo_text", DEFAULT_LOGO_TEXT),
            logo_url=data.get("logo_url", ""),
            include_appendix=data.get("include_appendix", True),
            include_recommendations=data.get("include_recommendations", True),
            include_toc=data.get("include_toc", True),
            include_owasp=data.get("include_owasp", True),
            include_trust_score=data.get("include_trust_score", True),
            generated_at=data.get("generated_at", ""),
            custom_css=data.get("custom_css", ""),
        )


@dataclass
class ReportSection:
    """A section in the executive report."""

    id: str
    title: str
    content: str
    order: int = 0

    def to_dict(self) -> dict[str, Any]:
        return {
            "id": self.id,
            "title": self.title,
            "content": self.content,
            "order": self.order,
        }


@dataclass
class ExecutiveReport:
    """Complete executive report data."""

    title: str = DEFAULT_TITLE
    generated_at: str = ""
    sections: list[ReportSection] = field(default_factory=list)
    total_findings: int = 0
    severity_counts: dict[str, int] = field(default_factory=dict)
    trust_score: int = 100
    grade: str = "A+"
    files_scanned: int = 0
    lines_scanned: int = 0
    owasp_coverage: dict[str, int] = field(default_factory=dict)
    recommendations: list[str] = field(default_factory=list)

    def __post_init__(self) -> None:
        if not self.generated_at:
            self.generated_at = _now_iso()

    def to_dict(self) -> dict[str, Any]:
        return {
            "title": self.title,
            "generated_at": self.generated_at,
            "total_findings": self.total_findings,
            "severity_counts": self.severity_counts,
            "trust_score": self.trust_score,
            "grade": self.grade,
            "files_scanned": self.files_scanned,
            "lines_scanned": self.lines_scanned,
            "owasp_coverage": self.owasp_coverage,
            "recommendations": self.recommendations,
            "sections": [s.to_dict() for s in self.sections],
        }


# ---------------------------------------------------------------------------
# Core functions
# ---------------------------------------------------------------------------

def _now_iso() -> str:
    """Return current UTC time as ISO string."""
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def _extract_findings(scan_data: dict[str, Any]) -> list[dict[str, Any]]:
    """Extract all findings from scan data."""
    findings: list[dict[str, Any]] = []
    results = scan_data.get("results", [])
    for result in results:
        for f in result.get("findings", []):
            findings.append(f)
    return findings


def _extract_severity_counts(scan_data: dict[str, Any]) -> dict[str, int]:
    """Extract severity counts from scan data."""
    summary = scan_data.get("summary", {})
    counts = summary.get("severity_counts", {})
    result: dict[str, int] = {}
    for sev in _SEVERITY_ORDER:
        result[sev] = counts.get(sev, 0)
    return result


def _compute_owasp_coverage(
    findings: list[dict[str, Any]],
) -> dict[str, int]:
    """Map findings to OWASP Agentic Top 10 categories."""
    coverage: dict[str, int] = {k: 0 for k in _OWASP_AGENTIC_TOP10}
    for f in findings:
        rule_id = f.get("rule_id", "")
        prefix = rule_id.split("-")[0] if "-" in rule_id else rule_id
        owasp_id = _RULE_PREFIX_TO_OWASP.get(prefix, "")
        if owasp_id in coverage:
            coverage[owasp_id] += 1
    return coverage


def _generate_recommendations(
    severity_counts: dict[str, int],
    findings: list[dict[str, Any]],
    trust_score: int,
) -> list[str]:
    """Generate actionable recommendations based on findings."""
    recs: list[str] = []

    critical = severity_counts.get("CRITICAL", 0)
    high = severity_counts.get("HIGH", 0)
    medium = severity_counts.get("MEDIUM", 0)

    if critical > 0:
        recs.append(
            f"URGENT: Address {critical} critical finding(s) immediately. "
            "These represent active exploit risks."
        )

    if high > 0:
        recs.append(
            f"HIGH PRIORITY: Resolve {high} high-severity finding(s) "
            "within the next sprint cycle."
        )

    if medium > 0:
        recs.append(
            f"Plan remediation for {medium} medium-severity finding(s) "
            "in the upcoming release."
        )

    # Check for common categories
    rule_ids = [f.get("rule_id", "") for f in findings]
    has_auth = any(r.startswith("AUTH") for r in rule_ids)
    has_cmd = any(r.startswith("CMD") for r in rule_ids)
    has_injection = any(r.startswith(("INJ", "SQL")) for r in rule_ids)

    if has_auth:
        recs.append(
            "Implement comprehensive authentication and authorization "
            "checks on all MCP tool endpoints."
        )

    if has_cmd:
        recs.append(
            "Sanitize all command execution inputs. Use allowlists "
            "instead of blocklists for shell commands."
        )

    if has_injection:
        recs.append(
            "Use parameterized queries and input validation to prevent "
            "injection attacks."
        )

    if trust_score < 50:
        recs.append(
            "Trust score is critically low. Consider a comprehensive "
            "security review before deployment."
        )
    elif trust_score < 75:
        recs.append(
            "Trust score indicates significant room for improvement. "
            "Prioritize high-severity findings."
        )

    if not recs:
        recs.append(
            "No critical findings detected. Maintain current security "
            "posture and continue regular audits."
        )

    return recs


def _compute_trust_score(severity_counts: dict[str, int]) -> int:
    """Compute a simple trust score from severity counts."""
    score = 100
    deductions = {
        "CRITICAL": 25,
        "HIGH": 15,
        "MEDIUM": 8,
        "LOW": 3,
        "INFO": 1,
    }
    for sev, ded in deductions.items():
        count = severity_counts.get(sev, 0)
        score -= min(count, 4) * ded
    return max(0, min(100, score))


def build_executive_report(
    scan_data: dict[str, Any],
    options: ReportOptions | None = None,
) -> ExecutiveReport:
    """Build an ExecutiveReport from scan data.

    Args:
        scan_data: Scan result dict (from ScanSummary.to_dict()).
        options: Report configuration options.

    Returns:
        ExecutiveReport with all sections populated.
    """
    if options is None:
        options = ReportOptions()

    summary = scan_data.get("summary", {})
    findings = _extract_findings(scan_data)
    severity_counts = _extract_severity_counts(scan_data)
    total_findings = summary.get("total_findings", len(findings))
    files_scanned = summary.get("files_scanned", 0)
    lines_scanned = summary.get("lines_scanned", 0)
    grade = summary.get("overall_grade", "A+")

    # Trust score: from scan data or computed
    ts_data = summary.get("trust_score", {})
    trust_score = ts_data.get("score", _compute_trust_score(severity_counts))

    owasp_coverage = _compute_owasp_coverage(findings)
    recommendations = _generate_recommendations(
        severity_counts, findings, trust_score,
    )

    report = ExecutiveReport(
        title=options.title,
        generated_at=options.generated_at,
        total_findings=total_findings,
        severity_counts=severity_counts,
        trust_score=trust_score,
        grade=grade,
        files_scanned=files_scanned,
        lines_scanned=lines_scanned,
        owasp_coverage=owasp_coverage,
        recommendations=recommendations,
    )

    return report


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

_CSS = """\
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
                 'Helvetica Neue', Arial, sans-serif;
    color: #1a1a2e; background: #ffffff;
    line-height: 1.6; font-size: 14px;
}
@media print {
    body { font-size: 11pt; }
    @page { size: A4; margin: 20mm 15mm; }
    .no-print { display: none !important; }
    .page-break { page-break-before: always; }
    header, footer { position: fixed; }
    header { top: 0; }
    footer { bottom: 0; }
}
header {
    background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
    color: #ffffff; padding: 32px 40px;
    border-bottom: 4px solid #e94560;
}
header .logo {
    font-size: 24px; font-weight: 700;
    letter-spacing: 2px; text-transform: uppercase;
}
header .logo-img { max-height: 48px; margin-right: 16px; vertical-align: middle; }
header h1 { font-size: 28px; margin-top: 8px; font-weight: 300; }
header .subtitle { font-size: 14px; opacity: 0.8; margin-top: 4px; }
header .meta { font-size: 12px; opacity: 0.6; margin-top: 8px; }
.container { max-width: 900px; margin: 0 auto; padding: 24px 40px; }
nav.toc {
    background: #f8f9fa; border: 1px solid #e0e0e0;
    border-radius: 8px; padding: 20px 28px; margin: 24px 0;
}
nav.toc h2 { font-size: 18px; margin-bottom: 12px; color: #1a1a2e; }
nav.toc ol { padding-left: 20px; }
nav.toc li { margin: 6px 0; }
nav.toc a { color: #0f3460; text-decoration: none; }
nav.toc a:hover { text-decoration: underline; }
section { margin: 32px 0; }
section h2 {
    font-size: 22px; color: #1a1a2e;
    border-bottom: 2px solid #e94560;
    padding-bottom: 8px; margin-bottom: 16px;
}
section h3 { font-size: 16px; color: #16213e; margin: 12px 0 8px; }
.kpi-grid {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: 16px; margin: 16px 0;
}
.kpi-card {
    background: #f8f9fa; border: 1px solid #e0e0e0;
    border-radius: 8px; padding: 16px; text-align: center;
}
.kpi-card .value { font-size: 36px; font-weight: 700; }
.kpi-card .label { font-size: 12px; color: #666; text-transform: uppercase; }
.severity-bar {
    display: flex; align-items: center; margin: 6px 0;
}
.severity-bar .label {
    width: 80px; font-weight: 600; font-size: 13px;
}
.severity-bar .bar-container {
    flex: 1; height: 24px; background: #f0f0f0;
    border-radius: 4px; overflow: hidden; margin: 0 12px;
}
.severity-bar .bar {
    height: 100%; border-radius: 4px;
    transition: width 0.3s ease;
}
.severity-bar .count { width: 40px; text-align: right; font-weight: 600; }
.trust-gauge {
    width: 200px; height: 100px; margin: 16px auto;
    position: relative; overflow: hidden;
}
.trust-gauge .gauge-bg {
    width: 200px; height: 100px;
    border-radius: 100px 100px 0 0;
    background: conic-gradient(
        from 0.75turn at 50% 100%,
        #dc2626 0%, #f97316 25%, #eab308 50%, #22c55e 75%, #16a34a 100%
    );
}
.trust-gauge .gauge-mask {
    position: absolute; bottom: 0; left: 20px;
    width: 160px; height: 80px;
    border-radius: 80px 80px 0 0;
    background: #ffffff;
}
.trust-gauge .gauge-value {
    position: absolute; bottom: 8px; width: 100%;
    text-align: center; font-size: 32px; font-weight: 700;
}
.trust-gauge .gauge-label {
    position: absolute; bottom: -20px; width: 100%;
    text-align: center; font-size: 12px; color: #666;
}
.owasp-table {
    width: 100%; border-collapse: collapse; margin: 16px 0;
}
.owasp-table th, .owasp-table td {
    padding: 10px 14px; text-align: left;
    border-bottom: 1px solid #e0e0e0;
}
.owasp-table th {
    background: #f8f9fa; font-weight: 600;
    font-size: 13px; text-transform: uppercase;
}
.owasp-table tr:hover { background: #f8f9fa; }
.findings-table {
    width: 100%; border-collapse: collapse; margin: 16px 0;
    font-size: 13px;
}
.findings-table th, .findings-table td {
    padding: 8px 12px; text-align: left;
    border-bottom: 1px solid #e0e0e0;
}
.findings-table th { background: #f8f9fa; font-weight: 600; }
.badge {
    display: inline-block; padding: 2px 8px;
    border-radius: 4px; font-size: 11px;
    font-weight: 600; color: #ffffff;
}
.badge-critical { background: #dc2626; }
.badge-high { background: #f97316; }
.badge-medium { background: #eab308; color: #1a1a2e; }
.badge-low { background: #22c55e; }
.badge-info { background: #6b7280; }
.recommendation {
    background: #f0f9ff; border-left: 4px solid #0284c7;
    padding: 12px 16px; margin: 8px 0; border-radius: 0 4px 4px 0;
}
.recommendation strong { color: #0284c7; }
footer {
    background: #f8f9fa; border-top: 1px solid #e0e0e0;
    padding: 16px 40px; text-align: center;
    font-size: 12px; color: #666;
}
.page-number::after {
    content: counter(page);
    counter-increment: page;
}
"""


# ---------------------------------------------------------------------------
# HTML generation helpers
# ---------------------------------------------------------------------------

def _esc(text: str) -> str:
    """HTML-escape text."""
    return html_mod.escape(str(text))


def _severity_badge(severity: str) -> str:
    """Generate a severity badge HTML."""
    sev = severity.upper()
    css_class = f"badge-{sev.lower()}"
    return f'<span class="badge {css_class}">{_esc(sev)}</span>'


def _build_header(options: ReportOptions) -> str:
    """Build the report header HTML."""
    logo_html = ""
    if options.logo_url:
        logo_html = f'<img src="{_esc(options.logo_url)}" class="logo-img" alt="Logo">'
    else:
        logo_html = f'<span class="logo">{_esc(options.logo_text)}</span>'

    subtitle_html = ""
    if options.subtitle:
        subtitle_html = f'<div class="subtitle">{_esc(options.subtitle)}</div>'

    author_html = ""
    if options.author:
        author_html = f" | Author: {_esc(options.author)}"

    return f"""\
<header>
  {logo_html}
  <h1>{_esc(options.title)}</h1>
  {subtitle_html}
  <div class="meta">Generated: {_esc(options.generated_at)}{author_html}</div>
</header>"""


def _build_toc(sections: list[ReportSection]) -> str:
    """Build table of contents HTML."""
    items = ""
    for s in sorted(sections, key=lambda x: x.order):
        items += f'    <li><a href="#{_esc(s.id)}">{_esc(s.title)}</a></li>\n'
    return f"""\
<nav class="toc">
  <h2>Table of Contents</h2>
  <ol>
{items}  </ol>
</nav>"""


def _build_executive_summary(report: ExecutiveReport) -> str:
    """Build executive summary section HTML."""
    grade_color = {
        "A+": "#16a34a", "A": "#16a34a", "B": "#eab308",
        "C": "#f97316", "D": "#dc2626", "F": "#dc2626",
    }.get(report.grade, "#666")

    return f"""\
<div class="kpi-grid">
  <div class="kpi-card">
    <div class="value" style="color:{grade_color}">{_esc(report.grade)}</div>
    <div class="label">Security Grade</div>
  </div>
  <div class="kpi-card">
    <div class="value">{report.total_findings}</div>
    <div class="label">Total Findings</div>
  </div>
  <div class="kpi-card">
    <div class="value">{report.files_scanned}</div>
    <div class="label">Files Scanned</div>
  </div>
  <div class="kpi-card">
    <div class="value">{report.lines_scanned:,}</div>
    <div class="label">Lines Analyzed</div>
  </div>
</div>"""


def _build_severity_bars(severity_counts: dict[str, int]) -> str:
    """Build severity distribution bar chart HTML."""
    max_count = max(severity_counts.values()) if severity_counts else 1
    if max_count == 0:
        max_count = 1

    bars = ""
    for sev in _SEVERITY_ORDER:
        count = severity_counts.get(sev, 0)
        pct = (count / max_count) * 100 if max_count > 0 else 0
        color = _SEVERITY_COLORS.get(sev, "#666")
        bars += f"""\
<div class="severity-bar">
  <span class="label">{sev}</span>
  <div class="bar-container">
    <div class="bar" style="width:{pct:.0f}%;background:{color}"></div>
  </div>
  <span class="count">{count}</span>
</div>
"""
    return bars


def _build_trust_gauge(score: int) -> str:
    """Build CSS trust score gauge HTML."""
    color = "#16a34a"
    if score < 50:
        color = "#dc2626"
    elif score < 75:
        color = "#f97316"
    elif score < 90:
        color = "#eab308"

    return f"""\
<div class="trust-gauge">
  <div class="gauge-bg"></div>
  <div class="gauge-mask"></div>
  <div class="gauge-value" style="color:{color}">{score}</div>
  <div class="gauge-label">Trust Score</div>
</div>"""


def _build_owasp_table(owasp_coverage: dict[str, int]) -> str:
    """Build OWASP coverage table HTML."""
    rows = ""
    for cat_id, cat_name in _OWASP_AGENTIC_TOP10.items():
        count = owasp_coverage.get(cat_id, 0)
        status = "Findings detected" if count > 0 else "No findings"
        status_color = "#dc2626" if count > 0 else "#16a34a"
        rows += f"""\
  <tr>
    <td><strong>{_esc(cat_id)}</strong></td>
    <td>{_esc(cat_name)}</td>
    <td style="text-align:center">{count}</td>
    <td style="color:{status_color}">{status}</td>
  </tr>
"""
    return f"""\
<table class="owasp-table">
  <thead>
    <tr>
      <th>ID</th>
      <th>Category</th>
      <th style="text-align:center">Findings</th>
      <th>Status</th>
    </tr>
  </thead>
  <tbody>
{rows}  </tbody>
</table>"""


def _build_findings_table(findings: list[dict[str, Any]]) -> str:
    """Build findings detail table HTML."""
    if not findings:
        return "<p>No findings to display.</p>"

    # Sort by severity
    sev_idx = {s: i for i, s in enumerate(_SEVERITY_ORDER)}
    sorted_findings = sorted(
        findings,
        key=lambda f: sev_idx.get(f.get("severity", "INFO"), 99),
    )

    rows = ""
    for f in sorted_findings:
        sev = f.get("severity", "INFO")
        rows += f"""\
  <tr>
    <td>{_severity_badge(sev)}</td>
    <td><code>{_esc(f.get('rule_id', ''))}</code></td>
    <td>{_esc(f.get('title', ''))}</td>
    <td><code>{_esc(f.get('file', ''))}</code></td>
    <td>{f.get('line', 0)}</td>
  </tr>
"""

    return f"""\
<table class="findings-table">
  <thead>
    <tr>
      <th>Severity</th>
      <th>Rule</th>
      <th>Finding</th>
      <th>File</th>
      <th>Line</th>
    </tr>
  </thead>
  <tbody>
{rows}  </tbody>
</table>"""


def _build_recommendations(recommendations: list[str]) -> str:
    """Build recommendations section HTML."""
    if not recommendations:
        return "<p>No recommendations at this time.</p>"

    items = ""
    for i, rec in enumerate(recommendations, 1):
        items += f"""\
<div class="recommendation">
  <strong>#{i}</strong> {_esc(rec)}
</div>
"""
    return items


def _build_appendix(
    scan_data: dict[str, Any],
    findings: list[dict[str, Any]],
) -> str:
    """Build appendix section with raw scan metadata."""
    summary = scan_data.get("summary", {})
    scan_time = summary.get("scan_time_ms", 0)

    details = f"""\
<h3>Scan Metadata</h3>
<table class="findings-table">
  <tr><td><strong>Files Scanned</strong></td><td>{summary.get('files_scanned', 0)}</td></tr>
  <tr><td><strong>Lines Scanned</strong></td><td>{summary.get('lines_scanned', 0):,}</td></tr>
  <tr><td><strong>Scan Time</strong></td><td>{scan_time:.1f} ms</td></tr>
  <tr><td><strong>Exit Code</strong></td><td>{summary.get('exit_code', 0)}</td></tr>
</table>
"""

    # Unique rules found
    rule_ids = sorted(set(f.get("rule_id", "") for f in findings))
    if rule_ids:
        details += "\n<h3>Rules Triggered</h3>\n<ul>\n"
        for rid in rule_ids:
            count = sum(1 for f in findings if f.get("rule_id") == rid)
            details += f"  <li><code>{_esc(rid)}</code> ({count} occurrence(s))</li>\n"
        details += "</ul>\n"

    return details


def _build_footer(options: ReportOptions) -> str:
    """Build report footer HTML."""
    return f"""\
<footer>
  <div>Generated by {_esc(options.logo_text)} | {_esc(options.generated_at)}</div>
  <div class="page-number">Page </div>
</footer>"""


# ---------------------------------------------------------------------------
# Main generation function
# ---------------------------------------------------------------------------

def generate_executive_report(
    scan_data: dict[str, Any],
    options: ReportOptions | dict[str, Any] | None = None,
) -> str:
    """Generate a professional executive HTML report.

    Args:
        scan_data: Scan result dict (from ScanSummary.to_dict()).
        options: Report options (ReportOptions, dict, or None for defaults).

    Returns:
        Complete HTML string ready for display or print.
    """
    if options is None:
        opts = ReportOptions()
    elif isinstance(options, dict):
        opts = ReportOptions.from_dict(options)
    else:
        opts = options

    report = build_executive_report(scan_data, opts)
    findings = _extract_findings(scan_data)

    # Build sections
    sections: list[ReportSection] = []

    # 1. Executive Summary
    sections.append(ReportSection(
        id="executive-summary",
        title="Executive Summary",
        content=_build_executive_summary(report),
        order=1,
    ))

    # 2. Findings by Severity
    sections.append(ReportSection(
        id="findings-severity",
        title="Findings by Severity",
        content=_build_severity_bars(report.severity_counts)
        + _build_findings_table(findings),
        order=2,
    ))

    # 3. OWASP Coverage
    if opts.include_owasp:
        sections.append(ReportSection(
            id="owasp-coverage",
            title="OWASP Agentic Top 10 Coverage",
            content=_build_owasp_table(report.owasp_coverage),
            order=3,
        ))

    # 4. Trust Score
    if opts.include_trust_score:
        sections.append(ReportSection(
            id="trust-score",
            title="Trust Score",
            content=_build_trust_gauge(report.trust_score),
            order=4,
        ))

    # 5. Recommendations
    if opts.include_recommendations:
        sections.append(ReportSection(
            id="recommendations",
            title="Recommendations",
            content=_build_recommendations(report.recommendations),
            order=5,
        ))

    # 6. Appendix
    if opts.include_appendix:
        sections.append(ReportSection(
            id="appendix",
            title="Appendix",
            content=_build_appendix(scan_data, findings),
            order=6,
        ))

    report.sections = sections

    # Build full HTML
    header_html = _build_header(opts)
    toc_html = _build_toc(sections) if opts.include_toc else ""

    sections_html = ""
    for section in sorted(sections, key=lambda s: s.order):
        sections_html += f"""\
<section id="{_esc(section.id)}" class="page-break">
  <h2>{_esc(section.title)}</h2>
  {section.content}
</section>
"""

    footer_html = _build_footer(opts)
    custom_css = opts.custom_css or ""

    return f"""\
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{_esc(opts.title)}</title>
  <style>{_CSS}{custom_css}</style>
</head>
<body>
{header_html}
<div class="container">
{toc_html}
{sections_html}
</div>
{footer_html}
</body>
</html>"""


# ---------------------------------------------------------------------------
# Format helpers
# ---------------------------------------------------------------------------

def format_report_json(report: ExecutiveReport) -> str:
    """Format executive report as JSON.

    Args:
        report: ExecutiveReport to format.

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


def format_report_text(report: ExecutiveReport) -> str:
    """Format executive report as plain text summary.

    Args:
        report: ExecutiveReport to format.

    Returns:
        Text string.
    """
    lines: list[str] = [
        f"{'=' * 60}",
        f"  {report.title}",
        f"  Generated: {report.generated_at}",
        f"{'=' * 60}",
        "",
        "  EXECUTIVE SUMMARY",
        f"  Grade: {report.grade}  |  Trust Score: {report.trust_score}/100",
        f"  Findings: {report.total_findings}  |  Files: {report.files_scanned}",
        "",
        "  SEVERITY DISTRIBUTION",
    ]

    for sev in _SEVERITY_ORDER:
        count = report.severity_counts.get(sev, 0)
        bar = "#" * min(count, 40)
        lines.append(f"  {sev:<10} {count:>4}  {bar}")

    lines.append("")
    lines.append("  RECOMMENDATIONS")
    for i, rec in enumerate(report.recommendations, 1):
        lines.append(f"  {i}. {rec}")

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