//! Schema analysis module for understanding Nickel configuration schemas
//!
//! Wraps the existing Nickel integration (`NickelCli`, `MetadataParser`) to:
//! - Load and parse Nickel schema files
//! - Generate natural language descriptions for LLM prompts
//! - Extract field metadata and constraints
//! - Format schema information for prompt engineering

use anyhow::Result;
use std::path::Path;
use typedialog_core::nickel::cli::NickelCli;
use typedialog_core::nickel::parser::MetadataParser;
use typedialog_core::nickel::schema_ir::{NickelFieldIR, NickelSchemaIR, NickelType};

/// Analyzes Nickel configuration schemas and generates descriptions
#[derive(Debug, Clone)]
pub struct SchemaAnalyzer;

/// Analyzed schema with natural language descriptions
#[derive(Debug, Clone)]
pub struct AnalyzedSchema {
    /// Schema name
    pub name: String,

    /// Natural language description of the entire schema
    pub description: String,

    /// Individual field analyses
    pub fields: Vec<AnalyzedField>,

    /// Field categories grouped by semantic meaning
    pub categories: Vec<FieldCategory>,
}

/// Analysis of a single schema field
#[derive(Debug, Clone)]
pub struct AnalyzedField {
    /// Field path (e.g., ["user", "name"] for user.name)
    pub path: Vec<String>,

    /// Flattened field name for display
    pub flat_name: String,

    /// Natural language description of the field
    pub description: String,

    /// Type description (e.g., "string", "number", "array of strings")
    pub type_description: String,

    /// Whether the field is required
    pub required: bool,

    /// Default value as string representation
    pub default_value: Option<String>,

    /// Constraints and validations
    pub constraints: Vec<String>,

    /// Field category (grouping)
    pub category: Option<String>,
}

/// Grouped collection of fields
#[derive(Debug, Clone)]
pub struct FieldCategory {
    /// Category name
    pub name: String,

    /// Fields in this category
    pub field_names: Vec<String>,

    /// Category description
    pub description: String,
}

impl SchemaAnalyzer {
    /// Load and analyze a Nickel schema file
    ///
    /// # Arguments
    ///
    /// * `schema_path` - Path to the .ncl schema file
    ///
    /// # Returns
    ///
    /// Analyzed schema with natural language descriptions
    ///
    /// # Errors
    ///
    /// Returns error if the schema file cannot be read or parsed
    pub fn analyze_file(schema_path: &Path) -> Result<AnalyzedSchema> {
        // Query schema metadata using existing Nickel CLI
        let metadata_json = NickelCli::query(schema_path, None)?;

        // Parse JSON into structured schema IR
        let schema_ir = MetadataParser::parse(metadata_json)?;

        // Generate natural language analysis
        Self::analyze_schema_ir(schema_ir)
    }

    /// Analyze an already-parsed schema IR
    pub fn analyze_schema_ir(schema: NickelSchemaIR) -> Result<AnalyzedSchema> {
        // Generate schema-level description
        let description = Self::describe_schema(&schema);

        // Analyze each field
        let fields: Vec<AnalyzedField> = schema.fields.iter().map(Self::analyze_field).collect();

        // Group fields by category
        let categories = Self::group_fields_by_category(&fields);

        Ok(AnalyzedSchema {
            name: schema.name,
            description,
            fields,
            categories,
        })
    }

    /// Generate a natural language description of the schema
    fn describe_schema(schema: &NickelSchemaIR) -> String {
        let field_count = schema.fields.len();
        let required_count = schema.fields.iter().filter(|f| !f.optional).count();

        let mut parts = vec![];

        if let Some(doc) = &schema.description {
            parts.push(doc.clone());
        } else {
            parts.push(format!("Configuration schema with {} fields", field_count));
        }

        parts.push(format!(
            "{} required, {} optional",
            required_count,
            field_count - required_count
        ));

        // List field names for quick reference
        let field_names: Vec<String> = schema
            .fields
            .iter()
            .map(|f| {
                if f.optional {
                    format!("{}?", f.flat_name)
                } else {
                    f.flat_name.clone()
                }
            })
            .collect();

        if !field_names.is_empty() {
            parts.push(format!("Fields: {}", field_names.join(", ")));
        }

        parts.join(". ")
    }

    /// Analyze a single field and generate descriptions
    fn analyze_field(field: &NickelFieldIR) -> AnalyzedField {
        let type_description = Self::describe_type(&field.nickel_type);
        let constraints = Self::extract_constraints(field);
        let default_value = field.default.as_ref().map(|v| match v {
            serde_json::Value::String(s) => format!("\"{}\"", s),
            serde_json::Value::Number(n) => n.to_string(),
            serde_json::Value::Bool(b) => b.to_string(),
            serde_json::Value::Null => "null".to_string(),
            serde_json::Value::Array(_) => "[...]".to_string(),
            serde_json::Value::Object(_) => "{...}".to_string(),
        });

        let description = if let Some(doc) = &field.doc {
            doc.clone()
        } else {
            format!(
                "{}{}{}",
                Self::capitalize_first_letter(&field.flat_name),
                if field.optional { " (optional)" } else { "" },
                if !constraints.is_empty() {
                    format!(" with constraints: {}", constraints.join(", "))
                } else {
                    String::new()
                }
            )
        };

        AnalyzedField {
            path: field.path.clone(),
            flat_name: field.flat_name.clone(),
            description,
            type_description,
            required: !field.optional,
            default_value,
            constraints,
            category: field.group.clone(),
        }
    }

    /// Generate a description of a Nickel type
    fn describe_type(nickel_type: &NickelType) -> String {
        match nickel_type {
            NickelType::String => "string".to_string(),
            NickelType::Number => "number".to_string(),
            NickelType::Bool => "boolean".to_string(),
            NickelType::Array(elem_type) => {
                format!("array of {}", Self::describe_type(elem_type))
            }
            NickelType::Record(_) => "object/record".to_string(),
            NickelType::Custom(name) => format!("custom type: {}", name),
        }
    }

    /// Extract constraint descriptions from a field
    fn extract_constraints(field: &NickelFieldIR) -> Vec<String> {
        let mut constraints = vec![];

        // Add contract/predicate constraints
        if let Some(contract) = &field.contract_call {
            constraints.push(format!(
                "{}{}",
                contract.function,
                if let Some(args) = &contract.args {
                    format!("({})", args)
                } else {
                    String::new()
                }
            ));
        }

        // Add encryption constraint if present
        if let Some(enc) = &field.encryption_metadata {
            if enc.sensitive {
                let backend = enc
                    .backend
                    .as_ref()
                    .map(|b| format!(" via {}", b))
                    .unwrap_or_default();
                constraints.push(format!("sensitive/encrypted{}", backend));
            }
        }

        // Add array-of-records marker
        if field.is_array_of_records {
            constraints.push("repeating group".to_string());
        }

        constraints
    }

    /// Group fields into categories based on semantic grouping
    fn group_fields_by_category(fields: &[AnalyzedField]) -> Vec<FieldCategory> {
        use std::collections::HashMap;

        let mut groups: HashMap<String, Vec<String>> = HashMap::new();

        for field in fields {
            let category = field
                .category
                .clone()
                .unwrap_or_else(|| "General".to_string());

            groups
                .entry(category)
                .or_default()
                .push(field.flat_name.clone());
        }

        groups
            .into_iter()
            .map(|(name, field_names)| FieldCategory {
                description: format!("{} configuration", name),
                name,
                field_names,
            })
            .collect()
    }

    /// Capitalize the first letter of a string
    fn capitalize_first_letter(s: &str) -> String {
        let mut chars = s.chars();
        match chars.next() {
            None => String::new(),
            Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
        }
    }
}

impl AnalyzedSchema {
    /// Format the schema as a prompt for an LLM
    ///
    /// Returns a markdown-formatted description suitable for inclusion in LLM prompts
    pub fn format_for_prompt(&self) -> String {
        let mut lines = vec![];

        // Schema description
        lines.push(format!("# {}", self.name));
        lines.push(String::new());
        lines.push(self.description.clone());
        lines.push(String::new());

        // Field groups by category
        if !self.categories.is_empty() {
            lines.push("## Configuration Sections".to_string());
            lines.push(String::new());

            for category in &self.categories {
                lines.push(format!("### {}", category.name));
                lines.push(String::new());

                for field_name in &category.field_names {
                    if let Some(field) = self.fields.iter().find(|f| &f.flat_name == field_name) {
                        let required = if field.required {
                            "**required**"
                        } else {
                            "optional"
                        };
                        lines.push(format!(
                            "- **{}** ({}): {} [{}]",
                            field.flat_name, required, field.description, field.type_description
                        ));

                        if !field.constraints.is_empty() {
                            lines
                                .push(format!("  - Constraints: {}", field.constraints.join(", ")));
                        }

                        if let Some(default) = &field.default_value {
                            lines.push(format!("  - Default: {}", default));
                        }
                    }
                }

                lines.push(String::new());
            }
        } else {
            // No categories, list all fields
            lines.push("## Configuration Fields".to_string());
            lines.push(String::new());

            for field in &self.fields {
                let required = if field.required {
                    "**required**"
                } else {
                    "optional"
                };
                lines.push(format!(
                    "- **{}** ({}): {} [{}]",
                    field.flat_name, required, field.description, field.type_description
                ));

                if !field.constraints.is_empty() {
                    lines.push(format!("  - Constraints: {}", field.constraints.join(", ")));
                }

                if let Some(default) = &field.default_value {
                    lines.push(format!("  - Default: {}", default));
                }
            }

            lines.push(String::new());
        }

        lines.join("\n")
    }

    /// Get a summary of required fields for quick reference
    pub fn required_fields(&self) -> Vec<&AnalyzedField> {
        self.fields.iter().filter(|f| f.required).collect()
    }

    /// Get all optional fields
    pub fn optional_fields(&self) -> Vec<&AnalyzedField> {
        self.fields.iter().filter(|f| !f.required).collect()
    }

    /// Find a field by its flat name
    pub fn find_field(&self, name: &str) -> Option<&AnalyzedField> {
        self.fields.iter().find(|f| f.flat_name == name)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use typedialog_core::nickel::schema_ir::{ContractCall, NickelFieldIR, NickelSchemaIR};

    fn create_test_field(name: &str, optional: bool) -> NickelFieldIR {
        NickelFieldIR {
            path: vec![name.to_string()],
            flat_name: name.to_string(),
            alias: None,
            nickel_type: NickelType::String,
            doc: Some(format!("Test field: {}", name)),
            default: None,
            optional,
            contract: None,
            contract_call: None,
            group: None,
            fragment_marker: None,
            is_array_of_records: false,
            array_element_fields: None,
            encryption_metadata: None,
        }
    }

    fn create_test_schema() -> NickelSchemaIR {
        NickelSchemaIR {
            name: "TestSchema".to_string(),
            description: Some("A test configuration schema".to_string()),
            fields: vec![
                create_test_field("username", false),
                create_test_field("password", false),
                create_test_field("description", true),
            ],
        }
    }

    #[test]
    fn test_describe_schema() {
        let schema = create_test_schema();
        let desc = SchemaAnalyzer::describe_schema(&schema);

        assert!(desc.contains("A test configuration schema"));
        assert!(desc.contains("2 required"));
        assert!(desc.contains("1 optional"));
        assert!(desc.contains("username"));
        assert!(desc.contains("password"));
    }

    #[test]
    fn test_analyze_field() {
        let field = create_test_field("test_field", false);
        let analyzed = SchemaAnalyzer::analyze_field(&field);

        assert_eq!(analyzed.flat_name, "test_field");
        assert_eq!(analyzed.type_description, "string");
        assert!(analyzed.required); // optional=false means required=true
        assert!(analyzed.description.contains("Test field"));
    }

    #[test]
    fn test_describe_types() {
        assert_eq!(SchemaAnalyzer::describe_type(&NickelType::String), "string");
        assert_eq!(SchemaAnalyzer::describe_type(&NickelType::Number), "number");
        assert_eq!(SchemaAnalyzer::describe_type(&NickelType::Bool), "boolean");

        let array_type = NickelType::Array(Box::new(NickelType::String));
        assert_eq!(
            SchemaAnalyzer::describe_type(&array_type),
            "array of string"
        );
    }

    #[test]
    fn test_extract_constraints_with_contract() {
        let mut field = create_test_field("port", false);
        field.contract_call = Some(ContractCall {
            module: "validators".to_string(),
            function: "ValidPort".to_string(),
            args: Some("80".to_string()),
            expr: "validators.ValidPort 80".to_string(),
        });

        let constraints = SchemaAnalyzer::extract_constraints(&field);
        assert_eq!(constraints.len(), 1);
        assert_eq!(constraints[0], "ValidPort(80)");
    }

    #[test]
    fn test_analyzed_schema_required_fields() {
        let schema = create_test_schema();
        let analyzed = SchemaAnalyzer::analyze_schema_ir(schema).unwrap();

        let required = analyzed.required_fields();
        assert_eq!(required.len(), 2);

        let optional = analyzed.optional_fields();
        assert_eq!(optional.len(), 1);
    }

    #[test]
    fn test_analyzed_schema_find_field() {
        let schema = create_test_schema();
        let analyzed = SchemaAnalyzer::analyze_schema_ir(schema).unwrap();

        let field = analyzed.find_field("username");
        assert!(field.is_some());
        assert_eq!(field.unwrap().flat_name, "username");

        let missing = analyzed.find_field("nonexistent");
        assert!(missing.is_none());
    }

    #[test]
    fn test_format_for_prompt() {
        let schema = create_test_schema();
        let analyzed = SchemaAnalyzer::analyze_schema_ir(schema).unwrap();

        let formatted = analyzed.format_for_prompt();
        assert!(formatted.contains("# TestSchema"));
        assert!(formatted.contains("A test configuration schema"));
        assert!(formatted.contains("username"));
        assert!(formatted.contains("**required**"));
        assert!(formatted.contains("optional"));
        assert!(formatted.contains("string"));
    }

    #[test]
    fn test_capitalize_first_letter() {
        assert_eq!(SchemaAnalyzer::capitalize_first_letter("test"), "Test");
        assert_eq!(SchemaAnalyzer::capitalize_first_letter("t"), "T");
        assert_eq!(SchemaAnalyzer::capitalize_first_letter(""), "");
    }
}
