from __future__ import annotations

import hashlib
import json
from pathlib import Path
from typing import Any

import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from matplotlib import font_manager
from matplotlib.patches import FancyArrowPatch, FancyBboxPatch


ROOT = Path(__file__).resolve().parents[2]
DIR = Path(__file__).resolve().parent
OUTPUTS = DIR / "outputs"
FIGURES = OUTPUTS / "figures"
OUTPUTS.mkdir(parents=True, exist_ok=True)
FIGURES.mkdir(parents=True, exist_ok=True)

REPORT_NUMBER = "SHWRP-2026-026"
CAPTURE_DATE = "2026-07-27"
SYNTHETIC_WARNING = "合成假設，不是實際企業估計"
CONCEPT_WARNING = "作者提出之概念架構，尚未以企業樣本驗證"

FONT_PATH = ROOT / "public" / "fonts" / "NotoSansTC-Regular.ttf"
if FONT_PATH.exists():
    font_manager.fontManager.addfont(str(FONT_PATH))
    matplotlib.rcParams["font.family"] = "Noto Sans TC"
matplotlib.rcParams["axes.unicode_minus"] = False
matplotlib.rcParams["figure.dpi"] = 140

COLORS = {
    "brown": "#5B2E1F",
    "orange": "#C56D2D",
    "gold": "#D5A13E",
    "green": "#47745A",
    "teal": "#347C78",
    "blue": "#52749C",
    "purple": "#785B89",
    "cream": "#FAF5EC",
    "paper": "#FFFDF9",
    "ink": "#2A211C",
    "muted": "#74675E",
    "line": "#DCC9B7",
    "pale_green": "#E9F2EA",
    "pale_orange": "#F8E9DA",
}


def sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for block in iter(lambda: handle.read(65536), b""):
            digest.update(block)
    return digest.hexdigest()


def q(values: np.ndarray, probability: float) -> float:
    return float(np.quantile(values, probability))


def rounded(value: float, digits: int = 2) -> float:
    return round(float(value), digits)


def add_footer(fig: plt.Figure, text: str, *, color: str | None = None) -> None:
    fig.text(
        0.5,
        0.018,
        text,
        ha="center",
        va="bottom",
        fontsize=9,
        color=color or COLORS["muted"],
    )


def save_figure(fig: plt.Figure, filename: str) -> None:
    fig.savefig(
        FIGURES / filename,
        dpi=300,
        bbox_inches="tight",
        facecolor="white",
        metadata={
            "Title": filename,
            "Author": "CHOU, BING-HAN",
            "Subject": REPORT_NUMBER,
        },
    )
    plt.close(fig)


documents_path = DIR / "institutional-documents.csv"
context_path = DIR / "official-context.csv"
assumptions_path = DIR / "synthetic-assumptions.json"

documents = pd.read_csv(documents_path, dtype=str).fillna("")
context = pd.read_csv(context_path, dtype={"numeric_value": float})
assumptions: dict[str, Any] = json.loads(assumptions_path.read_text(encoding="utf-8"))

if assumptions["research_number"] != REPORT_NUMBER:
    raise ValueError("synthetic-assumptions.json has the wrong research number")
if assumptions["scope_zh"] != (
    "所有金額、機率、機會數與歸因權重均為合成假設，用於展示決策模型行為；"
    "不是實際企業估計、正式報價、市場平均或投資建議。"
):
    raise ValueError("Synthetic scope warning must remain exact and explicit")
if not documents["document_id"].is_unique:
    raise ValueError("document_id must be unique")
if not context["context_id"].is_unique:
    raise ValueError("context_id must be unique")
if not documents["source_url"].str.startswith("https://").all():
    raise ValueError("Every institutional document must have an HTTPS source URL")
if not context["source_url"].str.startswith("https://").all():
    raise ValueError("Every official context row must have an HTTPS source URL")
if not documents["capture_date"].eq(CAPTURE_DATE).all():
    raise ValueError("Institutional document capture dates must be fixed")
if not context["capture_date"].eq(CAPTURE_DATE).all():
    raise ValueError("Official context capture dates must be fixed")
if not set(context["source_document_id"]).issubset(set(documents["document_id"])):
    raise ValueError("Every official context row must reference an institutional document")
if not np.isfinite(context["numeric_value"]).all():
    raise ValueError("Official context values must be finite")


# Purposeful institutional-document coding. This is a sample of documents selected
# for their relevance to institutional boundaries. It is not a probability sample
# of organizations and is never used to estimate organization-level prevalence.
coding_rules: dict[str, set[str]] = {
    "activity_resource_context": {"INST-01"},
    "organization_classification": {"INST-02", "INST-03", "INST-04"},
    "membership_status_rule": {"INST-05"},
    "general_governance_rule": {"INST-04", "INST-05"},
    "competition_boundary": {"INST-06", "INST-07"},
    "privacy_boundary": {"INST-08"},
    "tax_accounting_context": {"INST-09", "INST-10", "INST-11"},
}
document_coding = documents.copy()
document_coding.insert(1, "purposeful_institutional_sample", 1)
document_coding.insert(
    2,
    "sampling_scope",
    "目的性制度文件樣本；不是組織母體或組織績效樣本",
)
for rule, ids in coding_rules.items():
    document_coding[rule] = document_coding["document_id"].isin(ids).astype(int)
document_coding.to_csv(
    OUTPUTS / "institutional-document-coding.csv",
    index=False,
    encoding="utf-8-sig",
)

context_summary = context.copy()
context_summary["is_percentage"] = context_summary["unit"].eq("百分比").astype(int)
figure_two_labels = {
    "CTX-05": "有辦理活動",
    "CTX-06": "主辦或合辦活動",
    "CTX-08": "協辦或贊助活動",
    "CTX-14": "使用電腦或網路",
    "CTX-15": "有專屬社群網址／網站",
    "CTX-16": "申請組織及團體憑證",
    "CTX-17": "收發電子公文",
}
context_summary["figure_two_label"] = context_summary["context_id"].map(
    figure_two_labels
).fillna("")
context_summary["included_in_figure_two"] = (
    context_summary["figure_two_label"].ne("").astype(int)
)
context_summary.to_csv(
    OUTPUTS / "official-context-summary.csv",
    index=False,
    encoding="utf-8-sig",
)


scenarios: dict[str, dict[str, float]] = assumptions["scenarios"]
seed = int(assumptions["seed"])
draws = int(assumptions["simulation_draws"])
rng = np.random.default_rng(seed)

cost_component_rows: list[dict[str, float | str | int]] = []
benefit_quantile_rows: list[dict[str, float | str | int]] = []
simulation_cache: dict[str, dict[str, np.ndarray]] = {}

for scenario_name, values in scenarios.items():
    time_hours = rng.triangular(
        values["hours_per_month_low"],
        values["hours_per_month_mode"],
        values["hours_per_month_high"],
        size=draws,
    )
    hourly_cost = rng.triangular(
        values["loaded_hourly_cost_low"],
        values["loaded_hourly_cost_mode"],
        values["loaded_hourly_cost_high"],
        size=draws,
    )
    annual_time_cost = time_hours * 12 * hourly_cost
    risk_cost = rng.triangular(
        values["expected_risk_cost_annual_low"],
        values["expected_risk_cost_annual_mode"],
        values["expected_risk_cost_annual_high"],
        size=draws,
    )
    fixed_cost = (
        values["membership_dues_annual"]
        + values["events_travel_annual"]
        + values["service_sponsorship_annual"]
        + values["admin_compliance_annual"]
    )
    total_cost = fixed_cost + annual_time_cost + risk_cost

    opportunities = rng.poisson(
        values["qualified_opportunities_lambda"],
        size=draws,
    )
    conversion_probability = rng.triangular(
        values["conversion_probability_low"],
        values["conversion_probability_mode"],
        values["conversion_probability_high"],
        size=draws,
    )
    wins = rng.binomial(opportunities, conversion_probability)
    contribution_margin_per_win = rng.triangular(
        values["contribution_margin_per_win_low"],
        values["contribution_margin_per_win_mode"],
        values["contribution_margin_per_win_high"],
        size=draws,
    )
    attribution_weight = rng.triangular(
        values["attribution_weight_low"],
        values["attribution_weight_mode"],
        values["attribution_weight_high"],
        size=draws,
    )
    attributed_new_margin = (
        wins * contribution_margin_per_win * attribution_weight
    )
    retention_margin = rng.triangular(
        values["retention_margin_annual_low"],
        values["retention_margin_annual_mode"],
        values["retention_margin_annual_high"],
        size=draws,
    )
    verified_savings = rng.triangular(
        values["verified_savings_annual_low"],
        values["verified_savings_annual_mode"],
        values["verified_savings_annual_high"],
        size=draws,
    )
    monetized_benefit = attributed_new_margin + retention_margin + verified_savings
    net_value = monetized_benefit - total_cost
    rroi = np.divide(
        net_value,
        total_cost,
        out=np.full_like(net_value, np.nan),
        where=total_cost > 0,
    )

    mode_time_cost = (
        values["hours_per_month_mode"]
        * 12
        * values["loaded_hourly_cost_mode"]
    )
    cost_components = {
        "會費": values["membership_dues_annual"],
        "活動與交通": values["events_travel_annual"],
        "服務與贊助": values["service_sponsorship_annual"],
        "工時機會成本": mode_time_cost,
        "行政與法遵": values["admin_compliance_annual"],
        "預期風險成本": values["expected_risk_cost_annual_mode"],
    }
    for component, amount in cost_components.items():
        cost_component_rows.append(
            {
                "scenario": scenario_name,
                "cost_component": component,
                "annual_mode_twd": rounded(amount, 2),
                "synthetic": 1,
                "warning": SYNTHETIC_WARNING,
            }
        )

    benefit_quantile_rows.append(
        {
            "scenario": scenario_name,
            "draws": draws,
            "cost_p10_twd": rounded(q(total_cost, 0.10)),
            "cost_p50_twd": rounded(q(total_cost, 0.50)),
            "cost_p90_twd": rounded(q(total_cost, 0.90)),
            "benefit_p10_twd": rounded(q(monetized_benefit, 0.10)),
            "benefit_p50_twd": rounded(q(monetized_benefit, 0.50)),
            "benefit_p90_twd": rounded(q(monetized_benefit, 0.90)),
            "net_value_p10_twd": rounded(q(net_value, 0.10)),
            "net_value_p50_twd": rounded(q(net_value, 0.50)),
            "net_value_p90_twd": rounded(q(net_value, 0.90)),
            "rroi_p10": rounded(q(rroi, 0.10), 4),
            "rroi_p50": rounded(q(rroi, 0.50), 4),
            "rroi_p90": rounded(q(rroi, 0.90), 4),
            "probability_benefit_exceeds_cost": rounded(
                np.mean(monetized_benefit > total_cost),
                4,
            ),
            "synthetic": 1,
            "warning": SYNTHETIC_WARNING,
        }
    )
    simulation_cache[scenario_name] = {
        "cost": total_cost,
        "benefit": monetized_benefit,
        "net_value": net_value,
        "rroi": rroi,
    }

cost_components = pd.DataFrame(cost_component_rows)
cost_components.to_csv(
    OUTPUTS / "synthetic-scenario-costs.csv",
    index=False,
    encoding="utf-8-sig",
)
benefit_quantiles = pd.DataFrame(benefit_quantile_rows)
benefit_quantiles.to_csv(
    OUTPUTS / "synthetic-benefit-quantiles.csv",
    index=False,
    encoding="utf-8-sig",
)


# Attribution sensitivity is a deterministic grid over synthetic values. It
# deliberately exposes how strongly the decision changes when attribution
# assumptions change.
attribution = assumptions["attribution_sensitivity"]
base_scenario = attribution["scenario"]
base_values = scenarios[base_scenario]
base_mode_cost = (
    base_values["membership_dues_annual"]
    + base_values["events_travel_annual"]
    + base_values["service_sponsorship_annual"]
    + (
        base_values["hours_per_month_mode"]
        * 12
        * base_values["loaded_hourly_cost_mode"]
    )
    + base_values["admin_compliance_annual"]
    + base_values["expected_risk_cost_annual_mode"]
)
attribution_rows: list[dict[str, float | str | int]] = []
for margin_pool in attribution["incremental_contribution_margin_pool"]:
    for weight in attribution["attribution_weights"]:
        attributed_margin = margin_pool * weight
        benefit = (
            attributed_margin
            + attribution["retention_and_verified_savings"]
        )
        net_value = benefit - base_mode_cost
        attribution_rows.append(
            {
                "scenario": base_scenario,
                "incremental_contribution_margin_pool_twd": margin_pool,
                "attribution_weight": weight,
                "attributed_margin_twd": attributed_margin,
                "retention_and_verified_savings_twd": attribution[
                    "retention_and_verified_savings"
                ],
                "mode_cost_twd": base_mode_cost,
                "net_value_twd": net_value,
                "rroi": net_value / base_mode_cost,
                "synthetic": 1,
                "warning": SYNTHETIC_WARNING,
            }
        )
attribution_sensitivity = pd.DataFrame(attribution_rows)
attribution_sensitivity.to_csv(
    OUTPUTS / "attribution-sensitivity.csv",
    index=False,
    encoding="utf-8-sig",
)


maturity_rows = [
    {
        "level": 0,
        "maturity_label": "個人化參與",
        "service_ledger": 0,
        "relationship_ledger": 0,
        "commercial_ledger": 0,
        "organizational_ledger": 0,
        "institutional_features": "依賴負責人記憶；無企業目標、預算或移交",
        "boundary_controls": "未制度化",
        "interpretation": CONCEPT_WARNING,
    },
    {
        "level": 1,
        "maturity_label": "基本留痕",
        "service_ledger": 1,
        "relationship_ledger": 1,
        "commercial_ledger": 0,
        "organizational_ledger": 0,
        "institutional_features": "記錄會籍、活動、直接支出與參與者",
        "boundary_controls": "基本用途與保存期間說明",
        "interpretation": CONCEPT_WARNING,
    },
    {
        "level": 2,
        "maturity_label": "預算與角色",
        "service_ledger": 2,
        "relationship_ledger": 2,
        "commercial_ledger": 1,
        "organizational_ledger": 1,
        "institutional_features": "年度目標、預算、企業代表與出席政策",
        "boundary_controls": "區分企業與個人名義；建立利益衝突規則",
        "interpretation": CONCEPT_WARNING,
    },
    {
        "level": 3,
        "maturity_label": "四帳治理",
        "service_ledger": 3,
        "relationship_ledger": 3,
        "commercial_ledger": 3,
        "organizational_ledger": 2,
        "institutional_features": "服務、關係、商業、組織成果分帳；記錄工時與歸因",
        "boundary_controls": "個資最小化、拒絕行銷、競爭敏感資訊禁區",
        "interpretation": CONCEPT_WARNING,
    },
    {
        "level": 4,
        "maturity_label": "組合檢討與退出",
        "service_ledger": 4,
        "relationship_ledger": 4,
        "commercial_ledger": 4,
        "organizational_ledger": 4,
        "institutional_features": "敏感度分析、定期檢討、組織移交與退出規則",
        "boundary_controls": "服務—銷售雙帳覆核與年度法遵檢查",
        "interpretation": CONCEPT_WARNING,
    },
]
maturity = pd.DataFrame(maturity_rows)
maturity.to_csv(
    OUTPUTS / "governance-maturity-framework.csv",
    index=False,
    encoding="utf-8-sig",
)


# Figure 1: theory and governance pathway
fig, ax = plt.subplots(figsize=(15.5, 8.5))
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.axis("off")
ax.set_title(
    "圖1　從個人參與到可治理關係資本：理論與治理路徑",
    fontsize=19,
    fontweight="bold",
    color=COLORS["ink"],
    pad=18,
)
boxes = [
    (
        0.035,
        0.55,
        0.17,
        0.23,
        "企業投入",
        "會費與活動費\n服務／贊助\n工時機會成本\n行政與法遵",
        COLORS["pale_orange"],
    ),
    (
        0.235,
        0.55,
        0.17,
        0.23,
        "制度化常規",
        "目標與預算\n角色與移交\n資料紀錄\n檢討與退出",
        "#F4E7D8",
    ),
    (
        0.435,
        0.55,
        0.17,
        0.23,
        "關係機制",
        "信任累積\n非重複資訊\n同儕學習\n引薦與媒合",
        "#E8F0E7",
    ),
    (
        0.635,
        0.55,
        0.17,
        0.23,
        "四類成果",
        "服務成果\n關係資本\n商業貢獻\n組織學習",
        "#E5EFF2",
    ),
    (
        0.835,
        0.55,
        0.13,
        0.23,
        "決策",
        "繼續\n調整\n縮減\n退出",
        "#EEE7F1",
    ),
]
for x, y, width, height, title, body, color in boxes:
    patch = FancyBboxPatch(
        (x, y),
        width,
        height,
        boxstyle="round,pad=0.012,rounding_size=0.018",
        linewidth=1.6,
        edgecolor=COLORS["line"],
        facecolor=color,
    )
    ax.add_patch(patch)
    ax.text(
        x + width / 2,
        y + height * 0.78,
        title,
        ha="center",
        va="center",
        fontsize=14,
        fontweight="bold",
        color=COLORS["brown"],
    )
    ax.text(
        x + width / 2,
        y + height * 0.40,
        body,
        ha="center",
        va="center",
        fontsize=11,
        linespacing=1.45,
        color=COLORS["ink"],
    )
for left, right in zip(boxes[:-1], boxes[1:]):
    arrow = FancyArrowPatch(
        (left[0] + left[2] + 0.006, left[1] + left[3] / 2),
        (right[0] - 0.006, right[1] + right[3] / 2),
        arrowstyle="-|>",
        mutation_scale=18,
        linewidth=1.7,
        color=COLORS["orange"],
    )
    ax.add_patch(arrow)
guardrail = FancyBboxPatch(
    (0.13, 0.19),
    0.74,
    0.18,
    boxstyle="round,pad=0.016,rounding_size=0.018",
    linewidth=1.8,
    edgecolor=COLORS["green"],
    facecolor=COLORS["pale_green"],
)
ax.add_patch(guardrail)
ax.text(
    0.5,
    0.315,
    "全程治理護欄",
    ha="center",
    va="center",
    fontsize=14,
    fontweight="bold",
    color=COLORS["green"],
)
ax.text(
    0.5,
    0.245,
    "服務與銷售分界　｜　會員與受益人個資最小化　｜　競爭敏感資訊禁區　｜　歸因敏感度　｜　不把公益成果強制貨幣化",
    ha="center",
    va="center",
    fontsize=11,
    color=COLORS["ink"],
)
feedback = FancyArrowPatch(
    (0.90, 0.54),
    (0.32, 0.41),
    connectionstyle="arc3,rad=-0.20",
    arrowstyle="-|>",
    mutation_scale=16,
    linewidth=1.5,
    linestyle="--",
    color=COLORS["purple"],
)
ax.add_patch(feedback)
ax.text(
    0.65,
    0.41,
    "檢討結果回饋制度與資源配置",
    ha="center",
    fontsize=10,
    color=COLORS["purple"],
)
add_footer(fig, CONCEPT_WARNING)
save_figure(fig, "figure-1-governance-pathway.png")


# Figure 2: official 2021 context
official_plot = context_summary[
    context_summary["included_in_figure_two"].eq(1)
].copy()
official_plot = official_plot.sort_values("numeric_value")
fig, ax = plt.subplots(figsize=(12.5, 7.3))
bar_colors = [
    COLORS["blue"] if group == "digitalization" else COLORS["orange"]
    for group in official_plot["metric_group"]
]
bars = ax.barh(
    official_plot["figure_two_label"],
    official_plot["numeric_value"],
    color=bar_colors,
)
ax.set_xlim(0, 100)
ax.set_xlabel("占各級人民團體比例（%）", fontsize=11)
ax.set_title(
    "圖2　臺灣人民團體活動與資訊化的官方背景（2021）",
    fontsize=17,
    fontweight="bold",
    color=COLORS["ink"],
    pad=14,
)
for bar, value in zip(bars, official_plot["numeric_value"]):
    ax.text(
        value + 1.1,
        bar.get_y() + bar.get_height() / 2,
        f"{value:.2f}%",
        va="center",
        fontsize=10,
        color=COLORS["ink"],
    )
ax.grid(axis="x", alpha=0.18)
ax.spines[["top", "right", "left"]].set_visible(False)
legend_handles = [
    plt.Line2D([0], [0], color=COLORS["orange"], lw=8, label="活動"),
    plt.Line2D([0], [0], color=COLORS["blue"], lw=8, label="資訊化"),
]
ax.legend(handles=legend_handles, frameon=False, loc="lower right")
add_footer(
    fig,
    "資料來源：內政部《110年各級人民團體活動概況調查》。團體層級描述，不是會員企業投入或ROI。",
)
fig.subplots_adjust(left=0.26, right=0.95, bottom=0.13, top=0.88)
save_figure(fig, "figure-2-official-context.png")


# Figure 3: synthetic annual cost composition
scenario_order = list(scenarios)
component_order = [
    "會費",
    "活動與交通",
    "服務與贊助",
    "工時機會成本",
    "行政與法遵",
    "預期風險成本",
]
cost_pivot = (
    cost_components.pivot(
        index="scenario",
        columns="cost_component",
        values="annual_mode_twd",
    )
    .reindex(index=scenario_order, columns=component_order)
    .fillna(0)
)
fig, ax = plt.subplots(figsize=(12.5, 7.3))
bottom = np.zeros(len(cost_pivot))
component_colors = [
    COLORS["brown"],
    COLORS["orange"],
    COLORS["gold"],
    COLORS["teal"],
    COLORS["blue"],
    COLORS["purple"],
]
for component, color in zip(component_order, component_colors):
    values = cost_pivot[component].to_numpy() / 1000
    ax.bar(
        cost_pivot.index,
        values,
        bottom=bottom,
        label=component,
        color=color,
        width=0.63,
    )
    bottom += values
for position, total in enumerate(bottom):
    ax.text(
        position,
        total + max(bottom) * 0.025,
        f"{total:,.1f}",
        ha="center",
        fontsize=11,
        fontweight="bold",
    )
ax.set_ylabel("年度成本（千元）")
ax.set_title(
    "圖3　三種參與情境的年度成本組成",
    fontsize=17,
    fontweight="bold",
    color=COLORS["ink"],
    pad=14,
)
ax.legend(frameon=False, ncol=3, loc="upper left")
ax.grid(axis="y", alpha=0.18)
ax.spines[["top", "right"]].set_visible(False)
add_footer(fig, SYNTHETIC_WARNING, color=COLORS["brown"])
fig.subplots_adjust(bottom=0.13, top=0.87)
save_figure(fig, "figure-3-annual-cost-composition.png")


# Figure 4: P10/P50/P90 monetized benefit and cost
fig, ax = plt.subplots(figsize=(12.8, 7.5))
y = np.arange(len(benefit_quantiles))
offset = 0.16
for metric, label, color, marker, delta in [
    ("cost", "總經濟成本", COLORS["orange"], "s", -offset),
    ("benefit", "可貨幣化效益", COLORS["green"], "o", offset),
]:
    p10 = benefit_quantiles[f"{metric}_p10_twd"].to_numpy() / 1000
    p50 = benefit_quantiles[f"{metric}_p50_twd"].to_numpy() / 1000
    p90 = benefit_quantiles[f"{metric}_p90_twd"].to_numpy() / 1000
    ax.errorbar(
        p50,
        y + delta,
        xerr=[p50 - p10, p90 - p50],
        fmt=marker,
        markersize=8,
        capsize=5,
        linewidth=2.1,
        color=color,
        ecolor=color,
        label=label,
    )
ax.set_yticks(y, labels=benefit_quantiles["scenario"])
ax.set_xlabel("年度金額（千元；線段為P10至P90，點為P50）")
ax.set_title(
    "圖4　三種合成情境的效益與成本不確定區間",
    fontsize=17,
    fontweight="bold",
    color=COLORS["ink"],
    pad=14,
)
ax.grid(axis="x", alpha=0.18)
ax.spines[["top", "right", "left"]].set_visible(False)
ax.legend(frameon=False, loc="lower right")
for position, row in benefit_quantiles.iterrows():
    probability = row["probability_benefit_exceeds_cost"]
    ax.text(
        max(
            row["cost_p90_twd"],
            row["benefit_p90_twd"],
        )
        / 1000
        + 8,
        position,
        f"P(效益>成本)={probability:.1%}",
        va="center",
        fontsize=9,
        color=COLORS["muted"],
    )
add_footer(
    fig,
    f"{SYNTHETIC_WARNING}；公益服務、信任及員工學習未強制貨幣化。",
    color=COLORS["brown"],
)
fig.subplots_adjust(left=0.17, right=0.90, bottom=0.13, top=0.87)
save_figure(fig, "figure-4-benefit-cost-quantiles.png")


# Figure 5: attribution sensitivity heat map
heat = attribution_sensitivity.pivot(
    index="incremental_contribution_margin_pool_twd",
    columns="attribution_weight",
    values="net_value_twd",
).sort_index()
fig, ax = plt.subplots(figsize=(11.5, 7.0))
limit = float(np.abs(heat.to_numpy()).max())
image = ax.imshow(
    heat.to_numpy() / 1000,
    cmap="RdYlGn",
    vmin=-limit / 1000,
    vmax=limit / 1000,
    aspect="auto",
)
ax.set_xticks(
    np.arange(len(heat.columns)),
    labels=[f"{value:.0%}" for value in heat.columns],
)
ax.set_yticks(
    np.arange(len(heat.index)),
    labels=[f"{value / 1000:,.0f}" for value in heat.index],
)
ax.set_xlabel("社團參與的增量歸因權重")
ax.set_ylabel("成交前的增量貢獻毛利池（千元）")
ax.set_title(
    "圖5　制度化基準情境的歸因敏感度：年度淨值",
    fontsize=17,
    fontweight="bold",
    color=COLORS["ink"],
    pad=14,
)
for row_index in range(heat.shape[0]):
    for column_index in range(heat.shape[1]):
        value = heat.iloc[row_index, column_index] / 1000
        ax.text(
            column_index,
            row_index,
            f"{value:+.0f}",
            ha="center",
            va="center",
            fontsize=10,
            color=COLORS["ink"],
        )
colorbar = fig.colorbar(image, ax=ax)
colorbar.set_label("年度淨值（千元）")
add_footer(
    fig,
    f"{SYNTHETIC_WARNING}；正值不代表已實現收益，僅顯示假設改變對模型的影響。",
    color=COLORS["brown"],
)
fig.subplots_adjust(bottom=0.14, top=0.87)
save_figure(fig, "figure-5-attribution-sensitivity.png")


# Figure 6: maturity and four-ledger governance framework
ledger_columns = [
    "service_ledger",
    "relationship_ledger",
    "commercial_ledger",
    "organizational_ledger",
]
ledger_labels = ["服務帳", "關係帳", "商業帳", "組織帳"]
matrix = maturity[ledger_columns].to_numpy()
fig, ax = plt.subplots(figsize=(12.5, 7.5))
image = ax.imshow(matrix, cmap="YlGnBu", vmin=0, vmax=4, aspect="auto")
ax.set_xticks(np.arange(len(ledger_labels)), labels=ledger_labels)
ax.set_yticks(
    np.arange(len(maturity)),
    labels=[
        f"L{row.level}　{row.maturity_label}"
        for row in maturity.itertuples()
    ],
)
ax.set_title(
    "圖6　制度化成熟度與四帳治理架構",
    fontsize=17,
    fontweight="bold",
    color=COLORS["ink"],
    pad=14,
)
for row_index in range(matrix.shape[0]):
    for column_index in range(matrix.shape[1]):
        value = int(matrix[row_index, column_index])
        label = "—" if value == 0 else f"L{value}"
        ax.text(
            column_index,
            row_index,
            label,
            ha="center",
            va="center",
            fontsize=12,
            fontweight="bold",
            color="white" if value >= 3 else COLORS["ink"],
        )
colorbar = fig.colorbar(image, ax=ax, ticks=[0, 1, 2, 3, 4])
colorbar.set_label("制度覆蓋層級（概念標記）")
ax.set_xlabel("四帳分開記錄；成熟度提高不等於收益提高")
add_footer(fig, CONCEPT_WARNING)
fig.subplots_adjust(left=0.24, right=0.92, bottom=0.14, top=0.87)
save_figure(fig, "figure-6-maturity-four-ledger.png")


figure_files = [
    "outputs/figures/figure-1-governance-pathway.png",
    "outputs/figures/figure-2-official-context.png",
    "outputs/figures/figure-3-annual-cost-composition.png",
    "outputs/figures/figure-4-benefit-cost-quantiles.png",
    "outputs/figures/figure-5-attribution-sensitivity.png",
    "outputs/figures/figure-6-maturity-four-ledger.png",
]
for relative_path in figure_files:
    if not (DIR / relative_path).exists():
        raise RuntimeError(f"Expected figure was not created: {relative_path}")

results = {
    "report_number": REPORT_NUMBER,
    "research_number": REPORT_NUMBER,
    "release_date": CAPTURE_DATE,
    "seed": seed,
    "simulation_draws": draws,
    "simulation_draws_per_scenario": draws,
    "institutional_document_count": int(len(documents)),
    "synthetic_profile_count": int(len(assumptions["profiles"])),
    "institutional_sample_design": (
        "Purposefully selected official institutional documents; "
        "not an organization population sample."
    ),
    "official_context_row_count": int(len(context)),
    "official_context_is_firm_roi_evidence": False,
    "synthetic_results_are_empirical_estimates": False,
    "synthetic_results_are_actual_firm_estimates": False,
    "synthetic_warning": SYNTHETIC_WARNING,
    "scenario_summary": {
        str(row["scenario"]): {
            "cost_p10_twd": row["cost_p10_twd"],
            "cost_p50_twd": row["cost_p50_twd"],
            "cost_p90_twd": row["cost_p90_twd"],
            "benefit_p10_twd": row["benefit_p10_twd"],
            "benefit_p50_twd": row["benefit_p50_twd"],
            "benefit_p90_twd": row["benefit_p90_twd"],
            "net_value_p10_twd": row["net_value_p10_twd"],
            "net_value_p50_twd": row["net_value_p50_twd"],
            "net_value_p90_twd": row["net_value_p90_twd"],
            "probability_benefit_exceeds_cost": row[
                "probability_benefit_exceeds_cost"
            ],
        }
        for _, row in benefit_quantiles.iterrows()
    },
    "public_outputs": [
        "outputs/institutional-document-coding.csv",
        "outputs/official-context-summary.csv",
        "outputs/synthetic-scenario-costs.csv",
        "outputs/synthetic-benefit-quantiles.csv",
        "outputs/attribution-sensitivity.csv",
        "outputs/governance-maturity-framework.csv",
        *figure_files,
    ],
    "input_sha256": {
        "institutional-documents.csv": sha256(documents_path),
        "official-context.csv": sha256(context_path),
        "synthetic-assumptions.json": sha256(assumptions_path),
    },
    "figures": len(figure_files),
    "figure_count": len(figure_files),
    "second_independent_human_coder": False,
    "external_peer_review": False,
}
(DIR / "analysis-results.json").write_text(
    json.dumps(results, ensure_ascii=False, indent=2),
    encoding="utf-8",
)
print(json.dumps(results, ensure_ascii=False, indent=2))
