//! Schema declarativo de la metainterfaz (nahual meta-schema).
//!
//! Cada **módulo** declara aquí qué menús, vistas, listas y
//! formularios expone, sin escribir código GPUI ni Rust. Cualquier
//! runtime de UI dirigida por datos (Nakui hoy, otros mañana) lo
//! carga y monta la UI correspondiente.
//!
//! ## Filosofía
//!
//! - **UI como datos**: agregar un módulo = escribir un JSON o un
//!   `.ncl`. Ningún recompile, ningún acoplamiento con el binario
//!   del runtime.
//! - **Backend-agnostic**: este crate sólo describe la *forma* de
//!   la UI. La conexión a un store/log/executor concretos vive en
//!   el runtime que lo consume (ej: el meta-runtime de Nakui que
//!   wirea esto a `nakui_core` + KCL post-checks).
//! - **Schema primero, semántica después**: validación semántica
//!   (referencias rotas a entities, campos faltantes, etc.) vive
//!   en el runtime que lo carga, no acá.
//!
//! ## Anatomía de un módulo
//!
//! ```json
//! {
//!   "id": "customers",
//!   "label": "Clientes",
//!   "entities": [
//!     { "name": "customer", "fields": [ ... ] }
//!   ],
//!   "menu": [
//!     { "label": "Listar", "view": "list" },
//!     { "label": "Nuevo", "view": "form" }
//!   ],
//!   "views": {
//!     "list": { "kind": "list", "entity": "customer", "columns": [...] },
//!     "form": { "kind": "form", "entity": "customer", "fields": [...] }
//!   }
//! }
//! ```

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

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

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

/// Manifiesto de un módulo declarativo de UI.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Module {
    /// Identificador estable. Único dentro del directorio cargado.
    pub id: String,

    /// Nombre legible para mostrar en el sidebar.
    pub label: String,

    /// Descripción corta opcional (tooltip / subtítulo).
    #[serde(default)]
    pub description: Option<String>,

    /// Entities que el módulo introduce o consume. El runtime las
    /// usa para validar columns/fields y para inicializar el store
    /// cuando son nuevas.
    #[serde(default)]
    pub entities: Vec<EntitySpec>,

    /// Path opaco al backend que va a manejar este módulo. Lo
    /// interpreta el runtime concreto (no este schema).
    ///
    /// Convención actual de Nakui: directorio con `nsmc.json` +
    /// schemas KCL + scripts Rhai. Cuando está set, el runtime
    /// Nakui carga un `Executor` para ese path y permite que las
    /// acciones `Morphism { name }` despachen al pipeline real
    /// (compute → log → apply). Otro backend puede ignorar este
    /// campo o darle un significado distinto.
    ///
    /// Path resuelto relativo al directorio del `module.json`
    /// o absoluto.
    ///
    /// Si es `None`, los backends que requieren manifest deberían
    /// degradar (toast informativo, deshabilitar morphisms, etc.);
    /// los `SeedEntity` siguen funcionando — son altas
    /// administrativas que no necesitan validación de manifest.
    ///
    /// Nombre conservado por compat con módulos ya escritos.
    /// Renombrar a `backend_module_dir` o similar si emerge un
    /// segundo backend que también lo use.
    #[serde(default, alias = "backend_module_dir")]
    pub nakui_module_dir: Option<String>,

    /// Items del menú. Cada uno apunta a una key de `views`. Orden
    /// importa (es el orden en que se presentan en el sidebar).
    pub menu: Vec<MenuItem>,

    /// Vistas indexadas por key. Las keys son referenciadas por
    /// `MenuItem.view` y por `Action::OpenView.view`.
    pub views: BTreeMap<String, View>,
}

/// Item del menú lateral.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MenuItem {
    pub label: String,
    /// Key de la vista a abrir. Debe existir en `Module.views`.
    pub view: String,
    /// Icono opcional (texto unicode o emoji; el runtime decide
    /// renderización).
    #[serde(default)]
    pub icon: Option<String>,
}

/// Una vista renderizada en el área principal cuando su menú es seleccionado.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum View {
    /// Tabla de instancias de una entity, columnas + acciones por fila.
    List(ListView),
    /// Formulario de creación / edición.
    Form(FormView),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListView {
    pub title: String,
    /// Entity (del nakui store) cuyas instancias se listan.
    pub entity: String,
    /// Columnas a mostrar. Orden importa.
    pub columns: Vec<Column>,
    /// Acciones disponibles a nivel de la lista (ej. "Nuevo" → form).
    /// Renderizadas como botones en el header.
    #[serde(default)]
    pub actions: Vec<Action>,
    /// Cuando está set, se muestra una caja de búsqueda que filtra
    /// las filas por substring contra los valores de estas columnas.
    #[serde(default)]
    pub search_in: Vec<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Column {
    /// Path del campo dentro del record. Para tipos planos: nombre.
    /// Para nested: puntos (`address.city`). El runtime navega.
    pub field: String,
    /// Texto del header.
    pub label: String,
    /// Ancho relativo (peso flex). Default 1.
    #[serde(default = "default_weight")]
    pub weight: f32,
}

fn default_weight() -> f32 {
    1.0
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FormView {
    pub title: String,
    /// Entity destino del seed/morphism al submit.
    pub entity: String,
    pub fields: Vec<FieldSpec>,
    /// Acción al submit. Típicamente `Action::SeedEntity` para alta
    /// directa o `Action::Morphism` cuando hay validación/cálculo.
    pub on_submit: Action,
}

/// Especificación de un campo de formulario o columna implícita.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldSpec {
    /// Nombre del campo en el record (clave del JSON).
    pub name: String,
    /// Etiqueta legible.
    pub label: String,
    /// Tipo del valor — define el widget de input + parseo.
    pub kind: FieldKind,
    /// Valor por defecto al abrir el form (string raw; el parseo
    /// según `kind` lo hace el runtime).
    #[serde(default)]
    pub default: Option<String>,
    /// Si `true`, el form rechaza submit con campo vacío.
    #[serde(default)]
    pub required: bool,
    /// Texto de ayuda mostrado bajo el input.
    #[serde(default)]
    pub help: Option<String>,
    /// Si `kind == EntityRef`, indica qué entity referencia. Sin
    /// esto, el runtime no sabe qué records ofrecer en el selector
    /// y la validación `Module::validate` rechaza el manifest.
    /// Para los demás kinds, este campo se ignora.
    #[serde(default)]
    pub ref_entity: Option<String>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FieldKind {
    /// Texto libre.
    Text,
    /// Texto multilínea.
    Multiline,
    /// Número (i64 o f64; runtime intenta parsear como i64 primero).
    Number,
    /// Booleano (renderizado como checkbox).
    Boolean,
    /// Fecha (formato ISO YYYY-MM-DD; almacenada como string).
    Date,
    /// Referencia a otro record. El runtime renderiza un selector
    /// clickable de records existentes de la entity declarada en
    /// `FieldSpec.ref_entity`; el value almacenado es el UUID del
    /// seleccionado, parseable como cualquier text/UUID al submit.
    EntityRef,
}

/// Acciones disparables por menús, botones o submit de formularios.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Action {
    /// Cambia la vista activa a otra del mismo módulo.
    OpenView {
        view: String,
        /// Etiqueta del botón / item; default = nombre humano de la vista.
        #[serde(default)]
        label: Option<String>,
    },
    /// Crea un seed directo en la entity con los valores del form.
    /// Equivalente a `nakui_core::event_log::seed_and_log`.
    /// Sin pipeline de morphism — alta administrativa.
    SeedEntity {
        entity: String,
        /// Tras el submit exitoso, opcionalmente abrir esta vista
        /// (por convención: `"list"` para volver al listado).
        #[serde(default)]
        next_view: Option<String>,
    },
    /// Ejecuta un morphism declarado en el manifest del módulo
    /// nakui-core (cuyo path vive en `Module.nakui_module_dir`).
    /// Inputs (records existentes) y params (valores escalares) se
    /// mapean desde los campos del form.
    Morphism {
        /// Nombre del morphism declarado en `nsmc.json` del manifest
        /// nakui apuntado por el módulo.
        name: String,
        /// Mapeo `role → field_name`: por cada input declarado en
        /// el `MorphismSpec.inputs`, indica qué field del form
        /// contiene el UUID del record. El runtime parsea el value
        /// como `Uuid` y lo pasa como input al `execute_and_log`.
        ///
        /// Ej: `{ "stock": "stock_id", "caja": "caja_id" }` para un
        /// morphism `vender` que toma roles `stock` y `caja`.
        #[serde(default)]
        inputs: BTreeMap<String, String>,
        /// Lista de fields del form cuyos values van al `params`
        /// JSON object pasado al morphism. Si está vacío, todos los
        /// fields que no estén en `inputs` van a params.
        #[serde(default)]
        params: Vec<String>,
        #[serde(default)]
        next_view: Option<String>,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntitySpec {
    /// Nombre de la entity (clave en el store). Único dentro del módulo.
    pub name: String,
    /// Label legible (singular).
    pub label: String,
    /// Campos esperados en records de esta entity. Usados por el
    /// runtime para inferir columnas / validar formularios.
    #[serde(default)]
    pub fields: Vec<FieldSpec>,
}

#[derive(Debug, Error)]
pub enum SchemaError {
    #[error("io leyendo {path}: {source}")]
    Io {
        path: PathBuf,
        #[source]
        source: std::io::Error,
    },
    #[error("parseo de {path}: {source}")]
    Parse {
        path: PathBuf,
        #[source]
        source: serde_json::Error,
    },
    #[error("módulo {id}: la vista '{view}' referenciada por menu_item '{item}' no existe")]
    DanglingMenuView {
        id: String,
        item: String,
        view: String,
    },
    #[error("módulos con id duplicado: '{id}' aparece en {first} y {second}")]
    DuplicateModuleId {
        id: String,
        first: PathBuf,
        second: PathBuf,
    },
    #[error(
        "módulo {id} vista '{view}': field '{field}' tiene kind=entity_ref \
         pero no declaró ref_entity"
    )]
    EntityRefMissingTarget {
        id: String,
        view: String,
        field: String,
    },
}

impl Module {
    /// Carga un module.json desde disco. Validación estructural
    /// posterior (vistas referenciadas existen, etc.) la ejecuta el
    /// runtime — acá sólo parseamos.
    pub fn from_path(path: impl AsRef<Path>) -> Result<Self, SchemaError> {
        let path = path.as_ref();
        let bytes = std::fs::read(path).map_err(|source| SchemaError::Io {
            path: path.to_path_buf(),
            source,
        })?;
        serde_json::from_slice(&bytes).map_err(|source| SchemaError::Parse {
            path: path.to_path_buf(),
            source,
        })
    }

    /// Validación post-parse:
    /// - Cada `MenuItem.view` debe existir en `views`.
    /// - Cada `FieldSpec` con `kind=EntityRef` debe declarar
    ///   `ref_entity`.
    pub fn validate(&self) -> Result<(), SchemaError> {
        for item in &self.menu {
            if !self.views.contains_key(&item.view) {
                return Err(SchemaError::DanglingMenuView {
                    id: self.id.clone(),
                    item: item.label.clone(),
                    view: item.view.clone(),
                });
            }
        }
        for (view_key, view) in &self.views {
            if let View::Form(form) = view {
                for f in &form.fields {
                    if f.kind == FieldKind::EntityRef && f.ref_entity.is_none() {
                        return Err(SchemaError::EntityRefMissingTarget {
                            id: self.id.clone(),
                            view: view_key.clone(),
                            field: f.name.clone(),
                        });
                    }
                }
            }
        }
        Ok(())
    }
}

/// Carga todos los `module.json` encontrados bajo `dir` (recursivo
/// 1 nivel — espera `dir/<modulo>/module.json`). Devuelve la lista
/// ordenada por id, validada.
///
/// Falla si: I/O, parseo, módulo inválido, o ids duplicados.
pub fn load_modules_from_dir(dir: impl AsRef<Path>) -> Result<Vec<Module>, SchemaError> {
    let dir = dir.as_ref();
    let mut modules: Vec<(PathBuf, Module)> = Vec::new();
    let entries = std::fs::read_dir(dir).map_err(|source| SchemaError::Io {
        path: dir.to_path_buf(),
        source,
    })?;
    for entry in entries.flatten() {
        let p = entry.path();
        if p.is_dir() {
            let manifest = p.join("module.json");
            if manifest.exists() {
                let m = Module::from_path(&manifest)?;
                m.validate()?;
                modules.push((manifest, m));
            }
        }
    }
    // Detectar duplicados de id.
    modules.sort_by(|a, b| a.1.id.cmp(&b.1.id));
    let mut prev: Option<&(PathBuf, Module)> = None;
    for cur in &modules {
        if let Some(p) = prev {
            if p.1.id == cur.1.id {
                return Err(SchemaError::DuplicateModuleId {
                    id: cur.1.id.clone(),
                    first: p.0.clone(),
                    second: cur.0.clone(),
                });
            }
        }
        prev = Some(cur);
    }
    Ok(modules.into_iter().map(|(_, m)| m).collect())
}

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

    fn sample_module() -> Module {
        Module {
            id: "customers".into(),
            label: "Clientes".into(),
            description: Some("Gestión de clientes".into()),
            nakui_module_dir: None,
            entities: vec![EntitySpec {
                name: "customer".into(),
                label: "Cliente".into(),
                fields: vec![
                    FieldSpec {
                        name: "name".into(),
                        label: "Nombre".into(),
                        kind: FieldKind::Text,
                        default: None,
                        required: true,
                        help: None,
                        ref_entity: None,
                    },
                    FieldSpec {
                        name: "email".into(),
                        label: "Email".into(),
                        kind: FieldKind::Text,
                        default: None,
                        required: false,
                        help: Some("Opcional".into()),
                        ref_entity: None,
                    },
                ],
            }],
            menu: vec![
                MenuItem {
                    label: "Listar".into(),
                    view: "list".into(),
                    icon: None,
                },
                MenuItem {
                    label: "Nuevo".into(),
                    view: "form".into(),
                    icon: None,
                },
            ],
            views: BTreeMap::from([
                (
                    "list".into(),
                    View::List(ListView {
                        title: "Clientes".into(),
                        entity: "customer".into(),
                        columns: vec![
                            Column {
                                field: "name".into(),
                                label: "Nombre".into(),
                                weight: 2.0,
                            },
                            Column {
                                field: "email".into(),
                                label: "Email".into(),
                                weight: 3.0,
                            },
                        ],
                        actions: vec![Action::OpenView {
                            view: "form".into(),
                            label: Some("Nuevo".into()),
                        }],
                        search_in: vec!["name".into(), "email".into()],
                    }),
                ),
                (
                    "form".into(),
                    View::Form(FormView {
                        title: "Nuevo cliente".into(),
                        entity: "customer".into(),
                        fields: vec![FieldSpec {
                            name: "name".into(),
                            label: "Nombre".into(),
                            kind: FieldKind::Text,
                            default: None,
                            required: true,
                            help: None,
                            ref_entity: None,
                        }],
                        on_submit: Action::SeedEntity {
                            entity: "customer".into(),
                            next_view: Some("list".into()),
                        },
                    }),
                ),
            ]),
        }
    }

    #[test]
    fn module_serialize_deserialize_roundtrip() {
        let m = sample_module();
        let s = serde_json::to_string_pretty(&m).unwrap();
        let m2: Module = serde_json::from_str(&s).unwrap();
        assert_eq!(m.id, m2.id);
        assert_eq!(m.menu.len(), m2.menu.len());
        assert_eq!(m.views.len(), m2.views.len());
    }

    #[test]
    fn validate_passes_for_well_formed_module() {
        sample_module().validate().unwrap();
    }

    #[test]
    fn validate_catches_dangling_menu_view() {
        let mut m = sample_module();
        m.menu.push(MenuItem {
            label: "Roto".into(),
            view: "no_existe".into(),
            icon: None,
        });
        let err = m.validate().unwrap_err();
        assert!(matches!(err, SchemaError::DanglingMenuView { .. }));
    }

    #[test]
    fn from_path_loads_real_file() {
        let tmp = tempfile::tempdir().unwrap();
        let path = tmp.path().join("module.json");
        std::fs::write(&path, serde_json::to_vec_pretty(&sample_module()).unwrap()).unwrap();
        let m = Module::from_path(&path).unwrap();
        assert_eq!(m.id, "customers");
    }

    #[test]
    fn load_modules_from_dir_finds_subdirs() {
        let tmp = tempfile::tempdir().unwrap();
        let cust_dir = tmp.path().join("customers");
        std::fs::create_dir(&cust_dir).unwrap();
        let mut m1 = sample_module();
        m1.id = "customers".into();
        std::fs::write(
            cust_dir.join("module.json"),
            serde_json::to_vec_pretty(&m1).unwrap(),
        )
        .unwrap();
        let prod_dir = tmp.path().join("products");
        std::fs::create_dir(&prod_dir).unwrap();
        let mut m2 = sample_module();
        m2.id = "products".into();
        std::fs::write(
            prod_dir.join("module.json"),
            serde_json::to_vec_pretty(&m2).unwrap(),
        )
        .unwrap();

        let mods = load_modules_from_dir(tmp.path()).unwrap();
        assert_eq!(mods.len(), 2);
        assert_eq!(mods[0].id, "customers");
        assert_eq!(mods[1].id, "products");
    }

    #[test]
    fn validate_catches_entity_ref_without_target() {
        let mut m = sample_module();
        // Inyectamos un form con un campo EntityRef sin ref_entity.
        m.views.insert(
            "broken_form".into(),
            View::Form(FormView {
                title: "Roto".into(),
                entity: "customer".into(),
                fields: vec![FieldSpec {
                    name: "ref_to_nowhere".into(),
                    label: "Referencia".into(),
                    kind: FieldKind::EntityRef,
                    default: None,
                    required: true,
                    help: None,
                    ref_entity: None,
                }],
                on_submit: Action::SeedEntity {
                    entity: "customer".into(),
                    next_view: None,
                },
            }),
        );
        let err = m.validate().unwrap_err();
        assert!(
            matches!(err, SchemaError::EntityRefMissingTarget { .. }),
            "got: {err:?}"
        );
    }

    #[test]
    fn entity_ref_with_target_validates_clean() {
        let mut m = sample_module();
        m.views.insert(
            "ok_form".into(),
            View::Form(FormView {
                title: "OK".into(),
                entity: "customer".into(),
                fields: vec![FieldSpec {
                    name: "supplier".into(),
                    label: "Proveedor".into(),
                    kind: FieldKind::EntityRef,
                    default: None,
                    required: true,
                    help: None,
                    ref_entity: Some("supplier".into()),
                }],
                on_submit: Action::SeedEntity {
                    entity: "customer".into(),
                    next_view: None,
                },
            }),
        );
        m.menu.push(MenuItem {
            label: "OK".into(),
            view: "ok_form".into(),
            icon: None,
        });
        m.validate().unwrap();
    }

    #[test]
    fn load_modules_detects_duplicate_id() {
        let tmp = tempfile::tempdir().unwrap();
        let a_dir = tmp.path().join("a");
        let b_dir = tmp.path().join("b");
        std::fs::create_dir_all(&a_dir).unwrap();
        std::fs::create_dir_all(&b_dir).unwrap();
        let m = sample_module(); // id = "customers"
        std::fs::write(
            a_dir.join("module.json"),
            serde_json::to_vec(&m).unwrap(),
        )
        .unwrap();
        std::fs::write(
            b_dir.join("module.json"),
            serde_json::to_vec(&m).unwrap(),
        )
        .unwrap();
        let err = load_modules_from_dir(tmp.path()).unwrap_err();
        assert!(matches!(err, SchemaError::DuplicateModuleId { .. }));
    }
}
