//! Schema generator: produces Nickel schemas from domain features.

use crate::error::Result;
use crate::models::*;
use std::path::Path;

/// Generates Nickel schema files (.ncl) from domain features.
pub struct SchemaGenerator;

impl SchemaGenerator {
    /// Generate schemas for all domain features.
    pub fn generate(spec: &ProjectSpec, output_dir: impl AsRef<Path>) -> Result<()> {
        let output_dir = output_dir.as_ref();
        tracing::info!(
            "Generating schemas for project: {} with {} features",
            spec.name,
            spec.domain_features.len()
        );

        // Ensure schemas directory exists
        let schemas_dir = output_dir.join("schemas");
        std::fs::create_dir_all(&schemas_dir).map_err(|e| crate::error::SchemaGenerationError {
            feature_name: "root".to_string(),
            reason: format!("Failed to create schemas directory: {}", e),
        })?;

        // Generate a schema file for each domain feature
        for feature in &spec.domain_features {
            Self::generate_feature_schema(spec, feature, &schemas_dir)?;
        }

        // Generate a main schema that imports all features
        Self::generate_main_schema(spec, output_dir)?;

        // Generate README.md explaining the schema system
        Self::generate_schemas_readme(spec, &schemas_dir)?;

        tracing::info!("Successfully generated schemas for project: {}", spec.name);
        Ok(())
    }

    /// Generate a Nickel schema file for a single domain feature.
    fn generate_feature_schema(
        spec: &ProjectSpec,
        feature: &DomainFeature,
        schemas_dir: &Path,
    ) -> Result<()> {
        tracing::debug!("Generating schema for feature: {}", feature.name);

        let mut schema_content = String::new();

        // Add file header
        schema_content.push_str(&format!(
            "# Schema for {} feature\n# Generated for project: {}\n",
            feature.name, spec.name
        ));

        if let Some(desc) = &feature.description {
            schema_content.push_str(&format!("# {}\n", desc));
        }

        schema_content.push('\n');

        // Add Nickel imports if feature has them
        if !feature.nickel_imports.is_empty() {
            schema_content.push_str("# Schema imports\n");
            for import in &feature.nickel_imports {
                schema_content.push_str(&format!(
                    "let {} = import \"{}\" in\n",
                    import.name, import.path
                ));
            }
            schema_content.push('\n');
        }

        // Define the feature type/schema
        let type_name = Self::capitalize_first(&feature.name);
        schema_content.push_str(&format!("# {} type schema\n", type_name));

        if feature.use_type_contracts {
            schema_content.push_str(&format!("{} = {{\n", type_name));
        } else {
            schema_content.push_str(&format!("let {} = {{\n", feature.name));
        }

        // Add fields to the record
        for field in &feature.fields {
            schema_content.push_str(&Self::generate_field_schema(field, feature)?);
        }

        if feature.use_type_contracts {
            schema_content.push_str("}\n\n");
            // Export the type
            schema_content.push_str(&format!("{{ {} }}\n", type_name));
        } else {
            schema_content.push_str("}\n\n");
            // Export the schema
            schema_content.push_str(&format!("{{ {} }}\n", feature.name));
        }

        // Write the schema file
        let schema_file = schemas_dir.join(format!("{}.ncl", feature.name));
        std::fs::write(&schema_file, schema_content).map_err(|e| {
            crate::error::SchemaGenerationError {
                feature_name: feature.name.clone(),
                reason: format!(
                    "Failed to write schema file {}: {}",
                    schema_file.display(),
                    e
                ),
            }
        })?;

        tracing::debug!("Generated schema file: {}", schema_file.display());
        Ok(())
    }

    /// Generate Nickel schema syntax for a single field with type contracts.
    fn generate_field_schema(field: &ConfigField, feature: &DomainFeature) -> Result<String> {
        let mut field_def = String::new();

        // Add field comment if help text exists
        if let Some(help) = &field.help {
            field_def.push_str(&format!("  # {}\n", help));
        }

        // Field name
        let optional_marker = if !field.required { " | optional" } else { "" };

        // Check if this field references an imported schema
        let nickel_type = if feature.use_type_contracts {
            // Look for import that matches this field
            let matching_import = feature
                .nickel_imports
                .iter()
                .find(|imp| field.name.contains(&imp.name) || imp.name.contains(&field.name));

            if let Some(import) = matching_import {
                // Use imported type with contract: database | database_schema.Database
                format!("{}.{}", import.name, Self::capitalize_first(&field.name))
            } else {
                Self::map_field_type_to_nickel(&field.field_type).to_string()
            }
        } else {
            Self::map_field_type_to_nickel(&field.field_type).to_string()
        };

        // Generate field definition with type contract (same format regardless)
        field_def.push_str(&format!(
            "  {} | {}{},\n",
            field.name, nickel_type, optional_marker
        ));

        Ok(field_def)
    }

    /// Map ProjectSpec field types to Nickel type annotations.
    fn map_field_type_to_nickel(field_type: &FieldType) -> &'static str {
        match field_type {
            FieldType::Text => "String",
            FieldType::Number => "Number",
            FieldType::Password => "String",
            FieldType::Confirm => "Bool",
            FieldType::Select => "String",
            FieldType::MultiSelect => "Array String",
            FieldType::Editor => "String",
            FieldType::Date => "String",
            FieldType::RepeatingGroup => "Array Dyn", // Will be refined with type contracts
        }
    }

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

    /// Generate main schema file that imports and assembles all features.
    fn generate_main_schema(spec: &ProjectSpec, output_dir: &Path) -> Result<()> {
        tracing::debug!("Generating main schema for project: {}", spec.name);

        let mut main_schema = String::new();

        // Add header
        main_schema.push_str(&format!(
            "# Main configuration schema for {}\n# Generated for provisioning setup\n\n",
            spec.name
        ));

        // Add infrastructure configuration
        main_schema.push_str(&Self::generate_infrastructure_schema(&spec.infrastructure)?);

        // Import all feature schemas
        main_schema.push_str("\n# Domain features\n");
        for feature in &spec.domain_features {
            main_schema.push_str(&format!(
                "let {} = (import \"./schemas/{}.ncl\").{}\n",
                feature.name, feature.name, feature.name
            ));
        }

        // Add main configuration record
        main_schema.push_str("\n# Main configuration object\n{\n  config = {\n");

        for feature in &spec.domain_features {
            main_schema.push_str(&format!("    {}: {},\n", feature.name, feature.name));
        }

        main_schema.push_str("  },\n}\n");

        // Write main schema file
        let main_file = output_dir.join("config.ncl");
        std::fs::write(&main_file, main_schema).map_err(|e| {
            crate::error::SchemaGenerationError {
                feature_name: "config".to_string(),
                reason: format!("Failed to write main schema {}: {}", main_file.display(), e),
            }
        })?;

        tracing::debug!("Generated main schema file: {}", main_file.display());
        Ok(())
    }

    /// Generate Nickel schema for infrastructure configuration.
    fn generate_infrastructure_schema(infra: &InfrastructureSpec) -> Result<String> {
        let mut infra_schema = String::new();

        infra_schema.push_str("# Infrastructure configuration\n");
        infra_schema.push_str("let infrastructure = {\n");

        if infra.ssh {
            infra_schema.push_str("  ssh | Bool = true,\n");
        }

        if let Some(db) = &infra.database {
            infra_schema.push_str(&format!(
                "  database = {{\n    type | String = \"{:?}\",\n    required | Bool = {},\n  }},\n",
                db.db_type, db.required
            ));
        }

        if !infra.providers.is_empty() {
            infra_schema.push_str("  providers | [String] = [");
            let provider_strs: Vec<String> = infra
                .providers
                .iter()
                .map(|p| format!("\"{}\"", format!("{:?}", p).to_lowercase()))
                .collect();
            infra_schema.push_str(&provider_strs.join(", "));
            infra_schema.push_str("],\n");
        }

        if !infra.monitoring.is_empty() {
            infra_schema.push_str("  monitoring | [String] = [");
            let monitoring_strs: Vec<String> = infra
                .monitoring
                .iter()
                .map(|m| format!("\"{}\"", format!("{:?}", m).to_lowercase()))
                .collect();
            infra_schema.push_str(&monitoring_strs.join(", "));
            infra_schema.push_str("],\n");
        }

        infra_schema.push_str("}\n");

        Ok(infra_schema)
    }

    /// Generate README.md documenting the schema system.
    fn generate_schemas_readme(spec: &ProjectSpec, schemas_dir: &Path) -> Result<()> {
        let mut content = String::new();

        content.push_str(&format!("# Nickel Schemas - {}\n\n", spec.name));

        content.push_str(
            "This directory contains Nickel type schemas for configuration validation.\n\n\
             ## Overview\n\n\
             Schemas define the structure and types of configuration values. They use Nickel's \
             type contracts to enforce correctness at evaluation time.\n\n\
             ## Schema Files\n\n",
        );

        for feature in &spec.domain_features {
            let type_name = Self::capitalize_first(&feature.name);
            content.push_str(&format!(
                "### `{}.ncl` - {} Schema\n\n",
                feature.name, type_name
            ));

            if let Some(desc) = &feature.description {
                content.push_str(&format!("{}\n\n", desc));
            }

            if !feature.nickel_imports.is_empty() {
                content.push_str("**Imports:**\n");
                for import in &feature.nickel_imports {
                    content.push_str(&format!("- `{}` from `{}`\n", import.name, import.path));
                }
                content.push('\n');
            }

            content.push_str(&format!("**Fields:** {}\n\n", feature.fields.len()));

            if feature.use_type_contracts {
                content.push_str("*This schema uses type contracts for validation.*\n\n");
            }
        }

        content.push_str(
            "## Usage\n\n\
             ### Importing Schemas\n\n\
             ```nickel\n\
             let schemas = import \"./schemas/config.ncl\" in\n\
             let my_config = {...} in\n\
             my_config | schemas.Config\n\
             ```\n\n\
             ### Type Contracts\n\n\
             Type contracts ensure values match expected types:\n\n\
             ```nickel\n\
             {\n\
               server | schemas.Server = {\n\
                 port | Number = 8080,\n\
               },\n\
             }\n\
             ```\n\n\
             ### Nested Contracts\n\n\
             Schemas can reference imported types:\n\n\
             ```nickel\n\
             let database_schema = import \"./database.ncl\" in\n\
             {\n\
               database | database_schema.Database | optional,\n\
             }\n\
             ```\n\n\
             ### Array Type Contracts\n\n\
             Arrays can have typed elements:\n\n\
             ```nickel\n\
             udp_trackers | Array TrackerUdp | optional,\n\
             ```\n\n\
             ## Validation\n\n\
             Type-check your configuration:\n\n\
             ```bash\n\
             nickel typecheck schemas/config.ncl\n\
             ```\n\n",
        );

        let readme_file = schemas_dir.join("README.md");
        std::fs::write(&readme_file, content).map_err(|e| crate::error::SchemaGenerationError {
            feature_name: "readme".to_string(),
            reason: format!("Failed to write schemas README: {}", e),
        })?;

        tracing::debug!("Generated schemas README: {}", readme_file.display());
        Ok(())
    }
}
