import json
import requests
import os
import glob
import unicodedata
import tkinter as tk
import matplotlib.pyplot as plt
import numpy as np
from tkinter import ttk, messagebox
from datetime import datetime
from PIL import Image
from matplotlib.offsetbox import OffsetImage, AnnotationBbox 
from tkhtmlview import HTMLLabel

# =============================================================================
# CONFIGURATION ET PARAMÈTRES GLOBAUX
# =============================================================================

# Dimensions des graphiques Matplotlib (en pouces)
# 1 pouce = 150 pixels avec le DPI par défaut défini lors de la sauvegarde.
graphique_largeur = 10 
graphique_hauteur = 3
plt.switch_backend('Agg')  # Empêche l'ouverture de fenêtres Matplotlib intempestives

# --- GESTION DE LA COMPATIBILITÉ PILLOW ---
# Assure le fonctionnement du redimensionnement d'image sur différentes versions de Pillow
if not hasattr(Image, 'ANTIALIAS'):
    if hasattr(Image.Resampling, 'LANCZOS'):
        Image.ANTIALIAS = Image.Resampling.LANCZOS 
    else:
        Image.ANTIALIAS = Image.BICUBIC

# Détermination de la méthode de redimensionnement pour les graphiques
IMAGE_RESIZE_METHOD = None
if 'Image' in globals():
    if hasattr(Image, 'LANCZOS'):
        IMAGE_RESIZE_METHOD = Image.LANCZOS
    elif hasattr(Image.Resampling) and hasattr(Image.Resampling, 'LANCZOS'): 
        IMAGE_RESIZE_METHOD = Image.Resampling.LANCZOS
    elif hasattr(Image, 'ANTIALIAS'): 
        IMAGE_RESIZE_METHOD = Image.ANTIALIAS 
    else:
        IMAGE_RESIZE_METHOD = Image.BICUBIC

# Fichier de persistance pour sauvegarder l'état de l'application
CONFIG_FILE = "window_config.json"
DEFAULT_CONFIG = {
    "width": 1000,
    "height": 700,
    "x": 100,
    "y": 100,
    "selected_locations": []
}

# =============================================================================
# UTILITAIRES DE GESTION DES DONNÉES
# =============================================================================

def to_ascii(text):
    """
    Normalise un texte : supprime les accents, met en minuscule et nettoie les espaces.
    Indispensable pour la génération de noms de fichiers fiables.
    """
    try:
        text = str(text).lower()
        text = unicodedata.normalize('NFD', text).encode('ascii', 'ignore').decode("utf-8")
        return text.strip()
    except Exception:
        return str(text).lower().strip()

def load_app_config():
    """Charge la configuration utilisateur (taille fenêtre, favoris) depuis le JSON."""
    if os.path.exists(CONFIG_FILE):
        try:
            with open(CONFIG_FILE, 'r') as f:
                config = json.load(f)
                if 'selected_locations' not in config:
                    config['selected_locations'] = DEFAULT_CONFIG['selected_locations']
                return config
        except (json.JSONDecodeError, IOError):
            return DEFAULT_CONFIG
    return DEFAULT_CONFIG

def save_app_config(root, selected_locations_data):
    """Enregistre la géométrie actuelle de la fenêtre et la liste des villes sélectionnées."""
    try:
        geometry = root.geometry()
        size_part, pos_part = geometry.split('+', 1) 
        width, height = map(int, size_part.split('x'))
        x, y = map(int, pos_part.split('+'))        
        config = {
            "width": width, "height": height, "x": x, "y": y,
            "selected_locations": selected_locations_data
        }
        with open(CONFIG_FILE, 'w') as f:
            json.dump(config, f, indent=4)
    except ValueError:
        pass

# =============================================================================
# GESTION DES LOCALITÉS
# =============================================================================

class Location:
    """Objet représentant une ville avec son nom et ses coordonnées GPS."""
    def __init__(self, name_original, lat, lon):
        self.name = name_original
        self.lat = float(lat)
        self.lon = float(lon)
        self.ascii_name = to_ascii(name_original)
        
    def to_tuple(self):
        """Retourne les données sous forme de tuple pour le stockage JSON."""
        return (self.name, self.lat, self.lon)

def parse_location_line(line):
    """Analyse une ligne issue de localites.txt (format attendu : Nom TAB Lat TAB Lon)."""
    parts = line.strip().split('\t')
    if len(parts) >= 3:
        try:
            return Location(parts[0].strip(), parts[1].strip(), parts[2].strip())
        except ValueError:
            return None 
    return None 

def load_all_locations(filename="localites.txt"):
    """Lit la base de données des localités mondiales."""
    all_locations = []
    try:
        with open(filename, 'r', encoding='utf-8') as f:
            for line in f:
                location = parse_location_line(line)
                if location:
                    all_locations.append(location)
    except FileNotFoundError:
        messagebox.showerror("Erreur", f"Fichier '{filename}' manquant.")
    return all_locations

def cleanup_old_files(location_name):
    """Supprime les rapports et graphiques obsolètes pour éviter l'encombrement."""
    filename_base = to_ascii(location_name).replace(' ', '_')
    patterns = [f"report_{filename_base}_*.html", f"chart_temp_{filename_base}_*.png", f"chart_wind_{filename_base}_*.png"]
    for pattern in patterns:
        for file_path in glob.glob(pattern):
            try: os.remove(file_path)
            except OSError: pass 

# =============================================================================
# MAPPINGS MÉTÉO ET VENT
# =============================================================================

# Codes WMO (Open-Meteo) vers descriptions et icônes
WMO_CODES = {
    0: ("Ciel clair", "0.png"), 1: ("Dégagé", "1.png"), 2: ("Partiellement nuageux", "2.png"),
    3: ("Couvert", "3.png"), 45: ("Brouillard", "45.png"), 48: ("Brouillard givrant", "48.png"),
    51: ("Bruine légère", "51.png"), 53: ("Bruine modérée", "53.png"), 55: ("Bruine dense", "55.png"),
    56: ("Bruine verglaçante légère", "56.png"), 57: ("Bruine verglaçante dense", "57.png"),
    61: ("Pluie légère", "61.png"), 63: ("Pluie modérée", "63.png"), 65: ("Pluie forte", "65.png"),
    66: ("Pluie verglaçante légère", "66.png"), 67: ("Pluie verglaçante forte", "67.png"),
    71: ("Neige légère", "71.png"), 73: ("Neige modérée", "73.png"), 75: ("Neige forte", "75.png"),
    77: ("Neige en grains", "77.png"), 80: ("Averses légères", "80.png"), 81: ("Averses modérées", "81.png"),
    82: ("Averses violentes", "82.png"), 85: ("Neige légère", "85.png"), 86: ("Neige forte", "86.png"),
    95: ("Orage", "95.png"), 96: ("Orage avec grêle", "96.png"), 99: ("Orage violent grêle", "99.png"),
}

def get_wind_direction_icon(degrees):
    """Associe un angle de vent à une icône de direction (n, ne, e, etc.)."""
    if degrees is None: return "calm.png", "Calme"
    degrees = float(degrees) % 360
    directions = [
        (22.5, "n", "Nord"), (67.5, "ne", "Nord-Est"), (112.5, "e", "Est"),
        (157.5, "se", "Sud-Est"), (202.5, "s", "Sud"), (247.5, "sw", "Sud-Ouest"),
        (292.5, "w", "Ouest"), (337.5, "nw", "Nord-Ouest"), (360, "n", "Nord")
    ]
    for limit, icon_name, direction_text in directions:
        if degrees <= limit: return f"{icon_name}.png", direction_text
    return "calm.png", "Calme"

FRENCH_DAY_ABBREVIATIONS = ["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"]

# =============================================================================
# GÉNÉRATION DES GRAPHIQUES (MATPLOTLIB)
# =============================================================================

def generate_png_daily_temperature_chart(dates, temps_max, temps_min, wmo_codes, precipitation_sum, precip_unit, temp_unit, filename):
    """Génère le graphique température/précipitation avec deux axes Y."""
    if plt is None or not dates: return False
    fig, ax = plt.subplots(figsize=(graphique_largeur, graphique_hauteur)) 
    x = np.arange(len(dates))
    
    # 1. Précipitations (Barres vertes, axe droit)
    ax2 = ax.twinx() 
    precip_plot = [p if p is not None else 0 for p in precipitation_sum]
    ax2.bar(x, precip_plot, 0.8, color='green', alpha=0.3, label='Précip.')
    ax2.set_ylabel(f'Précip. ({precip_unit})', color='green')
    ax2.set_ylim(bottom=0) 

    # 2. Températures (Lignes, axe gauche)
    temps_max_plot = [t if t is not None else np.mean([v for v in temps_max if v]) for t in temps_max]
    temps_min_plot = [t if t is not None else np.mean([v for v in temps_min if v]) for t in temps_min]
    ax.plot(x, temps_max_plot, marker='o', color='#ff6384', label='Max') 
    ax.plot(x, temps_min_plot, marker='o', color='#36a2eb', label='Min') 
    ax.set_ylabel(f'Temp ({temp_unit})')
    ax.set_title('Prévisions Température et Précipitations')
    ax.set_xticks(x)
    ax.set_xticklabels(dates, rotation=45, ha="right")
    ax.grid(True, linestyle='--', alpha=0.6)

    # Annotations des valeurs
    for i, (max_val, precip_val) in enumerate(zip(temps_max, precipitation_sum)):
        if max_val is not None:
            ax.annotate(f'{round(max_val)}', (x[i], temps_max_plot[i]), textcoords="offset points", xytext=(0,10), ha='center', fontsize=9, color='#ff6384')
        if precip_val and precip_val > 0:
            ax2.annotate(f'{round(precip_val)}', (x[i], precip_plot[i]), textcoords="offset points", xytext=(0,3), ha='center', fontsize=9, color='darkgreen')

    plt.tight_layout()
    try:
        plt.savefig(filename, dpi=150)
        plt.close(fig) 
        return True
    except Exception: return False
        
def generate_png_wind_line_chart(dates, wind_max, wind_icons, unit, filename):
    """Génère le graphique de la vitesse du vent avec icônes de direction."""
    if plt is None or not dates: return False
    fig, ax = plt.subplots(figsize=(graphique_largeur, graphique_hauteur)) 
    x = np.arange(len(dates))
    wind_max_plot = [w if w is not None else 0 for w in wind_max]
    ax.plot(x, wind_max_plot, marker='o', color='#4BC0C0') 
    ax.set_ylabel(f'Vent ({unit})')
    ax.set_title('Rafales et Direction du Vent')
    ax.set_xticks(x)
    ax.set_xticklabels(dates, rotation=45, ha="right")
    ax.grid(True, linestyle='--', alpha=0.6)
    ax.set_ylim(bottom=-5, top=max(wind_max_plot)*1.2 if wind_max_plot else 10)

    # Intégration des icônes de direction
    for i, (speed, icon_file) in enumerate(zip(wind_max, wind_icons)):
        if speed is not None:
            ax.annotate(f'{round(speed)}', (x[i], wind_max_plot[i]), textcoords="offset points", xytext=(0,10), ha='center', fontsize=9)
        icon_path = os.path.join("wind_icons", icon_file)
        if os.path.exists(icon_path):
            try:
                img = Image.open(icon_path).convert("RGBA").resize((40, 20), IMAGE_RESIZE_METHOD)
                ab = AnnotationBbox(OffsetImage(img, zoom=1), (x[i], 4), xycoords='data', frameon=False)
                ax.add_artist(ab)
            except Exception: pass
    plt.tight_layout()
    plt.savefig(filename, dpi=150)
    plt.close(fig)
    return True

# =============================================================================
# GÉNÉRATION DU RAPPORT HTML
# =============================================================================

def generate_html_report(json_file_path, location_name):
    """Compile les données et graphiques dans un fichier HTML pour l'affichage TK."""
    try:
        with open(json_file_path, 'r', encoding='utf-8') as f:
            data = json.load(f)
    except Exception: return None, "Erreur JSON"

    filename_base = to_ascii(location_name).replace(' ', '_')
    uid = datetime.now().strftime("%Y%m%d%H%M%S")
    html_name = f"report_{filename_base}_{uid}.html"
    png_temp = f"chart_temp_{filename_base}_{uid}.png" 
    png_wind = f"chart_wind_{filename_base}_{uid}.png" 

    daily = data['daily']
    dates, t_max, t_min, w_max, w_icons, precips = [], [], [], [], [], []
    
    # Traitement des 16 jours de prévisions
    for i in range(len(daily['time'])):
        dt = datetime.strptime(daily['time'][i], "%Y-%m-%d")
        dates.append(f"{FRENCH_DAY_ABBREVIATIONS[dt.weekday()]} {dt.strftime('%d/%m')}")
        t_max.append(daily['temperature_2m_max'][i])
        t_min.append(daily['temperature_2m_min'][i])
        w_max.append(daily['wind_gusts_10m_max'][i])
        precips.append(daily['precipitation_sum'][i])
        icon, _ = get_wind_direction_icon(daily['wind_direction_10m_dominant'][i])
        w_icons.append(icon)

    # Génération des PNG
    generate_png_daily_temperature_chart(dates, t_max, t_min, daily['weather_code'], precips, data['daily_units']['precipitation_sum'], data['daily_units']['temperature_2m_max'], png_temp)
    generate_png_wind_line_chart(dates, w_max, w_icons, data['daily_units']['wind_gusts_10m_max'], png_wind)

    current = data['current']
    units = data['current_units']
    cond_desc, cond_icon = WMO_CODES.get(current['weather_code'], ("Inconnu", "default.png"))
    wind_icon, _ = get_wind_direction_icon(current['wind_direction_10m'])

    html_content = f"""
    <!DOCTYPE html><html><head><link rel="stylesheet" type="text/css" href="meteo_style.css"></head>
    <body>
        <div align="center" class="header-info"><p>Mise à jour : {datetime.fromisoformat(current['time']).strftime("%d/%m/%Y à %H:%M")}</p></div>
        <div style="padding-left: 5px;">
            <img src="weather_icons/{cond_icon}" width="64" height="64"> <img src="wind_icons/{wind_icon}" width="64" height="64"><br>
            <b>Température :</b> {current['temperature_2m']} {units['temperature_2m']}<br>
            <b>Conditions :</b> {cond_desc}<br>
            <b>Pression :</b> {current['pressure_msl']} {units['pressure_msl']}<br>
            <b>Vent :</b> {current['wind_gusts_10m']} {units['wind_gusts_10m']}<br>
        </div>
        <p align="center"><img src="{png_temp}" width="650"></p>
        <p align="center"><img src="{png_wind}" width="650"></p>
    </body></html>
    """
    with open(html_name, 'w', encoding='utf-8') as f: f.write(html_content)
    return html_name, "Succès"

# =============================================================================
# INTERFACE GRAPHIQUE
# =============================================================================

class LocationTab(ttk.Frame):
    """Onglet de recherche et de gestion des localités favorites."""
    def __init__(self, master, all_locations, saved_selected_locations, save_callback, update_details_callback):
        super().__init__(master, padding="10")
        self.all_locations = all_locations
        self.save_callback = save_callback
        self.update_details_callback = update_details_callback
        self.selected_locations = [Location(*loc) for loc in saved_selected_locations]
        self._current_search_results = []
        self.search_var = tk.StringVar()
        self.create_widgets()
        self.update_selected_listbox()

    def create_widgets(self):
        self.columnconfigure(0, weight=1); self.columnconfigure(1, weight=1); self.rowconfigure(0, weight=1)
        # Recherche
        s_frame = ttk.LabelFrame(self, text="Recherche", padding="10")
        s_frame.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")
        ttk.Entry(s_frame, textvariable=self.search_var).grid(row=0, column=0, sticky="ew")
        self.search_var.trace_add("write", lambda *args: self.update_search_listbox(to_ascii(self.search_var.get())))
        self.search_lb = tk.Listbox(s_frame)
        self.search_lb.grid(row=1, column=0, sticky="nsew", pady=5)
        ttk.Button(s_frame, text="Ajouter >>", command=self.add_selected_location).grid(row=2, column=0)
        # Sélectionnés
        sel_frame = ttk.LabelFrame(self, text="Mes Lieux", padding="10")
        sel_frame.grid(row=0, column=1, padx=10, pady=10, sticky="nsew")
        self.selected_lb = tk.Listbox(sel_frame)
        self.selected_lb.grid(row=0, column=0, sticky="nsew")
        self.selected_lb.bind('<<ListboxSelect>>', self.on_selected_select)
        ttk.Button(sel_frame, text="Supprimer", command=self.remove_selected_location).grid(row=1, column=0)

    def update_search_listbox(self, term):
        self.search_lb.delete(0, tk.END)
        self._current_search_results = [loc for loc in self.all_locations if loc.ascii_name.startswith(term)][:50]
        for loc in self._current_search_results: self.search_lb.insert(tk.END, loc.name)

    def on_selected_select(self, event):
        idx = self.selected_lb.curselection()
        if idx: self.update_details_callback(self.selected_locations[idx[0]])

    def add_selected_location(self):
        idx = self.search_lb.curselection()
        if idx:
            loc = self._current_search_results[idx[0]]
            if loc.ascii_name not in [l.ascii_name for l in self.selected_locations]:
                self.selected_locations.append(loc)
                self.update_selected_listbox()
                self.save_callback([l.to_tuple() for l in self.selected_locations])

    def remove_selected_location(self):
        idx = self.selected_lb.curselection()
        if idx:
            self.selected_locations.pop(idx[0])
            self.update_selected_listbox()
            self.save_callback([l.to_tuple() for l in self.selected_locations])

    def update_selected_listbox(self):
        self.selected_lb.delete(0, tk.END)
        for loc in self.selected_locations:
            self.selected_lb.insert(tk.END, f"{loc.name} ({loc.lat}, {loc.lon})")

class App(tk.Tk):
    """Application principale pilotant les mises à jour et l'affichage."""
    def __init__(self, all_locations, config):
        super().__init__()
        self.title("Prévisions Météo")
        self.geometry(f"{config['width']}x{config['height']}+{config['x']}+{config['y']}")
        self.all_locations, self.config = all_locations, config
        self.selected_locations_data = config['selected_locations']
        self.current_location = None
        self.notebook = ttk.Notebook(self)
        self.notebook.pack(expand=True, fill='both', padx=5, pady=5)
        self.create_details_tab() 
        self.create_location_selection_tab()
        if self.selected_locations_data:
            self.update_details_tab_content(Location(*self.selected_locations_data[0]))
        self.protocol("WM_DELETE_WINDOW", self.on_closing)

    def create_details_tab(self):
        self.details_frame = ttk.Frame(self.notebook, padding="10")
        self.refresh_btn = ttk.Button(self.details_frame, text="Rafraîchir", command=lambda: self.handle_refresh("Manual"), state=tk.DISABLED)
        self.refresh_btn.pack(anchor="nw")
        self.html_display = HTMLLabel(self.details_frame, html="<h1>Sélectionnez une ville</h1>")
        self.html_display.pack(expand=True, fill="both")
        self.notebook.add(self.details_frame, text="Détails")

    def handle_refresh(self, origin):
        """Récupère les données sur l'API Open-Meteo et régénère le rapport."""
        if not self.current_location: return
        path = f"{self.current_location.ascii_name}.json" 
        if origin == "Manual" or not os.path.exists(path):
            url = f"https://api.open-meteo.com/v1/forecast?latitude={self.current_location.lat}&longitude={self.current_location.lon}&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,wind_gusts_10m_max,wind_direction_10m_dominant&current=temperature_2m,precipitation,weather_code,pressure_msl,wind_direction_10m,wind_gusts_10m&timezone=auto&forecast_days=16"
            try:
                r = requests.get(url)
                with open(path, 'w', encoding='utf-8') as f: json.dump(r.json(), f, indent=4)
            except Exception as e: print(f"Erreur API: {e}")
        cleanup_old_files(self.current_location.name)
        html_file, _ = generate_html_report(path, self.current_location.name)
        if html_file:
            with open(html_file, 'r', encoding='utf-8') as f: self.html_display.set_html(f.read())

    def update_details_tab_content(self, loc):
        self.current_location = loc
        if loc:
            self.refresh_btn.config(state=tk.NORMAL)
            self.notebook.tab(0, text=loc.name[:20])
            self.handle_refresh("Auto")

    def create_location_selection_tab(self):
        tab = LocationTab(self.notebook, self.all_locations, self.selected_locations_data, self.update_save, self.update_details_tab_content)
        self.notebook.add(tab, text="Configuration 🌍")
        
    def update_save(self, data): self.selected_locations_data = data

    def on_closing(self):
        save_app_config(self, self.selected_locations_data)
        self.destroy()

# =============================================================================
# LANCEMENT
# =============================================================================

if __name__ == "__main__":
    locs = load_all_locations("localites.txt")
    if locs:
        App(locs, load_app_config()).mainloop()
    else:
        print("Erreur : 'localites.txt' introuvable.")
