from __future__ import annotations

import json
from pathlib import Path

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


ROOT = Path(__file__).resolve().parent
OUTPUT = ROOT / "outputs" / "figures"
FONT = ROOT / "assets" / "fonts" / "NotoSansTC-Regular.ttf"
EVALUATION = ROOT / "outputs" / "demo" / "artifacts" / "evaluation.json"

INK = "#2a201a"
BROWN = "#6f3519"
ORANGE = "#c8752f"
GREEN = "#47704f"
PALE = "#f4eadf"
PALE_GREEN = "#e9f0e6"
GREY = "#7b746e"
RED = "#a74b3d"


def setup() -> font_manager.FontProperties:
    if not FONT.is_file():
        raise FileNotFoundError(FONT)
    font_manager.fontManager.addfont(str(FONT))
    family = font_manager.FontProperties(fname=str(FONT)).get_name()
    plt.rcParams.update({
        "font.family": family,
        "axes.unicode_minus": False,
        "figure.facecolor": "white",
        "axes.facecolor": "white",
        "text.color": INK,
        "axes.labelcolor": INK,
        "xtick.color": GREY,
        "ytick.color": GREY,
    })
    return font_manager.FontProperties(fname=str(FONT))


def save(fig: plt.Figure, name: str) -> None:
    OUTPUT.mkdir(parents=True, exist_ok=True)
    fig.savefig(OUTPUT / name, dpi=220, bbox_inches="tight", facecolor="white")
    plt.close(fig)


def figure_sensor_domain() -> None:
    fig, ax = plt.subplots(figsize=(10.4, 4.6))
    sensors = {
        "公開 BLB／Phantom 4M": [(450, 16, "B"), (560, 16, "G"), (650, 16, "R"), (730, 16, "RE"), (840, 26, "NIR")],
        "DJI Mavic 3M": [(560, 16, "G"), (650, 16, "R"), (730, 16, "RE"), (860, 26, "NIR")],
    }
    colours = {"B": "#4788c7", "G": "#4f9f63", "R": "#d45745", "RE": "#a85b79", "NIR": "#6a4f8a"}
    for row, (sensor, bands) in enumerate(sensors.items(), start=1):
        y = 3 - row
        for centre, half_width, label in bands:
            ax.add_patch(Rectangle((centre - half_width, y - 0.18), 2 * half_width, 0.36,
                                   facecolor=colours[label], edgecolor="white", linewidth=1.2, alpha=0.92))
            ax.text(centre, y, label, ha="center", va="center", color="white", fontsize=9, fontweight="bold")
        ax.text(400, y, sensor, ha="left", va="center", fontsize=10.5, fontweight="bold")
    ax.annotate("遷移共同域：G / R / RE / NIR", xy=(700, 0.43), xytext=(700, 0.05),
                ha="center", color=GREEN, fontsize=10.5, fontweight="bold",
                arrowprops=dict(arrowstyle="-[,widthB=12.5,lengthB=0.8", lw=1.5, color=GREEN))
    ax.text(450, -0.18, "藍光不得進入 Mavic 3M 遷移權重", ha="center", color=RED, fontsize=9)
    ax.text(850, -0.18, "NIR 中心波長差異須視為 sensor-domain shift", ha="center", color=BROWN, fontsize=9)
    ax.set_xlim(390, 905)
    ax.set_ylim(-0.35, 2.35)
    ax.set_yticks([])
    ax.set_xlabel("中心波長（nm）；矩形半寬依官方標示 ±nm")
    ax.set_title("感測器波段不是完全相同：跨感測器模型只保留共同波段", fontsize=14, loc="left", pad=14, fontweight="bold")
    for spine in ax.spines.values():
        spine.set_visible(False)
    ax.grid(axis="x", color="#e6ded6", linewidth=0.7)
    save(fig, "figure-1-sensor-domain.png")


def figure_dataset_audit() -> None:
    fig, (ax, note) = plt.subplots(1, 2, figsize=(10.4, 4.7), gridspec_kw={"width_ratios": [1.45, 1]})
    splits = ["Train", "Validation", "Test"]
    counts = [1299, 277, 281]
    x = np.arange(len(splits))
    width = 0.23
    for index, (dataset, colour) in enumerate(zip(["D1：5 bands", "D2：+NDVI", "D3：+NDRE"], [GREEN, ORANGE, BROWN])):
        bars = ax.bar(x + (index - 1) * width, counts, width, label=dataset, color=colour, alpha=0.9)
        if index == 1:
            ax.bar_label(bars, labels=[f"{value:,}" for value in counts], padding=3, fontsize=8.5)
    ax.set_xticks(x, splits)
    ax.set_ylabel("影像數；另各有一一對應遮罩")
    ax.set_title("公開資料結構", loc="left", fontsize=13, fontweight="bold")
    ax.legend(frameon=False, fontsize=8.5, ncols=3, loc="upper right")
    ax.spines[["top", "right"]].set_visible(False)
    ax.grid(axis="y", color="#e6ded6", linewidth=0.7)

    note.axis("off")
    note.add_patch(FancyBboxPatch((0.04, 0.56), 0.92, 0.34, boxstyle="round,pad=0.03,rounding_size=0.025",
                                 transform=note.transAxes, facecolor=PALE_GREEN, edgecolor="#b9cbb4"))
    note.text(0.09, 0.83, "本研究實際相容性查核", transform=note.transAxes, fontsize=11, fontweight="bold", color=GREEN)
    note.text(0.09, 0.75, "1 組 D1 影像—遮罩\n＋類別 CSV＋原作者 notebook", transform=note.transAxes, fontsize=10, va="top", linespacing=1.5)
    note.text(0.09, 0.61, "PASSED：256×256×5 float32\n未計算任何模型效能", transform=note.transAxes, fontsize=9, va="top", color=GREY)
    note.add_patch(FancyBboxPatch((0.04, 0.10), 0.92, 0.34, boxstyle="round,pad=0.03,rounding_size=0.025",
                                 transform=note.transAxes, facecolor=PALE, edgecolor="#d8c3ae"))
    note.text(0.09, 0.37, "發布邊界", transform=note.transAxes, fontsize=11, fontweight="bold", color=BROWN)
    note.text(0.09, 0.29, "第三方 TIFF、完整 notebook、\nGoogle Drive 鏡像不進研究發布包", transform=note.transAxes, fontsize=10, va="top", linespacing=1.5)
    note.text(0.09, 0.15, "保留 DOI、檔案雜湊與衍生查核結果", transform=note.transAxes, fontsize=9, va="top", color=GREY)
    fig.suptitle("BLB UAV Dataset：公開總體結構與有限本地查核須分開解讀", x=0.03, ha="left", fontsize=14, fontweight="bold")
    save(fig, "figure-2-dataset-audit.png")


def add_box(ax: plt.Axes, x: float, y: float, width: float, height: float, title: str, subtitle: str, colour: str) -> None:
    ax.add_patch(FancyBboxPatch((x, y), width, height, boxstyle="round,pad=0.018,rounding_size=0.025",
                                facecolor="white", edgecolor=colour, linewidth=1.6))
    ax.text(x + 0.025, y + height - 0.055, title, fontsize=10, fontweight="bold", color=colour, va="top")
    ax.text(x + 0.025, y + height - 0.12, subtitle, fontsize=8.2, color=GREY, va="top", linespacing=1.45)


def arrow(ax: plt.Axes, start: tuple[float, float], end: tuple[float, float], colour: str = GREY) -> None:
    ax.add_patch(FancyArrowPatch(start, end, arrowstyle="-|>", mutation_scale=13, linewidth=1.25, color=colour))


def figure_pipeline() -> None:
    fig, ax = plt.subplots(figsize=(11, 5.2))
    ax.set_xlim(0, 1)
    ax.set_ylim(0, 1)
    ax.axis("off")
    stages = [
        (0.02, "Capture", "RGB + G/R/RE/NIR\nCapture UUID / RTK"),
        (0.21, "Calibration", "DN→反射尺度\n暗角／畸變／日照"),
        (0.40, "QC gate", "共同網格／模糊\n陰影／飽和／配準"),
        (0.59, "Feature model", "18 項可稽核特徵\nL2 logistic baseline"),
        (0.78, "Human review map", "patch、機率、雜湊\n現勘與採樣優先序"),
    ]
    for index, (x, title, subtitle) in enumerate(stages):
        colour = GREEN if index in {0, 1, 3} else BROWN if index == 2 else ORANGE
        add_box(ax, x, 0.58, 0.16, 0.25, title, subtitle, colour)
        if index < len(stages) - 1:
            arrow(ax, (x + 0.16, 0.705), (stages[index + 1][0] - 0.01, 0.705))

    add_box(ax, 0.31, 0.15, 0.18, 0.22, "REJECTED_QC", "缺波段、未校正、\n配準或影像品質失敗", RED)
    add_box(ax, 0.55, 0.15, 0.18, 0.22, "REJECTED_OOD", "落在訓練域之外；\n不輸出偽精確機率", RED)
    add_box(ax, 0.79, 0.15, 0.18, 0.22, "REVIEW_REQUIRED", "高機率或不確定區；\n一律由人員複查", ORANGE)
    arrow(ax, (0.48, 0.58), (0.40, 0.38), RED)
    arrow(ax, (0.67, 0.58), (0.64, 0.38), RED)
    arrow(ax, (0.86, 0.58), (0.88, 0.38), ORANGE)
    ax.text(0.02, 0.94, "Fail-closed 病害疑似區篩檢管線", fontsize=15, fontweight="bold")
    ax.text(0.02, 0.89, "模型不控制飛行、不確診病原、不產生施藥處方；每個輸出都留下可查核 receipt。", fontsize=9.5, color=GREY)
    save(fig, "figure-3-model-pipeline.png")


def figure_confusion() -> None:
    if not EVALUATION.is_file():
        raise FileNotFoundError(f"Run the engineering demo before generating figures: {EVALUATION}")
    evaluation = json.loads(EVALUATION.read_text(encoding="utf-8"))
    confusion = evaluation["confusion"]
    matrix = np.asarray([
        [confusion["trueNegative"], confusion["falsePositive"]],
        [confusion["falseNegative"], confusion["truePositive"]],
    ], dtype=int)
    fig, ax = plt.subplots(figsize=(6.6, 5.5))
    im = ax.imshow(matrix, cmap="YlGn", vmin=0, vmax=max(1, int(matrix.max())))
    for row in range(2):
        for column in range(2):
            value = int(matrix[row, column])
            ax.text(column, row, str(value), ha="center", va="center", fontsize=18, fontweight="bold",
                    color="white" if value > matrix.max() * 0.55 else INK)
    ax.set_xticks([0, 1], ["預測無旗標", "預測需複查"])
    ax.set_yticks([0, 1], ["合成負例", "合成病斑"])
    ax.set_xlabel("模型輸出")
    ax.set_ylabel("固定種子 latent truth")
    metrics = evaluation["metrics"]
    ax.set_title("合成工程測試混淆矩陣", fontsize=14, fontweight="bold", pad=14)
    ax.text(0.5, -0.19,
            f"n={evaluation['sampleCount']}；F1={metrics['f1']:.3f}；Brier={metrics['brier']:.3f}\n僅證明軟體行為，不是 BLB 或 Mavic 3M 田間準確率",
            ha="center", va="top", transform=ax.transAxes, fontsize=9, color=RED, linespacing=1.45)
    fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04, label="patch 數")
    save(fig, "figure-4-synthetic-confusion.png")


def figure_promotion() -> None:
    fig, ax = plt.subplots(figsize=(10.4, 5.6))
    ax.set_xlim(0, 10)
    ax.set_ylim(0, 7)
    ax.axis("off")
    levels = [
        (0.6, 0.6, 8.8, 0.8, "L0｜可執行工程", "合成 fixture、測試、JSON 模型、QC/OOD、receipt｜本版已完成", GREEN),
        (1.3, 1.8, 7.4, 0.8, "L1｜單場域可行性", "具同意的 Mavic 3M 航拍＋現勘；只能主張場內效能", ORANGE),
        (2.0, 3.0, 6.0, 0.8, "L2｜病理真值", "A 級樣本、第二編碼者、非病害壓力對照", ORANGE),
        (2.7, 4.2, 4.6, 0.8, "L3｜外部驗證", "不同田區／日期／品種／期作，預先門檻與信賴區間", BROWN),
        (3.4, 5.4, 3.2, 0.8, "L4｜限定場域部署", "版本監測、漂移偵測、人工覆核；仍非病原確診", BROWN),
    ]
    for x, y, w, h, title, subtitle, colour in levels:
        ax.add_patch(FancyBboxPatch((x, y), w, h, boxstyle="round,pad=0.03,rounding_size=0.08",
                                    facecolor=PALE_GREEN if colour == GREEN else PALE, edgecolor=colour, linewidth=1.5))
        ax.text(x + 0.18, y + 0.54, title, fontsize=10.5, fontweight="bold", color=colour, va="center")
        ax.text(x + 0.18, y + 0.22, subtitle, fontsize=8.2, color=GREY, va="center")
    ax.text(0.6, 6.65, "模型升級不是按版本號，而是按證據層級", fontsize=15, fontweight="bold")
    ax.text(0.6, 6.25, "任一必要證據缺失，promotionStatus 強制維持 RESEARCH_ONLY。", fontsize=9.5, color=RED)
    save(fig, "figure-5-promotion-ladder.png")


def main() -> None:
    setup()
    figure_sensor_domain()
    figure_dataset_audit()
    figure_pipeline()
    figure_confusion()
    figure_promotion()
    print(json.dumps({"status": "PASSED", "figureCount": 5, "output": str(OUTPUT)}, ensure_ascii=False))


if __name__ == "__main__":
    main()
