#!/usr/bin/env python3
"""
event_form.py — Saisie d'événements → CSV/ICS
concerts-paris.fr

Prérequis : Python 3.8+
  pip install tkinter   (inclus dans la plupart des distributions)

Linux Mint : sudo apt install python3-tk   (si absent)
Windows    : inclus dans l'installateur Python standard
macOS      : inclus dans Python.org installer

Lancement :
  python3 event_form.py
"""

import sys
import os

# ── Vérification tkinter ─────────────────────────────────────────────────────
try:
    import tkinter as tk
    from tkinter import ttk, messagebox, filedialog
except ImportError:
    print("ERREUR : tkinter n'est pas installé.")
    print("Linux  : sudo apt install python3-tk")
    print("Windows/macOS : réinstallez Python depuis python.org")
    sys.exit(1)

import csv
import uuid
from datetime import datetime, timedelta
from io import StringIO

# ══════════════════════════════════════════════════════════════════════════════
# TAXONOMIE
# ══════════════════════════════════════════════════════════════════════════════
TAXONOMY = [
    ("── LIEU ──", None, []),
    ("Grandes salles et théâtres",       False, []),
    ("Salles intimistes",                False, []),
    ("Concerts au musée",                False, []),
    ("Universités et conservatoires",    False, []),
    ("Festivals",                        False, []),
    ("Cafés et bars",                    False, []),
    ("Clubs de jazz",                    False, []),
    ("Centres culturels et fondations",  False, []),
    ("Églises et lieux de culte",        False, []),
    ("Monuments historiques et châteaux",False, []),
    ("Bibliothèques",                    False, []),

    ("── MUSIQUE CLASSIQUE ──", None, []),
    ("Musique classique",                False, []),
    ("Récital de piano",                 True,  []),
    ("Récital lyrique",                  True,  []),
    ("Lied et mélodie",                  True,  []),
    ("Récital de guitare",               True,  []),
    ("Musique de chambre",               True,  []),
    ("Musique sacrée",                   True,  []),
    ("Musique médiévale",                True,  []),
    ("Musique de la Renaissance",        True,  []),
    ("Musique baroque",                  True,  []),
    ("Musique contemporaine",            True,  []),
    ("Symphonique",                      True,  []),
    ("Orgue",                            True,  []),
    ("Musique chorale",                  True,  []),
    ("Électro-acoustique",               True,  []),
    ("Musique concrète",                 True,  []),

    ("── OPÉRA ──", None, []),
    ("Opéra",                            False, []),
    ("Opéra en concert",                 True,  []),
    ("Opéra mis en scène",               True,  []),
    ("Opérette",                         True,  []),
    ("Opéra baroque",                    True,  []),
    ("Tragédie en musique",              True,  []),
    ("Opera seria",                      True,  []),
    ("Opéra contemporain",               True,  []),

    ("── JAZZ ──", None, []),
    ("Jazz",                             False, []),
    ("Improvisation",                    True,  []),
    ("Blues",                            True,  []),
    ("Jazz vocal",                       True,  []),
    ("Free jazz et avant-garde",         True,  []),
    ("Jazz modal",                       True,  []),
    ("Bebop - néo-bop - post-bop",       True,  []),
    ("Fusion",                           True,  []),
    ("Ethno-jazz et afrobeat",           True,  []),
    ("Jazz latin et afro-cubain",        True,  []),
    ("Bossa nova",                       True,  []),
    ("Jazz manouche",                    True,  []),
    ("Nouvelle-Orléans",                 True,  []),

    ("── MUSIQUES DU MONDE ──", None, []),
    ("Musiques du monde",                False, []),
    ("Tango",                            True,  []),
    ("Musiques de l'Inde",               True,  []),
    ("Musiques d'Iran et de Perse",      True,  []),
    ("Musiques d'Arménie",               True,  []),
    ("Musiques de Grèce",                True,  []),
    ("Rebetiko",                         True,  []),
    ("Sevdah",                           True,  []),
    ("Fado",                             True,  []),
    ("Musiques d'Europe centrale",       True,  []),
    ("Musiques tibétaines",              True,  []),
    ("Musiques d'Asie centrale",         True,  []),
    ("Musiques d'Afrique subsaharienne", True,  []),
    ("Musiques du Maghreb",              True,  []),
    ("Musique ottomane et turque",       True,  []),
    ("Musiques de Chine",                True,  []),
    ("Musiques du Japon",                True,  []),
    ("Musiques de Corée",                True,  []),
    ("Flamenco",                         True,  []),
    ("Klezmer et yiddish",               True,  []),
    ("Ladino et romances judéo-espagnoles", True, []),
    ("Traditions juives",                True,  []),
    ("Musique irlandaise",               True,  []),
    ("Musiques d'Amérique latine",       True,  []),
    ("Musiques brésiliennes",            True,  []),

    ("── AUTRES GENRES ──", None, []),
    ("Chanson",                          False, []),
    ("Musiques de film et comédie musicale", False, []),
    ("Néoclassique",                     False, []),
    ("Folk",                             False, []),
    ("Trad-folk et bal",                 True,  []),
    ("Americana",                        True,  []),
    ("Rock",                             False, []),
    ("Musiques urbaines",                False, []),
    ("Électro",                          False, []),
    ("On the dark side",                 False, []),

    ("── TRANSVERSALES ──", None, []),
    ("Jeunesse en scène",                False, []),
    ("Compositrices en lumière",         False, []),
    ("Cultures en dialogue",             False, []),
    ("Les Muses en dialogue",            False, []),
    ("Musique et monde vivant",          False, []),
    ("La musique s'engage",              False, []),
]

CSV_COLS = [
    "Subject", "Start Date", "Start Time", "End Date", "End Time",
    "All Day Event", "Description", "Event Excerpt", "Event Featured Image",
    "Venue Name", "Address", "Zip", "City", "Country", "Venue Website",
    "Show Map", "Show Map Link",
    "Organizer", "Organizer Email", "Organizer Website", "Organizer Phone",
    "Tags", "Event Category", "Event Website", "Event Cost",
]

# ══════════════════════════════════════════════════════════════════════════════
# HELPERS
# ══════════════════════════════════════════════════════════════════════════════
def to_tec_date(d: str) -> str:
    """Accepte YYYY-MM-DD, DD/MM/YYYY, DD/MM/YY, MM/DD/YYYY → MM/DD/YYYY"""
    if not d:
        return ""
    d = d.strip()
    for fmt in ("%Y-%m-%d", "%d/%m/%Y", "%d/%m/%y", "%m/%d/%Y"):
        try:
            return datetime.strptime(d, fmt).strftime("%m/%d/%Y")
        except ValueError:
            continue
    return d  # retour brut si non reconnu

def to_tec_time(t: str) -> str:
    """HH:MM → HH:MM:SS"""
    if not t:
        return ""
    return t + ":00" if len(t) == 5 else t

def add_hours(time_str: str, hours: int = 2) -> str:
    if not time_str:
        return ""
    try:
        t = datetime.strptime(time_str, "%H:%M")
        t += timedelta(hours=hours)
        return t.strftime("%H:%M")
    except ValueError:
        return time_str

def ics_escape(s: str) -> str:
    return s.replace("\\", "\\\\").replace(";", "\\;") \
            .replace(",", "\\,").replace("\n", "\\n")

def ics_fold(line: str) -> str:
    result = ""
    while len(line.encode("utf-8")) > 75:
        cut = 74
        while len(line[:cut].encode("utf-8")) > 74:
            cut -= 1
        result += line[:cut] + "\r\n "
        line = line[cut:]
    return result + line

def to_ics_dt(date_tec: str, time_str: str, all_day: bool) -> tuple:
    """Returns (property_name, value)"""
    if not date_tec:
        return "", ""
    m, d, y = date_tec.split("/")
    if all_day:
        return "DATE", f"{y}{m}{d}"
    h, mi = (time_str[:5].split(":") if time_str else ("00", "00"))
    return "DATETIME", f"{y}{m}{d}T{h}{mi}00"

def strip_html(s: str) -> str:
    import re
    return re.sub(r"<[^>]+>", " ", s).strip()


# ══════════════════════════════════════════════════════════════════════════════
# APPLICATION
# ══════════════════════════════════════════════════════════════════════════════
class EventForm:
    def __init__(self, root: tk.Tk):
        self.root = root
        self.root.title("Saisie événement – concerts-paris.fr")
        self.root.configure(bg="#0f0f10")
        self.root.geometry("520x820")
        self.root.minsize(400, 600)

        self.events: list[dict] = []
        self.tags:   list[str]  = []
        self.cat_vars: dict[str, tk.BooleanVar] = {}

        self._build_styles()
        self._build_ui()
        self._reset_defaults()

    # ── Styles ────────────────────────────────────────────────────────────────
    def _build_styles(self):
        s = ttk.Style()
        s.theme_use("clam")
        bg, fg, acc = "#0f0f10", "#e8e4dc", "#c8a96e"
        surf, brd   = "#18181c", "#2a2a32"

        s.configure(".", background=bg, foreground=fg,
                     font=("Segoe UI", 9), relief="flat")
        s.configure("TFrame",      background=bg)
        s.configure("TLabel",      background=bg, foreground=fg)
        s.configure("TLabelframe", background=bg, foreground=acc,
                     font=("Courier New", 8), relief="flat",
                     borderwidth=1)
        s.configure("TLabelframe.Label", background=bg, foreground=acc,
                     font=("Courier New", 8))
        s.configure("TEntry",   fieldbackground=surf, foreground=fg,
                     bordercolor=brd, insertcolor=fg,
                     relief="flat", padding=4)
        s.map("TEntry", bordercolor=[("focus", acc)])
        s.configure("TCheckbutton", background=bg, foreground=fg,
                     font=("Segoe UI", 8))
        s.map("TCheckbutton", background=[("active", bg)])
        s.configure("Save.TButton", background=acc, foreground="#0f0f10",
                     font=("Courier New", 8, "bold"), padding=(8, 5),
                     relief="flat")
        s.map("Save.TButton", background=[("active", "#d4b87a")])
        s.configure("DL.TButton", background=surf, foreground="#7eb8c8",
                     font=("Courier New", 8), padding=(8, 5),
                     relief="flat")
        s.map("DL.TButton", background=[("active", "#1e2a30")])
        s.configure("Clear.TButton", background=surf, foreground="#c86e6e",
                     font=("Courier New", 8), padding=(8, 5),
                     relief="flat")
        s.configure("TScrollbar", background=brd, troughcolor=surf,
                     arrowcolor=fg, relief="flat")
        s.configure("Status.TLabel", background=bg,
                     font=("Courier New", 8), foreground="#7a7870")

    # ── UI ───────────────────────────────────────────────────────────────────
    def _build_ui(self):
        root = self.root
        BG = "#0f0f10"

        # Scrollable canvas
        canvas = tk.Canvas(root, bg=BG, highlightthickness=0)
        sb = ttk.Scrollbar(root, orient="vertical", command=canvas.yview)
        canvas.configure(yscrollcommand=sb.set)
        sb.pack(side="right", fill="y")
        canvas.pack(side="left", fill="both", expand=True)

        self.frame = ttk.Frame(canvas)
        win = canvas.create_window((0, 0), window=self.frame, anchor="nw")

        def on_configure(e):
            canvas.configure(scrollregion=canvas.bbox("all"))
        def on_canvas_resize(e):
            canvas.itemconfig(win, width=e.width)

        self.frame.bind("<Configure>", on_configure)
        canvas.bind("<Configure>", on_canvas_resize)

        # Mouse wheel
        def _scroll(e):
            canvas.yview_scroll(int(-1 * (e.delta / 120)), "units")
        canvas.bind_all("<MouseWheel>", _scroll)
        canvas.bind_all("<Button-4>",
                        lambda e: canvas.yview_scroll(-1, "units"))
        canvas.bind_all("<Button-5>",
                        lambda e: canvas.yview_scroll(1, "units"))

        f = self.frame
        pad = {"padx": 8, "pady": 2}

        # ── Header ──
        hdr = tk.Frame(f, bg="#18181c")
        hdr.pack(fill="x", padx=8, pady=(10, 6))
        tk.Label(hdr, text="⬡ SAISIE ÉVÉNEMENT",
                 bg="#18181c", fg="#c8a96e",
                 font=("Courier New", 10)).pack(side="left")
        self.lbl_counter = tk.Label(hdr, text="0 événement(s)",
                                    bg="#18181c", fg="#7a7870",
                                    font=("Courier New", 8))
        self.lbl_counter.pack(side="right", padx=6)

        # ── Sections ──
        self._section(f, "IDENTIFICATION")
        self.v_title = self._field(f, "Titre *", width=60, tab=1)

        self._section(f, "DATES & HEURES")
        row_d = ttk.Frame(f); row_d.pack(fill="x", **pad)
        self.v_start_date, self._e_start_date = self._mini(row_d, "Début date *", 0, tab=2, hint="JJ/MM/AAAA")
        self.v_start_time, self._e_start_time = self._mini(row_d, "Heure début", 1, tab=3,
                                        default="20:00", hint="HH:MM")
        self.v_end_date, _ = self._mini(row_d, "Fin date",   2, tab=4, hint="JJ/MM/AAAA (auto)")
        self.v_end_time, _ = self._mini(row_d, "Heure fin",  3, tab=5, hint="HH:MM (auto +2h)")

        self.all_day_var = tk.BooleanVar()
        ttk.Checkbutton(f, text="Événement toute la journée",
                        variable=self.all_day_var,
                        command=self._toggle_allday).pack(
            anchor="w", padx=8, pady=2)

        # Auto-complétion date/heure de fin
        self._e_start_date.bind("<FocusOut>", self._autocomplete_end_date)
        self._e_start_time.bind("<FocusOut>", self._autocomplete_end_time)

        # ── Contenu ──
        self._section(f, "CONTENU")
        self.v_excerpt = self._text(f, "Extrait court", height=3, tab=6,
            hint="Format : Artiste1 (instrument), Artiste2 (tessiture)\n"
                 "         Compositeur1 – Œuvre1, Œuvre2 ; Compositeur2 – Œuvre1")
        self.v_desc    = self._text(f, "Description complète", height=5, tab=7,
            hint="HTML accepté. Inclure un lien vers la source recommandé.\n"
                 "Ne pas répéter l'image si le champ 'Image à la une' est renseigné.")

        # ── Lieu ──
        self._section(f, "LIEU")
        row_l = ttk.Frame(f); row_l.pack(fill="x", **pad)
        # Venue : Combobox avec suggestions depuis VENUES
        _fr_venue = tk.Frame(row_l, bg="#0f0f10")
        _fr_venue.grid(row=0, column=0, columnspan=2, sticky="ew", padx=(0, 4))
        row_l.columnconfigure(0, weight=1)
        tk.Label(_fr_venue, text="Salle / lieu *", bg="#0f0f10", fg="#7a7870",
                 font=("Segoe UI", 8)).pack(anchor="w")
        self.v_venue = tk.StringVar()
        self._e_venue = ttk.Combobox(_fr_venue, textvariable=self.v_venue,
                                     values=[v["name"] for v in VENUES],
                                     font=("Segoe UI", 9))
        self._e_venue.pack(fill="x")
        self.v_addr, _   = self._mini(row_l, "Adresse", 2, span=2, tab=9)
        row_l2 = ttk.Frame(f); row_l2.pack(fill="x", **pad)
        self.v_zip,     _ = self._mini(row_l2, "Code postal", 0, tab=10)
        self.v_city,    _ = self._mini(row_l2, "Ville", 1, tab=11, default="Paris")
        self.v_country, _ = self._mini(row_l2, "Pays",  2, tab=12, default="France")

        # ── Organisateur ──
        self._section(f, "ORGANISATEUR")
        row_o = ttk.Frame(f); row_o.pack(fill="x", **pad)
        self.v_org_name, _  = self._mini(row_o, "Nom",       0, tab=12)
        self.v_org_phone, _ = self._mini(row_o, "Téléphone", 1, tab=13)
        row_o2 = ttk.Frame(f); row_o2.pack(fill="x", **pad)
        self.v_org_email, _ = self._mini(row_o2, "Email",    0, tab=14)
        self.v_org_web, _   = self._mini(row_o2, "Site web", 1, tab=15)

        # ── Image & liens ──
        self._section(f, "IMAGE & LIENS")
        self.v_image_url = self._field(f, "URL image à la une", tab=16)
        self.v_event_url = self._field(f, "URL source / réservation", tab=17)

        # ── Tarif ──
        self._section(f, "TARIF")
        self.v_cost = self._field(f, "Tarif(s) en texte libre",
                                   placeholder="Ex. : Plein tarif : 20€ | Réduit : 15€",
                                   tab=18)

        # ── Catégories ──
        self._section(f, "CATÉGORIES TEC")
        cat_frame = tk.Frame(f, bg="#18181c", relief="flat",
                             highlightthickness=1,
                             highlightbackground="#2a2a32")
        cat_frame.pack(fill="x", padx=8, pady=2)

        cat_inner = tk.Frame(cat_frame, bg="#18181c")
        cat_inner.pack(fill="both", expand=True)

        cat_canvas = tk.Canvas(cat_inner, bg="#18181c",
                               height=140, highlightthickness=0)
        cat_sb = ttk.Scrollbar(cat_inner, orient="vertical",
                               command=cat_canvas.yview)
        cat_canvas.configure(yscrollcommand=cat_sb.set)
        cat_sb.pack(side="right", fill="y")
        cat_canvas.pack(side="left", fill="both", expand=True)

        cat_list = tk.Frame(cat_canvas, bg="#18181c")
        cat_canvas.create_window((0, 0), window=cat_list, anchor="nw")
        cat_list.bind("<Configure>",
                      lambda e: cat_canvas.configure(
                          scrollregion=cat_canvas.bbox("all")))

        for name, is_child, _ in TAXONOMY:
            if name.startswith("──"):
                tk.Label(cat_list, text=name,
                         bg="#18181c", fg="#7eb8c8",
                         font=("Courier New", 7)).pack(
                    anchor="w", padx=6, pady=(4, 1))
            else:
                var = tk.BooleanVar()
                self.cat_vars[name] = var
                indent = 20 if is_child else 6
                ttk.Checkbutton(cat_list, text=name, variable=var).pack(
                    anchor="w", padx=indent)

        tk.Label(f, text="Catégories supplémentaires (texte libre, séparées par des virgules)",
                 bg="#0f0f10", fg="#7a7870",
                 font=("Segoe UI", 8)).pack(anchor="w", padx=8, pady=(4, 1))
        tk.Label(f, text="  Ex. : Jeunesse en scène, Compositrices en lumière",
                 bg="#0f0f10", fg="#555050",
                 font=("Segoe UI", 7)).pack(anchor="w", padx=8)
        self.v_cat_extra = self._field(f, "", tab=19)

        # ── Tags ──
        self._section(f, "TAGS")
        tk.Label(f,
                 text="Tags (mots-clés pour la recherche fine) — Entrée ou , pour valider — ← pour effacer",
                 bg="#0f0f10", fg="#7a7870",
                 font=("Segoe UI", 8)).pack(anchor="w", padx=8)
        tk.Label(f, text="  Ex. : setar, kamantché, Iran, musique persane",
                 bg="#0f0f10", fg="#555050",
                 font=("Segoe UI", 7)).pack(anchor="w", padx=8)

        self.tags_frame = tk.Frame(f, bg="#18181c",
                                   highlightthickness=1,
                                   highlightbackground="#2a2a32")
        self.tags_frame.pack(fill="x", padx=8, pady=2)
        self.tag_chips_frame = tk.Frame(self.tags_frame, bg="#18181c")
        self.tag_chips_frame.pack(fill="x", padx=2, pady=2)

        self.tag_entry_var = tk.StringVar()
        self.tag_entry = tk.Entry(self.tags_frame,
                                  textvariable=self.tag_entry_var,
                                  bg="#18181c", fg="#e8e4dc",
                                  insertbackground="#e8e4dc",
                                  relief="flat", font=("Segoe UI", 9))
        self.tag_entry.pack(fill="x", padx=4, pady=2)
        self.tag_entry.bind("<Return>",    self._add_tag_from_entry)
        self.tag_entry.bind("<comma>",     self._add_tag_from_entry)
        self.tag_entry.bind("<comma>",     self._add_tag_from_entry)
        self.tag_entry.bind("<BackSpace>", self._bs_tag)
        self.tag_entry.configure(takefocus=True)

        # ── Boutons ──
        btn_frame = tk.Frame(f, bg="#0f0f10")
        btn_frame.pack(fill="x", padx=8, pady=(12, 4))

        ttk.Button(btn_frame, text="＋ Enregistrer / suivant",
                   style="Save.TButton",
                   command=self._save).pack(side="left", padx=(0, 4))
        ttk.Button(btn_frame, text="↓ CSV",
                   style="DL.TButton",
                   command=self._dl_csv).pack(side="left", padx=2)
        ttk.Button(btn_frame, text="↓ ICS",
                   style="DL.TButton",
                   command=self._dl_ics).pack(side="left", padx=2)
        ttk.Button(btn_frame, text="✕ Effacer tout",
                   style="Clear.TButton",
                   command=self._clear_all).pack(side="right")

        # ── Status ──
        self.lbl_status = ttk.Label(f, text="", style="Status.TLabel")
        self.lbl_status.pack(anchor="w", padx=8, pady=(2, 6))

        # ── Liste événements ──
        self._section(f, "ÉVÉNEMENTS EN MÉMOIRE")
        self.list_frame = tk.Frame(f, bg="#0f0f10")
        self.list_frame.pack(fill="x", padx=8, pady=(0, 12))

    # ── Widgets helpers ───────────────────────────────────────────────────────
    def _section(self, parent, label):
        fr = tk.Frame(parent, bg="#0f0f10")
        fr.pack(fill="x", padx=8, pady=(8, 2))
        tk.Label(fr, text=label, bg="#0f0f10", fg="#7a7870",
                 font=("Courier New", 8)).pack(side="left")
        tk.Frame(fr, bg="#2a2a32", height=1).pack(
            side="left", fill="x", expand=True, padx=(6, 0))

    def _field(self, parent, label, width=None, placeholder="",
               tab=0, default=""):
        if label:
            tk.Label(parent, text=label, bg="#0f0f10", fg="#7a7870",
                     font=("Segoe UI", 8)).pack(anchor="w", padx=8)
        var = tk.StringVar(value=default)
        e = ttk.Entry(parent, textvariable=var, width=width)
        e.pack(fill="x", padx=8, pady=1)
        if tab:
            e.configure(takefocus=True)
        return var

    def _text(self, parent, label, height=3, tab=0, hint=""):
        lbl_text = label
        tk.Label(parent, text=lbl_text, bg="#0f0f10", fg="#7a7870",
                 font=("Segoe UI", 8)).pack(anchor="w", padx=8)
        if hint:
            tk.Label(parent, text=hint, bg="#0f0f10", fg="#555050",
                     font=("Segoe UI", 7), justify="left").pack(
                anchor="w", padx=8)
        t = tk.Text(parent, height=height, bg="#18181c", fg="#e8e4dc",
                    insertbackground="#e8e4dc", relief="flat",
                    font=("Segoe UI", 9), padx=4, pady=4,
                    wrap="word",
                    highlightthickness=1,
                    highlightbackground="#2a2a32",
                    highlightcolor="#c8a96e",
                    tabs="4")
        # Tab navigue vers le champ suivant (ne pas capturer pour indentation)
        t.bind("<Tab>",       lambda e: (t.tk_focusNext().focus(), "break")[1])
        t.bind("<Shift-Tab>", lambda e: (t.tk_focusPrev().focus(), "break")[1])
        t.pack(fill="x", padx=8, pady=1)
        return t

    def _mini(self, parent, label, col, span=1, tab=0, default="", hint=""):
        fr = tk.Frame(parent, bg="#0f0f10")
        fr.grid(row=0, column=col, columnspan=span,
                sticky="ew", padx=(0, 4))
        parent.columnconfigure(col, weight=1)
        tk.Label(fr, text=label, bg="#0f0f10", fg="#7a7870",
                 font=("Segoe UI", 8)).pack(anchor="w")
        if hint:
            tk.Label(fr, text=hint, bg="#0f0f10", fg="#555050",
                     font=("Segoe UI", 7)).pack(anchor="w")
        var = tk.StringVar(value=default)
        e = ttk.Entry(fr, textvariable=var)
        e.pack(fill="x")
        return var, e

    # ── Auto-complétion date/heure de fin ───────────────────────────────────
    def _autocomplete_end_date(self, event=None):
        """Copie la date de début dans la date de fin si vide."""
        if not self.v_end_date.get():
            self.v_end_date.set(self.v_start_date.get())

    def _autocomplete_end_time(self, event=None):
        """Calcule heure de fin = début + 2h si vide."""
        if not self.v_end_time.get():
            t = self.v_start_time.get().strip()
            self.v_end_time.set(add_hours(t, 2))

    # ── All-day toggle ────────────────────────────────────────────────────────
    def _toggle_allday(self):
        # nothing to disable in StringVar-based entries
        pass

    # ── Tags ─────────────────────────────────────────────────────────────────
    def _add_tag_from_entry(self, event=None):
        raw = self.tag_entry_var.get().strip().rstrip(",").strip()
        # Sur l'événement comma, la virgule est déjà dans le champ — on l'ignore
        raw = raw.rstrip(",").strip()
        if raw:
            for t in raw.split(","):
                t = t.strip()
                if t and t not in self.tags:
                    self.tags.append(t)
            self.tag_entry_var.set("")
            self._render_tags()
        return "break"

    def _bs_tag(self, event=None):
        if not self.tag_entry_var.get() and self.tags:
            self.tags.pop()
            self._render_tags()

    def _render_tags(self):
        for w in self.tag_chips_frame.winfo_children():
            w.destroy()
        for i, t in enumerate(self.tags):
            fr = tk.Frame(self.tag_chips_frame, bg="#2a2a38",
                          highlightthickness=1,
                          highlightbackground="#3a3a48")
            fr.pack(side="left", padx=2, pady=2)
            tk.Label(fr, text=t, bg="#2a2a38", fg="#e8e4dc",
                     font=("Segoe UI", 8), padx=4).pack(side="left")
            tk.Label(fr, text="×", bg="#2a2a38", fg="#7a7870",
                     font=("Segoe UI", 9), cursor="hand2",
                     padx=2).pack(side="left")
            fr.winfo_children()[-1].bind(
                "<Button-1>",
                lambda e, idx=i: self._remove_tag(idx))

    def _remove_tag(self, idx):
        self.tags.pop(idx)
        self._render_tags()

    # ── Collect ───────────────────────────────────────────────────────────────
    def _get_text(self, widget) -> str:
        return widget.get("1.0", "end").strip()

    def _collect(self) -> dict:
        all_day = self.all_day_var.get()
        sd = to_tec_date(self.v_start_date.get().strip())
        ed = to_tec_date(self.v_end_date.get().strip()) or sd
        st = to_tec_time(self.v_start_time.get().strip()) if not all_day else ""
        et = self.v_end_time.get().strip()
        if not all_day:
            if not et and st:
                et = add_hours(self.v_start_time.get().strip(), 2)
            et = to_tec_time(et)

        cats_checked = [name for name, var in self.cat_vars.items()
                        if var.get()]
        cats_extra = [c.strip() for c in
                      self.v_cat_extra.get().split(",") if c.strip()]
        cats = list(dict.fromkeys(cats_checked + cats_extra))

        return {
            "Subject":              self.v_title.get().strip(),
            "Start Date":           sd,
            "Start Time":           st,
            "End Date":             ed,
            "End Time":             et,
            "All Day Event":        "TRUE" if all_day else "FALSE",
            "Description":          self._get_text(self.v_desc),
            "Event Excerpt":        self._get_text(self.v_excerpt),
            "Event Featured Image": self.v_image_url.get().strip(),
            "Venue Name":           self.v_venue.get().strip(),
            "Address":              self.v_addr.get().strip(),
            "Zip":                  self.v_zip.get().strip(),
            "City":                 self.v_city.get().strip(),
            "Country":              self.v_country.get().strip(),
            "Venue Website":        self.v_org_web.get().strip(),
            "Show Map":             "TRUE",
            "Show Map Link":        "TRUE",
            "Organizer":            self.v_org_name.get().strip(),
            "Organizer Email":      self.v_org_email.get().strip(),
            "Organizer Website":    self.v_org_web.get().strip(),
            "Organizer Phone":      self.v_org_phone.get().strip(),
            "Tags":                 ",".join(self.tags),
            "Event Category":       ",".join(cats),
            "Event Website":        self.v_event_url.get().strip(),
            "Event Cost":           self.v_cost.get().strip(),
        }

    # ── Validation ────────────────────────────────────────────────────────────
    def _validate(self, ev: dict) -> list[str]:
        errs = []
        if not ev["Subject"]:     errs.append("Titre manquant")
        if not ev["Start Date"]:  errs.append("Date de début manquante")
        if not ev["Venue Name"]:  errs.append("Lieu manquant")
        return errs

    # ── Save ──────────────────────────────────────────────────────────────────
    def _save(self):
        ev = self._collect()
        errs = self._validate(ev)
        if errs:
            self._status(" | ".join(errs), "error")
            return
        self.events.append(ev)
        n = len(self.events)
        self._status(f"✓ Événement {n} enregistré : {ev['Subject']}", "ok")
        self._update_counter()
        self._render_list()
        self._reset_form()
        # focus sur le titre
        self.frame.winfo_children()[1].winfo_children()[1].focus_set()

    def _reset_form(self):
        self.v_title.set("")
        self.v_start_date.set("")
        self.v_start_time.set("20:00")
        self.v_end_date.set("")
        self.v_end_time.set("")
        self.all_day_var.set(False)
        self.v_excerpt.delete("1.0", "end")
        self.v_desc.delete("1.0", "end")
        self.v_venue.set("")
        self.v_addr.set("")
        self.v_image_url.set("")
        self.v_event_url.set("")
        self.v_cost.set("")
        self.v_cat_extra.set("")
        for var in self.cat_vars.values():
            var.set(False)
        self.tags = []
        self._render_tags()
        self.tag_entry_var.set("")

    def _reset_defaults(self):
        self.v_city.set("Paris")
        self.v_country.set("France")

    # ── List ──────────────────────────────────────────────────────────────────
    def _render_list(self):
        for w in self.list_frame.winfo_children():
            w.destroy()
        for i, ev in enumerate(self.events):
            fr = tk.Frame(self.list_frame, bg="#18181c",
                          highlightthickness=1,
                          highlightbackground="#2a2a32")
            fr.pack(fill="x", pady=1)
            tk.Label(fr, text=ev["Start Date"], bg="#18181c",
                     fg="#7a7870", font=("Courier New", 8),
                     width=10).pack(side="left", padx=4)
            title = ev["Subject"][:50] + ("…" if len(ev["Subject"]) > 50 else "")
            tk.Label(fr, text=title, bg="#18181c", fg="#e8e4dc",
                     font=("Segoe UI", 8), anchor="w").pack(
                side="left", fill="x", expand=True)
            tk.Label(fr, text="×", bg="#18181c", fg="#7a7870",
                     font=("Segoe UI", 10), cursor="hand2",
                     padx=4).pack(side="right")
            fr.winfo_children()[-1].bind(
                "<Button-1>",
                lambda e, idx=i: self._delete_event(idx))

    def _delete_event(self, idx):
        self.events.pop(idx)
        self._update_counter()
        self._render_list()

    def _update_counter(self):
        self.lbl_counter.config(
            text=f"{len(self.events)} événement(s) en mémoire")

    # ── Status ────────────────────────────────────────────────────────────────
    def _status(self, msg, kind="info"):
        colors = {"ok": "#6ec88a", "error": "#c86e6e", "info": "#7a7870"}
        self.lbl_status.config(text=msg,
                               foreground=colors.get(kind, "#7a7870"))

    # ── CSV ───────────────────────────────────────────────────────────────────
    def _dl_csv(self):
        if not self.events:
            self._status("Aucun événement à exporter.", "error")
            return
        path = filedialog.asksaveasfilename(
            defaultextension=".csv",
            filetypes=[("CSV", "*.csv"), ("Tous", "*.*")],
            initialfile=f"evenements_{datetime.now().strftime('%Y%m%d')}.csv"
        )
        if not path:
            return
        with open(path, "w", newline="", encoding="utf-8-sig") as f:
            w = csv.DictWriter(f, fieldnames=CSV_COLS, extrasaction="ignore")
            w.writeheader()
            w.writerows(self.events)
        self._status(f"✓ CSV enregistré : {os.path.basename(path)}", "ok")

    # ── ICS ───────────────────────────────────────────────────────────────────
    def _dl_ics(self):
        if not self.events:
            self._status("Aucun événement à exporter.", "error")
            return
        path = filedialog.asksaveasfilename(
            defaultextension=".ics",
            filetypes=[("iCalendar", "*.ics"), ("Tous", "*.*")],
            initialfile=f"evenements_{datetime.now().strftime('%Y%m%d')}.ics"
        )
        if not path:
            return

        now = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
        lines = [
            "BEGIN:VCALENDAR",
            "VERSION:2.0",
            "PRODID:-//concerts-paris.fr//Event Form//FR",
            "CALSCALE:GREGORIAN",
        ]
        for ev in self.events:
            all_day = ev["All Day Event"] == "TRUE"
            dt_type, dt_start = to_ics_dt(
                ev["Start Date"], ev["Start Time"], all_day)
            _,         dt_end   = to_ics_dt(
                ev["End Date"] or ev["Start Date"],
                ev["End Time"] or ev["Start Time"], all_day)
            loc = ", ".join(filter(None, [
                ev["Venue Name"], ev["Address"],
                ev["City"], ev["Country"]]))
            desc = strip_html(ev.get("Description", ""))

            lines += ["BEGIN:VEVENT"]
            lines += [ics_fold(f"UID:{uuid.uuid4()}@concerts-paris.fr")]
            lines += [ics_fold(f"DTSTAMP:{now}")]
            lines += [ics_fold(f"DTSTART;VALUE={dt_type}:{dt_start}")]
            lines += [ics_fold(f"DTEND;VALUE={dt_type}:{dt_end}")]
            lines += [ics_fold(f"SUMMARY:{icsEscape(ev['Subject'])}")]
            if loc:
                lines += [ics_fold(f"LOCATION:{ics_escape(loc)}")]
            if desc:
                lines += [ics_fold(f"DESCRIPTION:{ics_escape(desc)}")]
            if ev.get("Event Website"):
                lines += [ics_fold(f"URL:{ev['Event Website']}")]
            if ev.get("Event Category"):
                lines += [ics_fold(
                    f"CATEGORIES:{ics_escape(ev['Event Category'])}")]
            if ev.get("Organizer"):
                email = ev.get("Organizer Email") or "noreply@concerts-paris.fr"
                lines += [ics_fold(
                    f"ORGANIZER;CN={ics_escape(ev['Organizer'])}:MAILTO:{email}")]
            lines += ["END:VEVENT"]

        lines += ["END:VCALENDAR"]

        with open(path, "w", encoding="utf-8") as f:
            f.write("\r\n".join(lines))
        self._status(f"✓ ICS enregistré : {os.path.basename(path)}", "ok")

    # ── Clear all ─────────────────────────────────────────────────────────────
    def _clear_all(self):
        if self.events:
            if not messagebox.askyesno(
                    "Effacer tout",
                    f"Effacer les {len(self.events)} événement(s) en mémoire ?"):
                return
        self.events = []
        self._update_counter()
        self._render_list()
        self._reset_form()
        self._status("Session effacée.", "info")


def icsEscape(s):
    return ics_escape(s)


# ══════════════════════════════════════════════════════════════════════════════
# MAIN
# ══════════════════════════════════════════════════════════════════════════════
if __name__ == "__main__":
    root = tk.Tk()
    app = EventForm(root)
    root.mainloop()
