//! `tahuantinsuyu-engine` — bridge entre el modelo agnóstico y
//! `eternal-astrology`.
//!
//! Recibe un `Chart` del modelo + un `ChartKind` y devuelve un
//! [`RenderModel`] que describe la geometría a pintar **sin** acoplar
//! el canvas a tipos de la librería astronómica. El canvas habla
//! grados decimales, radios normalizados y kinds simbólicos.
//!
//! ## Por qué un RenderModel intermedio
//!
//! 1. El canvas no debería caer si cambia el shape de `NatalChart`
//!    upstream.
//! 2. Tests del canvas: podemos generar `RenderModel`s sintéticos sin
//!    arrancar eternal.
//! 3. Cada `ChartKind` produce el mismo shape genérico → el render
//!    coordina N módulos sin saber qué calcularon.
//!
//! ## Feature `eternal-bridge`
//!
//! - **on** (default): [`compute`] abre una `EphemerisSession` VSOP2013
//!   compartida y corre la pipeline real.
//! - **off**: [`compute`] cae a [`compute_mock`] — útil para tests +
//!   builds sin eternal checked out.

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

use serde::{Deserialize, Serialize};
use thiserror::Error;

pub use tahuantinsuyu_model::{Chart, ChartId, ChartKind};

// `Chart` reexportado arriba es lo que `PipelineRequest::Synastry`
// transporta — el caller (shell) lee del Store y pasa el Chart entero
// para que el bridge construya su NatalChart en eternal.

#[cfg(feature = "eternal-bridge")]
mod bridge;
#[cfg(feature = "eternal-bridge")]
mod dignity;
#[cfg(feature = "eternal-bridge")]
pub mod svg_export;

// =====================================================================
// RenderModel — lo que el canvas necesita pintar
// =====================================================================

/// Resultado agnóstico de un cómputo astrológico, listo para renderizar.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RenderModel {
    pub chart_id: ChartId,
    pub chart_kind: ChartKind,
    pub title: String,
    #[serde(default)]
    pub subtitle: Option<String>,
    pub compute_ms: u64,

    // ─── Ángulos del chart (grados eclípticos, 0..360) ───────────────
    /// Ascendente — punto fijo de rotación del lienzo. La rueda se gira
    /// de modo que el Asc cae a las 9 (lado izquierdo).
    pub ascendant_deg: f32,
    pub midheaven_deg: f32,
    pub descendant_deg: f32,
    pub imum_coeli_deg: f32,

    /// Capas a pintar. Orden = z-order ascendente.
    pub layers: Vec<Layer>,
    /// Metadata humana por overlay activo (transit, progresión,
    /// sinastría, retorno...). Vacío para una carta natal pura. La UI
    /// la pinta como badges en el footer.
    #[serde(default)]
    pub overlays: Vec<OverlayMeta>,
    /// Lista paralela a las LineSeg de aspectos — uno por aspecto
    /// natal o cross. Ordenado por `orb_deg` ascendente (los más
    /// cerrados primero). La UI lo usa para la lista textual.
    #[serde(default)]
    pub aspect_summary: Vec<AspectSummary>,
    /// Grupos uranianos detectados (cuerpos en la misma posición mod 90).
    /// Vacío sino se activó el módulo Uranian.
    #[serde(default)]
    pub uranian_groups: Vec<UranianGroup>,
}

/// Etiqueta legible de un overlay para el footer del canvas. La engine
/// la pushea desde cada `build_*_overlay`; el canvas solo lee y pinta.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OverlayMeta {
    pub module_id: String,
    /// Etiqueta corta — ej. "Tránsito ahora", "Progresión 38.2a",
    /// "Sinastría · Ana", "Saturn return 29a".
    pub label: String,
}

/// Grupo de cuerpos natales que caen en la misma posición del
/// dial uraniano de 90° (su longitud zodiacal módulo 90 es igual o
/// muy cercana). En la astrología uraniana esto es una "fórmula" o
/// "axis" — los cuerpos están en correspondencia simbólica directa
/// porque comparten un cuadrante simétrico.
///
/// Solo se emiten grupos con 2+ miembros (los singletons no son
/// fórmulas). La engine los ordena por proximidad al ε de tolerancia.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UranianGroup {
    /// Identificadores agnósticos de los cuerpos en el grupo
    /// (ej. `["sun", "jupiter", "saturn"]`).
    pub bodies: Vec<String>,
    /// Posición en el dial de 90° (la longitud módulo 90).
    pub mod90_deg: f64,
}

/// Resumen textual de un aspecto para listas legibles. La engine lo
/// emite en paralelo con las `LineSeg` de la capa de aspectos, así
/// el canvas no tiene que re-derivar nombres de cuerpos desde grados.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AspectSummary {
    /// Module al que pertenece — "natal", "transit", "synastry",
    /// "progression", "solar_arc", "planetary_return".
    pub module_id: String,
    /// Identificador agnóstico del cuerpo "a" — "sun", "moon", etc.
    pub from_body: String,
    pub to_body: String,
    /// Identificador del aspecto — "conjunction", "trine", etc.
    pub kind: String,
    pub orb_deg: f64,
    /// `Some(true)` = applying, `Some(false)` = separating. `None` para
    /// cross-aspects (sinastría/return) donde no se computa.
    #[serde(default)]
    pub applying: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Layer {
    pub module_id: String,
    pub kind: LayerKind,
    /// Radio normalizado [0, 1] sobre el lienzo — el canvas lo convierte
    /// a píxeles. Permite stack de anillos.
    pub ring: f32,
    #[serde(default)]
    pub z: i32,
    pub geometry: Geometry,
    #[serde(default)]
    pub glyphs: Vec<Glyph>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LayerKind {
    SignDial,
    Houses,
    Bodies,
    Aspects,
    Lots,
    FixedStars,
    Midpoints,
    Outer,
    Custom,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Geometry {
    GlyphsOnly,
    /// Anillo dividido en sectores. `cusps_deg` son los grados
    /// zodiacales donde van las divisiones radiales.
    Ring { cusps_deg: Vec<f32> },
    Lines(Vec<LineSeg>),
    Points(Vec<PointMark>),
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LineSeg {
    /// Grados zodiacales del extremo "a".
    pub from_deg: f32,
    /// Grados zodiacales del extremo "b".
    pub to_deg: f32,
    /// Categoría simbólica (`"conjunction"`, `"trine"`, …) — el theme la
    /// resuelve a color.
    pub kind: String,
    pub opacity: f32,
    /// Cuerpo en el extremo "a" — populado para LineSegs de aspectos
    /// (natal × natal, cross con overlays). Vacío en `Default::default`
    /// para serde back-compat.
    #[serde(default)]
    pub from_body: String,
    /// Cuerpo en el extremo "b".
    #[serde(default)]
    pub to_body: String,
    /// Orb absoluto en grados (para tooltips).
    #[serde(default)]
    pub orb_deg: f32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PointMark {
    pub deg: f32,
    pub label: String,
    pub tag: String,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Glyph {
    /// Grado eclíptico [0, 360).
    pub deg: f32,
    /// Glyph simbólico — el theme/canvas lo mapea a unicode o imagen.
    /// Ej: `"sun"`, `"moon"`, `"aries"`, `"asc"`, `"mc"`.
    pub symbol: String,
    #[serde(default)]
    pub annotation: Option<String>,
    #[serde(default)]
    pub retrograde: bool,
    #[serde(default)]
    pub house: Option<u8>,
    /// Marker de dignidad esencial, set solo cuando
    /// `NatalOptions::show_dignities` está activo: `"+"` (domicilio),
    /// `"·"` (exaltación), `"−"` (exilio), `"*"` (caída).
    #[serde(default)]
    pub dignity_marker: Option<String>,
}

// =====================================================================
// Errores
// =====================================================================

#[derive(Debug, Error)]
pub enum EngineError {
    #[error("bridge a eternal-astrology no disponible (recompilá con feature `eternal-bridge`)")]
    BridgeDisabled,
    #[error("model: {0}")]
    Model(#[from] tahuantinsuyu_model::ModelError),
    #[error("eternal: {0}")]
    Eternal(String),
    #[error("kind {0:?} todavía no implementado")]
    UnsupportedKind(ChartKind),
}

// =====================================================================
// API pública
// =====================================================================

/// Pedidos que el host (Shell) eleva a la engine para componer un
/// `RenderModel`. La capa natal **siempre** se computa; estos requests
/// son **overlays adicionales**.
///
/// Cada variante mapea 1-a-1 con un Module declarado en
/// `tahuantinsuyu-modules` por id string. Esto deja la engine como
/// dueña única del cómputo (no depende del trait Module — los módulos
/// son sólo metadata + UI controls).
/// Módulos overlay que pintan en el mismo slot (outer ring del wheel)
/// y por lo tanto son **mutuamente excluyentes** a nivel de UI: al
/// prender uno, el shell debe apagar los otros. Single source of truth
/// — el shell y el canvas leen de acá en vez de hardcodear listas.
pub const OUTER_RING_MODULES: &[&str] = &["transit", "synastry", "planetary_return"];

#[derive(Debug, Clone)]
pub enum PipelineRequest {
    /// `module_id = "transit"` — anillo externo con planetas al
    /// instante actual (reloj de pared) + cross aspects natal × transit.
    Transit,
    /// `module_id = "progression"` — anillo interno con los planetas
    /// progresados (método secundario "día por año") a la edad pedida
    /// + cross aspects natal × progresada.
    SecondaryProgression {
        /// Edad simbólica en años a la que avanzar la carta. Para "la
        /// edad de hoy", el shell la calcula a partir de `birth_data` +
        /// `SystemTime::now`.
        target_age_years: f64,
    },
    /// `module_id = "solar_arc"` — Solar Arc dirigido (default = "true
    /// progressed Sun"): cada cuerpo y cada cusp natal se desplazan por
    /// el mismo arco ≈ 1° por año de vida. Anillo interno bien adentro
    /// + cross aspects natal × dirigida.
    SolarArc {
        target_age_years: f64,
    },
    /// `module_id = "synastry"` — bi-wheel: la natal en el centro, la
    /// carta del partner en el anillo externo (compartido con Transit
    /// — mutuamente excluyentes), cross aspects natal × partner.
    /// El partner viene como `Chart` completo del shell.
    Synastry {
        partner_chart: Box<Chart>,
    },
    /// `module_id = "planetary_return"` — carta natal fresca al
    /// instante del próximo retorno del cuerpo elegido a su posición
    /// natal, para la edad pedida. Sun = retorno solar anual, Moon =
    /// mensual, Júpiter/Saturno = generacionales. Anillo externo
    /// compartido con Transit/Synastry — mutuamente excluyentes a
    /// nivel de Shell.
    PlanetaryReturn {
        /// Identificador agnóstico del cuerpo ("sun", "moon",
        /// "jupiter", …). El bridge lo mapea a `eternal_sky::Body`.
        body: String,
        target_age_years: f64,
        /// Días extra que se suman al anchor de búsqueda (birth +
        /// age*año). Para Solar return suele ser 0 (el return cae cerca
        /// del cumpleaños); para Lunar return permite saltar de un
        /// retorno mensual al siguiente (~28 días por click).
        shift_days: i64,
    },
    /// `module_id = "midpoints"` — anillo de puntos medios entre pares
    /// de cuerpos natales. Por simplicidad filtramos a los que
    /// involucran al Sol o a la Luna (~10 puntos).
    Midpoints,
    /// `module_id = "composite"` — carta compuesta (midpoint composite,
    /// método Davison) entre dos sujetos. Renderea los planetas
    /// compuestos en un anillo interno propio (radio 0.36, entre solar
    /// arc 0.40 y aspects). Útil para análisis de relaciones.
    Composite {
        partner_chart: Box<Chart>,
    },
    /// `module_id = "uranian"` — calcula los "ejes" del dial uraniano
    /// de 90°: agrupa los cuerpos natales cuya longitud módulo 90 cae
    /// dentro de una tolerancia (~2°). El resultado se publica en
    /// `RenderModel.uranian_groups` para que la UI lo liste como
    /// fórmulas analíticas. La visualización geométrica completa del
    /// dial de 90° queda pendiente para una fase posterior.
    Uranian,
}

/// Opciones que afectan la pasada natal (qué aspectos pintar, qué
/// multiplicador de orbe usar). Es independiente de los overlays.
#[derive(Debug, Clone)]
pub struct NatalOptions {
    /// Incluir aspectos mayores (conj/opp/trine/square/sextile).
    pub show_majors: bool,
    /// Incluir aspectos menores (quincunx/semi-sextile/etc).
    pub show_minors: bool,
    /// Multiplicador uniforme sobre los orbes default. `1.0` = orbes
    /// modern_western; `0.5` = tight; `2.0` = wide.
    pub orb_multiplier: f64,
    /// Si `true`, anota cada cuerpo natal con su dignidad esencial
    /// (domicilio +, exaltación ·, exilio −, caída *). El canvas lo
    /// renderea como sufijo del glifo.
    pub show_dignities: bool,
}

impl Default for NatalOptions {
    fn default() -> Self {
        Self {
            show_majors: true,
            show_minors: false,
            orb_multiplier: 1.0,
            show_dignities: false,
        }
    }
}

/// Composición canónica: carta natal + todos los overlays pedidos.
/// Equivalente a `compose_with_options` con `NatalOptions::default()`.
pub fn compose(
    chart: &Chart,
    offset_minutes: i64,
    requests: &[PipelineRequest],
) -> Result<RenderModel, EngineError> {
    compose_with_options(chart, offset_minutes, requests, &NatalOptions::default())
}

/// Variante que permite controlar qué aspectos natales se computan y
/// con qué multiplicador de orbe.
pub fn compose_with_options(
    chart: &Chart,
    offset_minutes: i64,
    requests: &[PipelineRequest],
    natal_options: &NatalOptions,
) -> Result<RenderModel, EngineError> {
    #[cfg(feature = "eternal-bridge")]
    {
        bridge::compose(chart, offset_minutes, requests, natal_options)
    }
    #[cfg(not(feature = "eternal-bridge"))]
    {
        let _ = (offset_minutes, requests, natal_options);
        Ok(compute_mock(chart))
    }
}

/// Atajo: natal sin overlays. Equivalente a `compose(chart, 0, &[])`.
pub fn compute(chart: &Chart) -> Result<RenderModel, EngineError> {
    compose(chart, 0, &[])
}

/// Atajo: natal con time-scrubbing pero sin overlays.
pub fn compute_at_offset(chart: &Chart, offset_minutes: i64) -> Result<RenderModel, EngineError> {
    compose(chart, offset_minutes, &[])
}

/// Atajo: natal + overlay de tránsitos al instante actual.
pub fn compute_with_transits_at_now(
    chart: &Chart,
    offset_minutes: i64,
) -> Result<RenderModel, EngineError> {
    compose(chart, offset_minutes, &[PipelineRequest::Transit])
}

/// Helper retrocompatible: construye un `PlanetaryReturn` con
/// `shift_days = 0`. Útil para llamadores que no necesitan ajuste
/// fino (todos los Solar return y muchos casos básicos).
pub fn planetary_return_request(body: String, target_age_years: f64) -> PipelineRequest {
    PipelineRequest::PlanetaryReturn {
        body,
        target_age_years,
        shift_days: 0,
    }
}

/// Stub determinista — útil para tests + para la UI sin eternal.
pub fn compute_mock(chart: &Chart) -> RenderModel {
    use std::time::Instant;
    let t0 = Instant::now();

    let sign_dial = Layer {
        module_id: "natal".into(),
        kind: LayerKind::SignDial,
        ring: 1.0,
        z: 0,
        geometry: Geometry::Ring {
            cusps_deg: (0..12).map(|i| (i as f32) * 30.0).collect(),
        },
        glyphs: (0..12)
            .map(|i| Glyph {
                deg: (i as f32) * 30.0 + 15.0,
                symbol: ZODIAC_GLYPHS[i].into(),
                annotation: None,
                retrograde: false,
                house: None,
            dignity_marker: None,
            })
            .collect(),
    };

    RenderModel {
        chart_id: chart.id,
        chart_kind: chart.kind,
        title: chart.label.clone(),
        subtitle: chart.birth_data.birthplace_label.clone(),
        compute_ms: t0.elapsed().as_millis() as u64,
        ascendant_deg: 0.0,
        midheaven_deg: 270.0,
        descendant_deg: 180.0,
        imum_coeli_deg: 90.0,
        layers: vec![sign_dial],
        overlays: Vec::new(),
        aspect_summary: Vec::new(),
        uranian_groups: Vec::new(),
    }
}

const ZODIAC_GLYPHS: [&str; 12] = [
    "aries",
    "taurus",
    "gemini",
    "cancer",
    "leo",
    "virgo",
    "libra",
    "scorpio",
    "sagittarius",
    "capricorn",
    "aquarius",
    "pisces",
];

// =====================================================================
// Tests
// =====================================================================

#[cfg(test)]
mod tests {
    use super::*;
    use tahuantinsuyu_model::{
        Chart, ChartKind, ContactId, StoredBirthData, StoredChartConfig,
    };

    fn sample_chart() -> Chart {
        Chart {
            id: ChartId::new(),
            contact_id: ContactId::new(),
            kind: ChartKind::Natal,
            label: "test".into(),
            birth_data: StoredBirthData {
                year: 1987,
                month: 3,
                day: 14,
                hour: 5,
                minute: 22,
                second: 0.0,
                tz_offset_minutes: -240,
                latitude_deg: 10.4806,
                longitude_deg: -66.9036,
                altitude_m: 900.0,
                time_certainty: Default::default(),
                subject_name: None,
                birthplace_label: None,
            },
            config: StoredChartConfig::default(),
            related_chart_id: None,
            created_at_ms: 0,
        }
    }

    #[test]
    fn mock_emits_sign_dial() {
        let model = compute_mock(&sample_chart());
        assert_eq!(model.layers.len(), 1);
        assert!(matches!(model.layers[0].kind, LayerKind::SignDial));
        assert_eq!(model.layers[0].glyphs.len(), 12);
    }

    #[cfg(feature = "eternal-bridge")]
    #[test]
    fn real_compute_natal_demo() {
        let model = compute(&sample_chart()).expect("compute con eternal");
        assert!(model.layers.iter().any(|l| matches!(l.kind, LayerKind::SignDial)));
        assert!(model.layers.iter().any(|l| matches!(l.kind, LayerKind::Houses)));
        assert!(model.layers.iter().any(|l| matches!(l.kind, LayerKind::Bodies)));
        // El Asc debe ser un grado válido.
        assert!(model.ascendant_deg.is_finite());
        assert!((0.0..360.0).contains(&model.ascendant_deg));
    }
}
