"""Notebook API for inventory & certificate operations.

CxO-friendly interface — hides subprocess calls, JSON parsing, plotly chart
building, and error handling behind clean function calls.

Usage in notebooks:
    from runbooks.notebooks.inventory_api import (
        run_cross_validate, run_cert_triage, run_resource_explorer,
        show_cert_expiry_chart, show_cert_risk_chart,
        show_accuracy_chart, show_v1_v2_chart,
        show_expiring_certs, show_removable_certs,
        generate_cxo_report, generate_stakeholder_email,
    )
"""

import json
import subprocess
import shutil
import sys
from datetime import datetime
from pathlib import Path

import pandas as pd
import plotly.express as px
import plotly.graph_objects as go


def _display_chart(fig) -> None:
    """Display a Plotly figure — renders in JupyterLab AND HTML export.

    Uses fig.to_html with CDN plotly.js — works in:
    - JupyterLab (script executes, chart is interactive)
    - nbconvert HTML export (self-contained HTML)
    - VS Code notebooks (HTML renderer)
    - Stored outputs (no kernel needed to render)
    """
    try:
        from IPython.display import display, HTML

        display(HTML(fig.to_html(include_plotlyjs="cdn", full_html=False)))
    except (ImportError, Exception):
        pass  # Headless — caller uses returned fig


def _runbooks_cmd() -> str:
    """Resolve the Python runbooks CLI path (not the Node.js ADLC binary).

    When both nnthanh101/adlc (Node.js) and runbooks (Python) are installed,
    bare 'runbooks' may resolve to the Node.js binary. This finds the Python one.
    """
    import importlib.util

    # Best: use the entry point from the same venv as this module
    venv_bin = Path(sys.executable).parent / "runbooks"
    if venv_bin.exists():
        return str(venv_bin)
    # Fallback: python -m runbooks.main
    return "runbooks"


# ── CLI Execution (subprocess wrappers) ───────────────────────


def run_cross_validate(
    profile: str,
    output_dir: str = "data/evidence/inventory",
    persona: str = "all",
    resource_type: str | None = None,
) -> dict:
    """Run inventory cross-validation and return parsed evidence dict.

    Args:
        profile: AWS SSO profile name (e.g. $AWS_OPERATIONS_PROFILE).
        output_dir: Directory for JSON evidence output.
        persona: Persona filter (default "all").
        resource_type: Optional single resource type to validate.

    Returns:
        dict with keys: per_type, overall_accuracy, overall_resources, timing, etc.

    Raises:
        RuntimeError: If CLI exits non-zero.
    """
    Path(output_dir).mkdir(parents=True, exist_ok=True)
    cmd = [
        _runbooks_cmd(),
        "inventory",
        "cross-validate",
        "--ops-profile",
        profile,
        "--persona",
        persona,
        "--export-json",
        "--verbose",
        "--output-dir",
        output_dir,
    ]
    if resource_type:
        cmd.extend(["--resource-type", resource_type])
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
    if result.returncode != 0:
        raise RuntimeError(f"cross-validate failed (exit {result.returncode}): {result.stderr[:500]}")
    files = sorted(Path(output_dir).glob("cross-validate-*.json"))
    if not files:
        raise RuntimeError(f"No evidence JSON in {output_dir}")
    return json.loads(files[-1].read_text())


def run_cert_triage(
    profile: str,
    output_dir: str = "data/evidence/security",
    days: int = 90,
    mode: str = "executive",
    all_accounts: bool = True,
    email: bool = True,
) -> dict:
    """Run certificate triage and return parsed evidence dict.

    Args:
        profile: AWS SSO profile name.
        output_dir: Directory for JSON evidence output.
        days: Certificate expiry window in days.
        mode: Display mode (executive/sre/cto/cloudops).
        all_accounts: Scan all accounts in Landing Zone.
        email: Generate stakeholder email artifact.

    Returns:
        dict with key 'certificates' (list of cert dicts).

    Raises:
        RuntimeError: If CLI exits non-zero.
    """
    Path(output_dir).mkdir(parents=True, exist_ok=True)
    json_path = str(Path(output_dir) / "cert-triage.json")
    cmd = [
        _runbooks_cmd(),
        "cert",
        "triage",
        "--ops-profile",
        profile,
        "--days",
        str(days),
        "--mode",
        mode,
        "--export-json",
        json_path,
        "--output-dir",
        output_dir,
    ]
    if all_accounts:
        cmd.append("--all-accounts")
    if email:
        cmd.append("--email")
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
    if result.returncode != 0:
        raise RuntimeError(f"cert triage failed (exit {result.returncode}): {result.stderr[:500]}")
    if not Path(json_path).exists():
        raise RuntimeError(f"No cert evidence at {json_path}")
    data = json.loads(Path(json_path).read_text())
    # CxO feedback: blank cell is confusing — show what was fetched
    certs = data.get("certificates", [])
    accts = data.get("summary", {}).get("accounts_scanned", 0)
    exp = [c for c in certs if 0 < (_safe_days(c)) <= (days or 90)]
    rem = [c for c in certs if _safe_days(c) <= 0]
    print(f"\u2705 {len(certs)} certificates across {accts} accounts | {len(exp)} expiring | {len(rem)} removable")
    return data


def run_resource_explorer(
    profile: str,
    resource_type: str,
    output_dir: str = "data/evidence/inventory",
) -> pd.DataFrame:
    """Run resource-explorer and return DataFrame.

    Args:
        profile: AWS SSO profile name.
        resource_type: Resource type to discover (e.g. "ec2", "s3").
        output_dir: Directory for CSV output.

    Returns:
        pandas DataFrame with discovered resources.

    Raises:
        RuntimeError: If CLI exits non-zero.
    """
    Path(output_dir).mkdir(parents=True, exist_ok=True)
    csv_path = str(Path(output_dir) / f"{resource_type}-fleet.csv")
    cmd = [
        _runbooks_cmd(),
        "inventory",
        "resource-explorer",
        "--resource-type",
        resource_type,
        "--profile",
        profile,
        "--output",
        csv_path,
    ]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
    if result.returncode != 0:
        raise RuntimeError(f"resource-explorer failed (exit {result.returncode}): {result.stderr[:500]}")
    if not Path(csv_path).exists():
        raise RuntimeError(f"No CSV at {csv_path}")
    return pd.read_csv(csv_path)


# ── Certificate Charts ────────────────────────────────────────


def _safe_days(cert: dict) -> int:
    """Extract days_until_expiry, handling None and missing values."""
    raw = cert.get("days_until_expiry")
    if raw is None:
        raw = cert.get("days_to_expiry")
    return raw if raw is not None else 999


def show_cert_expiry_chart(certs_or_data: list[dict] | dict) -> go.Figure:
    """Certificate expiry distribution bar chart for CxO dashboard.

    Args:
        certs_or_data: List of certificate dicts OR full data dict from run_cert_triage.

    Returns:
        Plotly Figure ready for fig.show() or display in notebook.
    """
    certs: list[dict] = certs_or_data.get("certificates", []) if isinstance(certs_or_data, dict) else certs_or_data
    buckets: dict[str, int] = {"EXPIRED": 0, "< 7d": 0, "< 30d": 0, "< 90d": 0, "> 90d": 0}
    for c in certs:
        d = _safe_days(c)
        if d <= 0:
            k = "EXPIRED"
        elif d <= 7:
            k = "< 7d"
        elif d <= 30:
            k = "< 30d"
        elif d <= 90:
            k = "< 90d"
        else:
            k = "> 90d"
        buckets[k] += 1
    df = pd.DataFrame({"Bucket": list(buckets.keys()), "Count": list(buckets.values())})
    fig = px.bar(
        df,
        x="Bucket",
        y="Count",
        color="Bucket",
        color_discrete_sequence=["#ef4444", "#f97316", "#f59e0b", "#3b82f6", "#22c55e"],
        title="Certificate Expiry Distribution (Live AWS)",
    )
    fig.update_layout(template="plotly_dark", showlegend=False)
    _display_chart(fig)
    return fig


def show_cert_risk_chart(certs_or_data: list[dict] | dict) -> go.Figure:
    """Certificate risk by account bar chart.

    Args:
        certs_or_data: List of certificate dicts OR full data dict from run_cert_triage.

    Returns:
        Plotly Figure ready for fig.show() or display in notebook.
    """
    certs: list[dict] = certs_or_data.get("certificates", []) if isinstance(certs_or_data, dict) else certs_or_data
    rows = []
    for c in certs:
        d = _safe_days(c)
        risk = "critical" if d <= 7 else "warning" if d <= 30 else "attention" if d <= 90 else "valid"
        rows.append(
            {
                "account": c.get("account_id", c.get("account_name", "?")),
                "risk": risk,
            }
        )
    if not rows:
        return px.bar(title="No certificates")
    df = pd.DataFrame(rows).groupby(["account", "risk"]).size().reset_index(name="count")
    fig = px.bar(
        df,
        x="account",
        y="count",
        color="risk",
        color_discrete_map={
            "critical": "#ef4444",
            "warning": "#f97316",
            "attention": "#3b82f6",
            "valid": "#22c55e",
        },
        title="Certificate Risk by Account",
    )
    fig.update_layout(template="plotly_dark", xaxis_tickangle=-45)
    _display_chart(fig)
    return fig


# ── Inventory Charts ──────────────────────────────────────────


def show_accuracy_chart(data: dict) -> go.Figure:
    """Cross-validation accuracy bar chart with PASS/WARN thresholds.

    Args:
        data: Evidence dict from run_cross_validate.

    Returns:
        Plotly Figure ready for fig.show() or display in notebook.
    """
    rows = []
    for rtype, m in data.get("per_type", {}).items():
        rows.append(
            {
                "Type": rtype.upper(),
                "Accuracy": m.get("accuracy", 0),
                "Status": m.get("status", "N/A"),
            }
        )
    df = pd.DataFrame(rows)
    fig = px.bar(
        df,
        x="Type",
        y="Accuracy",
        color="Status",
        color_discrete_map={"PASS": "#22c55e", "WARN": "#f97316", "FAIL": "#ef4444"},
        title="Cross-Validation Accuracy by Resource Type (Live AWS)",
    )
    fig.add_hline(y=99.5, line_dash="dash", line_color="green", annotation_text="PASS (99.5%)")
    fig.add_hline(y=95.0, line_dash="dash", line_color="orange", annotation_text="WARN (95%)")
    fig.update_layout(yaxis_range=[85, 101], template="plotly_dark")
    _display_chart(fig)
    return fig


def show_v1_v2_chart(data: dict) -> go.Figure:
    """V1 vs V2 resource count grouped bar chart.

    Args:
        data: Evidence dict from run_cross_validate.

    Returns:
        Plotly Figure ready for fig.show() or display in notebook.
    """
    types: list[str] = []
    v1s: list[int] = []
    v2s: list[int] = []
    for rtype, m in data.get("per_type", {}).items():
        types.append(rtype.upper())
        v1s.append(m.get("v1", 0))
        v2s.append(m.get("v2", 0))
    fig = go.Figure(
        data=[
            go.Bar(name="Config Aggregator (V1)", x=types, y=v1s, marker_color="#6366f1"),
            go.Bar(name="Resource Explorer (V2)", x=types, y=v2s, marker_color="#0ea5e9"),
        ]
    )
    fig.update_layout(
        barmode="group",
        title="V1 vs V2 Resource Count",
        template="plotly_dark",
    )
    _display_chart(fig)
    return fig


# ── Resource Distribution Charts ─────────────────────────────


def show_top_n_chart(
    df,
    group_col: str,
    count_label: str,
    title: str,
    n: int = 10,
    color_scale: str = "Blues",
    emoji: str = "",
    stacked_col: str | None = None,
    stacked_colors: dict | None = None,
    show_table: bool = False,
    table_cols: list | None = None,
    baseline: int | None = None,
    baseline_label: str = "",
    baseline_colors: tuple = ("#f59e0b", "#22c55e"),
) -> "go.Figure":
    """CxO-grade chart for resource distribution.

    Modes: simple bar (default), stacked bar, bar + baseline reference.
    """
    if df.empty:
        print(f"  {emoji} No {count_label.lower()} found")
        fig = px.bar(title=f"{emoji} {title} (0 resources)")
        _display_chart(fig)
        return fig

    col = group_col if group_col in df.columns else df.columns[0]

    if stacked_col and stacked_col in df.columns:
        counts = df.groupby(col).size().reset_index(name="count").sort_values("count", ascending=True)
        seg = df[df[stacked_col].notna()].groupby(col).size().reset_index(name="segment")
        counts = counts.merge(seg, on=col, how="left").fillna(0)
        counts["remainder"] = counts["count"] - counts["segment"]
        counts[col] = counts[col].astype(str)
        colors = stacked_colors or {"Tagged ✅": "#3b82f6", "Untagged ⚠️": "#93c5fd"}
        names = list(colors.keys())
        fig = go.Figure()
        fig.add_trace(
            go.Bar(
                y=counts[col],
                x=counts["segment"],
                name=names[0],
                orientation="h",
                marker_color=colors[names[0]],
                text=counts["segment"].astype(int),
                textposition="inside",
            )
        )
        fig.add_trace(
            go.Bar(
                y=counts[col],
                x=counts["remainder"],
                name=names[1],
                orientation="h",
                marker_color=colors[names[1]],
                text=counts["remainder"].astype(int),
                textposition="inside",
            )
        )
        fig.update_layout(
            barmode="stack",
            template="plotly_dark",
            title=f"{emoji} {title} ({len(df)} total)",
            xaxis_title=count_label,
            yaxis_title="",
            legend=dict(orientation="h", y=-0.15),
            height=max(400, len(counts) * 28),
        )
        seg_pct = df[stacked_col].notna().mean() * 100
        summary = f"{emoji} {len(df)} {count_label.lower()} | {df[col].nunique()} {col.replace('_', ' ')}s | {seg_pct:.0f}% {names[0].lower()}"

    elif baseline is not None:
        acct_counts = df.groupby(col).size().reset_index(name="count").sort_values("count", ascending=True)
        acct_counts[col] = acct_counts[col].astype(str)
        below_c, above_c = baseline_colors
        bar_colors = [below_c if c < baseline else above_c for c in acct_counts["count"]]
        fig = go.Figure(
            go.Bar(
                y=acct_counts[col],
                x=acct_counts["count"],
                orientation="h",
                marker_color=bar_colors,
                text=acct_counts["count"],
                textposition="outside",
            )
        )
        fig.add_vline(
            x=baseline,
            line_dash="dash",
            line_color="white",
            annotation_text=f"{baseline_label} ({baseline})" if baseline_label else str(baseline),
            annotation_position="top",
        )
        fig.update_layout(
            template="plotly_dark",
            title=f"{emoji} {title} ({len(df)} total)",
            xaxis_title=count_label,
            yaxis_title="",
            height=max(500, len(acct_counts) * 25),
        )
        below = (acct_counts["count"] < baseline).sum()
        above = len(acct_counts) - below
        summary = f"{emoji} {len(df)} {count_label.lower()} | {df[col].nunique()} {col.replace('_', ' ')}s\n     ⚠️ {below} below {baseline_label}\n     ✅ {above} above"

    else:
        counts = df.groupby(col).size().reset_index(name="count").sort_values("count", ascending=False)
        counts[col] = counts[col].astype(str)
        if len(counts) > n:
            top = counts.head(n)
            rest_n = len(counts) - n
            rest_count = counts.iloc[n:]["count"].sum()
            counts = pd.concat(
                [top, pd.DataFrame([{col: f"Others ({rest_n})", "count": rest_count}])], ignore_index=True
            )
        fig = px.bar(
            counts.sort_values("count", ascending=True),
            y=col,
            x="count",
            orientation="h",
            title=f"{emoji} {title} ({len(df)} total)",
            text="count",
            color="count",
            color_continuous_scale=color_scale,
        )
        fig.update_layout(
            template="plotly_dark",
            yaxis_title="",
            xaxis_title=count_label,
            coloraxis_showscale=False,
            height=max(400, len(counts) * 25),
        )
        fig.update_traces(textposition="outside", textfont_size=11)
        top_val = df[col].value_counts().index[0] if not df[col].value_counts().empty else "?"
        top_count = df[col].value_counts().iloc[0] if not df[col].value_counts().empty else 0
        summary = f"{emoji} {len(df)} {count_label.lower()} | {df[col].nunique()} {col.replace('_', ' ')}s | Top: {top_val} ({top_count})"

    _display_chart(fig)
    print(f"  {summary}")

    if show_table:
        from IPython.display import display as _disp

        cols = table_cols or [c for c in [col, "resource_id", "region"] if c in df.columns]
        tbl = df[cols].sort_values(col)
        styled = (
            tbl.style.set_properties(**{"text-align": "left"})
            .set_table_styles([{"selector": "th", "props": [("text-align", "left")]}])
            .hide(axis="index")
        )
        _disp(styled)

    return fig


def show_sunburst_chart(
    df,
    path_cols: list[str],
    title: str,
    n: int = 15,
    color_scale: str = "Greens_r",
    emoji: str = "",
) -> "go.Figure":
    """CxO-grade sunburst for hierarchical resource distribution.

    Args:
        df: DataFrame from run_resource_explorer.
        path_cols: Hierarchy columns e.g. ["region", "account_id"].
        title: Chart title.
        n: Top-N for the innermost level (rest aggregated as Others).
        color_scale: Plotly color sequence name.
        emoji: Title emoji.
    """
    if df.empty:
        fig = px.bar(title=f"{emoji} {title} (0 resources)")
        _display_chart(fig)
        return fig

    display_df = df.copy()
    inner_col = path_cols[-1]
    display_df[inner_col] = display_df[inner_col].astype(str)
    top_vals = display_df[inner_col].value_counts().head(n).index.tolist()
    others_n = display_df[inner_col].nunique() - n
    if others_n > 0:
        display_df.loc[~display_df[inner_col].isin(top_vals), inner_col] = f"Others ({others_n})"

    fig = px.sunburst(
        display_df,
        path=path_cols,
        title=f"{emoji} {title} ({len(df)} total)",
        color_discrete_sequence=getattr(px.colors.sequential, color_scale, px.colors.sequential.Greens_r),
    )
    fig.update_traces(textinfo="label+value", textfont_size=11)
    fig.update_layout(template="plotly_dark", height=550)
    _display_chart(fig)

    # Summary per outer level
    outer_col = path_cols[0]
    outer_counts = df[outer_col].value_counts()
    print(
        f"  {emoji} {len(df)} resources | {df[inner_col].nunique()} {inner_col.replace('_', ' ')}s | {len(outer_counts)} {outer_col}s"
    )
    for val, cnt in outer_counts.head(4).items():
        pct = cnt * 100 // len(df)
        flag = "⚠️" if pct > 90 else "✅"
        print(f"     {flag} {val}: {cnt} ({pct}%)")
    return fig


def generate_resource_email(
    resources: dict,
    profile: str,
    output_dir: str = "",
) -> str:
    """Generate CxO stakeholder email for resource management report.

    Args:
        resources: Dict mapping resource name to DataFrame or dict.
        profile: AWS profile used.
        output_dir: If set, saves email to file.
    """
    from datetime import datetime
    from pathlib import Path

    today = datetime.now().strftime("%d %b %Y")
    lines = [
        f"Subject: AWS Resource Management Report — {today}",
        "",
        "Dear Leadership Team,",
        "",
        "Resource inventory report across the Landing Zone:",
        "",
    ]

    for name, data in resources.items():
        if hasattr(data, "__len__"):
            if isinstance(data, dict):
                count = len(data.get("certificates", data))
                accts = data.get("summary", {}).get("accounts_scanned", 0)
                lines.append(f"  {name}: {count} across {accts} accounts")
            else:
                import pandas as _pd

                if isinstance(data, _pd.DataFrame):
                    col = "account_name" if "account_name" in data.columns else "account_id"
                    lines.append(f"  {name}: {len(data)} across {data[col].nunique()} accounts")

    lines.extend(
        [
            "",
            f"Reproduce: runbooks inventory resource-explorer --resource-type ec2 --profile {profile}",
        ]
    )

    email = "\n".join(lines)
    print(email)

    if output_dir:
        email_path = Path(output_dir) / f"resource-report-email-{datetime.now().strftime('%Y-%m-%d')}.txt"
        email_path.write_text(email)
        print(f"\nEmail saved to {email_path}")

    return email


# ── CxO Display Helpers ──────────────────────────────────────


def show_expiring_certs(certs_or_data: list[dict] | dict, days: int = 90, max_rows: int = 20) -> list[dict]:
    """Display expiring certificates table for CxO.

    Args:
        certs_or_data: List of certificate dicts OR full data dict from run_cert_triage.
        days: Show certificates expiring within this many days.
        max_rows: Maximum rows to display.

    Returns:
        Filtered list of expiring certificate dicts.
    """
    from IPython.display import display  # type: ignore[import]

    certs: list[dict] = certs_or_data.get("certificates", []) if isinstance(certs_or_data, dict) else certs_or_data
    expiring = [c for c in certs if 0 < _safe_days(c) <= days]
    if not expiring:
        print(f"No in-use certificates expiring within {days} days")
        return []
    df = pd.DataFrame(
        [
            {
                "Domain": c.get("domain_name", c.get("domain", "?")),
                "Days Left": _safe_days(c),
                "Account": c.get("account_name") or c.get("account_id", ""),
                "Region": c.get("region", ""),
                "Type": c.get("certificate_type", ""),
                "Auto-Renew": "Yes" if c.get("auto_renewal_eligible") else "No",
            }
            for c in sorted(expiring, key=_safe_days)[:max_rows]
        ]
    )
    print(f"{len(expiring)} certificates expiring within {days} days")
    styled = (
        df.style.set_properties(subset=["Days Left"], **{"text-align": "right"})
        .set_properties(subset=["Domain", "Account", "Region", "Type", "Auto-Renew"], **{"text-align": "left"})
        .set_table_styles([{"selector": "th", "props": [("text-align", "left")]}])
        .hide(axis="index")
    )
    display(styled)
    return expiring


def show_removable_certs(certs_or_data: list[dict] | dict, max_rows: int = 20) -> list[dict]:
    """Display expired/removable certificates table for CxO.

    Args:
        certs_or_data: List of certificate dicts OR full data dict from run_cert_triage.
        max_rows: Maximum rows to display.

    Returns:
        Filtered list of expired certificate dicts safe to remove.
    """
    from IPython.display import display  # type: ignore[import]

    certs: list[dict] = certs_or_data.get("certificates", []) if isinstance(certs_or_data, dict) else certs_or_data
    removable = [c for c in certs if _safe_days(c) <= 0]
    if not removable:
        print("No expired unused certificates to clean up")
        return []
    df = pd.DataFrame(
        [
            {
                "Domain": c.get("domain_name", c.get("domain", "?")),
                "Account": c.get("account_name") or c.get("account_id", ""),
                "Expired": f"{abs(_safe_days(c))} days ago",
                "Region": c.get("region", ""),
            }
            for c in sorted(removable, key=_safe_days)[:max_rows]
        ]
    )
    print(f"{len(removable)} expired certificates safe to remove")
    styled = (
        df.style.set_properties(subset=["Expired"], **{"text-align": "right"})
        .set_properties(subset=["Domain", "Account", "Region"], **{"text-align": "left"})
        .set_table_styles([{"selector": "th", "props": [("text-align", "left")]}])
        .hide(axis="index")
    )
    display(styled)
    return removable


# ── CxO Report Generation ────────────────────────────────────


def generate_cxo_report(data: dict, output_dir: str, name: str) -> dict:
    """Export evidence in CxO-friendly formats. Returns paths dict.

    Generates:
      - .csv  (opens in Excel)
      - .md   (renders in GitHub/Outlook)
      - .json (agent-consumable only — hidden from CxO display)

    Args:
        data: Evidence dict from run_cross_validate or run_cert_triage.
        output_dir: Directory to write exported files.
        name: Base filename (without extension).

    Returns:
        dict mapping format keys ("csv", "md", "json") to file paths.
    """
    from runbooks.notebooks.helpers import export_cxo_report

    return export_cxo_report(data, output_dir, name)


def generate_stakeholder_email(
    risk: str,
    total: int,
    accounts: int,
    expiring: list[dict],
    removable: list[dict],
    mode: str,
    evidence_paths: dict | None = None,
    profile: str = "",
    days: int = 90,
    output_dir: str = "",
) -> str:
    """Generate and save CxO stakeholder email with evidence references.

    Args:
        risk: Risk level string (CRITICAL/HIGH/MEDIUM/HEALTHY).
        total: Total certificate count.
        accounts: Number of accounts scanned.
        expiring: List of expiring certificate dicts.
        removable: List of expired+unused certificate dicts.
        mode: CLI mode used (executive/sre/cto/cloudops).
        evidence_paths: Optional paths dict from generate_cxo_report — non-JSON
                        paths listed as attachments; JSON is hidden from CxO.
        profile: AWS profile used (appended to reproduce command).
        days: Days window used (appended to reproduce command).
        output_dir: If set, saves email to {output_dir}/cert-email-YYYY-MM-DD.txt.

    Returns:
        Email string ready for copy-paste.
    """
    from runbooks.notebooks.helpers import build_email

    email = build_email(
        risk,
        total,
        accounts,
        expiring,
        removable,
        mode,
        evidence_paths=evidence_paths,
    )
    email += f"\nReproduce: runbooks cert triage --days {days} --mode {mode} --ops-profile {profile} --all-accounts\n"
    if output_dir:
        path = Path(output_dir) / f"cert-email-{datetime.now().strftime('%Y-%m-%d')}.txt"
        path.write_text(email)
        print(f"Email saved to {path}")
    return email


def print_summary(
    name: str,
    data: dict,
    output_dir: str,
    profile: str,
    days: int = 0,
    mode: str = "",
) -> None:
    """Print CxO-friendly one-pager summary with reproduce command.

    Args:
        name: Report title (e.g. "Certificate Triage" or "Cross-Validation").
        data: Evidence dict from run_cert_triage or run_cross_validate.
        output_dir: Output directory shown in summary footer.
        profile: AWS profile used (shown in reproduce command).
        days: Days window for cert triage (0 = omit from cert line).
        mode: CLI mode for cert triage (omit if empty).
    """
    separator = "=" * 60
    print(separator)
    print(f"  {name} — {datetime.now().strftime('%Y-%m-%d')}")
    print(separator)
    if "certificates" in data:
        certs = data["certificates"]
        exp = [c for c in certs if 0 < _safe_days(c) <= (days or 90)]
        rem = [c for c in certs if _safe_days(c) <= 0]
        print(f"  Total: {len(certs)} | Expiring: {len(exp)} | Removable: {len(rem)}")
        print(f"  Reproduce: runbooks cert triage --days {days} --mode {mode} --ops-profile {profile} --all-accounts")
    elif "per_type" in data:
        print(f"  Accuracy: {data.get('overall_accuracy', '?')}% | Resources: {data.get('overall_resources', '?')}")
        print(f"  Accounts: {data.get('v3_accounts', '?')} | Timing: {data.get('elapsed_seconds', '?')}s")
        print(f"  Reproduce: runbooks inventory cross-validate --ops-profile {profile} --persona all --export-json")
    print(f"  Output: {output_dir}/")
    print(separator)


def show_workflow_summary(output_dir: str, cert_json: str = "", ec2_csv: str = "") -> list:
    """Print workflow summary table from evidence files. Returns summary list."""
    summary = []
    # Cross-validation
    cv_files = sorted(Path(output_dir).glob("cross-validate*.json"))
    if cv_files:
        cv = json.loads(cv_files[-1].read_text())
        summary.append(
            (
                "Cross-Validate",
                f"{cv.get('overall_accuracy', '?')}% accuracy, {cv.get('overall_resources', '?')} resources",
                str(cv_files[-1]),
            )
        )
    # Cert triage
    cert_path = Path(cert_json) if cert_json else Path(output_dir) / "cert-triage.json"
    if cert_path.exists():
        cd = json.loads(cert_path.read_text())
        total = cd.get("summary", {}).get("total_certificates", len(cd.get("certificates", [])))
        summary.append(("Cert Triage", f"{total} certificates scanned", str(cert_path)))
    # EC2
    ec2_path = Path(ec2_csv) if ec2_csv else Path(output_dir) / "ec2-fleet.csv"
    if ec2_path.exists():
        ec2_count = len(pd.read_csv(ec2_path))
        summary.append(("Resource Explorer EC2", f"{ec2_count} instances", str(ec2_path)))

    print(f"{'─' * 80}")
    print(f"{'WORKFLOW':<28} {'RESULT':<35} FILE")
    print(f"{'─' * 80}")
    for wf, result, path in summary:
        print(f"{wf:<28} {result:<35} {Path(path).name}")
    print(f"{'─' * 80}")
    return summary


def show_showback_table(services: list, accounts: list, profile: str = "", mode: str = "", timeframe: str = ""):
    """Display showback analysis tables for CxO."""
    from IPython.display import display

    def _assign_tier(cost):
        if cost is None:
            return "Unknown"
        return "Critical" if cost > 10000 else "High" if cost > 1000 else "Medium" if cost > 100 else "Low"

    def _fmt_cost(cost):
        return f"${cost:,.2f}" if isinstance(cost, (int, float)) else str(cost)

    if services:
        df = pd.DataFrame(services)
        cost_col = next((c for c in df.columns if "cost" in c.lower()), None)
        name_col = next((c for c in df.columns if "name" in c.lower() or "service" in c.lower()), None)
        if cost_col and name_col:
            df["Tier"] = df[cost_col].apply(_assign_tier)
            df["Cost (USD)"] = df[cost_col].apply(_fmt_cost)
            display(df[[name_col, "Cost (USD)", "Tier"]].style.set_caption("Showback by Service").hide(axis="index"))
    else:
        print("No service cost data — run: runbooks finops dashboard --profile " + profile)

    if accounts:
        adf = pd.DataFrame(accounts)
        cost_col = next((c for c in adf.columns if "cost" in c.lower()), None)
        name_col = next((c for c in adf.columns if "name" in c.lower() or "account" in c.lower()), None)
        if cost_col and name_col:
            adf["Tier"] = adf[cost_col].apply(_assign_tier)
            adf["Cost (USD)"] = adf[cost_col].apply(_fmt_cost)
            display(adf[[name_col, "Cost (USD)", "Tier"]].style.set_caption("Showback by Account").hide(axis="index"))
    else:
        print("No account data — authenticate first")
