# -*- coding: utf-8 -*-
"""
============================================================
[교육용 예제]
태풍 중심 위치와 강풍반경 표시하기 (단일 태풍 베스트트랙)

기능
1. APIHUB 태풍 베스트트랙 자료 호출
2. 한 개 태풍의 베스트트랙 자료 파싱
3. 태풍 중심 위치(경로) 표시
4. 15m/s / 25m/s 풍속반경 표시
5. 생성-이동-소멸 과정을 한 장의 지도에 표현
6. PNG / CSV 저장

필요 라이브러리
    pip install requests pandas matplotlib cartopy numpy
============================================================
"""

import os
from datetime import datetime

import numpy as np
import requests
import pandas as pd

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib import font_manager
from matplotlib.lines import Line2D
from matplotlib.patches import Patch

import cartopy.crs as ccrs
import cartopy.feature as cfeature
from cartopy.geodesic import Geodesic


# ============================================================
# 1. 사용자 설정
# ============================================================

AUTH_KEY = "SEQk-aWQSbKEJPmlkPmy0w"

# 지금 로그에서 성공한 URL 기준으로 유지
BASE_URL = "https://apihub.kma.go.kr/api/typ01/url/typ_besttrack.php"

YEAR = 2023
TCID = "2311"
TY_NAME = f"Typhoon {TCID}"

HELP = 1

RAW_TEXT_FILE = f"typhoon_besttrack_{YEAR}_{TCID}_raw.txt"
OUTPUT_CSV = f"typhoon_besttrack_{YEAR}_{TCID}.csv"
OUTPUT_PNG = f"typhoon_besttrack_map_{YEAR}_{TCID}.png"

# 반경 원을 너무 많이 그리면 복잡하므로 간격 조절
DRAW_RADIUS_EVERY = 2

# 반경 단위: km -> meter
RADIUS_TO_METER = 1000.0

# 지도 여백
MARGIN_LON = 5.0
MARGIN_LAT = 5.0


# ============================================================
# 2. 실제 응답 순서 기준 컬럼 정의
#    로그를 보면 다음 순서가 맞음:
#    GRADE TYNO YY MM DD HH LON LAT MAX_WS PRES R15 R25
# ============================================================

COLS = [
    "GRADE",
    "TYNO",
    "YY",
    "MM",
    "DD",
    "HH",
    "LON",
    "LAT",
    "MAX_WS",
    "PRES",
    "R15",
    "R25",
]


# ============================================================
# 3. 한글 폰트 설정
# ============================================================

def set_korean_font():
    candidate_paths = [
        "/usr/share/fonts/truetype/nanum/NanumGothic.ttf",
        "/usr/share/fonts/truetype/nanum/NanumBarunGothic.ttf",
        "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
        "/usr/share/fonts/opentype/noto/NotoSansCJKkr-Regular.otf",
        "C:/Windows/Fonts/malgun.ttf",
    ]

    font_set = False

    for font_path in candidate_paths:
        if os.path.exists(font_path):
            font_prop = font_manager.FontProperties(fname=font_path)
            plt.rcParams["font.family"] = font_prop.get_name()
            print(f"[폰트 설정] {font_prop.get_name()} / {font_path}")
            font_set = True
            break

    if not font_set:
        font_names = [f.name for f in font_manager.fontManager.ttflist]
        for name in ["NanumGothic", "NanumBarunGothic", "Noto Sans CJK KR", "Malgun Gothic"]:
            if name in font_names:
                plt.rcParams["font.family"] = name
                print(f"[폰트 설정] {name}")
                font_set = True
                break

    if not font_set:
        print("[경고] 한글 폰트를 찾지 못했습니다. 한글이 깨질 수 있습니다.")

    plt.rcParams["axes.unicode_minus"] = False


# ============================================================
# 4. API 호출
# ============================================================

def fetch_besttrack_text(year, tcid):
    auth_key = AUTH_KEY.strip()
    if not auth_key:
        raise ValueError("AUTH_KEY가 비어 있습니다.")

    params = {
        "year": year,
        "tcid": tcid,
        "help": HELP,
        "authKey": auth_key,
    }

    response = requests.get(BASE_URL, params=params, timeout=60)
    response.raise_for_status()

    text = response.text

    with open(RAW_TEXT_FILE, "w", encoding="utf-8") as f:
        f.write(text)

    print("[저장 완료] 원문 응답:", RAW_TEXT_FILE)
    print("[호출 URL]", response.url)

    return text


# ============================================================
# 5. 데이터 줄 추출
# ============================================================

def extract_data_lines(text):
    lines = []

    for raw_line in text.splitlines():
        line = raw_line.strip()

        if not line:
            continue

        if line.startswith("#"):
            continue

        lines.append(line)

    return lines


# ============================================================
# 6. 베스트트랙 파싱
# ============================================================

def parse_besttrack_text(text):
    data_lines = extract_data_lines(text)

    rows = []

    for line in data_lines:
        clean = line.replace(",", " ")
        parts = [p for p in clean.split() if p]

        if len(parts) < 12:
            continue

        rows.append(parts[:12])

    if len(rows) == 0:
        return pd.DataFrame(columns=COLS)

    df = pd.DataFrame(rows, columns=COLS)
    return df


# ============================================================
# 7. 자료형 정리
# ============================================================

def clean_besttrack_df(df):
    if df.empty:
        return df

    df = df.copy()

    numeric_cols = ["TYNO", "YY", "MM", "DD", "HH", "LON", "LAT", "MAX_WS", "PRES", "R15", "R25"]
    for col in numeric_cols:
        df[col] = pd.to_numeric(df[col], errors="coerce")

    # 결측값 처리
    for col in ["MAX_WS", "PRES", "R15", "R25"]:
        df.loc[df[col] <= -9, col] = pd.NA

    # YY가 2자리면 2000년대 보정
    if df["YY"].dropna().max() < 100:
        df["YYYY"] = df["YY"].apply(lambda x: int(2000 + x) if pd.notna(x) else pd.NA)
    else:
        df["YYYY"] = df["YY"]

    def make_dt(row):
        try:
            return datetime(
                int(row["YYYY"]),
                int(row["MM"]),
                int(row["DD"]),
                int(row["HH"])
            )
        except Exception:
            return pd.NaT

    df["datetime"] = df.apply(make_dt, axis=1)

    # 유효한 범위만 남김
    df = df.dropna(subset=["LAT", "LON"]).copy()
    df = df[(df["LAT"] >= -90) & (df["LAT"] <= 90)]
    df = df[(df["LON"] >= 0) & (df["LON"] <= 360)]

    # 0~360 경도를 -180~180으로 바꾸고 싶으면 아래 사용
    # df["LON"] = df["LON"].apply(lambda x: x - 360 if x > 180 else x)

    df = df.sort_values("datetime").reset_index(drop=True)

    return df


# ============================================================
# 8. 요약 출력
# ============================================================

def print_summary(df):
    if df.empty:
        print("[안내] 수집된 태풍 데이터가 없습니다.")
        return

    print("\n================ 태풍 자료 요약 ================")
    print("자료 개수:", len(df))

    if df["datetime"].notna().any():
        print("시작 시각:", df["datetime"].min())
        print("종료 시각:", df["datetime"].max())

    if df["PRES"].notna().any():
        print("최저 중심기압(hPa):", df["PRES"].min())

    if df["MAX_WS"].notna().any():
        print("최대풍속(m/s):", df["MAX_WS"].max())

    print("\n상위 5개 행:")
    print(df.head())


# ============================================================
# 9. CSV 저장
# ============================================================

def save_csv(df):
    if df.empty:
        print("[안내] 저장할 데이터가 없습니다.")
        return

    df.to_csv(OUTPUT_CSV, index=False, encoding="utf-8-sig")
    print("[저장 완료] CSV:", OUTPUT_CSV)


# ============================================================
# 10. 지도 범위 계산
# ============================================================

def compute_map_extent(df):
    lon_min = df["LON"].min() - MARGIN_LON
    lon_max = df["LON"].max() + MARGIN_LON
    lat_min = df["LAT"].min() - MARGIN_LAT
    lat_max = df["LAT"].max() + MARGIN_LAT

    lon_min = max(100.0, lon_min)
    lon_max = min(180.0, lon_max)
    lat_min = max(0.0, lat_min)
    lat_max = min(60.0, lat_max)

    return [lon_min, lon_max, lat_min, lat_max]


# ============================================================
# 11. 반경 원 그리기
# ============================================================

def draw_wind_radius(ax, lon, lat, radius_value, edgecolor, facecolor, alpha=0.12, linewidth=1.0):
    """
    중심(lon, lat), 반경(km)을 이용해 geodesic circle을 그림
    """
    if pd.isna(radius_value):
        return

    lon = float(lon)
    lat = float(lat)
    radius_km = float(radius_value)

    if not (-180 <= lon <= 360):
        return
    if not (-90 <= lat <= 90):
        return
    if radius_km <= 0:
        return

    try:
        geod = Geodesic()
        circle = geod.circle(
            lon=lon,
            lat=lat,
            radius=radius_km * RADIUS_TO_METER,
            n_samples=180
        )

        circle = np.asarray(circle, dtype=float)

        # NaN 제거
        circle = circle[np.all(np.isfinite(circle), axis=1)]
        if len(circle) < 4:
            return

        # polygon 닫기
        if not np.allclose(circle[0], circle[-1]):
            circle = np.vstack([circle, circle[0]])

        # outline
        ax.plot(
            circle[:, 0],
            circle[:, 1],
            color=edgecolor,
            linewidth=linewidth,
            alpha=0.9,
            transform=ccrs.PlateCarree(),
            zorder=2
        )

        # fill
        ax.fill(
            circle[:, 0],
            circle[:, 1],
            facecolor=facecolor,
            edgecolor="none",
            alpha=alpha,
            transform=ccrs.PlateCarree(),
            zorder=1
        )

    except Exception as e:
        print(f"[경고] 반경 원 그리기 실패 (lon={lon}, lat={lat}, r={radius_km}): {e}")


# ============================================================
# 12. 태풍 경로 지도 그리기
# ============================================================

def plot_typhoon_track(df):
    if df.empty:
        print("[안내] 그래프로 그릴 태풍 데이터가 없습니다.")
        return

    idx_max_ws = None
    if df["MAX_WS"].notna().any():
        idx_max_ws = df["MAX_WS"].idxmax()

    fig = plt.figure(figsize=(12, 10))
    ax = plt.axes(projection=ccrs.PlateCarree())

    extent = compute_map_extent(df)
    ax.set_extent(extent, crs=ccrs.PlateCarree())

    ax.add_feature(cfeature.LAND, facecolor="#f2f2f2")
    ax.add_feature(cfeature.OCEAN, facecolor="#e6f2ff")
    ax.add_feature(cfeature.COASTLINE, linewidth=0.8)
    ax.add_feature(cfeature.BORDERS, linewidth=0.5)
    ax.gridlines(draw_labels=True, linewidth=0.4, linestyle="--", alpha=0.5)

    # 경로
    ax.plot(
        df["LON"], df["LAT"],
        color="black",
        linewidth=1.5,
        linestyle="-",
        transform=ccrs.PlateCarree(),
        zorder=3
    )

    # 중심점
    ax.scatter(
        df["LON"], df["LAT"],
        s=30,
        color="royalblue",
        edgecolors="black",
        linewidths=0.3,
        transform=ccrs.PlateCarree(),
        zorder=4
    )

    # 반경 원
    for i, row in df.iterrows():
        if i % DRAW_RADIUS_EVERY != 0 and i != len(df) - 1:
            continue

        draw_wind_radius(
            ax=ax,
            lon=row["LON"],
            lat=row["LAT"],
            radius_value=row["R15"],
            edgecolor="orange",
            facecolor="gold",
            alpha=0.10,
            linewidth=0.8
        )

        draw_wind_radius(
            ax=ax,
            lon=row["LON"],
            lat=row["LAT"],
            radius_value=row["R25"],
            edgecolor="red",
            facecolor="red",
            alpha=0.06,
            linewidth=1.0
        )

    # 생성점
    first_row = df.iloc[0]
    ax.scatter(
        first_row["LON"], first_row["LAT"],
        s=120,
        color="limegreen",
        edgecolors="black",
        linewidths=0.8,
        marker="o",
        transform=ccrs.PlateCarree(),
        zorder=6
    )
    ax.text(
        first_row["LON"] + 0.3, first_row["LAT"] + 0.3,
        "생성",
        fontsize=10,
        fontweight="bold",
        color="green",
        transform=ccrs.PlateCarree(),
        zorder=7
    )

    # 소멸점
    last_row = df.iloc[-1]
    ax.scatter(
        last_row["LON"], last_row["LAT"],
        s=120,
        color="black",
        edgecolors="white",
        linewidths=0.8,
        marker="X",
        transform=ccrs.PlateCarree(),
        zorder=6
    )
    ax.text(
        last_row["LON"] + 0.3, last_row["LAT"] + 0.3,
        "소멸",
        fontsize=10,
        fontweight="bold",
        color="black",
        transform=ccrs.PlateCarree(),
        zorder=7
    )

    # 최대세력
    if idx_max_ws is not None:
        peak_row = df.loc[idx_max_ws]
        ax.scatter(
            peak_row["LON"], peak_row["LAT"],
            s=180,
            color="magenta",
            edgecolors="black",
            linewidths=0.8,
            marker="*",
            transform=ccrs.PlateCarree(),
            zorder=7
        )
        ax.text(
            peak_row["LON"] + 0.3, peak_row["LAT"] - 0.5,
            "최대세력",
            fontsize=10,
            fontweight="bold",
            color="purple",
            transform=ccrs.PlateCarree(),
            zorder=8
        )

    # 일부 시각 라벨
    step = max(1, len(df) // 8)
    for i, row in df.iterrows():
        if i % step != 0 and i not in [0, len(df) - 1]:
            continue

        if pd.notna(row["datetime"]):
            label = row["datetime"].strftime("%m-%d %HZ")
            ax.text(
                row["LON"] + 0.2,
                row["LAT"] - 0.6,
                label,
                fontsize=8,
                transform=ccrs.PlateCarree(),
                zorder=7
            )

    # 요약 박스
    start_time = df["datetime"].min().strftime("%Y-%m-%d %HZ") if df["datetime"].notna().any() else "-"
    end_time = df["datetime"].max().strftime("%Y-%m-%d %HZ") if df["datetime"].notna().any() else "-"
    min_pres = f"{df['PRES'].min():.0f} hPa" if df["PRES"].notna().any() else "-"
    max_ws = f"{df['MAX_WS'].max():.1f} m/s" if df["MAX_WS"].notna().any() else "-"

    summary_text = (
        f"태풍: {TY_NAME}\n"
        f"기간: {start_time} ~ {end_time}\n"
        f"최저 중심기압: {min_pres}\n"
        f"최대풍속: {max_ws}\n"
        f"자료 개수: {len(df)}"
    )

    ax.text(
        0.02, 0.02,
        summary_text,
        transform=ax.transAxes,
        fontsize=10,
        va="bottom",
        ha="left",
        bbox=dict(facecolor="white", alpha=0.85, edgecolor="gray")
    )

    # 범례
    legend_elements = [
        Line2D([0], [0], color='black', lw=1.5, label='태풍 중심 경로'),
        Line2D([0], [0], marker='o', color='w', label='태풍 중심',
               markerfacecolor='royalblue', markeredgecolor='black', markersize=7),
        Line2D([0], [0], marker='o', color='w', label='생성',
               markerfacecolor='limegreen', markeredgecolor='black', markersize=10),
        Line2D([0], [0], marker='X', color='w', label='소멸',
               markerfacecolor='black', markeredgecolor='white', markersize=10),
        Line2D([0], [0], marker='*', color='w', label='최대세력',
               markerfacecolor='magenta', markeredgecolor='black', markersize=14),
        Patch(facecolor='gold', edgecolor='orange', alpha=0.25, label='15 m/s 풍속반경'),
        Patch(facecolor='red', edgecolor='red', alpha=0.18, label='25 m/s 풍속반경'),
    ]

    ax.legend(handles=legend_elements, loc="upper right", frameon=True)

    ax.set_title(f"{TY_NAME} Best Track and Wind Radius", fontsize=15)

    plt.savefig(OUTPUT_PNG, dpi=150, bbox_inches="tight")
    plt.close()

    print("[저장 완료] PNG:", OUTPUT_PNG)


# ============================================================
# 13. 메인 함수
# ============================================================

def main():
    set_korean_font()

    print("=" * 60)
    print("태풍 중심 위치와 강풍반경 표시하기")
    print("YEAR =", YEAR)
    print("TCID =", TCID)
    print("TY_NAME =", TY_NAME)
    print("=" * 60)

    text = fetch_besttrack_text(YEAR, TCID)
    df = parse_besttrack_text(text)
    df = clean_besttrack_df(df)

    print_summary(df)
    save_csv(df)
    plot_typhoon_track(df)


# ============================================================
# 14. 실행
# ============================================================

if __name__ == "__main__":
    main()
