use std::collections::HashMap;

use luci_core::{FieldId, LuciError, Result};
use serde_json::Value;

use crate::field_type::FieldType;
use crate::mapping::FieldMapping;

/// Controls how documents with unknown fields are handled.
///
/// Mappings are indexing instructions, not structural constraints. Documents
/// are never rejected — unknown fields are either auto-mapped or stored
/// without indexing.
///
/// See [[architecture-api-surface#Dynamic Mapping]].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DynamicMode {
    /// Infer types for unknown fields and index them (default).
    True,
    /// Store unknown fields in _source but don't index them.
    False,
}

impl DynamicMode {
    /// Parse from a string or boolean value.
    pub fn from_es_value(s: &str) -> Result<Self> {
        match s {
            "true" => Ok(Self::True),
            "false" => Ok(Self::False),
            _ => Err(LuciError::InvalidQuery(format!(
                "invalid dynamic mode: {s} (expected \"true\" or \"false\")"
            ))),
        }
    }

    /// String representation.
    pub fn es_value(self) -> &'static str {
        match self {
            Self::True => "true",
            Self::False => "false",
        }
    }
}

/// An ordered set of field mappings with name-to-ID lookup.
///
/// Fields are assigned sequential [`FieldId`]s starting at 0. The schema is
/// immutable after construction — new fields are added by building a new
/// schema that extends the previous one (see schema evolution in
/// [[architecture-api-surface#Schema Evolution]]).
///
/// See [[architecture-api-surface#Schema Definition]].
#[derive(Clone, Debug)]
pub struct Mapping {
    fields: Vec<FieldMapping>,
    name_to_id: HashMap<String, FieldId>,
    dynamic: DynamicMode,
}

impl Mapping {
    /// Start building a schema.
    pub fn builder() -> MappingBuilder {
        MappingBuilder {
            fields: Vec::new(),
            dynamic: DynamicMode::True,
        }
    }

    /// Ensure the `_id` field exists, appending it if needed.
    /// Called by `Index` at creation time. See [[feature-document-crud]].
    pub fn ensure_id_field(&mut self) {
        if self.name_to_id.contains_key("_id") {
            return;
        }
        let mut id_mapping = FieldMapping::new("_id", FieldType::Keyword);
        id_mapping.stored = false;
        let id = FieldId::new(self.fields.len() as u16);
        self.name_to_id.insert("_id".to_string(), id);
        self.fields.push(id_mapping);
    }

    /// Look up a field's ID by name.
    pub fn field_id(&self, name: &str) -> Option<FieldId> {
        self.name_to_id.get(name).copied()
    }

    /// Get a field mapping by ID.
    ///
    /// # Panics
    ///
    /// Panics if `id` is out of bounds.
    pub fn field(&self, id: FieldId) -> &FieldMapping {
        &self.fields[id.as_u16() as usize]
    }

    /// All field mappings in insertion order.
    pub fn fields(&self) -> &[FieldMapping] {
        &self.fields
    }

    /// Number of fields.
    pub fn len(&self) -> usize {
        self.fields.len()
    }

    /// Whether the schema has no fields.
    pub fn is_empty(&self) -> bool {
        self.fields.is_empty()
    }

    /// How unknown fields are handled.
    pub fn dynamic_mode(&self) -> DynamicMode {
        self.dynamic
    }

    /// Serialize to ES-compatible JSON mapping format.
    ///
    /// Produces `{"mappings": {"dynamic": "...", "properties": {...}}}`.
    pub fn to_json(&self) -> Value {
        let mut properties = serde_json::Map::new();

        for mapping in &self.fields {
            // Skip sub-fields — they'll be nested under their parent
            if mapping.parent_field.is_some() {
                continue;
            }

            let mut field_obj = serde_json::Map::new();
            field_obj.insert(
                "type".into(),
                Value::String(mapping.field_type.es_name().into()),
            );

            if let Some(ref analyzer) = mapping.analyzer {
                field_obj.insert("analyzer".into(), Value::String(analyzer.clone()));
            }

            // Only include non-default flags.
            let defaults = FieldMapping::new("", mapping.field_type.clone());
            if mapping.stored != defaults.stored {
                field_obj.insert("store".into(), Value::Bool(mapping.stored));
            }
            if mapping.indexed != defaults.indexed {
                field_obj.insert("index".into(), Value::Bool(mapping.indexed));
            }
            if mapping.doc_values != defaults.doc_values {
                field_obj.insert("doc_values".into(), Value::Bool(mapping.doc_values));
            }
            if mapping.norms != defaults.norms {
                field_obj.insert("norms".into(), Value::Bool(mapping.norms));
            }
            if !mapping.copy_to.is_empty() {
                if mapping.copy_to.len() == 1 {
                    field_obj.insert("copy_to".into(), Value::String(mapping.copy_to[0].clone()));
                } else {
                    field_obj.insert("copy_to".into(),
                        Value::Array(mapping.copy_to.iter().map(|s| Value::String(s.clone())).collect()));
                }
            }

            // Collect sub-fields for this parent
            let prefix = format!("{}.", mapping.name);
            let sub_fields: Vec<&FieldMapping> = self.fields.iter()
                .filter(|f| f.parent_field.as_deref() == Some(&mapping.name))
                .collect();
            if !sub_fields.is_empty() {
                let mut fields_obj = serde_json::Map::new();
                for sub in sub_fields {
                    let sub_name = sub.name.strip_prefix(&prefix).unwrap_or(&sub.name);
                    let mut sub_obj = serde_json::Map::new();
                    sub_obj.insert("type".into(), Value::String(sub.field_type.es_name().into()));
                    if let Some(ref a) = sub.analyzer {
                        sub_obj.insert("analyzer".into(), Value::String(a.clone()));
                    }
                    fields_obj.insert(sub_name.to_string(), Value::Object(sub_obj));
                }
                field_obj.insert("fields".into(), Value::Object(fields_obj));
            }

            properties.insert(mapping.name.clone(), Value::Object(field_obj));
        }

        let mut mappings = serde_json::Map::new();
        if self.dynamic != DynamicMode::True {
            mappings.insert(
                "dynamic".into(),
                Value::String(self.dynamic.es_value().into()),
            );
        }
        mappings.insert("properties".into(), Value::Object(properties));

        let mut root = serde_json::Map::new();
        root.insert("mappings".into(), Value::Object(mappings));
        Value::Object(root)
    }

    /// Parse from ES-compatible JSON mapping format.
    ///
    /// Accepts `{"mappings": {"properties": {...}}}` or the shorthand
    /// `{"properties": {...}}`.
    ///
    /// # Errors
    ///
    /// Returns `LuciError::InvalidQuery` if the JSON structure is invalid
    /// or contains unsupported field types.
    pub fn from_json(json: &Value) -> Result<Self> {
        // Accept both {"mappings": {...}} and direct {"properties": {...}}.
        let mappings_obj = if let Some(m) = json.get("mappings") {
            m
        } else {
            json
        };

        let mut builder = MappingBuilder {
            fields: Vec::new(),
            dynamic: DynamicMode::True,
        };

        // Parse dynamic mode.
        if let Some(dyn_val) = mappings_obj.get("dynamic") {
            let mode_str = match dyn_val {
                Value::String(s) => s.as_str(),
                Value::Bool(true) => "true",
                Value::Bool(false) => "false",
                _ => {
                    return Err(LuciError::InvalidQuery(
                        "\"dynamic\" must be a string or boolean".into(),
                    ))
                }
            };
            builder.dynamic = DynamicMode::from_es_value(mode_str)?;
        }

        // Parse properties.
        let properties = mappings_obj
            .get("properties")
            .and_then(|p| p.as_object())
            .ok_or_else(|| {
                LuciError::InvalidQuery("missing or invalid \"properties\" object".into())
            })?;

        for (name, field_def) in properties {
            let field_obj = field_def.as_object().ok_or_else(|| {
                LuciError::InvalidQuery(format!(
                    "field \"{name}\": expected object, got {field_def}"
                ))
            })?;

            let type_name = field_obj
                .get("type")
                .and_then(|t| t.as_str())
                .ok_or_else(|| {
                    LuciError::InvalidQuery(format!(
                        "field \"{name}\": missing \"type\" property"
                    ))
                })?;

            let mut field_type = FieldType::from_es_name(type_name)?;
            // Handle dims for dense_vector
            if let FieldType::DenseVector { ref mut dims } = field_type {
                if let Some(d) = field_obj.get("dims").and_then(|v| v.as_u64()) {
                    *dims = d as usize;
                }
            }
            let mut mapping = FieldMapping::new(name.clone(), field_type);

            if let Some(v) = field_obj.get("store").and_then(|v| v.as_bool()) {
                mapping.stored = v;
            }
            if let Some(v) = field_obj.get("index").and_then(|v| v.as_bool()) {
                mapping.indexed = v;
            }
            if let Some(v) = field_obj.get("doc_values").and_then(|v| v.as_bool()) {
                mapping.doc_values = v;
            }
            if let Some(v) = field_obj.get("norms").and_then(|v| v.as_bool()) {
                mapping.norms = v;
            }
            if let Some(v) = field_obj.get("analyzer").and_then(|v| v.as_str()) {
                mapping.analyzer = Some(v.to_string());
            }
            // Parse copy_to: string or array of strings
            if let Some(ct) = field_obj.get("copy_to") {
                match ct {
                    serde_json::Value::String(s) => mapping.copy_to = vec![s.clone()],
                    serde_json::Value::Array(arr) => {
                        mapping.copy_to = arr.iter()
                            .filter_map(|v| v.as_str().map(String::from))
                            .collect();
                    }
                    _ => {}
                }
            }

            builder.fields.push(mapping);

            // Parse multi-field sub-fields: "fields": {"raw": {"type": "keyword"}}
            // Flatten into top-level FieldMappings with dot-notation names.
            // See [[feature-mapping-multi-fields]].
            if let Some(sub_fields) = field_obj.get("fields").and_then(|v| v.as_object()) {
                for (sub_name, sub_def) in sub_fields {
                    let sub_obj = sub_def.as_object().ok_or_else(|| {
                        LuciError::InvalidQuery(format!(
                            "field \"{name}.{sub_name}\": expected object"
                        ))
                    })?;
                    let sub_type_name = sub_obj.get("type").and_then(|t| t.as_str())
                        .ok_or_else(|| {
                            LuciError::InvalidQuery(format!(
                                "field \"{name}.{sub_name}\": missing \"type\""
                            ))
                        })?;
                    let sub_type = FieldType::from_es_name(sub_type_name)?;
                    let mut sub_mapping = FieldMapping::new(
                        format!("{name}.{sub_name}"),
                        sub_type,
                    );
                    sub_mapping.stored = false; // sub-fields never in _source
                    sub_mapping.parent_field = Some(name.clone());
                    if let Some(v) = sub_obj.get("index").and_then(|v| v.as_bool()) {
                        sub_mapping.indexed = v;
                    }
                    if let Some(v) = sub_obj.get("doc_values").and_then(|v| v.as_bool()) {
                        sub_mapping.doc_values = v;
                    }
                    if let Some(v) = sub_obj.get("norms").and_then(|v| v.as_bool()) {
                        sub_mapping.norms = v;
                    }
                    if let Some(v) = sub_obj.get("analyzer").and_then(|v| v.as_str()) {
                        sub_mapping.analyzer = Some(v.to_string());
                    }
                    builder.fields.push(sub_mapping);
                }
            }
        }

        Ok(builder.build())
    }
}

/// Incrementally builds a [`Mapping`].
///
/// ```ignore
/// let mapping = Mapping::builder()
///     .field("title", FieldType::Text)
///     .field("status", FieldType::Keyword)
///     .field("price", FieldType::Float)
///     .build();
/// ```
pub struct MappingBuilder {
    fields: Vec<FieldMapping>,
    dynamic: DynamicMode,
}

impl MappingBuilder {
    /// Add a field with default flags for its type.
    pub fn field(mut self, name: impl Into<String>, field_type: FieldType) -> Self {
        self.fields.push(FieldMapping::new(name, field_type));
        self
    }

    /// Add a field with a fully customized mapping.
    pub fn field_with_mapping(mut self, mapping: FieldMapping) -> Self {
        self.fields.push(mapping);
        self
    }

    /// Set the dynamic mapping mode.
    pub fn dynamic(mut self, mode: DynamicMode) -> Self {
        self.dynamic = mode;
        self
    }

    /// Build the schema, assigning sequential `FieldId`s.
    pub fn build(self) -> Mapping {
        let mut name_to_id = HashMap::with_capacity(self.fields.len());
        for (i, mapping) in self.fields.iter().enumerate() {
            name_to_id.insert(mapping.name.clone(), FieldId::new(i as u16));
        }

        Mapping {
            fields: self.fields,
            name_to_id,
            dynamic: self.dynamic,
        }
    }
}

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

    #[test]
    fn builder_basic() {
        let mapping = Mapping::builder()
            .field("title", FieldType::Text)
            .field("status", FieldType::Keyword)
            .field("price", FieldType::Float)
            .build();

        assert_eq!(mapping.len(), 3);
        assert_eq!(mapping.field_id("title"), Some(FieldId::new(0)));
        assert_eq!(mapping.field_id("status"), Some(FieldId::new(1)));
        assert_eq!(mapping.field_id("price"), Some(FieldId::new(2)));
        assert_eq!(mapping.field_id("nonexistent"), None);

        assert_eq!(mapping.field(FieldId::new(0)).field_type, FieldType::Text);
        assert_eq!(mapping.field(FieldId::new(1)).field_type, FieldType::Keyword);
        assert_eq!(mapping.field(FieldId::new(2)).field_type, FieldType::Float);
    }

    #[test]
    fn builder_with_mapping() {
        let mapping = Mapping::builder()
            .field_with_mapping(
                FieldMapping::new("body", FieldType::Text)
                    .analyzer("whitespace")
                    .norms(false),
            )
            .build();

        let m = mapping.field(FieldId::new(0));
        assert_eq!(m.analyzer.as_deref(), Some("whitespace"));
        assert!(!m.norms);
    }

    #[test]
    fn dynamic_mode_default_is_true() {
        let mapping = Mapping::builder().build();
        assert_eq!(mapping.dynamic_mode(), DynamicMode::True);
    }

    #[test]
    fn dynamic_mode_false() {
        let mapping = Mapping::builder()
            .dynamic(DynamicMode::False)
            .build();
        assert_eq!(mapping.dynamic_mode(), DynamicMode::False);
    }

    #[test]
    fn json_round_trip() {
        let mapping = Mapping::builder()
            .field("title", FieldType::Text)
            .field("status", FieldType::Keyword)
            .field("price", FieldType::Float)
            .field("count", FieldType::Long)
            .field("active", FieldType::Boolean)
            .field("created", FieldType::Date)
            .dynamic(DynamicMode::False)
            .build();

        let json = mapping.to_json();
        let parsed = Mapping::from_json(&json).unwrap();

        assert_eq!(parsed.len(), mapping.len());
        assert_eq!(parsed.dynamic_mode(), DynamicMode::False);

        for mapping in mapping.fields() {
            let id = parsed.field_id(&mapping.name).unwrap();
            let parsed_mapping = parsed.field(id);
            assert_eq!(parsed_mapping.field_type, mapping.field_type);
            assert_eq!(parsed_mapping.stored, mapping.stored);
            assert_eq!(parsed_mapping.indexed, mapping.indexed);
            assert_eq!(parsed_mapping.doc_values, mapping.doc_values);
            assert_eq!(parsed_mapping.norms, mapping.norms);
        }
    }

    #[test]
    fn json_round_trip_with_analyzer() {
        let mapping = Mapping::builder()
            .field_with_mapping(
                FieldMapping::new("body", FieldType::Text).analyzer("standard"),
            )
            .build();

        let json = mapping.to_json();
        let parsed = Mapping::from_json(&json).unwrap();

        assert_eq!(
            parsed.field(FieldId::new(0)).analyzer.as_deref(),
            Some("standard")
        );
    }

    #[test]
    fn json_round_trip_with_custom_flags() {
        let mapping = Mapping::builder()
            .field_with_mapping(
                FieldMapping::new("body", FieldType::Text)
                    .stored(false)
                    .norms(false),
            )
            .build();

        let json = mapping.to_json();
        let parsed = Mapping::from_json(&json).unwrap();

        let m = parsed.field(FieldId::new(0));
        assert!(!m.stored);
        assert!(!m.norms);
    }

    #[test]
    fn parse_es_mapping_json() {
        let json: Value = serde_json::from_str(
            r#"{
                "mappings": {
                    "dynamic": "false",
                    "properties": {
                        "title": {"type": "text", "analyzer": "standard"},
                        "status": {"type": "keyword"},
                        "price": {"type": "float"}
                    }
                }
            }"#,
        )
        .unwrap();

        let mapping = Mapping::from_json(&json).unwrap();
        assert_eq!(mapping.len(), 3);
        assert_eq!(mapping.dynamic_mode(), DynamicMode::False);
        assert!(mapping.field_id("title").is_some());
        assert!(mapping.field_id("status").is_some());
        assert!(mapping.field_id("price").is_some());
    }

    #[test]
    fn parse_shorthand_json() {
        let json: Value = serde_json::from_str(
            r#"{
                "properties": {
                    "name": {"type": "keyword"}
                }
            }"#,
        )
        .unwrap();

        let mapping = Mapping::from_json(&json).unwrap();
        assert_eq!(mapping.len(), 1);
    }

    #[test]
    fn parse_dynamic_as_boolean() {
        let json: Value = serde_json::from_str(
            r#"{
                "properties": {"x": {"type": "keyword"}},
                "dynamic": false
            }"#,
        )
        .unwrap();

        let mapping = Mapping::from_json(&json).unwrap();
        assert_eq!(mapping.dynamic_mode(), DynamicMode::False);
    }

    #[test]
    fn parse_missing_type_is_error() {
        let json: Value = serde_json::from_str(
            r#"{"properties": {"x": {}}}"#,
        )
        .unwrap();
        assert!(Mapping::from_json(&json).is_err());
    }

    #[test]
    fn parse_unknown_type_is_error() {
        let json: Value = serde_json::from_str(
            r#"{"properties": {"x": {"type": "percolator"}}}"#,
        )
        .unwrap();
        assert!(Mapping::from_json(&json).is_err());
    }

    #[test]
    fn parse_missing_properties_is_error() {
        let json: Value = serde_json::from_str(r#"{"mappings": {}}"#).unwrap();
        assert!(Mapping::from_json(&json).is_err());
    }

    #[test]
    fn dynamic_mode_round_trip() {
        for mode in [DynamicMode::True, DynamicMode::False] {
            let parsed = DynamicMode::from_es_value(mode.es_value()).unwrap();
            assert_eq!(parsed, mode);
        }
    }

    #[test]
    fn empty_schema() {
        let mapping = Mapping::builder().build();
        assert!(mapping.is_empty());
        assert_eq!(mapping.len(), 0);
    }

    #[test]
    fn default_dynamic_mode_omitted_in_json() {
        let mapping = Mapping::builder()
            .field("x", FieldType::Keyword)
            .build();
        let json = mapping.to_json();
        // DynamicMode::True is the default — should not appear in output.
        assert!(json["mappings"].get("dynamic").is_none());
    }
}
