#!/usr/bin/env python3
"""燃料と電池を「車両システム」として比べ直す計算モデル。

入力: data/assumptions.csv（LOW / BASE / HIGH、出典 ID 付き）
出力: results/*.csv, results/summary.json, results/figures/*.png

使い方:
    python3 model/vehicle_system_model.py            # BASE で計算し results/ を更新
    python3 model/vehicle_system_model.py --no-fig   # 図を描かない

依存: Python 3.10+、matplotlib（図のみ）。numpy は使わない。

計算の境界:
  - 「単体」      : 燃料そのもの／電池パックそのもの（質量・体積・円/kWh）
  - 「車両」      : 国土交通省審査値（WLTC）の燃費・交流電力量消費率で測った車 1 台
  - 「設備費」    : 一度払って繰り返し使うもの（電池パック・車両本体）
  - 「消費費」    : 走るたびに消えるもの（ガソリン・電気）
  比べるときは、同じ境界・同じ分母・同じ期間に揃える。
"""
from __future__ import annotations

import argparse
import csv
import json
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
DATA = ROOT / "data" / "assumptions.csv"
RESULTS = ROOT / "results"
FIG = RESULTS / "figures"

MJ_PER_KWH = 3.6


def load(case: str = "base") -> dict[str, float]:
    out: dict[str, float] = {}
    with DATA.open(encoding="utf-8") as f:
        for row in csv.DictReader(f):
            out[row["key"]] = float(row[case])
    return out


# ---------------------------------------------------------------- 単体の比較

def storage_medium(a: dict[str, float]) -> dict[str, float]:
    """燃料・電池『単体』の数字。ここだけ見ると電池は桁で負ける（それ自体は正しい）。"""
    hhv_l = a["gasoline_hhv_mj_per_l"]
    lhv_l = hhv_l * a["gasoline_lhv_factor"]
    rho = a["gasoline_density_kg_per_l"]
    lhv_kg = lhv_l / rho
    gas_kwh_per_kg = lhv_kg / MJ_PER_KWH
    gas_kwh_per_l = lhv_l / MJ_PER_KWH
    pack_kwh_per_kg = a["battery_pack_wh_per_kg"] / 1000.0
    return {
        "gasoline_lhv_mj_per_l": round(lhv_l, 2),
        "gasoline_lhv_mj_per_kg": round(lhv_kg, 1),
        "gasoline_kwh_per_l": round(gas_kwh_per_l, 2),
        "gasoline_kwh_per_kg": round(gas_kwh_per_kg, 2),
        "battery_pack_kwh_per_kg": round(pack_kwh_per_kg, 3),
        "mass_density_ratio_gasoline_over_pack": round(gas_kwh_per_kg / pack_kwh_per_kg, 1),
        # 「円/kWh」を無造作に並べた場合の数字（記事で解体する対象）
        "gasoline_jpy_per_kwh_fuel": round(a["gasoline_retail_jpy_per_l"] / gas_kwh_per_l, 1),
        "battery_pack_jpy_per_kwh_capacity": round(a["battery_pack_usd_per_kwh"] * a["usd_jpy"], 0),
    }


# ---------------------------------------------------------------- 車両の比較

def vehicles(a: dict[str, float], sm: dict[str, float]) -> dict[str, dict[str, float]]:
    """同じ軽規格・同じメーカーの 2 台（デイズ 2WD NA／サクラ）を WLTC 審査値で並べる。"""
    gas_kwh_per_l = sm["gasoline_kwh_per_l"]
    # デイズ
    d_l_per_km = 1.0 / a["dayz_fuel_economy_km_per_l"]
    d_energy_wh_per_km = d_l_per_km * gas_kwh_per_l * 1000.0  # 燃料の化学エネルギー（低位）
    d_range_km = a["dayz_tank_l"] * a["dayz_fuel_economy_km_per_l"]
    d_stored_kwh = a["dayz_tank_l"] * gas_kwh_per_l
    d_fuel_mass_kg = a["dayz_tank_l"] * a["gasoline_density_kg_per_l"]
    d_jpy_per_km = d_l_per_km * a["gasoline_retail_jpy_per_l"]
    d_jpy_per_km_no_subsidy = d_l_per_km * (a["gasoline_retail_jpy_per_l"] + a["gasoline_subsidy_jpy_per_l"])
    # サクラ
    s_energy_wh_per_km = a["sakura_consumption_wh_per_km"]  # 交流側（充電損失込み）
    s_jpy_per_km = s_energy_wh_per_km / 1000.0 * a["electricity_jpy_per_kwh"]
    s_pack_mass_est_kg = a["sakura_battery_kwh"] * 1000.0 / a["battery_pack_wh_per_kg"]  # 推計（非公表）
    solar = a["solar_surplus_jpy_per_kwh"]

    def ev(prefix: str, battery_kwh: float, subsidy_key: str) -> dict[str, float]:
        wh_km = a[f"{prefix}_consumption_wh_per_km"]
        jpy_km = wh_km / 1000.0 * a["electricity_jpy_per_kwh"]
        return {
            "curb_mass_kg": a[f"{prefix}_curb_mass_kg"],
            "stored_energy_kwh": battery_kwh,
            "battery_pack_mass_estimate_kg": round(battery_kwh * 1000.0 / a["battery_pack_wh_per_kg"], 0),
            "range_km_wltc": a[f"{prefix}_range_km"],
            "energy_in_wh_per_km": round(wh_km, 0),
            "jpy_per_km": round(jpy_km, 2),
            "jpy_per_km_solar_surplus": round(wh_km / 1000.0 * solar, 2),
            "jpy_per_100km": round(jpy_km * 100, 0),
            "price_jpy": a[f"{prefix}_price_jpy"],
            "price_after_subsidy_jpy": a[f"{prefix}_price_jpy"] - a[subsidy_key],
            "subsidy_jpy": a[subsidy_key],
            "curb_mass_difference_vs_dayz_kg": a[f"{prefix}_curb_mass_kg"] - a["dayz_curb_mass_kg"],
            "range_dayz_over_this": round(d_range_km / a[f"{prefix}_range_km"], 2),
            "stored_energy_dayz_over_this": round(d_stored_kwh / battery_kwh, 1),
            "energy_in_per_km_dayz_over_this": round(d_energy_wh_per_km / wh_km, 2),
            "running_cost_dayz_over_this": round(d_jpy_per_km / jpy_km, 2),
            "running_cost_dayz_over_this_solar_surplus": round(d_jpy_per_km / (wh_km / 1000.0 * solar), 1),
        }

    return {
        "racco200": ev("racco200", a["racco200_battery_kwh"], "racco_subsidy_jpy"),
        "racco300": ev("racco300", a["racco300_battery_kwh"], "racco_subsidy_jpy"),
        "dayz": {
            "curb_mass_kg": a["dayz_curb_mass_kg"],
            "stored_energy_kwh_lhv": round(d_stored_kwh, 1),
            "fuel_mass_full_tank_kg": round(d_fuel_mass_kg, 1),
            "range_km_wltc": round(d_range_km, 0),
            "energy_in_wh_per_km": round(d_energy_wh_per_km, 0),
            "jpy_per_km": round(d_jpy_per_km, 2),
            "jpy_per_km_without_subsidy": round(d_jpy_per_km_no_subsidy, 2),
            "jpy_per_100km": round(d_jpy_per_km * 100, 0),
            "price_jpy": a["dayz_price_jpy"],
        },
        "sakura": {
            "curb_mass_kg": a["sakura_curb_mass_kg"],
            "stored_energy_kwh": a["sakura_battery_kwh"],
            "battery_pack_mass_estimate_kg": round(s_pack_mass_est_kg, 0),
            "range_km_wltc": a["sakura_range_km"],
            "energy_in_wh_per_km": round(s_energy_wh_per_km, 0),
            "jpy_per_km": round(s_jpy_per_km, 2),
            "jpy_per_km_solar_surplus": round(s_energy_wh_per_km / 1000.0 * solar, 2),
            "jpy_per_100km": round(s_jpy_per_km * 100, 0),
            "price_jpy": a["sakura_price_jpy"],
            "price_after_subsidy_jpy": a["sakura_price_jpy"] - a["sakura_subsidy_jpy"],
        },
        "ratios": {
            "stored_energy_dayz_over_sakura": round(d_stored_kwh / a["sakura_battery_kwh"], 1),
            "range_dayz_over_sakura": round(d_range_km / a["sakura_range_km"], 2),
            "curb_mass_sakura_over_dayz": round(a["sakura_curb_mass_kg"] / a["dayz_curb_mass_kg"], 2),
            "curb_mass_difference_kg": a["sakura_curb_mass_kg"] - a["dayz_curb_mass_kg"],
            "energy_in_per_km_dayz_over_sakura": round(d_energy_wh_per_km / s_energy_wh_per_km, 2),
            "running_cost_dayz_over_sakura": round(d_jpy_per_km / s_jpy_per_km, 2),
            "running_cost_dayz_over_sakura_solar_surplus": round(d_jpy_per_km / (s_energy_wh_per_km / 1000.0 * solar), 1),
        },
    }


# ---------------------------------------------------------------- 円/kWh の会計処理

def accounting(a: dict[str, float], sm: dict[str, float], v: dict[str, dict[str, float]]) -> dict[str, float]:
    """『電池の円/kWh』を設備費として寿命中の供給量で割り直す。"""
    pack_jpy = sm["battery_pack_jpy_per_kwh_capacity"] * a["sakura_battery_kwh"]
    km = a["sakura_battery_warranty_km"]
    kwh_delivered = km * a["sakura_consumption_wh_per_km"] / 1000.0
    pack_jpy_per_kwh_delivered = pack_jpy / kwh_delivered
    pack_jpy_per_km = pack_jpy / km
    # 仕事（車輪）あたりに揃える概算: 単体の円/kWh を変換効率で割る
    gas_jpy_per_kwh_wheel = sm["gasoline_jpy_per_kwh_fuel"] / a["ice_ttw_efficiency"]
    ev_jpy_per_kwh_wheel = (a["electricity_jpy_per_kwh"] + pack_jpy_per_kwh_delivered) / a["bev_grid_to_wheel_efficiency"]
    solar = a["solar_surplus_jpy_per_kwh"]
    ev_solar_jpy_per_kwh_wheel = (solar + pack_jpy_per_kwh_delivered) / a["bev_grid_to_wheel_efficiency"]
    lfp_jpy_per_kwh_capacity = a["battery_pack_lfp_usd_per_kwh"] * a["usd_jpy"]
    racco = {}
    for key, bat_key, wh_key in (("racco200", "racco200_battery_kwh", "racco200_consumption_wh_per_km"), ("racco300", "racco300_battery_kwh", "racco300_consumption_wh_per_km")):
        r_km = a["racco_battery_warranty_km"]
        r_kwh_delivered = r_km * a[wh_key] / 1000.0
        for label, unit_price in (("bev_avg", sm["battery_pack_jpy_per_kwh_capacity"]), ("lfp", lfp_jpy_per_kwh_capacity)):
            r_pack = unit_price * a[bat_key]
            racco[f"{key}_pack_cost_jpy_{label}"] = round(r_pack, 0)
            racco[f"{key}_pack_jpy_per_kwh_delivered_{label}"] = round(r_pack / r_kwh_delivered, 1)
            racco[f"{key}_pack_jpy_per_km_{label}"] = round(r_pack / r_km, 2)
            racco[f"{key}_electricity_plus_pack_jpy_per_kwh_at_wheel_{label}"] = round((a["electricity_jpy_per_kwh"] + r_pack / r_kwh_delivered) / a["bev_grid_to_wheel_efficiency"], 0)
        racco[f"{key}_kwh_delivered_over_warranty_km"] = round(r_kwh_delivered, 0)
    return {
        "lfp_pack_jpy_per_kwh_capacity": round(lfp_jpy_per_kwh_capacity, 0),
        "solar_surplus_jpy_per_kwh": solar,
        "solar_plus_pack_jpy_per_kwh_at_wheel_est": round(ev_solar_jpy_per_kwh_wheel, 0),
        "gasoline_over_solar_plus_pack_at_wheel": round(gas_jpy_per_kwh_wheel / ev_solar_jpy_per_kwh_wheel, 1),
        "sakura_pack_plus_solar_jpy_per_km": round(pack_jpy_per_km + a["sakura_consumption_wh_per_km"] / 1000.0 * solar, 2),
        "sakura_pack_plus_grid_jpy_per_km": round(pack_jpy_per_km + a["sakura_consumption_wh_per_km"] / 1000.0 * a["electricity_jpy_per_kwh"], 2),
        **racco,
        "battery_pack_cost_jpy_20kwh": round(pack_jpy, 0),
        "kwh_delivered_over_warranty_km": round(kwh_delivered, 0),
        "battery_pack_jpy_per_kwh_delivered": round(pack_jpy_per_kwh_delivered, 1),
        "battery_pack_jpy_per_km": round(pack_jpy_per_km, 2),
        "gasoline_jpy_per_kwh_fuel": sm["gasoline_jpy_per_kwh_fuel"],
        "gasoline_jpy_per_kwh_at_wheel_est": round(gas_jpy_per_kwh_wheel, 0),
        "electricity_plus_pack_jpy_per_kwh_at_wheel_est": round(ev_jpy_per_kwh_wheel, 0),
        "naive_ratio_pack_over_gasoline_jpy_per_kwh": round(
            sm["battery_pack_jpy_per_kwh_capacity"] / sm["gasoline_jpy_per_kwh_fuel"], 0
        ),
    }


# ---------------------------------------------------------------- 保有総費用（距離×年数の感度）

def tco_grid(a: dict[str, float], v: dict[str, dict[str, float]]) -> list[dict[str, float]]:
    rows = []
    for years in (5, 8, 13):
        for km_per_year in (5000, 10000, 20000):
            km = years * km_per_year
            d = v["dayz"]["price_jpy"] + v["dayz"]["jpy_per_km"] * km
            s = v["sakura"]["price_after_subsidy_jpy"] + v["sakura"]["jpy_per_km"] * km
            s_nosub = v["sakura"]["price_jpy"] + v["sakura"]["jpy_per_km"] * km
            rows.append(
                {
                    "years": years,
                    "km_per_year": km_per_year,
                    "total_km": km,
                    "dayz_vehicle_plus_fuel_jpy": round(d, 0),
                    "sakura_vehicle_after_subsidy_plus_electricity_jpy": round(s, 0),
                    "sakura_vehicle_no_subsidy_plus_electricity_jpy": round(s_nosub, 0),
                    "difference_sakura_minus_dayz_jpy": round(s - d, 0),
                    "difference_no_subsidy_jpy": round(s_nosub - d, 0),
                }
            )
    return rows


def breakeven_km(v: dict[str, dict[str, float]]) -> dict[str, float]:
    def _be(car: str, solar: bool = False) -> dict[str, float]:
        dp = v[car]["price_after_subsidy_jpy"] - v["dayz"]["price_jpy"]
        dp_nosub = v[car]["price_jpy"] - v["dayz"]["price_jpy"]
        ev_cost = v[car]["jpy_per_km_solar_surplus"] if solar else v[car]["jpy_per_km"]
        dr = v["dayz"]["jpy_per_km"] - ev_cost
        return {
            "price_gap_after_subsidy_jpy": dp,
            "price_gap_no_subsidy_jpy": dp_nosub,
            "running_cost_gap_jpy_per_km": round(dr, 2),
            "breakeven_km_after_subsidy": round(dp / dr, 0) if dr > 0 else float("inf"),
            "breakeven_km_no_subsidy": round(dp_nosub / dr, 0) if dr > 0 else float("inf"),
        }

    out = _be("sakura")
    out["sakura_solar_surplus"] = _be("sakura", solar=True)
    out["racco200"] = _be("racco200")
    out["racco200_solar_surplus"] = _be("racco200", solar=True)
    out["racco300"] = _be("racco300")
    out["racco300_solar_surplus"] = _be("racco300", solar=True)
    return out


# ---------------------------------------------------------------- ガソリン価格のシナリオ

def gasoline_price_scenarios(a: dict[str, float], v: dict[str, dict[str, float]]) -> list[dict[str, float]]:
    """ガソリン価格だけを動かしたとき、円/km・倍率・価格差が並ぶ距離がどう動くか。EV 側は固定。"""
    rows = []
    l_per_km = 1.0 / a["dayz_fuel_economy_km_per_l"]
    gas_kwh_per_l = storage_medium(a)["gasoline_kwh_per_l"]
    for label, price in (
        ("2026-10 全国平均（補助金込み）", a["gasoline_retail_jpy_per_l"]),
        ("補助金なし（170.2＋42.8）", a["gasoline_retail_jpy_per_l"] + a["gasoline_subsidy_jpy_per_l"]),
        ("仮に 220 円", 220.0),
        ("仮に 250 円", 250.0),
    ):
        d_km = l_per_km * price
        row = {"scenario": label, "gasoline_jpy_per_l": price, "dayz_jpy_per_km": round(d_km, 2), "gasoline_jpy_per_kwh_fuel": round(price / gas_kwh_per_l, 1), "gasoline_jpy_per_kwh_at_wheel_est": round(price / gas_kwh_per_l / a["ice_ttw_efficiency"], 0)}
        for car in ("sakura", "racco200", "racco300"):
            grid = v[car]["jpy_per_km"]
            solar = v[car]["jpy_per_km_solar_surplus"]
            row[f"{car}_ratio_grid"] = round(d_km / grid, 2)
            row[f"{car}_ratio_solar_surplus"] = round(d_km / solar, 1)
            gap = v[car]["price_after_subsidy_jpy"] - v["dayz"]["price_jpy"]
            gap_nosub = v[car]["price_jpy"] - v["dayz"]["price_jpy"]
            row[f"{car}_breakeven_km_after_subsidy"] = round(gap / (d_km - grid), 0)
            row[f"{car}_breakeven_km_no_subsidy"] = round(gap_nosub / (d_km - grid), 0)
            row[f"{car}_breakeven_km_solar_surplus"] = round(gap / (d_km - solar), 0)
        rows.append(row)
    return rows


# ---------------------------------------------------------------- 感度（一変数）

def sensitivity(base: dict[str, float]) -> list[dict[str, float]]:
    rows = []
    keys = [
        "gasoline_retail_jpy_per_l",
        "electricity_jpy_per_kwh",
        "dayz_fuel_economy_km_per_l",
        "sakura_consumption_wh_per_km",
        "battery_pack_usd_per_kwh",
        "usd_jpy",
        "ice_ttw_efficiency",
        "bev_grid_to_wheel_efficiency",
    ]
    low, high = load("low"), load("high")
    for k in keys:
        for case, src in (("low", low), ("high", high)):
            a = dict(base)
            a[k] = src[k]
            sm = storage_medium(a)
            v = vehicles(a, sm)
            acc = accounting(a, sm, v)
            rows.append(
                {
                    "key": k,
                    "case": case,
                    "value": src[k],
                    "dayz_jpy_per_km": v["dayz"]["jpy_per_km"],
                    "sakura_jpy_per_km": v["sakura"]["jpy_per_km"],
                    "running_cost_ratio": v["ratios"]["running_cost_dayz_over_sakura"],
                    "gasoline_jpy_per_kwh_fuel": sm["gasoline_jpy_per_kwh_fuel"],
                    "battery_pack_jpy_per_kwh_delivered": acc["battery_pack_jpy_per_kwh_delivered"],
                }
            )
    return rows


# ---------------------------------------------------------------- 図

def make_figures(sm, v, acc) -> list[str]:
    try:
        import matplotlib

        matplotlib.use("Agg")
        import matplotlib.pyplot as plt
        from matplotlib import font_manager
    except Exception:  # pragma: no cover
        return []
    for cand in ("Hiragino Sans", "Hiragino Kaku Gothic ProN", "Noto Sans CJK JP", "IPAexGothic"):
        if any(cand in f.name for f in font_manager.fontManager.ttflist):
            plt.rcParams["font.family"] = cand
            break
    plt.rcParams["axes.unicode_minus"] = False
    FIG.mkdir(parents=True, exist_ok=True)
    written = []

    # 図1: 単体 → 車両 で倍率がどう変わるか
    labels = [
        "質量あたり\n貯蔵エネルギー\n(燃料 vs パック)",
        "車載の貯蔵エネルギー\n(27 L vs 20 kWh)",
        "一充電/満タン\n航続 (WLTC)",
        "1 km あたり\n投入エネルギー",
        "1 km あたり\n走行費",
    ]
    vals = [
        sm["mass_density_ratio_gasoline_over_pack"],
        v["ratios"]["stored_energy_dayz_over_sakura"],
        v["ratios"]["range_dayz_over_sakura"],
        v["ratios"]["energy_in_per_km_dayz_over_sakura"],
        v["ratios"]["running_cost_dayz_over_sakura"],
    ]
    fig, ax = plt.subplots(figsize=(9.2, 5.2), dpi=150)
    bars = ax.bar(labels, vals, color=["#8a8f98", "#8a8f98", "#2f7a4d", "#2f7a4d", "#c98a2b"])
    ax.set_yscale("log")
    ax.set_ylim(0.7, max(vals) * 2.2)
    ax.set_ylabel("ガソリン側 ÷ 電池側（倍、対数軸）")
    ax.axhline(1.0, color="#333", lw=0.8, ls="--")
    for b, val in zip(bars, vals):
        ax.text(b.get_x() + b.get_width() / 2, val * 1.08, f"{val:g}倍", ha="center", va="bottom", fontsize=10)
    ax.set_title("図1　「単体」から「車両」へ境界を広げると、倍率はどう変わるか（デイズ 2WD NA／サクラ、WLTC 審査値）", fontsize=10.5)
    ax.text(
        0.0, -0.30,
        "左 2 本は燃料・電池そのものの数字（ガソリンが大きい＝正しい）。右 3 本は同じ軽規格の実車で測った数字。\n"
        "1 km あたり投入エネルギーはガソリン車が約 3 倍多く、走行費はガソリン車が約 1.9 倍（170.2 円/L・31 円/kWh）。",
        transform=ax.transAxes, fontsize=8.5, va="top",
    )
    fig.tight_layout()
    p = FIG / "fig01_boundary_ratios.png"
    fig.savefig(p, bbox_inches="tight")
    plt.close(fig)
    written.append(p.name)

    # 図2: 同じ「円/kWh」でも中身が違う
    fig, ax = plt.subplots(figsize=(9.2, 4.6), dpi=150)
    cats = ["ガソリン\n（消費費・燃料 1 kWh）", "電池パック\n（設備費・容量 1 kWh）", "電池パック\n（設備費 ÷ 保証走行で供給する kWh）", "電気\n（消費費・1 kWh）"]
    y = [
        sm["gasoline_jpy_per_kwh_fuel"],
        sm["battery_pack_jpy_per_kwh_capacity"],
        acc["battery_pack_jpy_per_kwh_delivered"],
        load()["electricity_jpy_per_kwh"],
    ]
    cols = ["#c98a2b", "#8a8f98", "#2f7a4d", "#2f7a4d"]
    bars = ax.bar(cats, y, color=cols)
    ax.set_yscale("log")
    ax.set_ylim(5, max(y) * 2.5)
    ax.set_ylabel("円/kWh（対数軸）")
    for b, val in zip(bars, y):
        ax.text(b.get_x() + b.get_width() / 2, val * 1.1, f"{val:,.0f} 円", ha="center", va="bottom", fontsize=10)
    ax.set_title("図2　同じ「円/kWh」でも、消費費と設備費は別の勘定（2026-10 時点の値）", fontsize=10.5)
    ax.text(
        0.0, -0.22,
        "電池の 1 kWh は「容量」の取得費で、保証期間（8 年 16 万 km）に繰り返し充放電して供給する。供給 1 kWh あたりに直すと十数円。\n"
        "ガソリンの 1 kWh は燃やせば消える。並べてよいのは右 2 本の和（電気＋設備の供給あたり費用）と左端であり、しかも効率補正が要る。",
        transform=ax.transAxes, fontsize=8.5, va="top",
    )
    fig.tight_layout()
    p = FIG / "fig02_jpy_per_kwh_accounting.png"
    fig.savefig(p, bbox_inches="tight")
    plt.close(fig)
    written.append(p.name)

    # 図3: 保有総費用（車両＋エネルギー）の距離依存
    fig, ax = plt.subplots(figsize=(9.2, 5.0), dpi=150)
    kms = list(range(0, 160001, 5000))
    d = [v["dayz"]["price_jpy"] + v["dayz"]["jpy_per_km"] * k for k in kms]
    s = [v["sakura"]["price_after_subsidy_jpy"] + v["sakura"]["jpy_per_km"] * k for k in kms]
    s2 = [v["sakura"]["price_jpy"] + v["sakura"]["jpy_per_km"] * k for k in kms]
    ax.plot(kms, [x / 1e4 for x in d], color="#c98a2b", lw=2.2, label="デイズ X 2WD：車両価格＋ガソリン代")
    ax.plot(kms, [x / 1e4 for x in s], color="#2f7a4d", lw=2.2, label="サクラ S：車両価格（補助金 58 万円後）＋電気代")
    ax.plot(kms, [x / 1e4 for x in s2], color="#2f7a4d", lw=1.4, ls="--", label="サクラ S：補助金なし＋電気代")
    be = breakeven_km(v)
    if be["breakeven_km_after_subsidy"] != float("inf") and be["breakeven_km_after_subsidy"] < 160000:
        ax.axvline(be["breakeven_km_after_subsidy"], color="#555", lw=0.8, ls=":")
        ax.text(be["breakeven_km_after_subsidy"], ax.get_ylim()[1] * 0.98, f"分岐 {be['breakeven_km_after_subsidy']:,.0f} km", rotation=90, va="top", ha="right", fontsize=8.5)
    ax.set_xlabel("累計走行距離（km）")
    ax.set_ylabel("車両価格＋エネルギー費の累計（万円）")
    ax.set_title("図3　「車両＋エネルギー費」の累計（税・保険・整備・残価は含まない。前提は assumptions.csv）", fontsize=10.5)
    ax.grid(alpha=0.3)
    ax.legend(fontsize=8.5, loc="upper left")
    fig.tight_layout()
    p = FIG / "fig03_vehicle_plus_energy_cumulative.png"
    fig.savefig(p, bbox_inches="tight")
    plt.close(fig)
    written.append(p.name)

    # 図4: 1 km あたりのエネルギー費。系統電力で充電した場合と、卒FIT の余剰太陽光（売れば 8 円/kWh）で充電した場合
    a = load()
    fig, ax = plt.subplots(figsize=(9.2, 4.8), dpi=150)
    names = ["デイズ X 2WD\n170.2 円/L", "デイズ\n補助金なし\n213 円/L", "サクラ S\n系統\n31 円/kWh", "RACCO 200\n系統\n31 円/kWh", "RACCO 300Plus\n系統\n31 円/kWh", "サクラ S\n余剰太陽光\n8 円/kWh", "RACCO 200\n余剰太陽光\n8 円/kWh"]
    yv = [
        v["dayz"]["jpy_per_km"], v["dayz"]["jpy_per_km_without_subsidy"],
        v["sakura"]["jpy_per_km"], v["racco200"]["jpy_per_km"], v["racco300"]["jpy_per_km"],
        v["sakura"]["jpy_per_km_solar_surplus"], v["racco200"]["jpy_per_km_solar_surplus"],
    ]
    colors = ["#c98a2b", "#e0b36a", "#2f7a4d", "#2f7a4d", "#2f7a4d", "#7fb77e", "#7fb77e"]
    bars = ax.bar(names, yv, color=colors)
    for b, val in zip(bars, yv):
        ax.text(b.get_x() + b.get_width() / 2, val + 0.12, f"{val:.2f} 円", ha="center", va="bottom", fontsize=9.5)
    ax.set_ylabel("1 km あたりの燃料・電気代（円）")
    ax.set_ylim(0, max(yv) * 1.25)
    ax.tick_params(axis="x", labelsize=8.5)
    ax.set_title("図4　1 km あたりのエネルギー費。充電する電気の値段で、EV 側の数字は 3.8 円から 1 円まで動く", fontsize=10.5)
    ax.text(
        0.0, -0.34,
        f"余剰太陽光の 8 円/kWh は、FIT 買取期間を終えた住宅用太陽光の余剰を売電した場合の単価（大手 10 社の基本買取 7〜9 円）。\n"
        "自家消費で車に充電すると、その電気の機会費用は売電単価になる。昼間に車が自宅にあること、または蓄電池を介することが前提。",
        transform=ax.transAxes, fontsize=8.5, va="top",
    )
    fig.tight_layout()
    p = FIG / "fig04_jpy_per_km_by_charging_source.png"
    fig.savefig(p, bbox_inches="tight")
    plt.close(fig)
    written.append(p.name)
    return written


# ---------------------------------------------------------------- main

def write_csv(path: Path, rows: list[dict]) -> None:
    if not rows:
        return
    with path.open("w", encoding="utf-8", newline="") as f:
        w = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
        w.writeheader()
        w.writerows(rows)


def main() -> None:
    ap = argparse.ArgumentParser()
    ap.add_argument("--no-fig", action="store_true")
    args = ap.parse_args()

    a = load("base")
    sm = storage_medium(a)
    v = vehicles(a, sm)
    acc = accounting(a, sm, v)
    be = breakeven_km(v)
    tco = tco_grid(a, v)
    sens = sensitivity(a)
    gas = gasoline_price_scenarios(a, v)

    RESULTS.mkdir(parents=True, exist_ok=True)
    write_csv(RESULTS / "storage_medium.csv", [{"metric": k, "value": val} for k, val in sm.items()])
    rows = []
    for car in ("dayz", "sakura", "racco200", "racco300"):
        for k, val in v[car].items():
            rows.append({"vehicle": car, "metric": k, "value": val})
    for k, val in v["ratios"].items():
        rows.append({"vehicle": "ratio", "metric": k, "value": val})
    write_csv(RESULTS / "vehicle_comparison.csv", rows)
    write_csv(RESULTS / "accounting_jpy_per_kwh.csv", [{"metric": k, "value": val} for k, val in acc.items()])
    write_csv(RESULTS / "tco_grid.csv", tco)
    write_csv(RESULTS / "sensitivity.csv", sens)
    write_csv(RESULTS / "gasoline_price_scenarios.csv", gas)
    figs = [] if args.no_fig else make_figures(sm, v, acc)
    summary = {
        "generated_from": "data/assumptions.csv (BASE)",
        "storage_medium": sm,
        "vehicles": v,
        "accounting": acc,
        "breakeven": be,
        "gasoline_price_scenarios": gas,
        "figures": figs,
    }
    (RESULTS / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
    print(json.dumps(summary, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
