from __future__ import annotations

import json
import math
from pathlib import Path

import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from matplotlib import font_manager

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)

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

BRAND = {
    "brown": "#6f3519",
    "orange": "#c96f2d",
    "gold": "#d7a33d",
    "teal": "#2f7d76",
    "blue": "#4b6f9c",
    "purple": "#76548e",
    "cream": "#f7f0e7",
    "ink": "#2a211c",
    "muted": "#74675e",
}

cases = pd.read_csv(DIR / "case-audit.csv")
binary_columns = [
    "fee_amount_observed",
    "minimum_order_observed",
    "distance_rule_observed",
    "free_delivery_observed",
    "advance_order_observed",
    "group_order_observed",
    "delivery_actor_observed",
]
for column in binary_columns:
    cases[column] = pd.to_numeric(cases[column], errors="raise").astype(int)

if not cases["case_id"].is_unique:
    raise ValueError("case_id must be unique")
if not cases["source_url"].str.startswith("https://").all():
    raise ValueError("Every public source must be HTTPS")
if not cases["capture_date"].eq("2026-07-24").all():
    raise ValueError("Capture dates must be fixed to the release audit date")
if not cases[binary_columns].isin([0, 1]).all().all():
    raise ValueError("Coding fields must be binary")
if not 50 <= len(cases) <= 60:
    raise ValueError(f"Expected 50-60 purposively selected cases, found {len(cases)}")

rule_labels = {
    "fee_amount_observed": "明確數字外送費",
    "minimum_order_observed": "訂購數量或金額條件",
    "distance_rule_observed": "距離或服務分區",
    "free_delivery_observed": "免費外送條件",
    "advance_order_observed": "預約或提前下單",
    "group_order_observed": "數量單位條件",
    "delivery_actor_observed": "配送執行者身分",
}
rule_summary = pd.DataFrame(
    [
        {
            "rule": column,
            "label": label,
            "observed_count": int(cases[column].sum()),
            "not_observed_count": int(len(cases) - cases[column].sum()),
            "observed_share": float(cases[column].mean()),
        }
        for column, label in rule_labels.items()
    ]
)
rule_summary.to_csv(OUTPUTS / "rule-observability.csv", index=False, encoding="utf-8-sig")

source_summary = (
    cases.groupby(["source_type", "source_document_period"], dropna=False)
    .size()
    .rename("case_count")
    .reset_index()
)
source_summary.to_csv(OUTPUTS / "source-strata.csv", index=False, encoding="utf-8-sig")

# Synthetic cost assumptions. These values demonstrate model behavior and are
# not estimates of a specific shop, city, or market.
assumptions = {
    "fully_loaded_labor_per_hour": 248.0,
    "vehicle_cost_per_km": 4.2,
    "dispatch_fixed_cost": 12.0,
    "kitchen_wait_minutes": 8.0,
    "drive_minutes_per_km": 3.4,
    "handoff_minutes_per_stop": 4.5,
    "expected_failure_cost_per_stop": 3.0,
    "average_bento_price": 110.0,
    "food_contribution_margin_rate": 0.32,
    "maximum_food_margin_subsidy_share": 0.30,
    "platform_fixed_cost_per_month": 3000.0,
}
(DIR / "synthetic-assumptions.json").write_text(
    json.dumps(
        {
            "scope": "Synthetic assumptions for sensitivity analysis; not observed market prices or formal quotations.",
            "platform_fixed_cost_allocated_in_outputs": False,
            **assumptions,
        },
        ensure_ascii=False,
        indent=2,
    ),
    encoding="utf-8",
)


def route_cost(
    route_km: float,
    stops: int,
    *,
    labor_per_hour: float = assumptions["fully_loaded_labor_per_hour"],
    vehicle_per_km: float = assumptions["vehicle_cost_per_km"],
    wait_minutes: float = assumptions["kitchen_wait_minutes"],
    drive_minutes_per_km: float = assumptions["drive_minutes_per_km"],
    handoff_minutes_per_stop: float = assumptions["handoff_minutes_per_stop"],
) -> tuple[float, float]:
    total_minutes = (
        wait_minutes
        + route_km * drive_minutes_per_km
        + stops * handoff_minutes_per_stop
    )
    total_cost = (
        assumptions["dispatch_fixed_cost"]
        + labor_per_hour / 60.0 * total_minutes
        + vehicle_per_km * route_km
        + assumptions["expected_failure_cost_per_stop"] * stops
    )
    return total_cost, total_minutes


route_rows: list[dict[str, float | int | str]] = []
zone_map = {3.0: "核心近距", 8.0: "中距", 14.0: "外圍遠距"}
for route_km, zone in zone_map.items():
    for stops in [1, 3, 6]:
        for boxes_per_stop in [1, 3, 5, 10]:
            cost, total_minutes = route_cost(route_km, stops)
            total_boxes = stops * boxes_per_stop
            food_contribution_per_stop = (
                boxes_per_stop
                * assumptions["average_bento_price"]
                * assumptions["food_contribution_margin_rate"]
            )
            allowed_subsidy_per_stop = (
                food_contribution_per_stop
                * assumptions["maximum_food_margin_subsidy_share"]
            )
            cost_per_stop = cost / stops
            break_even_customer_fee = max(0.0, cost_per_stop - allowed_subsidy_per_stop)
            route_rows.append(
                {
                    "zone": zone,
                    "route_km": route_km,
                    "stops": stops,
                    "boxes_per_stop": boxes_per_stop,
                    "total_boxes": total_boxes,
                    "route_minutes": round(total_minutes, 2),
                    "route_cost": round(cost, 2),
                    "cost_per_stop": round(cost_per_stop, 2),
                    "cost_per_box": round(cost / total_boxes, 2),
                    "food_contribution_per_stop": round(food_contribution_per_stop, 2),
                    "allowed_food_subsidy_per_stop": round(allowed_subsidy_per_stop, 2),
                    "break_even_customer_fee": round(break_even_customer_fee, 2),
                }
            )
route_scenarios = pd.DataFrame(route_rows)
route_scenarios.to_csv(OUTPUTS / "route-cost-scenarios.csv", index=False, encoding="utf-8-sig")

threshold_rows: list[dict[str, float]] = []
baseline_order_value = 300.0
for waived_fee in [29.0, 49.0, 69.0, 89.0]:
    for margin_rate in [0.22, 0.28, 0.34, 0.40, 0.46]:
        screening_threshold = baseline_order_value + waived_fee / margin_rate
        threshold_rows.append(
            {
                "baseline_order_value": baseline_order_value,
                "waived_delivery_fee": waived_fee,
                "incremental_contribution_margin_rate": margin_rate,
                "screening_threshold": round(screening_threshold, 2),
            }
        )
thresholds = pd.DataFrame(threshold_rows)
thresholds.to_csv(OUTPUTS / "free-delivery-threshold-screening.csv", index=False, encoding="utf-8-sig")

membership_rows: list[dict[str, float | int]] = []
for monthly_fee in [99, 199, 299]:
    for regular_fee in [39, 59, 79]:
        for member_fee in [0, 19, 29]:
            saving = regular_fee - member_fee
            if saving <= 0:
                continue
            membership_rows.append(
                {
                    "monthly_fee": monthly_fee,
                    "regular_delivery_fee": regular_fee,
                    "member_delivery_fee": member_fee,
                    "consumer_break_even_orders": int(math.ceil(monthly_fee / saving)),
                }
            )
membership = pd.DataFrame(membership_rows)
membership.to_csv(OUTPUTS / "membership-break-even.csv", index=False, encoding="utf-8-sig")

policy_rows: list[dict[str, float | int | str]] = []
policy_specs = [
    ("單一固定費", "flat", 39.0, 1.00, 0.00),
    ("透明距離分區", "zone", 49.0, 1.00, 0.00),
    ("滿額免運", "threshold", 0.0, 1.08, 38.4),
    ("最低盒數加固定費", "minimum", 39.0, 1.15, 28.0),
    ("預約批次折扣", "scheduled", 29.0, 1.45, 0.00),
    ("限額會員優惠", "membership", 15.0, 1.25, 25.0),
]
for density_label, base_batch in [("低密度", 1.0), ("中密度", 3.0), ("高密度", 6.0)]:
    for sensitivity_label, fee_sensitivity in [
        ("低敏感", 0.008),
        ("中敏感", 0.015),
        ("高敏感", 0.022),
    ]:
        for policy_name, policy_code, customer_fee, density_multiplier, added_contribution in policy_specs:
            effective_batch = min(6.0, base_batch * density_multiplier)
            route_km = 8.0
            stops = max(1, int(round(effective_batch)))
            cost, _ = route_cost(route_km, stops)
            cost_per_order = cost / stops
            base_conversion = 0.78
            conversion = max(
                0.08,
                min(
                    0.95,
                    base_conversion
                    * math.exp(-fee_sensitivity * customer_fee)
                    * (0.94 if policy_code == "scheduled" else 1.0),
                ),
            )
            food_contribution = (
                2
                * assumptions["average_bento_price"]
                * assumptions["food_contribution_margin_rate"]
                + added_contribution
            )
            expected_contribution_per_offer = conversion * (
                food_contribution + customer_fee - cost_per_order
            )
            policy_rows.append(
                {
                    "density_scenario": density_label,
                    "fee_sensitivity_scenario": sensitivity_label,
                    "policy": policy_name,
                    "customer_fee": customer_fee,
                    "effective_batch_orders": effective_batch,
                    "modeled_conversion": round(conversion, 4),
                    "cost_per_completed_order": round(cost_per_order, 2),
                    "food_contribution_per_completed_order": round(food_contribution, 2),
                    "expected_contribution_per_offer": round(expected_contribution_per_offer, 2),
                    "synthetic": 1,
                }
            )
policy_scenarios = pd.DataFrame(policy_rows)
policy_scenarios.to_csv(OUTPUTS / "policy-scenarios.csv", index=False, encoding="utf-8-sig")

sensitivity_rows: list[dict[str, float | str]] = []
base_cost, _ = route_cost(8.0, 3)
tests = [
    ("完整人力成本", {"labor_per_hour": assumptions["fully_loaded_labor_per_hour"] * 1.2}),
    ("廚房等待時間", {"wait_minutes": assumptions["kitchen_wait_minutes"] + 6}),
    ("每公里車輛成本", {"vehicle_per_km": assumptions["vehicle_cost_per_km"] * 1.5}),
    ("每站交付時間", {"handoff_minutes_per_stop": assumptions["handoff_minutes_per_stop"] + 3}),
    ("壅塞行車時間", {"drive_minutes_per_km": assumptions["drive_minutes_per_km"] * 1.25}),
]
for label, kwargs in tests:
    changed_cost, _ = route_cost(8.0, 3, **kwargs)
    sensitivity_rows.append(
        {
            "parameter": label,
            "base_route_cost": round(base_cost, 2),
            "changed_route_cost": round(changed_cost, 2),
            "increase_amount": round(changed_cost - base_cost, 2),
            "increase_percent": round((changed_cost / base_cost - 1) * 100, 2),
        }
    )
sensitivity = pd.DataFrame(sensitivity_rows).sort_values("increase_amount", ascending=True)
sensitivity.to_csv(OUTPUTS / "cost-sensitivity.csv", index=False, encoding="utf-8-sig")


def save_figure(fig: plt.Figure, filename: str) -> None:
    fig.tight_layout()
    fig.savefig(FIGURES / filename, dpi=220, bbox_inches="tight", facecolor="white")
    plt.close(fig)


# Figure 1: public rule observability
fig, ax = plt.subplots(figsize=(10.5, 5.8))
plot_rules = rule_summary.sort_values("observed_count")
colors = [
    BRAND["brown"],
    BRAND["orange"],
    BRAND["gold"],
    BRAND["teal"],
    BRAND["blue"],
    BRAND["purple"],
    "#a05b66",
]
bars = ax.barh(plot_rules["label"], plot_rules["observed_count"], color=colors)
ax.set_xlim(0, len(cases))
ax.set_xlabel(f"可觀察案例數（目的性樣本 n={len(cases)}）")
ax.set_title("圖1　公開頁面可觀察之外送規則")
for bar, value in zip(bars, plot_rules["observed_count"]):
    ax.text(value + 0.5, bar.get_y() + bar.get_height() / 2, f"{value}", va="center")
ax.grid(axis="x", alpha=0.2)
save_figure(fig, "figure-1-rule-observability.png")

# Figure 2: cost per box by grouping
fig, ax = plt.subplots(figsize=(10.5, 6))
palette = [BRAND["teal"], BRAND["orange"], BRAND["purple"]]
for (route_km, zone), color in zip(zone_map.items(), palette):
    subset = route_scenarios[(route_scenarios["route_km"] == route_km) & (route_scenarios["stops"] == 3)]
    ax.plot(
        subset["boxes_per_stop"],
        subset["cost_per_box"],
        marker="o",
        linewidth=2.6,
        color=color,
        label=f"{zone}（路線 {route_km:.0f} km）",
    )
    for _, row in subset.iterrows():
        ax.annotate(
            f"{row['cost_per_box']:.0f}",
            (row["boxes_per_stop"], row["cost_per_box"]),
            xytext=(0, 7),
            textcoords="offset points",
            ha="center",
            fontsize=8,
        )
ax.set_xlabel("每一停靠點的便當盒數")
ax.set_ylabel("合成情境配送成本（元／盒）")
ax.set_title("圖2　同址盒數如何攤薄最後一哩成本")
ax.legend(frameon=False)
ax.grid(alpha=0.2)
save_figure(fig, "figure-2-cost-per-box.png")

# Figure 3: customer fee floor heatmap
heat = (
    route_scenarios[route_scenarios["stops"] == 3]
    .pivot(index="route_km", columns="boxes_per_stop", values="break_even_customer_fee")
    .sort_index()
)
fig, ax = plt.subplots(figsize=(9.5, 5.8))
image = ax.imshow(heat.values, cmap="YlOrBr", aspect="auto")
ax.set_xticks(range(len(heat.columns)), labels=heat.columns)
ax.set_yticks(range(len(heat.index)), labels=[f"{value:.0f} km" for value in heat.index])
ax.set_xlabel("每一停靠點的便當盒數")
ax.set_ylabel("合成路線總里程")
ax.set_title("圖3　餐點補貼後的路線成本回收費用（未含系統固定費）")
for i in range(heat.shape[0]):
    for j in range(heat.shape[1]):
        ax.text(j, i, f"{heat.iloc[i, j]:.0f}", ha="center", va="center", color=BRAND["ink"])
fig.colorbar(image, ax=ax, label="元／停靠點")
save_figure(fig, "figure-3-break-even-fee.png")

# Figure 4: threshold screening
threshold_heat = thresholds.pivot(
    index="waived_delivery_fee",
    columns="incremental_contribution_margin_rate",
    values="screening_threshold",
).sort_index()
fig, ax = plt.subplots(figsize=(10, 5.8))
image = ax.imshow(threshold_heat.values, cmap="PuBuGn", aspect="auto")
ax.set_xticks(
    range(len(threshold_heat.columns)),
    labels=[f"{value:.0%}" for value in threshold_heat.columns],
)
ax.set_yticks(
    range(len(threshold_heat.index)),
    labels=[f"{value:.0f} 元" for value in threshold_heat.index],
)
ax.set_xlabel("增量貢獻毛利率（合成）")
ax.set_ylabel("免除的外送費（合成）")
ax.set_title("圖4　免運門檻第一輪篩選（基準客單300元）")
for i in range(threshold_heat.shape[0]):
    for j in range(threshold_heat.shape[1]):
        ax.text(j, i, f"{threshold_heat.iloc[i, j]:.0f}", ha="center", va="center", color=BRAND["ink"])
fig.colorbar(image, ax=ax, label="門檻篩選值（元）")
save_figure(fig, "figure-4-threshold-screening.png")

# Figure 5: policy scenario robustness, showing ranges rather than a single optimum
policy_plot = (
    policy_scenarios.groupby("policy")["expected_contribution_per_offer"]
    .agg(["min", "median", "max"])
    .sort_values("median")
)
fig, ax = plt.subplots(figsize=(10.5, 6))
positions = np.arange(len(policy_plot))
lower = policy_plot["median"] - policy_plot["min"]
upper = policy_plot["max"] - policy_plot["median"]
ax.errorbar(
    policy_plot["median"],
    positions,
    xerr=[lower, upper],
    fmt="o",
    capsize=5,
    color=BRAND["brown"],
    ecolor=BRAND["gold"],
    linewidth=2,
)
ax.axvline(0, color=BRAND["muted"], linewidth=1)
ax.set_yticks(positions, labels=policy_plot.index)
ax.set_xlabel("每一潛在訂單的期望貢獻（合成情境，元）")
ax.set_title("圖5　六種定價制度的純機制示範（九組合成情境）")
ax.grid(axis="x", alpha=0.2)
save_figure(fig, "figure-5-policy-range.png")

# Figure 6: cost sensitivity
fig, ax = plt.subplots(figsize=(10, 5.8))
bars = ax.barh(sensitivity["parameter"], sensitivity["increase_amount"], color=[
    BRAND["blue"], BRAND["teal"], BRAND["gold"], BRAND["orange"], BRAND["brown"]
])
ax.set_xlabel("基準路線成本增加（元／路線）")
ax.set_title("圖6　中距三站合成路線的單因子敏感度")
for bar, amount, percent in zip(bars, sensitivity["increase_amount"], sensitivity["increase_percent"]):
    ax.text(
        amount + 0.4,
        bar.get_y() + bar.get_height() / 2,
        f"+{amount:.1f}（{percent:.1f}%）",
        va="center",
        fontsize=8.5,
    )
ax.grid(axis="x", alpha=0.2)
save_figure(fig, "figure-6-cost-sensitivity.png")

base_mid_single = route_scenarios[
    (route_scenarios["route_km"] == 8.0)
    & (route_scenarios["stops"] == 3)
    & (route_scenarios["boxes_per_stop"] == 1)
].iloc[0]
base_mid_group = route_scenarios[
    (route_scenarios["route_km"] == 8.0)
    & (route_scenarios["stops"] == 3)
    & (route_scenarios["boxes_per_stop"] == 10)
].iloc[0]

results = {
    "report_number": "SHWRP-2026-025",
    "case_count": int(len(cases)),
    "unique_source_url_count": int(cases["source_url"].nunique()),
    "source_type_counts": {str(key): int(value) for key, value in cases["source_type"].value_counts().items()},
    "rule_observed_counts": {
        row["rule"]: int(row["observed_count"]) for _, row in rule_summary.iterrows()
    },
    "delivery_actor_observed_count": int(cases["delivery_actor_observed"].sum()),
    "route_scenario_count": int(len(route_scenarios)),
    "policy_scenario_count": int(len(policy_scenarios)),
    "threshold_scenario_count": int(len(thresholds)),
    "membership_scenario_count": int(len(membership)),
    "mid_route_single_box_cost_per_box": float(base_mid_single["cost_per_box"]),
    "mid_route_ten_box_cost_per_box": float(base_mid_group["cost_per_box"]),
    "mid_route_grouping_reduction_percent": round(
        (1 - base_mid_group["cost_per_box"] / base_mid_single["cost_per_box"]) * 100,
        2,
    ),
    "synthetic_results_are_market_estimates": False,
    "platform_fixed_cost_allocated_in_outputs": False,
    "second_independent_human_coder": False,
    "figures": 6,
}
(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))
