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

import ephem
from skyfield.api import Loader, wgs84
from skyfield import almanac

# ==========================================
# FUNGSI UTILITAS & FORMATTING
# ==========================================
def get_safe_events(t_obj, y_obj):
    y_arr = np.atleast_1d(y_obj)
    if len(y_arr) == 0: return []
    events = []
    for k in range(len(y_arr)):
        try: t_val = t_obj[k]
        except Exception: t_val = t_obj
        events.append((t_val, int(y_arr[k])))
    return events

def format_angle(deg, is_ra=False):
    if deg is None or math.isnan(deg): return "+00°:00':00\""
    if hasattr(deg, 'item'): deg = deg.item()
    sign = "+" if deg >= 0 else "-"
    deg = abs(deg)
    if is_ra:
        hours = deg / 15.0
        h = int(hours)
        m = int((hours - h) * 60)
        s = int(round((hours - h - m/60.0) * 3600))
        if s == 60: s = 0; m += 1
        if m == 60: m = 0; h += 1
        return f"{sign}{h:02d}H {m:02d}M {s:02d}S"
    else:
        d = int(deg)
        m = int((deg - d) * 60)
        s = int(round((deg - d - m/60.0) * 3600))
        if s == 60: s = 0; m += 1
        if m == 60: m = 0; d += 1
        return f"{sign}{d:02d}°:{m:02d}':{s:02d}\""

def format_time_hms(delta_hours):
    if hasattr(delta_hours, 'item'): delta_hours = delta_hours.item()
    sign = "+" if delta_hours >= 0 else "-"
    delta_hours = abs(delta_hours)
    h = int(delta_hours)
    m = int(round((delta_hours - h) * 60))
    if m == 60: m = 0; h += 1
    return f"{sign}{h:02d}H {m:02d}M"

def get_hms_str(time_obj, tz_offset):
    if time_obj is None: return "--.--"
    y, m, d, h, mn, s = time_obj.utc
    total_minutes = int(h * 60 + mn + tz_offset * 60 + round(s / 60.0))
    local_hour = (total_minutes // 60) % 24
    local_minute = total_minutes % 60
    return f"{local_hour:02d}.{local_minute:02d}"

def download_custom_bsp(filename, url):
    if not os.path.exists(filename):
        try:
            req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
            with urllib.request.urlopen(req) as response, open(filename, 'wb') as out_file:
                out_file.write(response.read())
        except Exception: pass

# ==========================================
# KELAS UTAMA APLIKASI
# ==========================================
class Menu1App(ctk.CTk):
    def __init__(self):
        super().__init__()

        self.title("Laporan Analisis Hilal & KHGT (Modul 1)")
        self.geometry("1150x700")
        self.minsize(900, 600)

        # Setup SSL bypass untuk unduhan Ephemeris
        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(os.path.dirname(os.path.abspath(__file__)), verbose=False)
        self.ts = self.load_obj.timescale()
        self.eph = None
        self.ephemeris_name = "de421.bsp"

        self.setup_ui()
        threading.Thread(target=self.load_ephemeris, daemon=True).start()

    def get_header(self, width):
        lines = [
            "By the Name of Allah",
            "KALENDER HIJRIAH GLOBAL TUNGGAL",
            "KHGT Times 7.2 - Visibility Hilal Module"
        ]
        return "\n".join(line.center(width) for line in lines)

    def load_ephemeris(self):
        try:
            if not os.path.exists(self.ephemeris_name):
                self.lbl_status.configure(text=f"Mengunduh ephemeris ({self.ephemeris_name})...", text_color="#00E5FF")
                download_custom_bsp(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_input_row(self, parent, label_text, default_val):
        row = ctk.CTkFrame(parent, fg_color="transparent")
        row.pack(fill="x", padx=10, pady=2)
        ctk.CTkLabel(row, text=label_text, font=("Segoe UI", 12)).pack(side="left")
        entry = ctk.CTkEntry(row, width=80, justify="right")
        entry.insert(0, default_val)
        entry.pack(side="right")
        return entry

    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, placeholder_text="dd", justify="center")
        d.insert(0, dd)
        d.grid(row=1, column=0, padx=(0, 5))
        
        m = ctk.CTkEntry(grid_frame, width=45, placeholder_text="mm", justify="center")
        m.insert(0, dm)
        m.grid(row=1, column=1, padx=5)

        y = ctk.CTkEntry(grid_frame, width=70, placeholder_text="yyyy", 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 = str(now.year)
        curr_m = f"{now.month:02d}"
        curr_d = 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="KHGT ENGINE", font=("Segoe UI", 24, "bold"), text_color="#00E5FF").pack(pady=(20, 15))

        # 1. Tanggal Observasi
        frame_vdate = ctk.CTkFrame(self.sidebar, fg_color="#212121")
        frame_vdate.pack(fill="x", padx=15, pady=5)
        ctk.CTkLabel(frame_vdate, text="TANGGAL OBSERVASI", font=("Segoe UI", 12, "bold")).pack(anchor="w", padx=10, pady=(8, 2))
        self.entry_vyear, self.entry_vmonth, self.entry_vday = self.create_ymd_row(frame_vdate, curr_y, curr_m, curr_d)

        # 2. Lokasi
        frame_vloc = ctk.CTkFrame(self.sidebar, fg_color="#212121")
        frame_vloc.pack(fill="x", padx=15, pady=5)
        ctk.CTkLabel(frame_vloc, text="KOORDINAT LOKASI", font=("Segoe UI", 12, "bold")).pack(anchor="w", padx=10, pady=(8, 2))
        self.entry_vlat = self.create_input_row(frame_vloc, "Lat (Lintang):", "-7.0667")
        self.entry_vlon = self.create_input_row(frame_vloc, "Lon (Bujur):", "110.4100")
        self.entry_velev = self.create_input_row(frame_vloc, "Elevasi (m):", "230.0")
        self.entry_vtz = self.create_input_row(frame_vloc, "Timezone:", "7.0")

        # 3. Atmosfer
        frame_vatm = ctk.CTkFrame(self.sidebar, fg_color="#212121")
        frame_vatm.pack(fill="x", padx=15, pady=5)
        ctk.CTkLabel(frame_vatm, text="PARAMETER ATMOSFER", font=("Segoe UI", 12, "bold")).pack(anchor="w", padx=10, pady=(8, 2))
        self.entry_vtemp = self.create_input_row(frame_vatm, "Suhu (°C):", "25.0")
        self.entry_vpres = self.create_input_row(frame_vatm, "Tekanan (mb):", "1010.0")
        self.entry_vhum = self.create_input_row(frame_vatm, "Kelembapan (%):", "60.0")

        # 4. Tombol Proses
        self.btn_hitung = ctk.CTkButton(self.sidebar, text="▶ PROSES DATA", 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="Memuat Ephemeris...", 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="Laporan Analisis Hilal & KHGT", 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="none")
        self.textbox.grid(row=1, column=0, sticky="nsew")
        self.textbox.insert("1.0", "Menunggu input dari pengguna...\nIsi parameter di samping kiri lalu klik 'PROSES DATA'.")
        self.textbox.configure(state="disabled")

    def run_calculation(self):
        self.lbl_status.configure(text="Menghitung...", text_color="#FFAB40")
        self.btn_hitung.configure(state="disabled")
        self.textbox.configure(state="normal")
        self.textbox.delete("1.0", "end")
        self.textbox.insert("1.0", "Memproses data astrometri...\n")
        self.textbox.configure(state="disabled")
        
        threading.Thread(target=self.calculate_visibility, daemon=True).start()

    def generate_visibility_report(self):
        # 1. Ambil Data UI
        year = int(self.entry_vyear.get())
        month = int(self.entry_vmonth.get())
        day = int(self.entry_vday.get())

        lat = float(self.entry_vlat.get())
        lon = float(self.entry_vlon.get())
        elev = float(self.entry_velev.get())
        tz = float(self.entry_vtz.get())
        
        temp = float(self.entry_vtemp.get())
        pres = float(self.entry_vpres.get())
        hum = float(self.entry_vhum.get())

        earth, sun, moon = self.eph['earth'], self.eph['sun'], self.eph['moon']
        lokasi_obs = wgs84.latlon(lat, lon, elevation_m=elev)

        # 2. Cari Waktu Sunset Lokal
        t0 = self.ts.utc(year, month, day, 0 - int(tz)) 
        t1 = self.ts.utc(year, month, day, 23 - int(tz)) 
        
        t_sunset = None
        t_evs, y_evs = almanac.find_discrete(t0, t1, almanac.sunrise_sunset(self.eph, lokasi_obs))
        for t_ev, is_sunrise in get_safe_events(t_evs, y_evs):
            if not is_sunrise: 
                t_sunset = t_ev
                break
                
        if t_sunset is None:
            raise ValueError("Sunset (Matahari Terbenam) tidak ditemukan pada tanggal/koordinat tersebut.")

        ts_tt = t_sunset.tt.item() if hasattr(t_sunset.tt, 'item') else t_sunset.tt

        # 3. Cari Waktu Moonset
        t_bound_moon = self.ts.tt_jd(ts_tt + 1.5)
        t_moonset = None
        t_mevs, y_mevs = almanac.find_discrete(t0, t_bound_moon, almanac.risings_and_settings(self.eph, moon, lokasi_obs))
        
        for t_ev, is_moonrise in get_safe_events(t_mevs, y_mevs):
            tm_tt = t_ev.tt.item() if hasattr(t_ev.tt, 'item') else t_ev.tt
            if not is_moonrise and tm_tt > (ts_tt - 0.5):
                t_moonset = t_ev
                break

        # 4. Cari Waktu Ijtimak
        t_start_conj = self.ts.tt_jd(ts_tt - 5.0)
        t_end_conj = self.ts.tt_jd(ts_tt + 5.0)
        
        t_phases, y_phases = almanac.find_discrete(t_start_conj, t_end_conj, almanac.moon_phases(self.eph))
        t_ijtima = None
        for t_ev, phase in get_safe_events(t_phases, y_phases):
            if phase == 0:  
                t_ijtima = t_ev
                break

        # 5. Kalkulasi Geosentris & Toposentris
        geo_earth = earth.at(t_sunset)
        app_moon_geo = geo_earth.observe(moon).apparent()
        app_sun_geo = geo_earth.observe(sun).apparent()

        ra_moon, dec_moon, dist_moon = app_moon_geo.radec(epoch=t_sunset)
        ra_sun, dec_sun, dist_sun = app_sun_geo.radec(epoch=t_sunset)

        gast = t_sunset.gast
        lst_deg = (gast * 15.0) + lon
        
        def get_geo_altaz(ra, dec):
            ra_h = ra.hours.item() if hasattr(ra.hours, 'item') else ra.hours
            dec_r = dec.radians.item() if hasattr(dec.radians, 'item') else dec.radians
            ha_deg = lst_deg - (ra_h * 15.0)
            lat_rad, dec_rad, ha_rad = math.radians(lat), dec_r, math.radians(ha_deg)
            sin_alt = math.sin(dec_rad) * math.sin(lat_rad) + math.cos(dec_rad) * math.cos(lat_rad) * math.cos(ha_rad)
            sin_alt = max(-1.0, min(1.0, sin_alt)) 
            alt = math.degrees(math.asin(sin_alt))
            y_val = -math.sin(ha_rad)
            x_val = math.tan(dec_rad) * math.cos(lat_rad) - math.sin(lat_rad) * math.cos(ha_rad)
            az = (math.degrees(math.atan2(y_val, x_val)) + 360) % 360
            return alt, az

        alt_moon_geo, az_moon_geo = get_geo_altaz(ra_moon, dec_moon)
        alt_sun_geo, az_sun_geo = get_geo_altaz(ra_sun, dec_sun)
        
        topo_earth = (earth + lokasi_obs).at(t_sunset)
        app_moon_topo = topo_earth.observe(moon).apparent()
        app_sun_topo = topo_earth.observe(sun).apparent()
        
        alt_moon_topo_obj, az_moon_topo_obj, _ = app_moon_topo.altaz(temperature_C=temp, pressure_mbar=pres)
        alt_sun_topo_obj, az_sun_topo_obj, _ = app_sun_topo.altaz(temperature_C=temp, pressure_mbar=pres)
        
        alt_moon_topo = alt_moon_topo_obj.degrees
        alt_sun_topo = alt_sun_topo_obj.degrees

        # Umur & Waktu
        if t_ijtima is not None:
            ti_tt = t_ijtima.tt.item() if hasattr(t_ijtima.tt, 'item') else t_ijtima.tt
            moon_age_hours = (ts_tt - ti_tt) * 24.0
            
            t_ijtima_local_dt = t_ijtima.astimezone(pytz.FixedOffset(tz * 60))
            y_i, m_i, d_i = t_ijtima_local_dt.year, t_ijtima_local_dt.month, t_ijtima_local_dt.day
            h_i, mn_i = t_ijtima_local_dt.hour, t_ijtima_local_dt.minute
            y_str = f"{y_i}" if y_i > 0 else f"{abs(y_i-1)} SM"
            str_ijtima = f"{d_i:02d}/{m_i:02d}/{y_str}, {int(h_i):02d}:{int(mn_i):02d} LT"
            str_ijtima_utc = t_ijtima.utc_strftime('%d/%m/%Y, %H:%M UTC')
        else:
            moon_age_hours = 0.0
            str_ijtima = "N/A"
            str_ijtima_utc = "N/A"
            
        if t_moonset is not None:
            tm_tt = t_moonset.tt.item() if hasattr(t_moonset.tt, 'item') else t_moonset.tt
            lag_time_hours = (tm_tt - ts_tt) * 24.0
        else:
            lag_time_hours = 0.0
        
        # Parameter Fisik & Optis
        sep_deg = app_sun_geo.separation_from(app_moon_geo).degrees
        elongation = sep_deg.item() if hasattr(sep_deg, 'item') else sep_deg
        
        rel_alt_topo = alt_moon_topo - alt_sun_topo
        
        illum_val = almanac.fraction_illuminated(self.eph, 'moon', t_sunset)
        illumination = (illum_val.item() if hasattr(illum_val, 'item') else illum_val) * 100.0
        
        dist_km = dist_moon.km.item() if hasattr(dist_moon.km, 'item') else dist_moon.km
        sd_moon = math.degrees(math.asin(1737.4 / dist_km))

        # =====================================================================
        # EVALUASI KRITERIA VISIBILITAS INTERNASIONAL
        # =====================================================================
        is_khgt = alt_moon_geo >= 5.0 and elongation >= 8.0
        str_khgt = "TERPENUHI" if is_khgt else "Gagal"

        is_mabims = alt_moon_topo >= 3.0 and elongation >= 6.4
        str_mabims = "TERPENUHI" if is_mabims else "Gagal"

        is_ilyas = alt_moon_topo >= 4.0 and elongation >= 10.5
        str_ilyas = "TERPENUHI" if is_ilyas else "Gagal"

        is_danjon = elongation >= 7.0
        str_danjon = "LOLOS (Sabit Terbentuk)" if is_danjon else "GAGAL (Sabit Tidak Terbentuk)"

        W_arcmin = (sd_moon * 60.0) * (1 - math.cos(math.radians(elongation)))
        q_yallop = rel_alt_topo - (11.8371 - 6.3226 * W_arcmin + 0.7319 * (W_arcmin**2) - 0.1018 * (W_arcmin**3))
        
        if q_yallop > 0.216: yallop_stat = "A (Mudah Terlihat / Mata Telanjang)"
        elif q_yallop > -0.014: yallop_stat = "B (Terlihat dgn Cuaca Sempurna)"
        elif q_yallop > -0.160: yallop_stat = "C (Butuh Alat Optik utk Menemukan)"
        elif q_yallop > -0.232: yallop_stat = "D (Hanya Terlihat via Alat Optik)"
        else: yallop_stat = "F (TIDAK TERLIHAT / Bawah Limit)"

        # =====================================================================
        # RENDER LAPORAN
        # =====================================================================
        moonset_str = get_hms_str(t_moonset, tz) if t_moonset is not None else '--.--'
        lat_str = format_angle(lat).replace("+", "").replace("-", "-") 
        lon_str = format_angle(lon).replace("+", "")

        hari_idx = int(t0.whole % 7)
        nama_hari = ["Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu", "Ahad"][hari_idx]
        tahun_str = f"{year} CE" if year > 0 else f"{abs(year-1)} BCE (SM)"
        display_date = f"{nama_hari}, {day:02d}/{month:02d}/{tahun_str}"

        report = f"""{self.get_header(85)}

* Settings:-
- Calculations for Waxing Crescent (New, Evening).
- Crescent Visibility on: {display_date}
- Calculations are Done at Sunset Time at:  {get_hms_str(t_sunset, tz)} LT
- Calculations are Geocentric & Topocentric.
- LOKASI: Long: {lon_str}, Lat: {lat_str}, Ele: {elev}, Zone: {tz}
=====================================================================================

- G. Conjunction (UTC)  : {str_ijtima_utc}
- L. Conjunction (LT)   : {str_ijtima}
- Julian Date at Time of Calculations: {ts_tt:.5f}

- Sunset                 : {get_hms_str(t_sunset, tz) + " LT":<16} G. Moon Age            : {format_time_hms(moon_age_hours):<16}
- Moonset                : {moonset_str + " LT":<16} Moon Lag Time          : {format_time_hms(lag_time_hours):<16}

- G. Moon Altitude       : {format_angle(alt_moon_geo):<16} T. Moon Altitude       : {format_angle(alt_moon_topo):<16}
- G. Sun Altitude        : {format_angle(alt_sun_geo):<16} T. Sun Altitude        : {format_angle(alt_sun_topo):<16}

- G. Elongation          : {format_angle(elongation):<16} G. Illumination        : {f"{illumination:05.2f} %":<16}

-------------------------------------------------------------------------------------
[ MULTI-CRITERIA GLOBAL RESEARCH RESULT ]
-------------------------------------------------------------------------------------
1. KHGT / Diyanet Turki (G.Alt>=5°, G.Eln>=8°)   : {str_khgt}
2. Neo MABIMS           (T.Alt>=3°, G.Eln>=6.4°) : {str_mabims}
3. Ilyas Criterion      (T.Alt>=4°, G.Eln>=10.5°): {str_ilyas}
4. Danjon Limit         (G.Eln >= 7.0°)          : {str_danjon}
5. Yallop (1998) Param  (q-Value = {q_yallop:+.3f})      : {yallop_stat}

=====================================================================================
"""
        return report

    def calculate_visibility(self):
        try:
            report = self.generate_visibility_report()
            self.after(0, self.display_result, report)
        except Exception as e:
            import traceback
            self.after(0, self.display_error, f"{str(e)}\n\n{traceback.format_exc()}")

    def display_result(self, report_text):
        self.textbox.configure(state="normal")
        self.textbox.delete("1.0", "end")
        self.textbox.insert("1.0", report_text)
        self.textbox.configure(state="disabled")
        self.lbl_status.configure(text="Kalkulasi Selesai", text_color="#00E676")
        self.btn_hitung.configure(state="normal")

    def display_error(self, error_msg):
        self.textbox.configure(state="normal")
        self.textbox.delete("1.0", "end")
        self.textbox.insert("1.0", f"TERJADI KESALAHAN:\n{error_msg}")
        self.textbox.configure(state="disabled")
        self.lbl_status.configure(text="Error Kalkulasi", text_color="#FF1744")
        self.btn_hitung.configure(state="normal")

if __name__ == "__main__":
    app = Menu1App()
    app.mainloop()