//! `tahuantinsuyu-tree` — explorador jerárquico Groups → Contacts → Charts.
//!
//! Envuelve [`yahweh_widget_tree::TreeView`] con la lógica de dominio
//! de Tahuantinsuyu. Los `RowId` codifican el tipo con prefijo:
//!
//! - `g:<ulid>` → Group
//! - `c:<ulid>` → Contact
//! - `h:<ulid>` → Chart
//!
//! ## Fase 2 — CRUD UX
//!
//! - **Right-click** abre un menú contextual cuyas opciones dependen
//!   del target (raíz, group, contact o chart).
//! - **Renombrar** y **crear** abren un modal con un `TextInput`.
//! - **Crear carta** abre un formulario con los campos mínimos de
//!   `StoredBirthData` (year/month/day/hour/min/tz/lat/lon).
//! - **Borrar** pide confirmación con `window.prompt`.
//!
//! El host (la app) se suscribe a [`TreeEvent`] y traduce a `AppEvent`
//! del bus de yahweh para que el canvas/panel reaccionen.

#![forbid(unsafe_code)]
#![warn(rust_2018_idioms)]

use std::collections::HashSet;

use gpui::{
    ClickEvent, Context, Entity, EventEmitter, IntoElement, Pixels, Point, PromptLevel, Render,
    SharedString, Window, div, hsla, prelude::*, px,
};

use tahuantinsuyu_model::{
    ChartId, ChartKind, ContactId, GroupId, StoredBirthData, StoredChartConfig, TimeCertainty,
    TreeSelection,
};
use tahuantinsuyu_store::Store;
use yahweh_theme::Theme;
use yahweh_widget_text_input::{TextInput, TextInputEvent};
use yahweh_widget_tree::{RowId, RowKind, TreeEvent as InnerTreeEvent, TreeRow, TreeView};

const PREFIX_GROUP: &str = "g:";
const PREFIX_CONTACT: &str = "c:";
const PREFIX_CHART: &str = "h:";

// =====================================================================
// Eventos públicos
// =====================================================================

#[derive(Clone, Debug)]
pub enum TreeEvent {
    Selected(TreeSelection),
    Opened(TreeSelection),
    /// Una mutación de la jerarquía aconteció (crear, borrar, renombrar).
    /// El host puede usarlo para invalidar caches en otros widgets.
    HierarchyChanged,
}

// =====================================================================
// Estado interno
// =====================================================================

/// Target del menú contextual / acciones.
#[derive(Clone, Debug)]
enum MenuTarget {
    Root,
    Group(GroupId),
    Contact(ContactId),
    Chart(ChartId),
}

impl MenuTarget {
    fn from_selection(sel: &TreeSelection) -> Self {
        match sel {
            TreeSelection::Group(id) => MenuTarget::Group(*id),
            TreeSelection::Contact(id) => MenuTarget::Contact(*id),
            TreeSelection::Chart(id) => MenuTarget::Chart(*id),
        }
    }
}

#[derive(Clone, Debug)]
struct MenuState {
    target: MenuTarget,
    position: Point<Pixels>,
}

/// Modal flotante. Una sola `Modal` activa a la vez — la app no
/// soporta editar varias cosas en simultáneo.
enum Modal {
    RenameGroup {
        id: GroupId,
        input: Entity<TextInput>,
    },
    RenameContact {
        id: ContactId,
        input: Entity<TextInput>,
    },
    RenameChart {
        id: ChartId,
        input: Entity<TextInput>,
    },
    CreateGroup {
        parent: Option<GroupId>,
        input: Entity<TextInput>,
    },
    CreateContact {
        group: Option<GroupId>,
        input: Entity<TextInput>,
    },
    CreateChart {
        contact: ContactId,
        form: ChartForm,
        error: Option<SharedString>,
    },
}

struct ChartForm {
    name: Entity<TextInput>,
    place: Entity<TextInput>,
    year: Entity<TextInput>,
    month: Entity<TextInput>,
    day: Entity<TextInput>,
    hour: Entity<TextInput>,
    minute: Entity<TextInput>,
    tz_offset_min: Entity<TextInput>,
    lat: Entity<TextInput>,
    lon: Entity<TextInput>,
    alt: Entity<TextInput>,
}

// =====================================================================
// Widget
// =====================================================================

pub struct TahuantinsuyuTree {
    store: Store,
    inner: Entity<TreeView>,
    expanded: HashSet<String>,
    menu: Option<MenuState>,
    modal: Option<Modal>,
}

impl EventEmitter<TreeEvent> for TahuantinsuyuTree {}

impl TahuantinsuyuTree {
    pub fn new(store: Store, cx: &mut Context<Self>) -> Self {
        cx.observe_global::<Theme>(|_, cx| cx.notify()).detach();

        let inner = cx.new(|cx| TreeView::new("tahuantinsuyu-tree", cx));
        cx.subscribe(&inner, |this: &mut Self, _, ev, cx| {
            this.on_inner(ev, cx);
        })
        .detach();

        let mut me = Self {
            store,
            inner,
            expanded: HashSet::new(),
            menu: None,
            modal: None,
        };
        me.refresh(cx);
        me
    }

    pub fn refresh(&mut self, cx: &mut Context<Self>) {
        let mut rows = Vec::new();
        self.append_groups(None, 0, &mut rows);
        self.append_contacts(None, 0, &mut rows);
        self.inner.update(cx, |t, cx| t.set_rows(rows, cx));
    }

    fn append_groups(&self, parent: Option<GroupId>, depth: u32, out: &mut Vec<TreeRow>) {
        let groups = match self.store.list_groups(parent) {
            Ok(v) => v,
            Err(_) => return,
        };
        for g in groups {
            let id_str = format!("{}{}", PREFIX_GROUP, g.id);
            let expanded = self.expanded.contains(&id_str);
            out.push(TreeRow {
                id: RowId::new(id_str.clone()),
                label: g.name.clone(),
                depth,
                kind: RowKind::Branch,
                expanded,
                icon: Some("📁".into()),
            });
            if expanded {
                self.append_groups(Some(g.id), depth + 1, out);
                self.append_contacts(Some(g.id), depth + 1, out);
            }
        }
    }

    fn append_contacts(&self, parent: Option<GroupId>, depth: u32, out: &mut Vec<TreeRow>) {
        let contacts = match self.store.list_contacts(parent) {
            Ok(v) => v,
            Err(_) => return,
        };
        for c in contacts {
            let id_str = format!("{}{}", PREFIX_CONTACT, c.id);
            let expanded = self.expanded.contains(&id_str);
            out.push(TreeRow {
                id: RowId::new(id_str.clone()),
                label: c.name.clone(),
                depth,
                kind: RowKind::Branch,
                expanded,
                icon: Some("🜨".into()),
            });
            if expanded {
                self.append_charts(c.id, depth + 1, out);
            }
        }
    }

    fn append_charts(&self, contact: ContactId, depth: u32, out: &mut Vec<TreeRow>) {
        let charts = match self.store.list_charts(contact) {
            Ok(v) => v,
            Err(_) => return,
        };
        for h in charts {
            let id_str = format!("{}{}", PREFIX_CHART, h.id);
            out.push(TreeRow {
                id: RowId::new(id_str),
                label: h.label.clone(),
                depth,
                kind: RowKind::Leaf,
                expanded: false,
                icon: Some("✦".into()),
            });
        }
    }

    fn on_inner(&mut self, ev: &InnerTreeEvent, cx: &mut Context<Self>) {
        match ev {
            InnerTreeEvent::ChevronToggled(id) => {
                let s = id.as_str().to_string();
                if !self.expanded.remove(&s) {
                    self.expanded.insert(s);
                }
                self.refresh(cx);
            }
            InnerTreeEvent::RowClicked(id) => {
                if self.menu.is_some() {
                    self.menu = None;
                    cx.notify();
                }
                if let Some(sel) = parse_row(id) {
                    cx.emit(TreeEvent::Selected(sel));
                }
            }
            InnerTreeEvent::RowDoubleClicked(id) => {
                if let Some(sel) = parse_row(id) {
                    cx.emit(TreeEvent::Opened(sel));
                }
            }
            InnerTreeEvent::ContextMenuRequested { id, position } => {
                let target = match id.as_ref().and_then(parse_row) {
                    Some(sel) => MenuTarget::from_selection(&sel),
                    None => MenuTarget::Root,
                };
                self.menu = Some(MenuState {
                    target,
                    position: *position,
                });
                cx.notify();
            }
            InnerTreeEvent::ActiveChanged(_) => {}
        }
    }

    // -----------------------------------------------------------------
    // Acciones del menú
    // -----------------------------------------------------------------

    fn close_menu(&mut self, cx: &mut Context<Self>) {
        if self.menu.take().is_some() {
            cx.notify();
        }
    }

    fn close_modal(&mut self, cx: &mut Context<Self>) {
        if self.modal.take().is_some() {
            cx.notify();
        }
    }

    fn open_create_group(
        &mut self,
        parent: Option<GroupId>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let input = self.make_input("Nombre del grupo", "", window, cx);
        self.modal = Some(Modal::CreateGroup { parent, input });
        self.close_menu(cx);
    }

    fn open_create_contact(
        &mut self,
        group: Option<GroupId>,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let input = self.make_input("Nombre del contacto", "", window, cx);
        self.modal = Some(Modal::CreateContact { group, input });
        self.close_menu(cx);
    }

    fn open_create_chart(
        &mut self,
        contact: ContactId,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        // Pre-cargamos el nombre del contacto en el campo "Sujeto" del
        // form como conveniencia — la mayoría de las cartas se nombran
        // igual que la persona.
        let subject_name = self
            .store
            .list_contacts(None)
            .ok()
            .and_then(|all| {
                // Buscamos linealmente — list_contacts solo lista hijos
                // directos del group, no nos sirve para encontrar un
                // contact arbitrario. Para fase 2 nos quedamos con
                // "Carta natal" como label genérico si no podemos
                // resolver. Resolver-por-id viene si lo necesitamos.
                let _ = all;
                None::<String>
            })
            .unwrap_or_else(|| "Carta natal".into());

        let form = ChartForm {
            name: self.make_input("Etiqueta de la carta", &subject_name, window, cx),
            place: self.make_input("Lugar (ciudad, país)", "", window, cx),
            year: self.make_input("Año", "1987", window, cx),
            month: self.make_input("Mes", "3", window, cx),
            day: self.make_input("Día", "14", window, cx),
            hour: self.make_input("Hora (0-23)", "5", window, cx),
            minute: self.make_input("Minuto", "22", window, cx),
            tz_offset_min: self.make_input("TZ offset (min)", "-240", window, cx),
            lat: self.make_input("Latitud (°)", "10.4806", window, cx),
            lon: self.make_input("Longitud (°)", "-66.9036", window, cx),
            alt: self.make_input("Altitud (m)", "900", window, cx),
        };
        // El primer field es el que recibe focus.
        form.name.update(cx, |i, _| i.request_focus(window));

        self.modal = Some(Modal::CreateChart {
            contact,
            form,
            error: None,
        });
        self.close_menu(cx);
    }

    fn open_rename(&mut self, target: MenuTarget, window: &mut Window, cx: &mut Context<Self>) {
        let modal = match target {
            MenuTarget::Group(id) => {
                let current = self
                    .store
                    .list_groups(None)
                    .ok()
                    .and_then(|all| find_group_name(&all, &self.store, id))
                    .unwrap_or_default();
                Modal::RenameGroup {
                    id,
                    input: self.make_input("Nuevo nombre", &current, window, cx),
                }
            }
            MenuTarget::Contact(id) => {
                let current = self
                    .store
                    .list_contacts(None)
                    .ok()
                    .and_then(|all| find_contact_name(&all, &self.store, id))
                    .unwrap_or_default();
                Modal::RenameContact {
                    id,
                    input: self.make_input("Nuevo nombre", &current, window, cx),
                }
            }
            MenuTarget::Chart(id) => {
                let current = self
                    .store
                    .get_chart(id)
                    .ok()
                    .map(|c| c.label)
                    .unwrap_or_default();
                Modal::RenameChart {
                    id,
                    input: self.make_input("Nueva etiqueta", &current, window, cx),
                }
            }
            MenuTarget::Root => return,
        };
        self.modal = Some(modal);
        self.close_menu(cx);
    }

    /// Crea un `TextInput` con focus y suscripción a Confirmed/Cancelled.
    /// La closure decide qué hacer con cada evento — guardamos la subscripción
    /// detached para que viva mientras el modal exista.
    fn make_input(
        &self,
        placeholder: &str,
        initial: &str,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) -> Entity<TextInput> {
        let placeholder = placeholder.to_string();
        let input = cx.new(|cx| {
            TextInput::new(initial.to_string(), cx)
                .with_placeholder(SharedString::from(placeholder.clone()))
        });
        cx.subscribe(&input, |this: &mut Self, _, ev: &TextInputEvent, cx| {
            this.on_input_event(ev, cx);
        })
        .detach();
        input.update(cx, |i, _| i.request_focus(window));
        input
    }

    fn on_input_event(&mut self, ev: &TextInputEvent, cx: &mut Context<Self>) {
        match ev {
            TextInputEvent::Cancelled => self.close_modal(cx),
            TextInputEvent::Confirmed(value) => self.submit_modal(value.clone(), cx),
        }
    }

    fn submit_modal(&mut self, value: String, cx: &mut Context<Self>) {
        let trimmed = value.trim().to_string();
        // Tomamos ownership del modal — si el submit falla en mitad,
        // lo restablecemos. Esto evita un borrow-mut sobre self.modal.
        let modal = match self.modal.take() {
            Some(m) => m,
            None => return,
        };
        match modal {
            Modal::RenameGroup { id, input } => {
                if !trimmed.is_empty() {
                    let _ = self.store.rename_group(id, &trimmed);
                }
                drop(input);
                self.after_mutation(cx);
            }
            Modal::RenameContact { id, input } => {
                if !trimmed.is_empty() {
                    let _ = self.store.rename_contact(id, &trimmed);
                }
                drop(input);
                self.after_mutation(cx);
            }
            Modal::RenameChart { id, input } => {
                if !trimmed.is_empty() {
                    let _ = self.store.rename_chart(id, &trimmed);
                }
                drop(input);
                self.after_mutation(cx);
            }
            Modal::CreateGroup { parent, input } => {
                if !trimmed.is_empty() {
                    let _ = self.store.create_group(parent, &trimmed, None);
                }
                drop(input);
                self.after_mutation(cx);
            }
            Modal::CreateContact { group, input } => {
                if !trimmed.is_empty() {
                    let _ = self.store.create_contact(group, &trimmed, None);
                }
                drop(input);
                self.after_mutation(cx);
            }
            Modal::CreateChart {
                contact,
                form,
                error: _,
            } => {
                // `value` viene del campo que disparó Enter. Para el
                // form, ignoramos el value puntual y leemos todos los
                // campos del form.
                let _ = value;
                match build_chart_from_form(&form, cx) {
                    Ok((birth, label)) => {
                        match self.store.create_chart(
                            contact,
                            ChartKind::Natal,
                            &label,
                            &birth,
                            &StoredChartConfig::default(),
                            None,
                        ) {
                            Ok(_) => {
                                // Auto-expand del contact para que se vea
                                // la carta recién creada.
                                self.expanded
                                    .insert(format!("{}{}", PREFIX_CONTACT, contact));
                                self.after_mutation(cx);
                            }
                            Err(e) => {
                                self.modal = Some(Modal::CreateChart {
                                    contact,
                                    form,
                                    error: Some(SharedString::from(format!("Store: {}", e))),
                                });
                                cx.notify();
                            }
                        }
                    }
                    Err(msg) => {
                        self.modal = Some(Modal::CreateChart {
                            contact,
                            form,
                            error: Some(SharedString::from(msg)),
                        });
                        cx.notify();
                    }
                }
            }
        }
    }

    fn after_mutation(&mut self, cx: &mut Context<Self>) {
        self.modal = None;
        self.refresh(cx);
        cx.emit(TreeEvent::HierarchyChanged);
        cx.notify();
    }

    fn confirm_and_delete(
        &mut self,
        target: MenuTarget,
        window: &mut Window,
        cx: &mut Context<Self>,
    ) {
        let (label, kind) = match target {
            MenuTarget::Group(_) => ("este grupo (incluye sus subgrupos y contactos)", "group"),
            MenuTarget::Contact(_) => ("este contacto (incluye sus cartas)", "contact"),
            MenuTarget::Chart(_) => ("esta carta", "chart"),
            MenuTarget::Root => return,
        };
        let answer = window.prompt(
            PromptLevel::Warning,
            &format!("¿Borrar {}?", label),
            None,
            &["Borrar", "Cancelar"],
            cx,
        );
        let target_clone = target.clone();
        cx.spawn(async move |this, cx| {
            let Ok(idx) = answer.await else { return };
            if idx != 0 {
                return;
            }
            let _ = this.update(cx, |this, cx| {
                match target_clone {
                    MenuTarget::Group(id) => {
                        let _ = this.store.delete_group(id);
                    }
                    MenuTarget::Contact(id) => {
                        let _ = this.store.delete_contact(id);
                    }
                    MenuTarget::Chart(id) => {
                        let _ = this.store.delete_chart(id);
                    }
                    MenuTarget::Root => {}
                }
                this.after_mutation(cx);
            });
        })
        .detach();
        let _ = kind;
        self.close_menu(cx);
    }
}

// =====================================================================
// Form helpers
// =====================================================================

fn build_chart_from_form(
    form: &ChartForm,
    cx: &mut Context<TahuantinsuyuTree>,
) -> Result<(StoredBirthData, String), String> {
    let name = form.name.read(cx).text().trim().to_string();
    let place = form.place.read(cx).text().trim().to_string();
    let year: i32 = parse_field(form.year.read(cx).text(), "Año")?;
    let month: u32 = parse_field(form.month.read(cx).text(), "Mes")?;
    let day: u32 = parse_field(form.day.read(cx).text(), "Día")?;
    let hour: u32 = parse_field(form.hour.read(cx).text(), "Hora")?;
    let minute: u32 = parse_field(form.minute.read(cx).text(), "Minuto")?;
    let tz_offset_minutes: i32 = parse_field(form.tz_offset_min.read(cx).text(), "TZ offset")?;
    let latitude_deg: f64 = parse_field(form.lat.read(cx).text(), "Latitud")?;
    let longitude_deg: f64 = parse_field(form.lon.read(cx).text(), "Longitud")?;
    let altitude_m: f64 = parse_field(form.alt.read(cx).text(), "Altitud")?;

    if !(1..=12).contains(&month) {
        return Err(format!("Mes fuera de rango: {}", month));
    }
    if !(1..=31).contains(&day) {
        return Err(format!("Día fuera de rango: {}", day));
    }
    if hour > 23 {
        return Err(format!("Hora fuera de rango: {}", hour));
    }
    if minute > 59 {
        return Err(format!("Minuto fuera de rango: {}", minute));
    }

    let label = if name.is_empty() {
        "Carta natal".to_string()
    } else {
        name
    };
    let birth = StoredBirthData {
        year,
        month,
        day,
        hour,
        minute,
        second: 0.0,
        tz_offset_minutes,
        latitude_deg,
        longitude_deg,
        altitude_m,
        time_certainty: TimeCertainty::Exact,
        subject_name: None,
        birthplace_label: if place.is_empty() { None } else { Some(place) },
    };
    Ok((birth, label))
}

fn parse_field<T: std::str::FromStr>(s: &str, field: &str) -> Result<T, String> {
    s.trim()
        .parse::<T>()
        .map_err(|_| format!("Campo \"{}\" inválido: {:?}", field, s))
}

// =====================================================================
// Lookups auxiliares (DFS por la jerarquía)
// =====================================================================

fn find_group_name(roots: &[tahuantinsuyu_model::Group], store: &Store, id: GroupId) -> Option<String> {
    for g in roots {
        if g.id == id {
            return Some(g.name.clone());
        }
        if let Ok(children) = store.list_groups(Some(g.id)) {
            if let Some(n) = find_group_name(&children, store, id) {
                return Some(n);
            }
        }
    }
    None
}

fn find_contact_name(
    in_group: &[tahuantinsuyu_model::Contact],
    store: &Store,
    id: ContactId,
) -> Option<String> {
    for c in in_group {
        if c.id == id {
            return Some(c.name.clone());
        }
    }
    // Buscar también en todos los groups recursivamente.
    if let Ok(groups) = store.list_groups(None) {
        if let Some(n) = find_contact_in_groups(&groups, store, id) {
            return Some(n);
        }
    }
    None
}

fn find_contact_in_groups(
    groups: &[tahuantinsuyu_model::Group],
    store: &Store,
    id: ContactId,
) -> Option<String> {
    for g in groups {
        if let Ok(cs) = store.list_contacts(Some(g.id)) {
            for c in &cs {
                if c.id == id {
                    return Some(c.name.clone());
                }
            }
        }
        if let Ok(children) = store.list_groups(Some(g.id)) {
            if let Some(n) = find_contact_in_groups(&children, store, id) {
                return Some(n);
            }
        }
    }
    None
}

fn parse_row(id: &RowId) -> Option<TreeSelection> {
    let s = id.as_str();
    if let Some(rest) = s.strip_prefix(PREFIX_GROUP) {
        return rest.parse().ok().map(TreeSelection::Group);
    }
    if let Some(rest) = s.strip_prefix(PREFIX_CONTACT) {
        return rest.parse().ok().map(TreeSelection::Contact);
    }
    if let Some(rest) = s.strip_prefix(PREFIX_CHART) {
        return rest.parse().ok().map(TreeSelection::Chart);
    }
    None
}

// =====================================================================
// Render
// =====================================================================

const MENU_WIDTH: f32 = 220.0;

impl Render for TahuantinsuyuTree {
    fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let theme = Theme::global(cx).clone();
        let mut root = div()
            .id("tahuantinsuyu-tree-root")
            .size_full()
            .relative()
            .bg(theme.bg_panel.clone())
            .flex()
            .flex_col()
            .child(self.inner.clone());

        if let Some(menu) = self.menu.clone() {
            root = root.child(self.render_menu(&theme, menu, cx));
        }
        if self.modal.is_some() {
            root = root.child(self.render_modal(&theme, cx));
        }
        root
    }
}

impl TahuantinsuyuTree {
    fn render_menu(
        &self,
        theme: &Theme,
        menu: MenuState,
        cx: &mut Context<Self>,
    ) -> impl IntoElement {
        let mut items = div()
            .flex()
            .flex_col()
            .py(px(4.0))
            .min_w(px(MENU_WIDTH))
            .bg(theme.bg_panel_alt.clone())
            .border_1()
            .border_color(theme.border_strong)
            .rounded(px(6.0));

        match menu.target.clone() {
            MenuTarget::Root => {
                items = items.child(menu_item("tts-menu-new-group", "Nuevo grupo", theme).on_click(
                    cx.listener(|this, _: &ClickEvent, w, cx| {
                        this.open_create_group(None, w, cx);
                    }),
                ));
                items = items.child(
                    menu_item("tts-menu-new-contact-root", "Nuevo contacto", theme).on_click(
                        cx.listener(|this, _: &ClickEvent, w, cx| {
                            this.open_create_contact(None, w, cx);
                        }),
                    ),
                );
            }
            MenuTarget::Group(id) => {
                items = items.child(
                    menu_item("tts-menu-new-subgroup", "Nuevo subgrupo", theme).on_click(
                        cx.listener(move |this, _: &ClickEvent, w, cx| {
                            this.open_create_group(Some(id), w, cx);
                        }),
                    ),
                );
                items = items.child(
                    menu_item("tts-menu-new-contact", "Nuevo contacto", theme).on_click(
                        cx.listener(move |this, _: &ClickEvent, w, cx| {
                            this.open_create_contact(Some(id), w, cx);
                        }),
                    ),
                );
                items = items.child(separator(theme));
                let t = menu.target.clone();
                items = items.child(menu_item("tts-menu-rename-g", "Renombrar…", theme).on_click(
                    cx.listener(move |this, _: &ClickEvent, w, cx| {
                        this.open_rename(t.clone(), w, cx);
                    }),
                ));
                let t = menu.target.clone();
                items = items.child(menu_item("tts-menu-delete-g", "Borrar…", theme).on_click(
                    cx.listener(move |this, _: &ClickEvent, w, cx| {
                        this.confirm_and_delete(t.clone(), w, cx);
                    }),
                ));
            }
            MenuTarget::Contact(id) => {
                items = items.child(menu_item("tts-menu-new-chart", "Nueva carta…", theme).on_click(
                    cx.listener(move |this, _: &ClickEvent, w, cx| {
                        this.open_create_chart(id, w, cx);
                    }),
                ));
                items = items.child(separator(theme));
                let t = menu.target.clone();
                items = items.child(menu_item("tts-menu-rename-c", "Renombrar…", theme).on_click(
                    cx.listener(move |this, _: &ClickEvent, w, cx| {
                        this.open_rename(t.clone(), w, cx);
                    }),
                ));
                let t = menu.target.clone();
                items = items.child(menu_item("tts-menu-delete-c", "Borrar…", theme).on_click(
                    cx.listener(move |this, _: &ClickEvent, w, cx| {
                        this.confirm_and_delete(t.clone(), w, cx);
                    }),
                ));
            }
            MenuTarget::Chart(id) => {
                items = items.child(menu_item("tts-menu-open-h", "Abrir", theme).on_click(
                    cx.listener(move |this, _: &ClickEvent, _w, cx| {
                        cx.emit(TreeEvent::Opened(TreeSelection::Chart(id)));
                        this.close_menu(cx);
                    }),
                ));
                items = items.child(separator(theme));
                let t = menu.target.clone();
                items = items.child(menu_item("tts-menu-rename-h", "Renombrar…", theme).on_click(
                    cx.listener(move |this, _: &ClickEvent, w, cx| {
                        this.open_rename(t.clone(), w, cx);
                    }),
                ));
                let t = menu.target.clone();
                items = items.child(menu_item("tts-menu-delete-h", "Borrar…", theme).on_click(
                    cx.listener(move |this, _: &ClickEvent, w, cx| {
                        this.confirm_and_delete(t.clone(), w, cx);
                    }),
                ));
            }
        }

        div()
            .absolute()
            .left(menu.position.x)
            .top(menu.position.y)
            .child(items)
    }

    fn render_modal(&self, theme: &Theme, cx: &mut Context<Self>) -> impl IntoElement {
        let modal = self.modal.as_ref().expect("render_modal sin modal activo");
        let inner = match modal {
            Modal::RenameGroup { input, .. }
            | Modal::RenameContact { input, .. }
            | Modal::RenameChart { input, .. } => {
                modal_box(theme, "Renombrar", input.clone(), "Enter = guardar — Escape = cancelar")
            }
            Modal::CreateGroup { input, .. } => {
                modal_box(theme, "Nuevo grupo", input.clone(), "Enter = crear — Escape = cancelar")
            }
            Modal::CreateContact { input, .. } => modal_box(
                theme,
                "Nuevo contacto",
                input.clone(),
                "Enter = crear — Escape = cancelar",
            ),
            Modal::CreateChart { form, error, .. } => render_chart_form(theme, form, error.clone(), cx),
        };

        div()
            .absolute()
            .top(px(0.0))
            .left(px(0.0))
            .size_full()
            .flex()
            .items_center()
            .justify_center()
            .bg(hsla(0.0, 0.0, 0.0, 0.55))
            .child(inner)
    }
}

// =====================================================================
// Helpers de UI
// =====================================================================

fn menu_item(
    id: &'static str,
    label: &'static str,
    theme: &Theme,
) -> gpui::Stateful<gpui::Div> {
    div()
        .id(id)
        .px(px(12.0))
        .py(px(6.0))
        .text_size(px(12.0))
        .text_color(theme.fg_text)
        .hover(|s| s.bg(theme.bg_row_hover))
        .child(label)
}

fn separator(theme: &Theme) -> gpui::Div {
    div()
        .my(px(3.0))
        .h(px(1.0))
        .w_full()
        .bg(theme.border)
}

fn modal_box(
    theme: &Theme,
    title: &'static str,
    input: Entity<TextInput>,
    hint: &'static str,
) -> gpui::Div {
    div()
        .min_w(px(380.0))
        .p(px(16.0))
        .flex()
        .flex_col()
        .gap(px(10.0))
        .bg(theme.bg_panel_alt.clone())
        .border_1()
        .border_color(theme.border_strong)
        .rounded(px(8.0))
        .child(
            div()
                .text_size(px(13.0))
                .text_color(theme.fg_text)
                .child(title),
        )
        .child(input)
        .child(
            div()
                .text_size(px(10.0))
                .text_color(theme.fg_muted)
                .child(hint),
        )
}

fn render_chart_form(
    theme: &Theme,
    form: &ChartForm,
    error: Option<SharedString>,
    cx: &mut Context<TahuantinsuyuTree>,
) -> gpui::Div {
    let labeled = |label: &'static str, input: Entity<TextInput>| -> gpui::Div {
        div()
            .flex()
            .flex_col()
            .gap(px(2.0))
            .child(
                div()
                    .text_size(px(10.0))
                    .text_color(theme.fg_muted)
                    .child(label),
            )
            .child(input)
    };

    let date_row = div()
        .flex()
        .flex_row()
        .gap(px(8.0))
        .child(labeled("Año", form.year.clone()))
        .child(labeled("Mes", form.month.clone()))
        .child(labeled("Día", form.day.clone()))
        .child(labeled("Hora", form.hour.clone()))
        .child(labeled("Minuto", form.minute.clone()))
        .child(labeled("TZ (min)", form.tz_offset_min.clone()));

    let loc_row = div()
        .flex()
        .flex_row()
        .gap(px(8.0))
        .child(labeled("Latitud", form.lat.clone()))
        .child(labeled("Longitud", form.lon.clone()))
        .child(labeled("Altitud (m)", form.alt.clone()));

    let create_btn = div()
        .id("tts-chart-form-create")
        .px(px(14.0))
        .py(px(8.0))
        .rounded(px(6.0))
        .bg(theme.bg_button())
        .hover(|s| s.bg(theme.bg_button_hover()))
        .text_size(px(12.0))
        .text_color(theme.fg_text)
        .child("Crear carta")
        .on_click(cx.listener(|this, _: &ClickEvent, _, cx| {
            // Disparamos un submit "vacío" — el handler de submit
            // re-lee todos los campos del form. El value que pasamos
            // se ignora dentro del branch CreateChart.
            this.submit_modal(String::new(), cx);
        }));

    let cancel_btn = div()
        .id("tts-chart-form-cancel")
        .px(px(14.0))
        .py(px(8.0))
        .rounded(px(6.0))
        .bg(theme.bg_panel.clone())
        .hover(|s| s.bg(theme.bg_row_hover))
        .text_size(px(12.0))
        .text_color(theme.fg_muted)
        .child("Cancelar")
        .on_click(cx.listener(|this, _: &ClickEvent, _, cx| {
            this.close_modal(cx);
        }));

    let mut body = div()
        .min_w(px(640.0))
        .p(px(18.0))
        .flex()
        .flex_col()
        .gap(px(12.0))
        .bg(theme.bg_panel_alt.clone())
        .border_1()
        .border_color(theme.border_strong)
        .rounded(px(8.0))
        .child(
            div()
                .text_size(px(14.0))
                .text_color(theme.fg_text)
                .child("Nueva carta natal"),
        )
        .child(
            div()
                .flex()
                .flex_row()
                .gap(px(8.0))
                .child(labeled("Etiqueta", form.name.clone()))
                .child(labeled("Lugar (texto libre)", form.place.clone())),
        )
        .child(date_row)
        .child(loc_row);

    if let Some(err) = error {
        body = body.child(
            div()
                .px(px(10.0))
                .py(px(6.0))
                .rounded(px(4.0))
                .bg(theme.bg_destructive_hover())
                .text_size(px(11.0))
                .text_color(theme.accent_destructive())
                .child(err),
        );
    }

    body = body.child(
        div()
            .flex()
            .flex_row()
            .gap(px(8.0))
            .justify_end()
            .child(cancel_btn)
            .child(create_btn),
    );

    body
}
