import os
import math
import datetime
import threading
import urllib.request
import ssl
import numpy as np
import pytz

import customtkinter as ctk
from tkinter import messagebox, filedialog

import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.colors import ListedColormap, BoundaryNorm

from PIL import Image
import ephem
from skyfield.api import Loader, wgs84
from skyfield import almanac

# Pastikan SciPy terinstall untuk fitur HD Map (Interpolasi)
try:
    import scipy.interpolate as interp
    HAS_MAP_TOOLS = True
except ImportError:
    HAS_MAP_TOOLS = False

BASE_DIR = os.path.dirname(os.path.abspath(__file__))

# ==========================================
# FUNGSI UTILITAS & UNDUHAN
# ==========================================
def download_custom_file(filename, url):
    filepath = os.path.join(BASE_DIR, filename)
    if not os.path.exists(filepath):
        try:
            req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
            with urllib.request.urlopen(req) as response, open(filepath, 'wb') as out_file:
                out_file.write(response.read())
        except Exception as e:
            print(f"Gagal mengunduh {filename}: {e}")

# ==========================================
# KELAS UTAMA APLIKASI (MODUL 2)
# ==========================================
class Menu2App(ctk.CTk):
    def __init__(self):
        super().__init__()

        self.title("Crescent Visibility HD Map Scanner (Modul 2)")
        self.geometry("1150x700")
        self.minsize(900, 600)

        # Setup SSL bypass untuk unduhan file
        try: _create_unverified_https_context = ssl._create_unverified_context
        except AttributeError: pass
        else: ssl._create_default_https_context = _create_unverified_https_context

        ctk.set_appearance_mode("Dark")
        ctk.set_default_color_theme("blue")

        # Inisialisasi Skyfield
        self.load_obj = Loader(BASE_DIR, verbose=False)
        self.ts = self.load_obj.timescale()
        self.eph = None
        self.ephemeris_name = "de421.bsp"

        self.setup_ui()
        
        # Mulai unduh & muat ephemeris di background
        threading.Thread(target=self.load_ephemeris, daemon=True).start()

    def load_ephemeris(self):
        try:
            self.lbl_status.configure(text=f"Mengunduh/Memuat ephemeris ({self.ephemeris_name})...", text_color="#00E5FF")
            download_custom_file(self.ephemeris_name, f"https://hisabmu.com/aifikih/{self.ephemeris_name}")
            self.eph = self.load_obj(self.ephemeris_name)
            self.lbl_status.configure(text=f"Sistem Siap ({self.ephemeris_name} Dimuat)", text_color="#00E676")
            self.btn_hitung.configure(state="normal")
        except Exception as e:
            self.lbl_status.configure(text="Gagal memuat ephemeris!", text_color="#FF1744")
            messagebox.showerror("Error", f"Gagal memuat file ephemeris.\nDetail: {str(e)}")

    def create_ymd_row(self, parent, dy, dm, dd):
        grid_frame = ctk.CTkFrame(parent, fg_color="transparent")
        grid_frame.pack(fill="x", padx=10, pady=(0, 10))

        ctk.CTkLabel(grid_frame, text="dd", font=("Segoe UI", 10), text_color="#9E9E9E").grid(row=0, column=0, pady=(0, 2))
        ctk.CTkLabel(grid_frame, text="mm", font=("Segoe UI", 10), text_color="#9E9E9E").grid(row=0, column=1, pady=(0, 2))
        ctk.CTkLabel(grid_frame, text="yyyy", font=("Segoe UI", 10), text_color="#9E9E9E").grid(row=0, column=2, pady=(0, 2))

        d = ctk.CTkEntry(grid_frame, width=45, justify="center")
        d.insert(0, dd)
        d.grid(row=1, column=0, padx=(0, 5))
        
        m = ctk.CTkEntry(grid_frame, width=45, justify="center")
        m.insert(0, dm)
        m.grid(row=1, column=1, padx=5)

        y = ctk.CTkEntry(grid_frame, width=70, justify="center")
        y.insert(0, dy)
        y.grid(row=1, column=2, padx=(5, 0))
        
        return y, m, d

    def setup_ui(self):
        self.grid_columnconfigure(0, weight=0) 
        self.grid_columnconfigure(1, weight=1) 
        self.grid_rowconfigure(0, weight=1)

        now = datetime.datetime.now()
        curr_y, curr_m, curr_d = str(now.year), f"{now.month:02d}", f"{now.day:02d}"

        # ================= SIDEBAR KIRI =================
        self.sidebar = ctk.CTkScrollableFrame(self, width=320, corner_radius=0, fg_color="#181818")
        self.sidebar.grid(row=0, column=0, sticky="nsew")

        ctk.CTkLabel(self.sidebar, text="VISIBILITY MAP", font=("Segoe UI", 24, "bold"), text_color="#00E5FF").pack(pady=(20, 15))

        # 1. Tanggal Observasi
        frame_vmdate = ctk.CTkFrame(self.sidebar, fg_color="#212121")
        frame_vmdate.pack(fill="x", padx=15, pady=5)
        ctk.CTkLabel(frame_vmdate, text="TANGGAL OBSERVASI", font=("Segoe UI", 12, "bold")).pack(anchor="w", padx=10, pady=(8, 2))
        self.entry_vmyear, self.entry_vmmonth, self.entry_vmday = self.create_ymd_row(frame_vmdate, curr_y, curr_m, curr_d)

        # 2. Toggle Fase
        frame_vmtoggle = ctk.CTkFrame(self.sidebar, fg_color="#212121")
        frame_vmtoggle.pack(fill="x", padx=15, pady=5)
        self.radio_vmphase = ctk.StringVar(value="new")
        ctk.CTkRadioButton(frame_vmtoggle, text="New Crescent (Evening)", variable=self.radio_vmphase, value="new").pack(anchor="w", padx=15, pady=5)
        ctk.CTkRadioButton(frame_vmtoggle, text="Old Crescent (Morning)", variable=self.radio_vmphase, value="old").pack(anchor="w", padx=15, pady=(0, 8))

        # 3. Kriteria Evaluasi
        frame_vmcrit = ctk.CTkFrame(self.sidebar, fg_color="#212121")
        frame_vmcrit.pack(fill="x", padx=15, pady=5)
        ctk.CTkLabel(frame_vmcrit, text="KRITERIA VISIBILITAS", font=("Segoe UI", 11, "bold")).pack(anchor="w", padx=10, pady=(5, 0))
        
        daftar_kriteria = [
            "KHGT / Diyanet Turki (Alt>=5, Eln>=8)", 
            "Neo MABIMS (Alt>=3, Eln>=6.4)", 
            "Yallop Criterion (q-Parameter)", 
            "Ilyas (Alt>=4, Eln>=10.5)", 
            "Danjon Limit (Eln>=7)", 
            "Odeh Criterion"
        ]
        self.combo_vmcrit = ctk.CTkOptionMenu(frame_vmcrit, values=daftar_kriteria)
        self.combo_vmcrit.pack(fill="x", padx=10, pady=10)

        # 4. Mode Peta (Layer)
        frame_vmparam = ctk.CTkFrame(self.sidebar, fg_color="#212121")
        frame_vmparam.pack(fill="x", padx=15, pady=5)
        ctk.CTkLabel(frame_vmparam, text="MODE PETA (LAYER)", font=("Segoe UI", 11, "bold")).pack(anchor="w", padx=10, pady=(5, 0))
        
        self.combo_vmparam = ctk.CTkOptionMenu(frame_vmparam, values=[
            "Kriteria Visibilitas", "Altitude Bulan", "Elongasi", "Iluminasi", 
            "Interseksi", "Interseksi (Peta KHGT 2D)"
        ])
        self.combo_vmparam.pack(fill="x", padx=10, pady=10)

        # 5. Tombol Proses
        self.btn_hitung = ctk.CTkButton(self.sidebar, text="▶ PROSES PETA GLOBAL", font=("Segoe UI", 14, "bold"), height=45, fg_color="#1565C0", hover_color="#0D47A1", command=self.run_calculation, state="disabled")
        self.btn_hitung.pack(fill="x", padx=15, pady=(20, 10))

        self.lbl_status = ctk.CTkLabel(self.sidebar, text="Menyiapkan Sistem...", font=("Consolas", 11), text_color="#FFAB40")
        self.lbl_status.pack(pady=5)

        # ================= MAIN AREA KANAN =================
        self.main_frame = ctk.CTkFrame(self, fg_color="transparent")
        self.main_frame.grid(row=0, column=1, sticky="nsew", padx=20, pady=20)
        self.main_frame.grid_rowconfigure(1, weight=1)
        self.main_frame.grid_columnconfigure(0, weight=1)

        self.lbl_main_title = ctk.CTkLabel(self.main_frame, text="Crescent Visibility HD Map Scanner", font=("Segoe UI", 20, "bold"))
        self.lbl_main_title.grid(row=0, column=0, sticky="w", pady=(0, 10))

        self.textbox = ctk.CTkTextbox(self.main_frame, font=("Consolas", 13), fg_color="#101010", text_color="#E0E0E0", wrap="word")
        self.textbox.grid(row=1, column=0, sticky="nsew")
        self.textbox.insert("1.0", "Silakan klik 'PROSES PETA GLOBAL' untuk melakukan pemindaian.\n\nProses ini akan menghitung data spasial di seluruh titik koordinat bumi dan melakukan interpolasi bicubic beresolusi tinggi. Harap tunggu beberapa saat setelah tombol ditekan...")
        self.textbox.configure(state="disabled")

    def display_message(self, msg, is_error=False):
        self.textbox.configure(state="normal")
        self.textbox.delete("1.0", "end")
        self.textbox.insert("1.0", msg)
        self.textbox.configure(state="disabled")
        if is_error:
            self.lbl_status.configure(text="Error Kalkulasi", text_color="#FF1744")
        self.btn_hitung.configure(state="normal")

    def run_calculation(self):
        if not HAS_MAP_TOOLS:
            self.display_message("Error: Fitur Peta HD dinonaktifkan.\nLibrary 'scipy' tidak ditemukan di sistem Anda. Silakan jalankan: pip install scipy", is_error=True)
            return

        self.lbl_status.configure(text="Memindai Koordinat Global...", text_color="#FFAB40")
        self.btn_hitung.configure(state="disabled")
        self.display_message("Sedang memproses pemindaian seluruh koordinat benua...\nOperasi ini membutuhkan tenaga CPU yang tinggi, mohon tunggu...")
        
        threading.Thread(target=self.calculate_visibility_map, daemon=True).start()

    def calculate_visibility_map(self):
        try:
            year = int(self.entry_vmyear.get())
            month = int(self.entry_vmmonth.get())
            day = int(self.entry_vmday.get())
            
            crit_name = self.combo_vmcrit.get()
            param_target = self.combo_vmparam.get()

            earth, sun, moon = self.eph['earth'], self.eph['sun'], self.eph['moon']

            lons_coarse, lats_coarse = [], []
            alt_data, elong_data, arcv_data, illum_data = [], [], [], []
            
            grid_step = 5
            is_khgt_map = param_target == "Interseksi (Peta KHGT 2D)"
            
            # Rentang Longitude: 0-360 untuk Peta KHGT Seamless, -180 to 180 untuk standar
            lon_range = range(0, 361, grid_step) if is_khgt_map else range(-180, 181, grid_step)
            
            for lat in range(-90, 91, grid_step):
                lat_rad = math.radians(lat)
                
                for plot_lon in lon_range:
                    if is_khgt_map:
                        if plot_lon < 180:
                            real_lon = plot_lon
                            # Belahan Timur (Kiri Peta) dihitung memakai Hari Besoknya (D+1)
                            dt_calc = datetime.date(year, month, day) + datetime.timedelta(days=1)
                        else:
                            real_lon = plot_lon - 360
                            # Belahan Barat (Kanan Peta) dihitung memakai Hari Input (D)
                            dt_calc = datetime.date(year, month, day)
                        calc_y, calc_m, calc_d = dt_calc.year, dt_calc.month, dt_calc.day
                    else:
                        real_lon = plot_lon
                        calc_y, calc_m, calc_d = year, month, day
                        
                    # Perhitungan Waktu Sunset Berdasarkan Bujur
                    t_noon_utc = self.ts.utc(calc_y, calc_m, calc_d, 12)
                    sun_geo = earth.at(t_noon_utc).observe(sun).apparent()
                    _, dec_sun, _ = sun_geo.radec()
                    dec_rad = dec_sun.radians

                    noon_utc_hour = 12.0 - (real_lon / 15.0)
                    cos_h = -math.tan(lat_rad) * math.tan(dec_rad)
                    
                    if cos_h < -1 or cos_h > 1:
                        sunset_utc_hour = noon_utc_hour + 6.0 # Wilayah Kutub
                    else:
                        h_rad = math.acos(cos_h)
                        h_hours = math.degrees(h_rad) / 15.0
                        sunset_utc_hour = noon_utc_hour + h_hours
                    
                    t_sunset = self.ts.utc(calc_y, calc_m, calc_d, sunset_utc_hour)
                    
                    obs = earth.at(t_sunset)
                    s_app = obs.observe(sun).apparent()
                    m_app = obs.observe(moon).apparent()
                    
                    # Apparent GAST (Greenwich Apparent Sidereal Time)
                    gmst = t_sunset.gast 
                    lst_deg = (gmst * 15.0) + real_lon
                    
                    def quick_alt_geo(app_obj, l_rad):
                        ra, dec_o, _ = app_obj.radec(epoch=t_sunset) 
                        ha_deg = lst_deg - (ra.hours * 15.0)
                        d_rad = dec_o.radians
                        ha_rad = math.radians(ha_deg)
                        sin_alt = math.sin(d_rad) * math.sin(l_rad) + math.cos(d_rad) * math.cos(l_rad) * math.cos(ha_rad)
                        return math.degrees(math.asin(max(-1.0, min(1.0, sin_alt))))

                    # 1. Kalkulasi GEOSENTRIK (Khusus KHGT)
                    m_alt_geo = quick_alt_geo(m_app, lat_rad)
                    s_alt_geo = quick_alt_geo(s_app, lat_rad)
                    elong_geo = s_app.separation_from(m_app).degrees
                    
                    # 2. Kalkulasi TOPOSENTRIK (Khusus MABIMS/Yallop)
                    loc_topo = wgs84.latlon(lat, real_lon, elevation_m=0)
                    topo_obs = (earth + loc_topo).at(t_sunset)
                    m_alt_topo = topo_obs.observe(moon).apparent().altaz(temperature_C=25, pressure_mbar=1010)[0].degrees
                    s_alt_topo = topo_obs.observe(sun).apparent().altaz(temperature_C=25, pressure_mbar=1010)[0].degrees

                    arcv_topo = m_alt_topo - s_alt_topo
                    
                    illum = almanac.fraction_illuminated(self.eph, 'moon', t_sunset)
                    illum_val = illum.item() if hasattr(illum, 'item') else illum

                    lons_coarse.append(plot_lon)
                    lats_coarse.append(lat)
                    
                    # Logika Pemisahan Data Sesuai Mode Peta
                    if is_khgt_map or "KHGT" in crit_name or "Danjon" in crit_name:
                        alt_data.append(m_alt_geo)     
                        arcv_data.append(m_alt_geo - s_alt_geo)
                    else:
                        alt_data.append(m_alt_topo)    
                        arcv_data.append(arcv_topo)
                        
                    elong_data.append(elong_geo)
                    illum_data.append(illum_val * 100.0)

            data_dict = {
                'lons': np.array(lons_coarse),
                'lats': np.array(lats_coarse),
                'alt': np.array(alt_data),
                'elong': np.array(elong_data),
                'arcv': np.array(arcv_data),
                'illum': np.array(illum_data)
            }

            import calendar
            bulan_nama = ["", "Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Ags", "Sep", "Okt", "Nov", "Des"]
            date_str = f"{day} {bulan_nama[month]} {year}"
            
            self.after(0, self.show_hd_vismap, data_dict, date_str, crit_name, param_target)

        except Exception as e:
            import traceback
            self.after(0, self.display_message, f"{str(e)}\n\n{traceback.format_exc()}", True)

    def show_hd_vismap(self, plot_data, date_str, crit_name, param_target):
        try:
            points = np.column_stack((plot_data['lons'], plot_data['lats']))
            is_khgt_map = param_target == "Interseksi (Peta KHGT 2D)"

            if is_khgt_map:
                lon_fine = np.arange(0, 360.25, 0.2)
            else:
                lon_fine = np.arange(-180, 180.25, 0.2)
                
            lat_fine = np.arange(-90, 90.25, 0.2) 
            LONS_fine, LATS_fine = np.meshgrid(lon_fine, lat_fine)

            # Interpolasi Bicubic Resolusi Tinggi
            grid_alt = interp.griddata(points, plot_data['alt'], (LONS_fine, LATS_fine), method='cubic')
            grid_elong = interp.griddata(points, plot_data['elong'], (LONS_fine, LATS_fine), method='cubic')
            grid_arcv = interp.griddata(points, plot_data['arcv'], (LONS_fine, LATS_fine), method='cubic')
            grid_illum = interp.griddata(points, plot_data['illum'], (LONS_fine, LATS_fine), method='cubic')

            # Penanganan NaN (Fallback ke Nearest)
            grid_alt_near = interp.griddata(points, plot_data['alt'], (LONS_fine, LATS_fine), method='nearest')
            grid_alt[np.isnan(grid_alt)] = grid_alt_near[np.isnan(grid_alt)]
            
            grid_elong_near = interp.griddata(points, plot_data['elong'], (LONS_fine, LATS_fine), method='nearest')
            grid_elong[np.isnan(grid_elong)] = grid_elong_near[np.isnan(grid_elong)]

            grid_arcv_near = interp.griddata(points, plot_data['arcv'], (LONS_fine, LATS_fine), method='nearest')
            grid_arcv[np.isnan(grid_arcv)] = grid_arcv_near[np.isnan(grid_arcv)]
            
            grid_illum_near = interp.griddata(points, plot_data['illum'], (LONS_fine, LATS_fine), method='nearest')
            grid_illum[np.isnan(grid_illum)] = grid_illum_near[np.isnan(grid_illum)]

            # Setup Window TopLevel Matplotlib
            win_plot = ctk.CTkToplevel(self)
            win_plot.title("High Resolution Crescent Map" if not is_khgt_map else "Peta Kalender Hijriah Global Tunggal (KHGT 2D)")
            win_plot.geometry("1100x750")
            win_plot.attributes("-topmost", True)
            
            fig, ax = plt.subplots(figsize=(12, 6.5), dpi=110)
            
            # Unduh dan muat peta topografi bumi
            map_file = "map_topografi.jpg" 
            map_url = "https://hisabmu.com/aifikih/berbagi/map_topografi.jpg"
            download_custom_file(map_file, map_url)
            full_map_path = os.path.join(BASE_DIR, map_file)
            
            if os.path.exists(full_map_path):
                try:
                    img = Image.open(full_map_path)
                    img_arr = np.array(img)
                    
                    if is_khgt_map:
                        # Geser peta agar bujur 180 (Pasifik) berada di tengah (untuk seamless 2 hari)
                        mid = img_arr.shape[1] // 2
                        img_shifted = np.concatenate((img_arr[:, mid:], img_arr[:, :mid]), axis=1)
                        ax.imshow(img_shifted, extent=[0, 360, -90, 90], aspect='auto', alpha=1.0, zorder=0)
                    else:
                        ax.imshow(img_arr, extent=[-180, 180, -90, 90], aspect='auto', alpha=1.0, zorder=0)
                except Exception as e:
                    print("Gagal memuat gambar peta:", e)

            # =======================================================
            # PLOTTING KHUSUS KHGT MAP (SEAMLESS 2 HARI)
            # =======================================================
            if is_khgt_map:
                grid_score = np.zeros_like(grid_alt)
                grid_score[(grid_alt >= 5.0) & (grid_elong >= 8.0)] = 1
                
                cmap_inter = ListedColormap([(0, 0, 0, 0), '#00E676'])
                bounds_inter = [-0.5, 0.5, 1.5]
                norm_inter = BoundaryNorm(bounds_inter, cmap_inter.N)
                
                ax.contourf(LONS_fine, LATS_fine, grid_score, cmap=cmap_inter, norm=norm_inter, alpha=0.2, zorder=2, antialiased=True)
                
                cs_alt = ax.contour(LONS_fine, LATS_fine, grid_alt, levels=[5.0], colors=['#1A237E'], linewidths=1.5, linestyles='solid', zorder=3)
                ax.clabel(cs_alt, inline=True, fontsize=10, fmt='Alt 5°')

                cs_elong = ax.contour(LONS_fine, LATS_fine, grid_elong, levels=[8.0], colors=['#D32F2F'], linewidths=1.5, linestyles='solid', zorder=3)
                ax.clabel(cs_elong, inline=True, fontsize=10, fmt='Elong 8°')
                
                # Garis Pembatas Penanggalan di tengah (Bujur 180 / Garis Tanggal Internasional)
                ax.axvline(180, color='black', linewidth=1.5, zorder=4)

                # Cetak Teks Tanggal di Puncak Garis
                try:
                    d, m, y = int(self.entry_vmday.get()), int(self.entry_vmmonth.get()), int(self.entry_vmyear.get())
                    dt_kanan = datetime.date(y, m, d)
                    dt_kiri = dt_kanan + datetime.timedelta(days=1)
                    
                    bulan_nama = ["", "Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Ags", "Sep", "Okt", "Nov", "Des"]
                    lbl_kanan = f"{dt_kanan.day} {bulan_nama[dt_kanan.month]} {dt_kanan.year}"
                    lbl_kiri = f"{dt_kiri.day} {bulan_nama[dt_kiri.month]} {dt_kiri.year}"
                except:
                    lbl_kanan, lbl_kiri = "Hari 1", "Hari 2"

                bbox_props = dict(boxstyle="square,pad=0.3", fc="white", ec="black", alpha=0.85)
                ax.text(178, 85, lbl_kiri, color='black', ha='right', va='top', fontsize=11, fontweight='bold', bbox=bbox_props, zorder=5)
                ax.text(182, 85, lbl_kanan, color='black', ha='left', va='top', fontsize=11, fontweight='bold', bbox=bbox_props, zorder=5)
                
                ax.set_xlim(0, 360)
                ax.set_ylim(-75, 90) 
                ax.set_title(f"Peta Kalender Hijriah Global Tunggal (KHGT)\nMode: Altitude (Geo), Elongasi (Geo)", fontweight='bold', pad=15)
                
                ticks_x = np.arange(0, 361, 30)
                labels_x = [f"{val}° BT" if val < 180 else f"{360 - val}° BB" if val > 180 else "180°" for val in ticks_x]
                
                ax.set_xticks(ticks_x)
                ax.set_xticklabels(labels_x)
                ax.set_yticks(np.arange(-60, 61, 30))
                
                p_interseksi = mpatches.Patch(color='#00E676', alpha=0.2, label='Area Hijau: Memenuhi Kriteria KHGT')
                line_alt = plt.Line2D([0], [0], color='#1A237E', lw=1.5, linestyle='solid', label='Batas Altitude 5° (G)')
                line_elong = plt.Line2D([0], [0], color='#D32F2F', lw=1.5, linestyle='solid', label='Batas Elongasi 8° (G)')
                ax.legend(handles=[p_interseksi, line_alt, line_elong], loc='lower center', ncol=3, bbox_to_anchor=(0.5, -0.15), fontsize='small')

            # =======================================================
            # RENDER UNTUK MODE PETA STANDAR LAINNYA
            # =======================================================
            else:
                if param_target == "Kriteria Visibilitas":
                    grid_score = np.zeros_like(grid_alt)
                    
                    if "KHGT" in crit_name or "Diyanet" in crit_name:
                        grid_score[(grid_alt >= 0) & (grid_arcv >= 0)] = 1
                        grid_score[(grid_alt >= 5) & (grid_elong >= 8)] = 2
                        cmap = ListedColormap(['#FF5252', '#FFFFFF', '#00E676'])
                        bounds = [-0.5, 0.5, 1.5, 2.5]
                        norm = BoundaryNorm(bounds, cmap.N)
                        
                        p1 = mpatches.Patch(color='#00E676', label='Green: Terpenuhi KHGT (Geo Alt>=5°, Geo Eln>=8°)')
                        p2 = mpatches.Patch(color='#FFFFFF', label='White: Belum Terpenuhi')
                        p3 = mpatches.Patch(color='#FF5252', label='Red: Impossible (Bawah Ufuk)')
                        ax.legend(handles=[p1, p2, p3], loc='lower center', ncol=3, bbox_to_anchor=(0.5, -0.18), fontsize='small')

                    elif "MABIMS" in crit_name:
                        grid_score[(grid_alt >= 0) & (grid_arcv >= 0)] = 1
                        grid_score[(grid_alt >= 3) & (grid_elong >= 6.4)] = 2
                        cmap = ListedColormap(['#FF5252', '#FFFFFF', '#00E676'])
                        bounds = [-0.5, 0.5, 1.5, 2.5]
                        norm = BoundaryNorm(bounds, cmap.N)
                        
                        p1 = mpatches.Patch(color='#00E676', label='Green: Terpenuhi Neo MABIMS (Topo Alt>=3°, Geo Eln>=6.4°)')
                        p2 = mpatches.Patch(color='#FFFFFF', label='White: Belum Terpenuhi')
                        p3 = mpatches.Patch(color='#FF5252', label='Red: Impossible')
                        ax.legend(handles=[p1, p2, p3], loc='lower center', ncol=3, bbox_to_anchor=(0.5, -0.18), fontsize='small')

                    elif "Ilyas" in crit_name:
                        grid_score[(grid_alt >= 0) & (grid_arcv >= 0)] = 1
                        grid_score[(grid_alt >= 4) & (grid_elong >= 10.5)] = 2
                        cmap = ListedColormap(['#FF5252', '#FFFFFF', '#00E676'])
                        bounds = [-0.5, 0.5, 1.5, 2.5]
                        norm = BoundaryNorm(bounds, cmap.N)
                        
                        p1 = mpatches.Patch(color='#00E676', label='Green: Terpenuhi Ilyas (Topo Alt>=4°, Geo Eln>=10.5°)')
                        p2 = mpatches.Patch(color='#FFFFFF', label='White: Belum Terpenuhi')
                        p3 = mpatches.Patch(color='#FF5252', label='Red: Impossible')
                        ax.legend(handles=[p1, p2, p3], loc='lower center', ncol=3, bbox_to_anchor=(0.5, -0.18), fontsize='small')

                    elif "Danjon" in crit_name:
                        grid_score[(grid_alt >= 0) & (grid_arcv >= 0)] = 1
                        grid_score[(grid_elong >= 7.0)] = 2
                        cmap = ListedColormap(['#FF5252', '#FFFFFF', '#00E676'])
                        bounds = [-0.5, 0.5, 1.5, 2.5]
                        norm = BoundaryNorm(bounds, cmap.N)
                        
                        p1 = mpatches.Patch(color='#00E676', label='Green: Melewati Limit Danjon (Geo Eln >= 7.0°)')
                        p2 = mpatches.Patch(color='#FFFFFF', label='White: Bawah Limit Danjon')
                        p3 = mpatches.Patch(color='#FF5252', label='Red: Impossible')
                        ax.legend(handles=[p1, p2, p3], loc='lower center', ncol=3, bbox_to_anchor=(0.5, -0.18), fontsize='small')

                    elif "Yallop" in crit_name:
                        W_grid = 15.5 * (1 - np.cos(np.radians(grid_elong)))
                        q_grid = grid_arcv - (11.8371 - 6.3226 * W_grid + 0.7319 * (W_grid**2) - 0.1018 * (W_grid**3))
                        
                        grid_score[:] = 0 # F
                        grid_score[(q_grid > -0.232)] = 1 # D
                        grid_score[(q_grid > -0.160)] = 2 # C
                        grid_score[(q_grid > -0.014)] = 3 # B
                        grid_score[(q_grid > 0.216)] = 4  # A
                        
                        cmap = ListedColormap(['#FF5252', '#2979FF', '#E040FB', '#FFD54F', '#00E676'])
                        bounds = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5]
                        norm = BoundaryNorm(bounds, cmap.N)
                        
                        p1 = mpatches.Patch(color='#FF5252', label='F: Not Visible (Topo q)')
                        p2 = mpatches.Patch(color='#2979FF', label='D: Only with Optical Aid (Topo q)')
                        p3 = mpatches.Patch(color='#E040FB', label='C: Need Optical Aid First (Topo q)')
                        p4 = mpatches.Patch(color='#FFD54F', label='B: Visible / Perfect Cond. (Topo q)')
                        p5 = mpatches.Patch(color='#00E676', label='A: Easily Visible / Naked Eye (Topo q)')
                        ax.legend(handles=[p1, p2, p3, p4, p5], loc='lower center', ncol=3, bbox_to_anchor=(0.5, -0.22), fontsize='small')

                    elif "Odeh" in crit_name:
                        grid_score[:] = 1 
                        grid_score[(grid_arcv < 0)] = 0
                        grid_score[(grid_arcv > 6.0) & (grid_elong > 8.0)] = 2
                        grid_score[(grid_arcv > 8.0) & (grid_elong > 10.0)] = 3
                        grid_score[(grid_arcv > 10.4) & (grid_elong > 12.0)] = 4
                        
                        cmap = ListedColormap(['#FF5252', '#FFFFFF', '#2979FF', '#E040FB', '#00E676'])
                        bounds = [-0.5, 0.5, 1.5, 2.5, 3.5, 4.5]
                        norm = BoundaryNorm(bounds, cmap.N)
                        
                        p1 = mpatches.Patch(color='#FF5252', label='Red: Impossible (Topo V)')
                        p2 = mpatches.Patch(color='#FFFFFF', label='White: Not Possible (Topo V)')
                        p3 = mpatches.Patch(color='#2979FF', label='Blue: Need Optical Aid (Topo V)')
                        p4 = mpatches.Patch(color='#E040FB', label='Magenta: Naked Eye / Perfect Cond (Topo V)')
                        p5 = mpatches.Patch(color='#00E676', label='Green: Easily Visible (Topo V)')
                        ax.legend(handles=[p1, p2, p3, p4, p5], loc='lower center', ncol=3, bbox_to_anchor=(0.5, -0.22), fontsize='small')

                    ax.contourf(LONS_fine, LATS_fine, grid_score, levels=bounds, cmap=cmap, norm=norm, alpha=0.2, zorder=2, antialiased=True)

                elif param_target == "Interseksi":
                    grid_score = np.zeros_like(grid_alt)
                    grid_score[(grid_alt >= 5.0) & (grid_elong >= 8.0)] = 1
                    cmap_inter = ListedColormap([(0, 0, 0, 0), '#00E676'])
                    bounds_inter = [-0.5, 0.5, 1.5]
                    norm_inter = BoundaryNorm(bounds_inter, cmap_inter.N)
                    
                    ax.contourf(LONS_fine, LATS_fine, grid_score, levels=bounds_inter, cmap=cmap_inter, norm=norm_inter, alpha=0.2, zorder=2, antialiased=True)
                    cs_alt = ax.contour(LONS_fine, LATS_fine, grid_alt, levels=[5.0], colors=['#FFD54F'], linewidths=2.5, linestyles='solid', zorder=3)
                    ax.clabel(cs_alt, inline=True, fontsize=11, fmt='Geo Alt 5°')
                    cs_elong = ax.contour(LONS_fine, LATS_fine, grid_elong, levels=[8.0], colors=['#00E5FF'], linewidths=2.5, linestyles='dashed', zorder=3)
                    ax.clabel(cs_elong, inline=True, fontsize=11, fmt='Geo Eln 8°')
                    
                    p_interseksi = mpatches.Patch(color='#00E676', alpha=0.2, label='Area Pemenuhan KHGT (Geo)')
                    line_alt = plt.Line2D([0], [0], color='#FFD54F', lw=2.5, linestyle='solid', label='Batas Altitude 5° (Geo)')
                    line_elong = plt.Line2D([0], [0], color='#00E5FF', lw=2.5, linestyle='dashed', label='Batas Elongasi 8° (Geo)')
                    ax.legend(handles=[p_interseksi, line_alt, line_elong], loc='lower center', ncol=3, bbox_to_anchor=(0.5, -0.20), fontsize='small')

                else:
                    if param_target == "Altitude Bulan":
                        target_grid, cmap_name, lbl = grid_alt, 'plasma', "Altitude Bulan (°)"
                    elif param_target == "Elongasi":
                        target_grid, cmap_name, lbl = grid_elong, 'inferno', "Sudut Elongasi (°)"
                    else:
                        target_grid, cmap_name, lbl = grid_illum, 'magma', "Fraksi Iluminasi (%)"
                        
                    contours = ax.contour(LONS_fine, LATS_fine, target_grid, cmap=cmap_name, levels=20, linewidths=1.5, alpha=1.0, zorder=3)
                    ax.clabel(contours, inline=True, fontsize=12, fmt='%1.1f')
                    cbar = fig.colorbar(contours, ax=ax, orientation='horizontal', fraction=0.046, pad=0.1)
                    cbar.set_label(f"Legend: {lbl}", fontweight='bold')

                if not is_khgt_map:
                    ax.set_title(f"High-Res Visibility Scanner\n{crit_name} - {date_str} - Layer: {param_target}", fontweight='bold', pad=10)
                    ax.set_ylim(-90, 90)
                    ax.set_xlabel("Longitude")
                    ax.set_ylabel("Latitude")
                    ax.set_xticks(np.arange(-180, 181, 30))
                    ax.set_yticks(np.arange(-60, 61, 20))
                    ax.grid(True, linestyle='--', color='gray', linewidth=0.5, zorder=4)

            fig.tight_layout()

            # Embed Figure ke Tkinter Toplevel
            frame_canvas = ctk.CTkFrame(win_plot)
            frame_canvas.pack(fill="both", expand=True, padx=10, pady=10)
            
            canvas_plot = FigureCanvasTkAgg(fig, master=frame_canvas)
            canvas_plot.draw()
            canvas_plot.get_tk_widget().pack(fill="both", expand=True)

            toolbar_frame = ctk.CTkFrame(win_plot, height=40, fg_color="transparent")
            toolbar_frame.pack(fill="x", side="bottom", padx=10, pady=(0, 10))
            
            toolbar = NavigationToolbar2Tk(canvas_plot, toolbar_frame)
            toolbar.update()
            
            def simpan_gambar_kustom():
                filepath = filedialog.asksaveasfilename(
                    initialfile=f"HD_VisMap_KHGT_Seamless.png" if is_khgt_map else f"HD_VisMap_{param_target.replace(' ', '')}.png",
                    defaultextension=".png",
                    filetypes=[("PNG Image", "*.png"), ("SVG Vector", "*.svg")]
                )
                if filepath:
                    fig.savefig(filepath, dpi=300, bbox_inches='tight')
                    messagebox.showinfo("Sukses", f"Peta HD berhasil diekspor ke:\n{filepath}")

            btn_export = ctk.CTkButton(toolbar_frame, text="📸 Export Gambar HD", font=("Segoe UI", 12, "bold"), fg_color="#E65100", hover_color="#BF360C", command=simpan_gambar_kustom)
            btn_export.pack(side="right", padx=10)

            self.display_message(f"[{datetime.datetime.now().strftime('%H:%M:%S')}] Pemrosesan Data Selesai.\nPeta Visualisasi Kualitas Tinggi (HD) sedang ditampilkan pada Window baru...\n\nLayer Heatmap : {param_target}")
            self.lbl_status.configure(text=f"Render Peta HD Selesai.", text_color="#00E676")

        except Exception as e:
            import traceback
            self.display_message(f"Gagal menampilkan HD Peta: {e}\n\n{traceback.format_exc()}", is_error=True)

if __name__ == "__main__":
    app = Menu2App()
    app.mainloop()