//! Nickel Serializer
//!
//! Serializes form results back to valid Nickel code with contracts and metadata.
//!
//! Handles:
//! - Unflattening flat field names back to nested record structure
//! - Type annotations (String, Number, Bool, Array)
//! - Nickel contracts and predicates
//! - Documentation comments from original schema
//! - Pretty printing with proper indentation

use super::schema_ir::{NickelFieldIR, NickelSchemaIR, NickelType};
use crate::Result;
use serde_json::Value;
use std::collections::HashMap;

/// Serializer for converting form results to Nickel output
pub struct NickelSerializer;

impl NickelSerializer {
    /// Serialize HashMap results to Nickel output with contracts and metadata
    ///
    /// # Arguments
    ///
    /// * `results` - HashMap of field names to values from form execution
    /// * `schema` - The Nickel schema IR with field metadata
    ///
    /// # Returns
    ///
    /// Valid Nickel code as string
    pub fn serialize(results: &HashMap<String, Value>, schema: &NickelSchemaIR) -> Result<String> {
        // Build nested structure from flat results
        let nested = Self::unflatten_results(results, schema);

        // Serialize to Nickel with contracts and docs
        let nickel_string = Self::serialize_value(&nested, schema, 0, None);

        Ok(nickel_string)
    }

    /// Unflatten HashMap results into nested structure
    fn unflatten_results(results: &HashMap<String, Value>, schema: &NickelSchemaIR) -> Value {
        let mut root = serde_json::json!({});

        for field in &schema.fields {
            if let Some(value) = results.get(&field.flat_name) {
                // Navigate/create nested structure and insert value
                Self::insert_nested(&mut root, &field.path, value.clone());
            }
        }

        root
    }

    /// Insert a value into nested structure using path
    fn insert_nested(obj: &mut Value, path: &[String], value: Value) {
        if path.is_empty() {
            return;
        }

        if path.len() == 1 {
            obj[&path[0]] = value;
            return;
        }

        // Ensure intermediate objects exist
        for i in 0..path.len() - 1 {
            if !obj[&path[i]].is_object() {
                obj[&path[i]] = serde_json::json!({});
            }
        }

        // Navigate to parent and insert
        let mut current = obj;
        for i in 0..path.len() - 1 {
            current = &mut current[&path[i]];
        }

        current[&path[path.len() - 1]] = value;
    }

    /// Serialize a value to Nickel code with type annotations
    ///
    /// # Arguments
    /// * `value` - The JSON value to serialize
    /// * `schema` - Schema IR for type annotations
    /// * `indent` - Current indentation level
    /// * `current_key` - Optional key name for field metadata lookup
    fn serialize_value(
        value: &Value,
        schema: &NickelSchemaIR,
        indent: usize,
        current_key: Option<&str>,
    ) -> String {
        match value {
            Value::Object(map) => {
                let indent_str = "  ".repeat(indent);
                let inner_indent_str = "  ".repeat(indent + 1);

                if map.is_empty() {
                    return "{}".to_string();
                }

                let mut lines = vec!["{\n".to_string()];

                for (key, val) in map {
                    // Find field metadata for type annotation
                    if let Some(field_meta) = Self::find_field_for_key(schema, key) {
                        // Add doc comment if present
                        if let Some(doc) = &field_meta.doc {
                            lines.push(format!("{}# {}\n", inner_indent_str, doc));
                        }

                        // Build field with type annotation and contract
                        let type_annotation = Self::build_type_annotation(&field_meta);
                        let contract = field_meta
                            .contract
                            .as_ref()
                            .map(|c| format!(" | {}", c))
                            .unwrap_or_default();

                        let field_value = Self::serialize_value(val, schema, indent + 1, Some(key));
                        let field_line = if field_value.contains('\n') {
                            format!(
                                "{}{} : {}{} = {}",
                                inner_indent_str, key, type_annotation, contract, field_value
                            )
                        } else {
                            format!(
                                "{}{} : {}{} = {},\n",
                                inner_indent_str, key, type_annotation, contract, field_value
                            )
                        };
                        lines.push(field_line);
                    } else {
                        // Fallback without metadata
                        let field_value = Self::serialize_value(val, schema, indent + 1, Some(key));
                        let field_line =
                            format!("{}{} = {},\n", inner_indent_str, key, field_value);
                        lines.push(field_line);
                    }
                }

                lines.push(format!("{}}}", indent_str));
                lines.join("")
            }
            Value::Array(arr) => {
                let indent_str = "  ".repeat(indent);
                let inner_indent_str = "  ".repeat(indent + 1);

                if arr.is_empty() {
                    return "[]".to_string();
                }

                // Check if this is an array-of-records (repeating group)
                let is_array_of_records = current_key
                    .and_then(|key| Self::find_field_for_key(schema, key))
                    .map(|field| field.is_array_of_records)
                    .unwrap_or(false);

                let mut lines = vec!["[\n".to_string()];

                if is_array_of_records {
                    // Serialize as array of records (from RepeatingGroup)
                    for item in arr {
                        if let Value::Object(item_map) = item {
                            // Serialize record with proper formatting
                            let record_str = Self::serialize_record(item_map, schema, indent + 1);
                            lines.push(format!("{}{},\n", inner_indent_str, record_str));
                        } else {
                            // Fallback for non-object items
                            let item_str = Self::serialize_value(item, schema, indent + 1, None);
                            lines.push(format!("{}{},\n", inner_indent_str, item_str));
                        }
                    }
                } else {
                    // Simple array - serialize items normally
                    for item in arr {
                        let item_str = Self::serialize_value(item, schema, indent + 1, None);
                        lines.push(format!("{}{},\n", inner_indent_str, item_str));
                    }
                }

                lines.push(format!("{}]", indent_str));
                lines.join("")
            }
            Value::String(s) => format!("\"{}\"", escape_string(s)),
            Value::Number(n) => n.to_string(),
            Value::Bool(b) => b.to_string(),
            Value::Null => "null".to_string(),
        }
    }

    /// Serialize a record (object) for use in arrays
    ///
    /// This formats a record on a single line or compact format for array elements
    fn serialize_record(
        map: &serde_json::Map<String, Value>,
        schema: &NickelSchemaIR,
        indent: usize,
    ) -> String {
        if map.is_empty() {
            return "{}".to_string();
        }

        let indent_str = "  ".repeat(indent);
        let mut lines = vec!["{".to_string()];

        let entries: Vec<String> = map
            .iter()
            .map(|(key, value)| {
                let value_str = Self::serialize_value(value, schema, indent, None);
                format!(" {} = {}", key, value_str)
            })
            .collect();

        lines.push(entries.join(","));
        lines.push(" }".to_string());

        // Join without newlines for compact record format
        if lines.join("").len() < 80 {
            // Single line for short records
            lines.join("")
        } else {
            // Multi-line for long records
            let inner_indent_str = "  ".repeat(indent + 1);
            let mut result = vec!["{\n".to_string()];

            for (key, value) in map {
                let value_str = Self::serialize_value(value, schema, indent + 1, None);
                result.push(format!("{}{} = {},\n", inner_indent_str, key, value_str));
            }

            result.push(format!("{}}}", indent_str));
            result.join("")
        }
    }

    /// Build type annotation from field metadata
    fn build_type_annotation(field: &NickelFieldIR) -> String {
        match &field.nickel_type {
            NickelType::String => "String".to_string(),
            NickelType::Number => "Number".to_string(),
            NickelType::Bool => "Bool".to_string(),
            NickelType::Array(elem_type) => {
                let elem_annotation = Self::type_to_nickel(elem_type);
                format!("[{}]", elem_annotation)
            }
            NickelType::Record(_) => "{...}".to_string(),
            NickelType::Custom(name) => name.clone(),
        }
    }

    /// Convert NickelType to Nickel type annotation
    fn type_to_nickel(nickel_type: &NickelType) -> String {
        match nickel_type {
            NickelType::String => "String".to_string(),
            NickelType::Number => "Number".to_string(),
            NickelType::Bool => "Bool".to_string(),
            NickelType::Array(elem) => {
                format!("[{}]", Self::type_to_nickel(elem))
            }
            NickelType::Record(_) => "{...}".to_string(),
            NickelType::Custom(name) => name.clone(),
        }
    }

    /// Find field metadata for a key
    fn find_field_for_key(schema: &NickelSchemaIR, key: &str) -> Option<NickelFieldIR> {
        schema
            .fields
            .iter()
            .find(|f| f.path.last().is_some_and(|p| p == key))
            .cloned()
    }
}

/// Escape string for Nickel double quotes
fn escape_string(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('\n', "\\n")
}

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

    #[test]
    fn test_serialize_simple_values() {
        let mut results = HashMap::new();
        results.insert("name".to_string(), json!("Alice"));
        results.insert("age".to_string(), json!(30));
        results.insert("active".to_string(), json!(true));

        let schema = NickelSchemaIR {
            name: "test".to_string(),
            description: None,
            fields: vec![
                NickelFieldIR {
                    path: vec!["name".to_string()],
                    flat_name: "name".to_string(),
                    alias: None,
                    nickel_type: NickelType::String,
                    doc: Some("User name".to_string()),
                    default: None,
                    optional: false,
                    contract: Some("String | std.string.NonEmpty".to_string()),
                    contract_call: None,
                    group: None,
                    fragment_marker: None,
                    is_array_of_records: false,
                    array_element_fields: None,
                    encryption_metadata: None,
                },
                NickelFieldIR {
                    path: vec!["age".to_string()],
                    flat_name: "age".to_string(),
                    alias: None,
                    nickel_type: NickelType::Number,
                    doc: None,
                    default: None,
                    optional: false,
                    contract: None,
                    contract_call: None,
                    group: None,
                    fragment_marker: None,
                    is_array_of_records: false,
                    array_element_fields: None,
                    encryption_metadata: None,
                },
                NickelFieldIR {
                    path: vec!["active".to_string()],
                    flat_name: "active".to_string(),
                    alias: None,
                    nickel_type: NickelType::Bool,
                    doc: None,
                    default: None,
                    optional: false,
                    contract: None,
                    contract_call: None,
                    group: None,
                    fragment_marker: None,
                    is_array_of_records: false,
                    array_element_fields: None,
                    encryption_metadata: None,
                },
            ],
        };

        let output = NickelSerializer::serialize(&results, &schema).unwrap();

        // Check that output contains key information
        assert!(output.contains("name"));
        assert!(output.contains("Alice"));
        assert!(output.contains("age"));
        assert!(output.contains("30"));
        assert!(output.contains("active"));
        assert!(output.contains("true"));
        assert!(output.contains("String | std.string.NonEmpty"));
    }

    #[test]
    fn test_unflatten_nested() {
        let mut results = HashMap::new();
        results.insert("user_name".to_string(), json!("Alice"));
        results.insert("user_email".to_string(), json!("alice@example.com"));
        results.insert("settings_theme".to_string(), json!("dark"));

        let schema = NickelSchemaIR {
            name: "test".to_string(),
            description: None,
            fields: vec![
                NickelFieldIR {
                    path: vec!["user".to_string(), "name".to_string()],
                    flat_name: "user_name".to_string(),
                    alias: None,
                    nickel_type: NickelType::String,
                    doc: None,
                    default: None,
                    optional: false,
                    contract: None,
                    contract_call: None,
                    group: None,
                    fragment_marker: None,
                    is_array_of_records: false,
                    array_element_fields: None,
                    encryption_metadata: None,
                },
                NickelFieldIR {
                    path: vec!["user".to_string(), "email".to_string()],
                    flat_name: "user_email".to_string(),
                    alias: None,
                    nickel_type: NickelType::String,
                    doc: None,
                    default: None,
                    optional: false,
                    contract: None,
                    contract_call: None,
                    group: None,
                    fragment_marker: None,
                    is_array_of_records: false,
                    array_element_fields: None,
                    encryption_metadata: None,
                },
                NickelFieldIR {
                    path: vec!["settings".to_string(), "theme".to_string()],
                    flat_name: "settings_theme".to_string(),
                    alias: None,
                    nickel_type: NickelType::String,
                    doc: None,
                    default: None,
                    optional: false,
                    contract: None,
                    contract_call: None,
                    group: None,
                    fragment_marker: None,
                    is_array_of_records: false,
                    array_element_fields: None,
                    encryption_metadata: None,
                },
            ],
        };

        let output = NickelSerializer::serialize(&results, &schema).unwrap();

        // Check nested structure is created
        assert!(output.contains("user"));
        assert!(output.contains("settings"));
        assert!(output.contains("Alice"));
        assert!(output.contains("alice@example.com"));
        assert!(output.contains("dark"));
    }

    #[test]
    fn test_escape_string() {
        assert_eq!(escape_string("hello"), "hello");
        assert_eq!(escape_string("hello\"world"), "hello\\\"world");
        assert_eq!(escape_string("hello\\world"), "hello\\\\world");
        assert_eq!(escape_string("line1\nline2"), "line1\\nline2");
    }

    #[test]
    fn test_type_to_nickel() {
        let string_type = NickelSerializer::type_to_nickel(&NickelType::String);
        assert_eq!(string_type, "String");

        let array_type =
            NickelSerializer::type_to_nickel(&NickelType::Array(Box::new(NickelType::String)));
        assert_eq!(array_type, "[String]");
    }

    #[test]
    fn test_serialize_with_arrays() {
        let mut results = HashMap::new();
        results.insert("tags".to_string(), json!(["rust", "nickel", "forms"]));

        let schema = NickelSchemaIR {
            name: "test".to_string(),
            description: None,
            fields: vec![NickelFieldIR {
                path: vec!["tags".to_string()],
                flat_name: "tags".to_string(),
                alias: None,
                nickel_type: NickelType::Array(Box::new(NickelType::String)),
                doc: None,
                default: None,
                optional: false,
                contract: None,
                contract_call: None,
                group: None,
                fragment_marker: None,
                is_array_of_records: false,
                array_element_fields: None,
                encryption_metadata: None,
            }],
        };

        let output = NickelSerializer::serialize(&results, &schema).unwrap();

        assert!(output.contains("rust"));
        assert!(output.contains("nickel"));
        assert!(output.contains("forms"));
        assert!(output.contains("["));
        assert!(output.contains("]"));
    }

    #[test]
    fn test_serialize_array_of_records() {
        let mut results = HashMap::new();
        results.insert(
            "udp_trackers".to_string(),
            json!([
                { "bind_address": "0.0.0.0:6969" },
                { "bind_address": "0.0.0.0:6970" }
            ]),
        );

        let schema = NickelSchemaIR {
            name: "test".to_string(),
            description: None,
            fields: vec![NickelFieldIR {
                path: vec!["udp_trackers".to_string()],
                flat_name: "udp_trackers".to_string(),
                alias: None,
                nickel_type: NickelType::Array(Box::new(NickelType::Record(vec![]))),
                doc: None,
                default: None,
                optional: false,
                contract: None,
                contract_call: None,
                group: None,
                fragment_marker: None,
                is_array_of_records: true,
                array_element_fields: None,
                encryption_metadata: None,
            }],
        };

        let output = NickelSerializer::serialize(&results, &schema).unwrap();

        // Verify array structure
        assert!(output.contains("udp_trackers"));
        assert!(output.contains("["));
        assert!(output.contains("]"));

        // Verify records are present
        assert!(output.contains("bind_address"));
        assert!(output.contains("0.0.0.0:6969"));
        assert!(output.contains("0.0.0.0:6970"));

        // Verify record syntax (curly braces for each item)
        assert!(output.contains("{"));
        assert!(output.contains("}"));
    }
}
