"""CECL/CCAR/DFAST regulatory report exporter.

Generates structured HTML and Markdown reports matching Fed disclosure
formats accepted by bank risk teams for CECL/CCAR/DFAST submissions.

Stdlib-only (Tier 0 compliant — zero external dependencies).
"""

from __future__ import annotations

import datetime
import html
import math
import pathlib
from dataclasses import dataclass, field

from orxaq.credit.cecl import ECLResult
from orxaq.credit.portfolio import LoanExposure
from orxaq.credit.reporting import RiskReport
from orxaq.credit.stress_report import StressTestReport

__all__ = [
    "RegulatorReportConfig",
    "RegulatorReportExporter",
]

# Basel III Tier-1 minimum capital ratio (8%)
_TIER1_MIN_RATIO = 0.08

# CECL/CCAR disclosure version
_DISCLOSURE_VERSION = "2.0"

# Inline CSS — no external dependencies
_CSS = """
body{font-family:'Helvetica Neue',Arial,sans-serif;font-size:14px;
  color:#1a1a2e;background:#fafafa;margin:0;padding:0}
.page{max-width:960px;margin:0 auto;padding:40px 48px;background:#fff;
  box-shadow:0 2px 8px rgba(0,0,0,.08)}
h1{font-size:24px;font-weight:700;border-bottom:3px solid #003087;
  padding-bottom:12px;margin-bottom:24px;color:#003087}
h2{font-size:18px;font-weight:600;color:#003087;margin-top:36px;
  border-left:4px solid #cc0000;padding-left:10px}
h3{font-size:14px;font-weight:600;color:#444;margin-top:16px}
.meta{font-size:12px;color:#777;margin-bottom:32px}
.meta span{margin-right:24px}
.kpi-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;
  margin:20px 0}
.kpi{background:#f4f7fb;border-radius:6px;padding:16px;
  border-top:3px solid #003087}
.kpi .label{font-size:11px;color:#666;text-transform:uppercase;
  letter-spacing:.05em}
.kpi .value{font-size:22px;font-weight:700;color:#003087;margin-top:4px}
.kpi .sub{font-size:11px;color:#888;margin-top:2px}
table{width:100%;border-collapse:collapse;font-size:13px;margin:16px 0}
th{background:#003087;color:#fff;padding:8px 12px;text-align:left;
  font-weight:500}
td{padding:7px 12px;border-bottom:1px solid #e8e8e8}
tr:nth-child(even) td{background:#f9f9f9}
.alert{background:#fff3cd;border:1px solid #ffc107;border-radius:4px;
  padding:12px 16px;margin:16px 0;font-size:13px}
.attestation{background:#e8f5e9;border:1px solid #4caf50;border-radius:4px;
  padding:16px;margin:16px 0;font-size:13px}
.rating-high{color:#c62828;font-weight:700}
.rating-moderate{color:#e65100;font-weight:700}
.rating-low{color:#2e7d32;font-weight:700}
footer{margin-top:48px;padding-top:16px;border-top:1px solid #ddd;
  font-size:11px;color:#999}
"""


@dataclass(frozen=True)
class RegulatorReportConfig:
    """Configuration for regulatory report generation."""

    institution_name: str = "Orxaq Financial, N.A."
    report_date: str = field(default_factory=lambda: datetime.date.today().isoformat())
    disclosure_version: str = _DISCLOSURE_VERSION
    include_fair_lending: bool = True
    include_capital_adequacy: bool = True


class RegulatorReportExporter:
    """Generates CECL/CCAR/DFAST regulatory HTML/Markdown reports.

    Accepts a StressTestReport, RiskReport, and ECLResult and produces
    a single consolidated regulatory document matching Fed disclosure
    format requirements.

    All rendering is stdlib-only (Tier 0 compliant).
    """

    def __init__(
        self,
        stress_report: StressTestReport,
        risk_report: RiskReport,
        ecl_result: ECLResult,
        portfolio: list[LoanExposure] | None = None,
        config: RegulatorReportConfig | None = None,
    ) -> None:
        self._stress = stress_report
        self._risk = risk_report
        self._ecl = ecl_result
        self._portfolio = portfolio or []
        self._cfg = config or RegulatorReportConfig()

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

    def to_html(self) -> str:
        """Render report as a complete HTML document with inline CSS."""
        body = "\n".join(
            [
                self._html_header(),
                self._html_executive_summary(),
                self._html_portfolio_composition(),
                self._html_ecl_estimates(),
                self._html_stress_results(),
                self._html_migration_matrices(),
                self._html_capital_adequacy(),
                self._html_fair_lending(),
                self._html_footer(),
            ]
        )
        inst = self._cfg.institution_name
        return (
            "<!DOCTYPE html>\n"
            '<html lang="en">\n'
            "<head>\n"
            '<meta charset="UTF-8">\n'
            '<meta name="viewport" content="width=device-width,initial-scale=1">\n'
            f"<title>CECL/CCAR Regulatory Report — {inst}</title>\n"
            f"<style>{_CSS}</style>\n"
            "</head>\n"
            "<body>\n"
            '<div class="page">\n'
            f"{body}\n"
            "</div>\n"
            "</body>\n"
            "</html>\n"
        )

    def to_markdown(self) -> str:
        """Render report as Markdown (fallback for non-HTML contexts)."""
        sections = [
            self._md_header(),
            self._md_executive_summary(),
            self._md_portfolio_composition(),
            self._md_ecl_estimates(),
            self._md_stress_results(),
            self._md_migration_matrices(),
            self._md_capital_adequacy(),
            self._md_fair_lending(),
        ]
        return "\n\n".join(sections)

    def save(self, path: str | pathlib.Path, format: str = "html") -> None:  # noqa: A002
        """Write the report to a file.

        Args:
            path: Output file path.
            format: "html" (default) or "markdown".
        """
        out = pathlib.Path(path)
        if format == "markdown":
            out.write_text(self.to_markdown(), encoding="utf-8")
        else:
            out.write_text(self.to_html(), encoding="utf-8")

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

    def _html_header(self) -> str:
        scenario = html.escape(self._stress.scenario_name.replace("_", " ").title())
        inst = html.escape(self._cfg.institution_name)
        risk_cls = {
            "low": "rating-low",
            "moderate": "rating-moderate",
            "high": "rating-high",
            "critical": "rating-high",
        }.get(self._risk.risk_rating, "rating-moderate")
        ver = html.escape(self._cfg.disclosure_version)
        rating = html.escape(self._risk.risk_rating.upper())
        gen = html.escape(self._stress.generated_at[:19])
        dt = html.escape(self._cfg.report_date)
        return (
            "<h1>CECL / CCAR / DFAST Regulatory Disclosure</h1>\n"
            '<div class="meta">\n'
            f"  <span><strong>Institution:</strong> {inst}</span>\n"
            f"  <span><strong>Report Date:</strong> {dt}</span>\n"
            f"  <span><strong>Scenario:</strong> {scenario}</span>\n"
            f"  <span><strong>Disclosure Version:</strong> {ver}</span>\n"
            "  <span><strong>Risk Rating:</strong> "
            f'<span class="{risk_cls}">{rating}</span></span>\n'
            f"  <span><strong>Generated:</strong> {gen} UTC</span>\n"
            "</div>\n"
        )

    def _html_executive_summary(self) -> str:
        ecl = self._ecl
        ci_low, ci_high = ecl.confidence_band
        total_ecl_9q = sum(self._stress.quarterly_ecl)
        narrative = html.escape(self._risk.summary)
        k1 = self._kpi("Total ECL (9Q)", self._fmt_currency(total_ecl_9q), "9-quarter horizon")
        k2 = self._kpi(
            "Portfolio Loss Rate",
            self._fmt_pct(self._stress.portfolio_loss_rate),
            "ECL / Total EAD",
        )
        k3 = self._kpi(
            "Capital Impact",
            self._fmt_pct(self._stress.capital_impact),
            "Tier-1 ratio delta",
        )
        k4 = self._kpi("ECL (1Q, point est.)", self._fmt_currency(ecl.total_ecl), "Current quarter")
        k5 = self._kpi("90% CI Low", self._fmt_currency(ci_low), "Confidence band")
        k6 = self._kpi("90% CI High", self._fmt_currency(ci_high), "Confidence band")
        return (
            "<h2>Executive Summary</h2>\n"
            '<div class="kpi-grid">\n'
            f"  {k1}\n"
            f"  {k2}\n"
            f"  {k3}\n"
            f"  {k4}\n"
            f"  {k5}\n"
            f"  {k6}\n"
            "</div>\n"
            f"<p>{narrative}</p>\n"
        )

    def _html_portfolio_composition(self) -> str:
        total_ead = self._ecl.ead_total
        sectors = self._sector_concentration()
        rows = "\n".join(
            f"  <tr><td>{html.escape(s)}</td><td>{html.escape(self._fmt_pct(w))}</td></tr>"
            for s, w in sectors
        )
        loan_cnt = str(len(self._portfolio)) if self._portfolio else "N/A"
        k1 = self._kpi("Loan Count", loan_cnt, "Active exposures")
        k2 = self._kpi("Total EAD", self._fmt_currency(total_ead), "Exposure at default")
        k3 = self._kpi("Mean PD", self._fmt_pct(self._ecl.pd_mean), "Portfolio weighted avg")
        return (
            "<h2>Portfolio Composition</h2>\n"
            '<div class="kpi-grid">\n'
            f"  {k1}\n"
            f"  {k2}\n"
            f"  {k3}\n"
            "</div>\n"
            "<h3>Sector Concentration</h3>\n"
            "<table>\n"
            "  <thead><tr><th>Sector</th><th>EAD Weight</th></tr></thead>\n"
            f"  <tbody>{rows}</tbody>\n"
            "</table>\n"
        )

    def _html_ecl_estimates(self) -> str:
        ecl = self._ecl
        ci_low, ci_high = ecl.confidence_band
        total_ecl_9q = sum(self._stress.quarterly_ecl)
        consistency_note = ""
        if not (ci_low <= ecl.total_ecl <= ci_high):
            consistency_note = (
                '<div class="alert">Note: Point estimate falls outside reported '
                "confidence band — verify model assumptions.</div>\n"
            )
        ecl_val = html.escape(self._fmt_currency(ecl.total_ecl))
        ecl9q_val = html.escape(self._fmt_currency(total_ecl_9q))
        ci_lo = html.escape(self._fmt_currency(ci_low))
        ci_hi = html.escape(self._fmt_currency(ci_high))
        pd_val = html.escape(self._fmt_pct(ecl.pd_mean))
        lgd_val = html.escape(self._fmt_pct(ecl.lgd))
        hz_val = html.escape(ecl.horizon_method)
        return (
            "<h2>ECL Estimates</h2>\n"
            f"{consistency_note}"
            "<table>\n"
            "  <thead><tr><th>Metric</th><th>Value</th><th>Notes</th></tr></thead>\n"
            "  <tbody>\n"
            f"  <tr><td>Current Quarter ECL</td><td>{ecl_val}</td>"
            "<td>CECL point estimate (1Q)</td></tr>\n"
            f"  <tr><td>9-Quarter Cumulative ECL</td><td>{ecl9q_val}</td>"
            "<td>CCAR horizon (9Q)</td></tr>\n"
            "  <tr><td>Confidence Interval (90%)</td>"
            f"<td>[{ci_lo}, {ci_hi}]</td>"
            "<td>Low / High band</td></tr>\n"
            f"  <tr><td>Mean PD</td><td>{pd_val}</td>"
            "<td>Portfolio weighted average</td></tr>\n"
            f"  <tr><td>LGD (assumed)</td><td>{lgd_val}</td>"
            "<td>Basel II foundation approach</td></tr>\n"
            f"  <tr><td>Horizon Method</td><td>{hz_val}</td>"
            "<td>Soliton-dynamic or fixed</td></tr>\n"
            "  </tbody>\n"
            "</table>\n"
        )

    def _html_stress_results(self) -> str:
        q_ecl = self._stress.quarterly_ecl
        ead = self._ecl.ead_total

        def _row(i: int, v: float) -> str:
            ecl_str = html.escape(self._fmt_currency(v))
            lr = html.escape(self._fmt_pct(v / ead if ead else 0))
            return f"  <tr><td>Q{i + 1}</td><td>{ecl_str}</td><td>{lr}</td></tr>"

        rows = "\n".join(_row(i, v) for i, v in enumerate(q_ecl))
        scenario = html.escape(self._stress.scenario_name.replace("_", " ").title())
        ci = self._stress.confidence_intervals
        ci_ecl = ci.get("total_ecl", (0.0, 0.0))
        lr_str = html.escape(self._fmt_pct(self._stress.portfolio_loss_rate))
        ci_str = html.escape(self._fmt_pct(self._stress.capital_impact))
        ci_lo = html.escape(self._fmt_currency(ci_ecl[0]))
        ci_hi = html.escape(self._fmt_currency(ci_ecl[1]))
        return (
            "<h2>Stress Test Results</h2>\n"
            f"<p><strong>Scenario:</strong> {scenario} &mdash; "
            "CCAR 9-quarter horizon. "
            f"Portfolio loss rate: <strong>{lr_str}</strong>. "
            f"Capital impact: <strong>{ci_str}</strong>.</p>\n"
            "<h3>Quarterly ECL Projections</h3>\n"
            "<table>\n"
            "  <thead>"
            "<tr><th>Quarter</th><th>Projected ECL</th><th>Loss Rate</th></tr>"
            "</thead>\n"
            f"  <tbody>{rows}</tbody>\n"
            "</table>\n"
            f"<p>9-quarter ECL confidence interval: [{ci_lo}, {ci_hi}]</p>\n"
        )

    def _html_migration_matrices(self) -> str:
        matrices = self._stress.migration_matrices
        if not matrices:
            return "<h2>Migration Matrix</h2>\n<p>No migration data available.</p>\n"
        sections = []
        for idx in [0, len(matrices) - 1]:
            label = f"Q{idx + 1}"
            mm = matrices[idx]
            ratings = sorted({r for row in mm.values() for r in row})
            from_ratings = sorted(mm.keys())
            header = (
                "<tr><th>From \\ To</th>"
                + "".join(f"<th>{html.escape(r)}</th>" for r in ratings)
                + "</tr>"
            )
            body_rows = []
            for fr in from_ratings:
                cells = "".join(
                    f"<td>{html.escape(self._fmt_pct(mm[fr].get(r, 0.0)))}</td>" for r in ratings
                )
                fr_esc = html.escape(fr)
                body_rows.append(f"<tr><td><strong>{fr_esc}</strong></td>{cells}</tr>")
            sections.append(
                f"<h3>Migration Matrix — {label}</h3>\n"
                "<table>\n"
                f"  <thead>{header}</thead>\n"
                f"  <tbody>{''.join(body_rows)}</tbody>\n"
                "</table>\n"
            )
        return "<h2>Migration Matrix</h2>\n" + "\n".join(sections)

    def _html_capital_adequacy(self) -> str:
        if not self._cfg.include_capital_adequacy:
            return ""
        tier1_impact = self._stress.capital_impact
        tier1_remaining = max(0.0, _TIER1_MIN_RATIO - tier1_impact)
        adequacy_status = "Adequate" if tier1_remaining > 0.02 else "Monitor"
        t1_base = html.escape(self._fmt_pct(_TIER1_MIN_RATIO))
        t1_impact = html.escape(self._fmt_pct(tier1_impact))
        t1_rem = html.escape(self._fmt_pct(tier1_remaining))
        t1_status = html.escape(adequacy_status)
        return (
            "<h2>Capital Adequacy</h2>\n"
            "<table>\n"
            "  <thead>"
            "<tr><th>Metric</th><th>Value</th><th>Minimum (Basel III)</th></tr>"
            "</thead>\n"
            "  <tbody>\n"
            "  <tr><td>Tier-1 Capital Ratio (baseline)</td>"
            f"<td>{t1_base}</td><td>8.0% (Basel III minimum)</td></tr>\n"
            "  <tr><td>Stress-Scenario Capital Impact</td>"
            f"<td>-{t1_impact}</td><td>&mdash;</td></tr>\n"
            "  <tr><td>Post-Stress Tier-1 Ratio</td>"
            f"<td>{t1_rem}</td><td>4.5% (CET1 floor)</td></tr>\n"
            "  <tr><td>Capital Adequacy Status</td>"
            f"<td><strong>{t1_status}</strong></td><td>&mdash;</td></tr>\n"
            "  </tbody>\n"
            "</table>\n"
        )

    def _html_fair_lending(self) -> str:
        if not self._cfg.include_fair_lending:
            return ""
        return (
            "<h2>Fair Lending Attestation</h2>\n"
            '<div class="attestation">\n'
            "<p><strong>Fair Lending Compliance Statement</strong></p>\n"
            "<p>The institution attests that all credit decisions reflected in this "
            "CECL/CCAR disclosure were made in compliance with the Equal Credit "
            "Opportunity Act (ECOA, 15 U.S.C. § 1691 <em>et seq.</em>), the Fair "
            "Housing Act (FHA, 42 U.S.C. § 3601 <em>et seq.</em>), and applicable "
            "Consumer Financial Protection Bureau (CFPB) supervisory guidance.</p>\n"
            "<p>Model inputs do not include prohibited characteristics (race, color, "
            "religion, national origin, sex, marital status, age, or familial status). "
            "Disparate impact analysis was conducted for all material credit criteria. "
            "Results are available upon regulatory request.</p>\n"
            "<p>This attestation is subject to annual independent model validation per "
            "SR 11-7 (Supervisory Guidance on Model Risk Management).</p>\n"
            "</div>\n"
        )

    def _html_footer(self) -> str:
        gen = html.escape(self._stress.generated_at[:10])
        return (
            "<footer>\n"
            f"<p>Generated by Orxaq RegulatorReportExporter v{_DISCLOSURE_VERSION}"
            " &mdash; CECL/CCAR/DFAST Disclosure &mdash; "
            f"{gen}. "
            "Confidential — for regulatory submission only.</p>\n"
            "</footer>\n"
        )

    # ------------------------------------------------------------------
    # Markdown sections
    # ------------------------------------------------------------------

    def _md_header(self) -> str:
        scenario = self._stress.scenario_name.replace("_", " ").title()
        return (
            "# CECL / CCAR / DFAST Regulatory Disclosure\n\n"
            f"**Institution:** {self._cfg.institution_name}  \n"
            f"**Report Date:** {self._cfg.report_date}  \n"
            f"**Scenario:** {scenario}  \n"
            f"**Risk Rating:** {self._risk.risk_rating.upper()}  \n"
            f"**Generated:** {self._stress.generated_at[:19]} UTC"
        )

    def _md_executive_summary(self) -> str:
        ecl = self._ecl
        total_ecl_9q = sum(self._stress.quarterly_ecl)
        ci_low, ci_high = ecl.confidence_band
        lr = self._fmt_pct(self._stress.portfolio_loss_rate)
        cap = self._fmt_pct(self._stress.capital_impact)
        ci_str = f"[{self._fmt_currency(ci_low)}, {self._fmt_currency(ci_high)}]"
        return (
            "## Executive Summary\n\n"
            f"{self._risk.summary}\n\n"
            "| Metric | Value |\n"
            "|--------|-------|\n"
            f"| Total ECL (9Q) | {self._fmt_currency(total_ecl_9q)} |\n"
            f"| Portfolio Loss Rate | {lr} |\n"
            f"| Capital Impact | {cap} |\n"
            f"| ECL Point Estimate (1Q) | {self._fmt_currency(ecl.total_ecl)} |\n"
            f"| 90% CI | {ci_str} |"
        )

    def _md_portfolio_composition(self) -> str:
        sectors = self._sector_concentration()
        rows = "\n".join(f"| {s} | {self._fmt_pct(w)} |" for s, w in sectors)
        loan_cnt = len(self._portfolio) if self._portfolio else "N/A"
        return (
            "## Portfolio Composition\n\n"
            "| Metric | Value |\n"
            "|--------|-------|\n"
            f"| Loan Count | {loan_cnt} |\n"
            f"| Total EAD | {self._fmt_currency(self._ecl.ead_total)} |\n"
            f"| Mean PD | {self._fmt_pct(self._ecl.pd_mean)} |\n\n"
            "### Sector Concentration\n\n"
            "| Sector | EAD Weight |\n"
            "|--------|------------|\n"
            f"{rows}"
        )

    def _md_ecl_estimates(self) -> str:
        ecl = self._ecl
        ci_low, ci_high = ecl.confidence_band
        total_ecl_9q = sum(self._stress.quarterly_ecl)
        ci_str = f"[{self._fmt_currency(ci_low)}, {self._fmt_currency(ci_high)}]"
        return (
            "## ECL Estimates\n\n"
            "| Metric | Value | Notes |\n"
            "|--------|-------|-------|\n"
            f"| Current Quarter ECL | {self._fmt_currency(ecl.total_ecl)}"
            " | CECL point estimate (1Q) |\n"
            f"| 9-Quarter Cumulative ECL | {self._fmt_currency(total_ecl_9q)}"
            " | CCAR horizon |\n"
            f"| Confidence Interval (90%) | {ci_str} | Low / High band |\n"
            f"| Mean PD | {self._fmt_pct(ecl.pd_mean)} | Portfolio weighted average |\n"
            f"| LGD (assumed) | {self._fmt_pct(ecl.lgd)} | Basel II foundation approach |"
        )

    def _md_stress_results(self) -> str:
        q_ecl = self._stress.quarterly_ecl
        ead = self._ecl.ead_total
        rows = "\n".join(
            f"| Q{i + 1} | {self._fmt_currency(v)} | {self._fmt_pct(v / ead if ead else 0)} |"
            for i, v in enumerate(q_ecl)
        )
        scenario = self._stress.scenario_name.replace("_", " ").title()
        lr = self._fmt_pct(self._stress.portfolio_loss_rate)
        cap = self._fmt_pct(self._stress.capital_impact)
        return (
            "## Stress Test Results\n\n"
            f"**Scenario:** {scenario}\n\n"
            f"Portfolio loss rate: **{lr}**  \n"
            f"Capital impact: **{cap}**\n\n"
            "### Quarterly ECL Projections\n\n"
            "| Quarter | Projected ECL | Loss Rate |\n"
            "|---------|---------------|----------|\n"
            f"{rows}"
        )

    def _md_migration_matrices(self) -> str:
        matrices = self._stress.migration_matrices
        if not matrices:
            return "## Migration Matrix\n\nNo migration data available."
        mm = matrices[0]
        ratings = sorted({r for row in mm.values() for r in row})
        from_ratings = sorted(mm.keys())
        header = "| From \\ To | " + " | ".join(ratings) + " |"
        sep = "|" + "---|" * (len(ratings) + 1)
        body = "\n".join(
            "| "
            + fr
            + " | "
            + " | ".join(self._fmt_pct(mm[fr].get(r, 0.0)) for r in ratings)
            + " |"
            for fr in from_ratings
        )
        return f"## Migration Matrix\n\n### Q1\n\n{header}\n{sep}\n{body}"

    def _md_capital_adequacy(self) -> str:
        tier1_impact = self._stress.capital_impact
        tier1_remaining = max(0.0, _TIER1_MIN_RATIO - tier1_impact)
        return (
            "## Capital Adequacy\n\n"
            "| Metric | Value | Minimum (Basel III) |\n"
            "|--------|-------|--------------------|\n"
            f"| Tier-1 Capital Ratio (baseline)"
            f" | {self._fmt_pct(_TIER1_MIN_RATIO)} | 8.0% |\n"
            f"| Stress-Scenario Capital Impact"
            f" | -{self._fmt_pct(tier1_impact)} | — |\n"
            f"| Post-Stress Tier-1 Ratio"
            f" | {self._fmt_pct(tier1_remaining)} | 4.5% (CET1 floor) |"
        )

    def _md_fair_lending(self) -> str:
        return (
            "## Fair Lending Attestation\n\n"
            "The institution attests that all credit decisions reflected in this "
            "CECL/CCAR disclosure were made in compliance with the Equal Credit "
            "Opportunity Act (ECOA), the Fair Housing Act (FHA), and applicable "
            "CFPB supervisory guidance.\n\n"
            "Model inputs do not include prohibited characteristics. Disparate "
            "impact analysis was conducted for all material credit criteria. "
            "Results are available upon regulatory request.\n\n"
            "This attestation is subject to annual independent model validation per "
            "SR 11-7 (Supervisory Guidance on Model Risk Management)."
        )

    # ------------------------------------------------------------------
    # Private helpers
    # ------------------------------------------------------------------

    def _sector_concentration(self) -> list[tuple[str, float]]:
        """Return list of (sector, ead_weight) sorted by weight descending."""
        if not self._portfolio:
            return [("N/A", 1.0)]
        total_ead = sum(e.ead for e in self._portfolio)
        if total_ead == 0.0:
            return [("N/A", 0.0)]
        sector_ead: dict[str, float] = {}
        for e in self._portfolio:
            sector_ead[e.sector] = sector_ead.get(e.sector, 0.0) + e.ead
        return sorted(
            [(s, v / total_ead) for s, v in sector_ead.items()],
            key=lambda x: x[1],
            reverse=True,
        )

    @staticmethod
    def _fmt_currency(v: float) -> str:
        """Format value as currency string (USD millions abbreviated)."""
        if math.isnan(v) or math.isinf(v):
            return "N/A"
        if abs(v) >= 1_000_000:
            return f"${v / 1_000_000:,.2f}M"
        if abs(v) >= 1_000:
            return f"${v:,.0f}"
        return f"${v:.2f}"

    @staticmethod
    def _fmt_pct(v: float) -> str:
        """Format value as percentage string."""
        if math.isnan(v) or math.isinf(v):
            return "N/A"
        return f"{v * 100:.2f}%"

    @staticmethod
    def _kpi(label: str, value: str, sub: str = "") -> str:
        """Render a KPI card HTML snippet."""
        sub_html = f'<div class="sub">{html.escape(sub)}</div>' if sub else ""
        return (
            '<div class="kpi">'
            f'<div class="label">{html.escape(label)}</div>'
            f'<div class="value">{html.escape(value)}</div>'
            f"{sub_html}"
            "</div>"
        )
