"""天文台天氣報告（M13 工程線）：把 notebook 的流程變成一個可以在終端機執行的腳本。

用法（在 python-course 資料夾裏）：
    uv run python report.py

讀同一資料夾的 config.yaml，結果寫到設定裏的 output_dir。
資料來源：香港天文台（經 DATA.GOV.HK）。
"""
import json
import urllib.error
import urllib.request
import zipfile
from pathlib import Path

import jsonschema
import matplotlib
import pandas as pd
import yaml

matplotlib.use("Agg")                       # 腳本不需要視窗，直接存成圖片
import matplotlib.pyplot as plt  # noqa: E402

HERE = Path(__file__).parent                 # 這個 .py 檔所在的資料夾（M7）
SNAPSHOT = HERE / "m13-hko-snapshot.zip"
API = "https://data.weather.gov.hk/weatherAPI/opendata"
SOURCE_NOTE = "資料來源：香港天文台（經 DATA.GOV.HK）"

CONFIG_SCHEMA = {
    "type": "object",
    "properties": {
        "station": {"type": "string"},
        "year": {"type": "integer", "minimum": 1884},
        "very_hot_day": {"type": "number"},
        "hot_night": {"type": "number"},
        "output_dir": {"type": "string"},
        "offline": {"type": "boolean"},
    },
    "required": ["station", "year", "very_hot_day", "hot_night", "output_dir"],
    "additionalProperties": False,
}

SUMMARY_SCHEMA = {
    "type": "object",
    "properties": {
        "station": {"type": "string"},
        "year": {"type": "integer"},
        "days": {"type": "integer", "minimum": 1},
        "very_hot_days": {"type": "integer", "minimum": 0},
        "hot_nights": {"type": "integer", "minimum": 0},
        "hottest": {"type": "object", "required": ["date", "max_temp"]},
        "coldest": {"type": "object", "required": ["date", "min_temp"]},
        "source": {"type": "string"},
    },
    "required": ["station", "year", "days", "very_hot_days", "hot_nights", "hottest", "coldest", "source"],
}


def load_config(path):
    """讀設定檔，先用 schema 檢查再交出去（M10、M12）。"""
    cfg = yaml.safe_load(Path(path).read_text(encoding="utf-8"))
    jsonschema.validate(cfg, CONFIG_SCHEMA)
    return cfg


def fetch_json(url, snapshot_name, offline=False):
    """先試 API；連不上、逾時或回來的不是 JSON，就改讀離線副本。回傳 (資料, 來源)。"""
    if not offline:
        try:
            with urllib.request.urlopen(url, timeout=10) as resp:
                return json.loads(resp.read().decode("utf-8")), "API"
        except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as err:
            print(f"⚠ API 失敗（{type(err).__name__}），改用離線副本：{snapshot_name}")
    with zipfile.ZipFile(SNAPSHOT) as z:
        name = f"hko-snapshot/{snapshot_name}"
        if name not in z.namelist():         # 副本裏沒有這個檔：與其報看不懂的 KeyError，不如說清楚
            # raise：自己報一個錯誤，並附上清楚的訊息
            raise FileNotFoundError(f"離線副本只有 2025 年的資料（找不到 {snapshot_name}），請連網或把 year 改回 2025")
        return json.loads(z.read(name).decode("utf-8")), "離線副本"


def load_daily(raw, column):
    """天文台的每日資料（全部是字串）→ 有日期和數字的 DataFrame，只保留完整的資料。"""
    df = pd.DataFrame(raw["data"], columns=["year", "month", "day", column, "complete"])
    df = df[df["complete"] == "C"].copy()     # .copy()：明確要一張獨立的新表（M4 的 copy），之後改它不影響原表
    df[column] = df[column].astype(float)
    df["date"] = pd.to_datetime(df[["year", "month", "day"]].astype(int))
    return df[["date", column]]


def build_report(cfg):
    out = HERE / cfg["output_dir"]
    out.mkdir(exist_ok=True)
    station, year, offline = cfg["station"], cfg["year"], cfg.get("offline", False)

    daily = {}
    for kind, col in [("CLMMAXT", "max_temp"), ("CLMMINT", "min_temp")]:
        url = f"{API}/opendata.php?dataType={kind}&station={station}&year={year}&rformat=json"
        raw, source = fetch_json(url, f"{kind.lower()}-{station}-{year}.json", offline)
        daily[col] = load_daily(raw, col)
        print(f"{kind}：{len(daily[col])} 天（來源：{source}）")
    df = daily["max_temp"].merge(daily["min_temp"], on="date")

    hottest = df.loc[df["max_temp"].idxmax()]
    coldest = df.loc[df["min_temp"].idxmin()]
    # int()、float()：pandas 算出來的是 numpy 的數字（例如 int64），json.dumps 不接受，要先轉成 Python 的數字
    # .date().isoformat()：pandas 的日期時間 → 只取日期 → "2025-01-27" 這樣的字串
    summary = {
        "station": station,
        "year": year,
        "days": int(len(df)),
        "very_hot_days": int((df["max_temp"] >= cfg["very_hot_day"]).sum()),
        "hot_nights": int((df["min_temp"] >= cfg["hot_night"]).sum()),
        "hottest": {"date": hottest["date"].date().isoformat(), "max_temp": float(hottest["max_temp"])},
        "coldest": {"date": coldest["date"].date().isoformat(), "min_temp": float(coldest["min_temp"])},
        "source": SOURCE_NOTE,
    }
    jsonschema.validate(summary, SUMMARY_SCHEMA)
    (out / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")

    monthly = df.groupby(df["date"].dt.month).agg(
        平均最高=("max_temp", "mean"),
        平均最低=("min_temp", "mean"),
        # lambda s: …：一個沒有名字的小函式，s 是每個月那一組的 max_temp
        酷熱日數=("max_temp", lambda s: int((s >= cfg["very_hot_day"]).sum())),
    ).round(1)
    monthly.index.name = "月"
    monthly.to_csv(out / "monthly.csv", encoding="utf-8-sig")

    plt.rcParams["font.sans-serif"] = ["Heiti TC", "Microsoft JhengHei", "Noto Sans CJK TC", "Arial Unicode MS", "DejaVu Sans"]
    ax = monthly[["平均最高", "平均最低"]].plot(marker="o", title=f"{station} {year} 年每月平均氣溫")
    ax.set_ylabel("℃")
    ax.figure.text(0.99, 0.01, SOURCE_NOTE, ha="right", fontsize=8, color="gray")
    ax.figure.tight_layout()
    ax.figure.savefig(out / "monthly-script.png", dpi=120)   # 另取名字，不蓋掉 notebook 畫的 monthly.png
    plt.close("all")

    (out / "config-used.yaml").write_text(yaml.safe_dump(cfg, allow_unicode=True, sort_keys=False), encoding="utf-8")
    return summary


if __name__ == "__main__":
    summary = build_report(load_config(HERE / "config.yaml"))
    print(json.dumps(summary, ensure_ascii=False, indent=2))
