//! `tahuantinsuyu-modules` — registry de módulos astrológicos.
//!
//! Cada tipo de astrología (natal, tránsito, progresión, sinastría,
//! Uraniano, …) es un **módulo** que declara:
//!
//! - Qué `Layer`s aporta al `RenderModel`.
//! - Qué `Control`s expone al panel inferior (toggles, sliders, selects).
//! - Hotkeys opcionales.
//! - Si su cómputo es lazy (sólo cuando se activa) o eager.
//!
//! El registry es un `Vec<&dyn Module>` estático: el canvas consulta
//! "para esta `ChartKind`, ¿qué módulos están disponibles?" y el panel
//! pinta sus controles. Activar / desactivar persiste en
//! `ModuleState` (en la store).
//!
//! Esta fase 1 trae el trait + un módulo `NatalModule` de placeholder.
//! En fases posteriores agregamos Transit, Progression, Synastry,
//! Composite, SolarArc, Uranian, FixedStars, Dignities, Lots…

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

use serde::{Deserialize, Serialize};

use tahuantinsuyu_engine::Layer;
use tahuantinsuyu_model::{Chart, ChartKind};

// =====================================================================
// Trait Module
// =====================================================================

/// Una capa de astrología enchufable.
///
/// `Send + Sync` para que el registry sea estático y se pueda consultar
/// desde cualquier thread (el cómputo pesado va a un background executor).
pub trait Module: Send + Sync {
    /// Identidad estable del módulo. Coincide con `ModuleState.module_id`
    /// en la store.
    fn id(&self) -> &'static str;

    /// Etiqueta amigable para el panel.
    fn label(&self) -> &'static str;

    /// Breve descripción para tooltip.
    fn description(&self) -> &'static str;

    /// Para qué tipos de carta tiene sentido este módulo. El panel filtra
    /// con esto al armar la lista de toggles disponibles.
    fn applies_to(&self, kind: ChartKind) -> bool;

    /// Si el módulo está activado por default al crear una carta.
    fn enabled_by_default(&self) -> bool {
        false
    }

    /// Controles que aporta al panel inferior.
    fn controls(&self) -> Vec<Control> {
        Vec::new()
    }

    /// Computa las capas que este módulo aporta al RenderModel de
    /// `chart`. La engine la llama solo si el módulo está activado
    /// para esa carta.
    ///
    /// Devuelve `Vec` (no Option) — un módulo puede no aportar capas
    /// si su config interna lo apaga (ej. "Uranian: mostrar simetría
    /// = false"); en ese caso retorna `Vec::new()`.
    fn compute_layers(&self, chart: &Chart, config: &serde_json::Value) -> Vec<Layer>;
}

// =====================================================================
// Controls expuestos al panel
// =====================================================================

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Control {
    Toggle {
        key: String,
        label: String,
        default: bool,
        hotkey: Option<String>,
    },
    Slider {
        key: String,
        label: String,
        min: f64,
        max: f64,
        step: f64,
        default: f64,
    },
    Select {
        key: String,
        label: String,
        options: Vec<SelectOption>,
        default: String,
    },
    /// Texto libre — útil para etiquetas, comentarios.
    TextInput {
        key: String,
        label: String,
        default: String,
    },
    /// Picker dinámico de una carta de la DB. Las opciones las inyecta
    /// el host (Shell) en el panel — el módulo solo declara la
    /// existencia del control. Valor emitido en `ControlChanged` =
    /// `Value::String(chart_id)` cuando se selecciona, `Value::Null`
    /// cuando se vuelve a "automático".
    ChartPicker {
        key: String,
        label: String,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SelectOption {
    pub value: String,
    pub label: String,
}

// =====================================================================
// Registry
// =====================================================================

/// Lista estática de módulos disponibles. La app los registra al boot.
pub struct Registry {
    modules: Vec<Box<dyn Module>>,
}

impl Registry {
    /// Registry con todos los módulos built-in. La app llama esto al
    /// boot y luego usa `find()` / `for_kind()` para consultar.
    pub fn with_builtins() -> Self {
        let mut r = Self { modules: Vec::new() };
        r.register(Box::new(natal::NatalModule));
        r.register(Box::new(transit::TransitModule));
        r.register(Box::new(progression::ProgressionModule));
        r.register(Box::new(solar_arc::SolarArcModule));
        r.register(Box::new(synastry::SynastryModule));
        r.register(Box::new(solar_return::SolarReturnModule));
        r
    }

    pub fn register(&mut self, m: Box<dyn Module>) {
        self.modules.push(m);
    }

    pub fn all(&self) -> &[Box<dyn Module>] {
        &self.modules
    }

    pub fn find(&self, id: &str) -> Option<&dyn Module> {
        self.modules
            .iter()
            .find(|m| m.id() == id)
            .map(|m| m.as_ref())
    }

    pub fn for_kind(&self, kind: ChartKind) -> Vec<&dyn Module> {
        self.modules
            .iter()
            .filter(|m| m.applies_to(kind))
            .map(|m| m.as_ref())
            .collect()
    }
}

// =====================================================================
// NatalModule — placeholder fase 1
// =====================================================================

pub mod natal {
    use super::*;
    use tahuantinsuyu_engine::compute_mock;

    pub struct NatalModule;

    impl Module for NatalModule {
        fn id(&self) -> &'static str {
            "natal"
        }
        fn label(&self) -> &'static str {
            "Carta natal"
        }
        fn description(&self) -> &'static str {
            "Posiciones natales, casas y aspectos."
        }
        fn applies_to(&self, kind: ChartKind) -> bool {
            matches!(kind, ChartKind::Natal)
        }
        fn enabled_by_default(&self) -> bool {
            true
        }

        fn controls(&self) -> Vec<Control> {
            vec![
                Control::Toggle {
                    key: "show_sign_dial".into(),
                    label: "Dial zodiacal".into(),
                    default: true,
                    hotkey: Some("D".into()),
                },
                Control::Toggle {
                    key: "show_houses".into(),
                    label: "Casas".into(),
                    default: true,
                    hotkey: Some("H".into()),
                },
                Control::Toggle {
                    key: "show_aspects".into(),
                    label: "Aspectos".into(),
                    default: true,
                    hotkey: Some("X".into()),
                },
                Control::Toggle {
                    key: "show_bodies".into(),
                    label: "Cuerpos".into(),
                    default: true,
                    hotkey: Some("P".into()),
                },
                Control::Slider {
                    key: "harmonic".into(),
                    label: "Armónico".into(),
                    min: 1.0,
                    max: 20.0,
                    step: 1.0,
                    default: 1.0,
                },
            ]
        }

        fn compute_layers(&self, chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
            // Fase 1: delega al mock de la engine para que la UI tenga
            // algo que pintar. Fase 3 reemplaza con `engine::compute`
            // contra `eternal-astrology`.
            compute_mock(chart).layers
        }
    }
}

// =====================================================================
// TransitModule — overlay del cielo del momento sobre la carta natal
// =====================================================================

pub mod transit {
    use super::*;

    /// Anillo externo con las posiciones planetarias del **instante
    /// actual** (reloj de pared) sobre el sujeto natal, más las
    /// cross-aspects natal × transit. La engine despacha al pipeline
    /// `PipelineRequest::Transit` cuando este módulo está activo en el
    /// `module_configs` del shell.
    pub struct TransitModule;

    impl Module for TransitModule {
        fn id(&self) -> &'static str {
            "transit"
        }
        fn label(&self) -> &'static str {
            "Tránsitos"
        }
        fn description(&self) -> &'static str {
            "Cielo del momento sobre la natal + cross aspects."
        }
        fn applies_to(&self, kind: ChartKind) -> bool {
            // Por ahora solo overlay sobre cartas natales — más adelante
            // podríamos overlayar tránsitos sobre Progresiones, etc.
            matches!(kind, ChartKind::Natal)
        }
        fn enabled_by_default(&self) -> bool {
            false
        }
        fn controls(&self) -> Vec<Control> {
            vec![Control::Toggle {
                key: "enabled".into(),
                label: "Activar".into(),
                default: false,
                hotkey: Some("T".into()),
            }]
        }
        fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
            // Las capas de tránsito se construyen en la engine vía
            // `PipelineRequest::Transit` porque necesitan acceso a la
            // NatalChart cruda + EphemerisSession. Este método queda
            // como no-op — el módulo es puramente declarativo.
            Vec::new()
        }
    }
}

// =====================================================================
// ProgressionModule — progresión secundaria (día por año)
// =====================================================================

pub mod progression {
    use super::*;

    /// Anillo interno con la carta progresada (método secundario,
    /// "un día de efemérides = un año de vida") + cross aspects natal ×
    /// progresada. La engine lo despacha vía
    /// `PipelineRequest::SecondaryProgression { target_age_years }`.
    pub struct ProgressionModule;

    impl Module for ProgressionModule {
        fn id(&self) -> &'static str {
            "progression"
        }
        fn label(&self) -> &'static str {
            "Progresión secundaria"
        }
        fn description(&self) -> &'static str {
            "Día-por-año: avanza la carta a la edad actual."
        }
        fn applies_to(&self, kind: ChartKind) -> bool {
            matches!(kind, ChartKind::Natal)
        }
        fn enabled_by_default(&self) -> bool {
            false
        }
        fn controls(&self) -> Vec<Control> {
            vec![
                Control::Toggle {
                    key: "enabled".into(),
                    label: "Activar".into(),
                    default: false,
                    hotkey: None,
                },
                // El default (30.0) es un placeholder — el shell empuja
                // la edad actual del sujeto al cargar una carta vía
                // panel.set_slider("progression", "target_age_years",
                // current_age).
                Control::Slider {
                    key: "target_age_years".into(),
                    label: "Edad objetivo (años)".into(),
                    min: 0.0,
                    max: 120.0,
                    step: 0.25,
                    default: 30.0,
                },
            ]
        }
        fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
            Vec::new()
        }
    }
}

// =====================================================================
// SynastryModule — bi-wheel con otra carta hermana del contacto actual
// =====================================================================

pub mod synastry {
    use super::*;

    /// Pone la carta del partner en el anillo externo (compartido con
    /// Transit — mutuamente excluyentes) y dibuja las cross aspects
    /// natal × partner. El shell elige el partner: la primera carta
    /// hermana del mismo contacto. Si no hay hermana, el request se
    /// salta silenciosamente.
    pub struct SynastryModule;

    impl Module for SynastryModule {
        fn id(&self) -> &'static str {
            "synastry"
        }
        fn label(&self) -> &'static str {
            "Sinastría"
        }
        fn description(&self) -> &'static str {
            "Bi-wheel con la primera carta hermana del contacto."
        }
        fn applies_to(&self, kind: ChartKind) -> bool {
            matches!(kind, ChartKind::Natal)
        }
        fn enabled_by_default(&self) -> bool {
            false
        }
        fn controls(&self) -> Vec<Control> {
            vec![
                Control::Toggle {
                    key: "enabled".into(),
                    label: "Activar".into(),
                    default: false,
                    hotkey: None,
                },
                Control::ChartPicker {
                    key: "partner_chart_id".into(),
                    label: "Partner".into(),
                },
            ]
        }
        fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
            Vec::new()
        }
    }
}

// =====================================================================
// SolarReturnModule — carta del año en curso (Sun back to natal Sun)
// =====================================================================

pub mod solar_return {
    use super::*;

    /// Computa la carta natal completa al instante del próximo retorno
    /// solar para la edad pedida. Comparte el outer ring con Transit y
    /// Synastry — mutuamente excluyentes a nivel de Shell.
    pub struct SolarReturnModule;

    impl Module for SolarReturnModule {
        fn id(&self) -> &'static str {
            "solar_return"
        }
        fn label(&self) -> &'static str {
            "Retorno solar"
        }
        fn description(&self) -> &'static str {
            "Carta del año — Sol de vuelta a su posición natal."
        }
        fn applies_to(&self, kind: ChartKind) -> bool {
            matches!(kind, ChartKind::Natal)
        }
        fn enabled_by_default(&self) -> bool {
            false
        }
        fn controls(&self) -> Vec<Control> {
            vec![
                Control::Toggle {
                    key: "enabled".into(),
                    label: "Activar".into(),
                    default: false,
                    hotkey: None,
                },
                Control::Slider {
                    key: "target_age_years".into(),
                    label: "Edad del retorno".into(),
                    min: 0.0,
                    max: 120.0,
                    step: 1.0,
                    default: 30.0,
                },
            ]
        }
        fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
            Vec::new()
        }
    }
}

// =====================================================================
// SolarArcModule — Solar Arc dirigido (true progressed Sun)
// =====================================================================

pub mod solar_arc {
    use super::*;

    /// Cada planeta y cusp natal se desplaza por el mismo arco
    /// (≈ 1° por año de vida, calculado como el delta del Sol
    /// progresado secundario). Anillo interno bien adentro + cross
    /// aspects natal × dirigida.
    pub struct SolarArcModule;

    impl Module for SolarArcModule {
        fn id(&self) -> &'static str {
            "solar_arc"
        }
        fn label(&self) -> &'static str {
            "Solar Arc"
        }
        fn description(&self) -> &'static str {
            "Dirección por arco solar — uniforme, ≈1°/año."
        }
        fn applies_to(&self, kind: ChartKind) -> bool {
            matches!(kind, ChartKind::Natal)
        }
        fn enabled_by_default(&self) -> bool {
            false
        }
        fn controls(&self) -> Vec<Control> {
            vec![
                Control::Toggle {
                    key: "enabled".into(),
                    label: "Activar".into(),
                    default: false,
                    hotkey: None,
                },
                Control::Slider {
                    key: "target_age_years".into(),
                    label: "Edad objetivo (años)".into(),
                    min: 0.0,
                    max: 120.0,
                    step: 0.25,
                    default: 30.0,
                },
            ]
        }
        fn compute_layers(&self, _chart: &Chart, _cfg: &serde_json::Value) -> Vec<Layer> {
            Vec::new()
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn registry_finds_builtins() {
        let r = Registry::with_builtins();
        assert!(r.find("natal").is_some());
        assert!(r.find("transit").is_some());
        assert!(r.find("progression").is_some());
        assert!(r.find("solar_arc").is_some());
        assert!(r.find("synastry").is_some());
        assert!(r.find("solar_return").is_some());
        // Natal kind tiene 6 módulos aplicables.
        assert_eq!(r.for_kind(ChartKind::Natal).len(), 6);
        assert!(r.for_kind(ChartKind::Synastry).is_empty());
    }
}
