// vapora-shared: Schema registry - Loads and caches Nickel schemas for
// validation Compiles Nickel schemas to internal representation for runtime
// validation

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;

use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use tracing::{debug, info, warn};

use super::nickel_bridge::NickelCli;
use crate::error::{Result, VaporaError};

/// Compiled schema representation (from Nickel)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompiledSchema {
    /// Schema name (e.g., "tools/kanban_create_task")
    pub name: String,

    /// Field definitions with types and contracts
    pub fields: Vec<FieldSchema>,

    /// Custom contract predicates (Nickel functions)
    pub custom_contracts: Vec<String>,

    /// Source file path
    pub source_path: PathBuf,
}

/// Field schema with type and validation contracts
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldSchema {
    /// Field name (flattened path for nested fields)
    pub name: String,

    /// Field type (Nickel type)
    pub field_type: FieldType,

    /// Is this field required?
    pub required: bool,

    /// Validation contracts (predicates)
    pub contracts: Vec<Contract>,

    /// Default value (if field is optional)
    pub default: Option<serde_json::Value>,

    /// Documentation string
    pub doc: Option<String>,
}

/// Nickel field types
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum FieldType {
    /// String type
    String,

    /// Number type (f64)
    Number,

    /// Boolean type
    Bool,

    /// Array type with element type
    Array(Box<FieldType>),

    /// Object/Record type
    Object,

    /// Enum with allowed values
    Enum(Vec<String>),

    /// Union of types (Nickel `|`)
    Union(Vec<FieldType>),
}

/// Validation contract (Nickel predicate)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Contract {
    /// Non-empty string (std.string.NonEmpty)
    NonEmpty,

    /// Minimum string length
    MinLength(usize),

    /// Maximum string length
    MaxLength(usize),

    /// Regex pattern match
    Pattern(String),

    /// Numeric range validation
    Range { min: f64, max: f64 },

    /// Greater than (exclusive)
    GreaterThan(f64),

    /// Less than (exclusive)
    LessThan(f64),

    /// Email validation
    Email,

    /// URL validation
    Url,

    /// UUID validation
    Uuid,

    /// Custom Nickel predicate (function source code)
    Custom(String),
}

/// Schema source (file or inline definition)
#[derive(Debug, Clone)]
pub enum SchemaSource {
    /// Load from Nickel file
    File(PathBuf),

    /// Inline Nickel code
    Inline(String),

    /// Pre-compiled schema (for testing)
    Compiled(CompiledSchema),
}

/// Registry that loads and caches Nickel schemas
pub struct SchemaRegistry {
    /// Cached compiled schemas
    schemas: Arc<RwLock<HashMap<String, CompiledSchema>>>,

    /// Base directory for schema files
    schema_dir: PathBuf,

    /// Nickel CLI bridge
    nickel_cli: NickelCli,
}

impl SchemaRegistry {
    /// Create a new schema registry
    pub fn new(schema_dir: PathBuf) -> Self {
        Self {
            schemas: Arc::new(RwLock::new(HashMap::new())),
            schema_dir,
            nickel_cli: NickelCli::new(),
        }
    }

    /// Create registry with custom Nickel CLI path
    pub fn with_nickel_cli(schema_dir: PathBuf, nickel_path: PathBuf) -> Self {
        Self {
            schemas: Arc::new(RwLock::new(HashMap::new())),
            schema_dir,
            nickel_cli: NickelCli::with_path(nickel_path),
        }
    }

    /// Load schema from Nickel file, compile and cache
    pub async fn load_schema(&self, schema_name: &str) -> Result<CompiledSchema> {
        // Check cache first
        {
            let cache = self.schemas.read().await;
            if let Some(schema) = cache.get(schema_name) {
                debug!("Schema {} loaded from cache", schema_name);
                return Ok(schema.clone());
            }
        }

        // Load from file
        let schema_path = self.resolve_schema_path(schema_name);
        let compiled = self.compile_schema(&schema_path, schema_name).await?;

        // Cache it
        {
            let mut cache = self.schemas.write().await;
            cache.insert(schema_name.to_string(), compiled.clone());
        }

        info!("Schema {} compiled and cached", schema_name);
        Ok(compiled)
    }

    /// Load schema from source (file, inline, or pre-compiled)
    pub async fn load_from_source(
        &self,
        source: SchemaSource,
        schema_name: &str,
    ) -> Result<CompiledSchema> {
        match source {
            SchemaSource::File(path) => self.compile_schema(&path, schema_name).await,
            SchemaSource::Inline(code) => self.compile_inline(&code, schema_name).await,
            SchemaSource::Compiled(schema) => Ok(schema),
        }
    }

    /// Resolve schema name to file path
    fn resolve_schema_path(&self, schema_name: &str) -> PathBuf {
        let filename = if schema_name.ends_with(".ncl") {
            schema_name.to_string()
        } else {
            format!("{}.ncl", schema_name)
        };

        self.schema_dir.join(filename)
    }

    /// Compile Nickel schema from file to internal representation
    async fn compile_schema(&self, path: &Path, name: &str) -> Result<CompiledSchema> {
        if !path.exists() {
            return Err(VaporaError::ValidationError(format!(
                "Schema file not found: {}",
                path.display()
            )));
        }

        // Use nickel query to extract schema metadata
        let metadata_json = self.nickel_cli.query(path, None).await?;

        self.parse_nickel_metadata(&metadata_json, name, path)
    }

    /// Compile inline Nickel code
    async fn compile_inline(&self, code: &str, name: &str) -> Result<CompiledSchema> {
        // Write to temp file for Nickel CLI processing
        let temp_dir = std::env::temp_dir();
        let temp_path = temp_dir.join(format!("vapora_schema_{}.ncl", name.replace('/', "_")));

        tokio::fs::write(&temp_path, code).await.map_err(|e| {
            VaporaError::ValidationError(format!("Failed to write temp file: {}", e))
        })?;

        let result = self.compile_schema(&temp_path, name).await;

        // Clean up temp file
        let _ = tokio::fs::remove_file(&temp_path).await;

        result
    }

    /// Parse Nickel metadata JSON into CompiledSchema
    fn parse_nickel_metadata(
        &self,
        json: &serde_json::Value,
        name: &str,
        source_path: &Path,
    ) -> Result<CompiledSchema> {
        // Extract schema name from metadata or use provided name
        let schema_name = json["tool_name"]
            .as_str()
            .or_else(|| json["schema_name"].as_str())
            .unwrap_or(name)
            .to_string();

        // Extract parameters/fields object
        let params = json["parameters"]
            .as_object()
            .or_else(|| json["fields"].as_object());

        let fields = match params {
            Some(params_obj) => params_obj
                .iter()
                .map(|(field_name, field_def)| self.parse_field(field_name, field_def))
                .collect::<Result<Vec<_>>>()?,
            None => {
                warn!("No parameters or fields found in schema {}", name);
                vec![]
            }
        };

        // Extract custom contracts
        let custom_contracts = json["contracts"]
            .as_object()
            .map(|obj| {
                obj.iter()
                    .map(|(name, _code)| name.clone())
                    .collect::<Vec<_>>()
            })
            .unwrap_or_default();

        Ok(CompiledSchema {
            name: schema_name,
            fields,
            custom_contracts,
            source_path: source_path.to_path_buf(),
        })
    }

    /// Parse a single field definition from Nickel metadata
    fn parse_field(&self, name: &str, def: &serde_json::Value) -> Result<FieldSchema> {
        let field_type = self.parse_type(def)?;
        let contracts = self.extract_contracts(def);
        let default = def.get("default").cloned();
        let required = default.is_none();
        let doc = def.get("doc").and_then(|v| v.as_str()).map(String::from);

        Ok(FieldSchema {
            name: name.to_string(),
            field_type,
            required,
            contracts,
            default,
            doc,
        })
    }

    /// Parse Nickel type from metadata
    fn parse_type(&self, def: &serde_json::Value) -> Result<FieldType> {
        // Type information comes from Nickel metadata
        let type_str = def["type"].as_str().unwrap_or("String");

        let field_type = match type_str {
            "String" => FieldType::String,
            "Number" => FieldType::Number,
            "Bool" => FieldType::Bool,
            "Object" | "Record" => FieldType::Object,
            t if t.starts_with("Array") => {
                // Parse array element type
                FieldType::Array(Box::new(FieldType::String))
            }
            t if t.starts_with("Enum") => {
                // Extract enum variants
                if let Some(variants) = def["variants"].as_array() {
                    let values = variants
                        .iter()
                        .filter_map(|v| v.as_str().map(String::from))
                        .collect();
                    FieldType::Enum(values)
                } else {
                    FieldType::String
                }
            }
            _ => FieldType::String,
        };

        Ok(field_type)
    }

    /// Extract validation contracts from Nickel metadata
    fn extract_contracts(&self, def: &serde_json::Value) -> Vec<Contract> {
        let mut contracts = vec![];

        // Parse contract annotations from Nickel metadata
        if let Some(annotations) = def["annotations"].as_array() {
            for ann in annotations {
                if let Some(contract) = self.parse_contract_annotation(ann) {
                    contracts.push(contract);
                }
            }
        }

        contracts
    }

    /// Parse a single contract annotation
    fn parse_contract_annotation(&self, ann: &serde_json::Value) -> Option<Contract> {
        let name = ann["name"].as_str()?;

        match name {
            "std.string.NonEmpty" => Some(Contract::NonEmpty),
            "std.string.Email" => Some(Contract::Email),
            "std.string.Url" => Some(Contract::Url),
            "std.string.Uuid" => Some(Contract::Uuid),
            n if n.starts_with("std.string.length.min") => {
                let min = ann["args"][0].as_u64()? as usize;
                Some(Contract::MinLength(min))
            }
            n if n.starts_with("std.string.length.max") => {
                let max = ann["args"][0].as_u64()? as usize;
                Some(Contract::MaxLength(max))
            }
            n if n.starts_with("std.number.between") => {
                let min = ann["args"][0].as_f64()?;
                let max = ann["args"][1].as_f64()?;
                Some(Contract::Range { min, max })
            }
            n if n.starts_with("std.number.greater_than") => {
                let min = ann["args"][0].as_f64()?;
                Some(Contract::GreaterThan(min))
            }
            n if n.starts_with("std.number.less_than") => {
                let max = ann["args"][0].as_f64()?;
                Some(Contract::LessThan(max))
            }
            n if n.starts_with("std.string.match") => {
                let pattern = ann["args"][0].as_str()?.to_string();
                Some(Contract::Pattern(pattern))
            }
            _ => {
                // Custom contract - store source code
                ann["source"]
                    .as_str()
                    .map(|source| Contract::Custom(source.to_string()))
            }
        }
    }

    /// Invalidate cache for a specific schema (for hot-reload)
    pub async fn invalidate(&self, schema_name: &str) {
        let mut cache = self.schemas.write().await;
        cache.remove(schema_name);
        debug!("Schema {} invalidated from cache", schema_name);
    }

    /// Invalidate all cached schemas
    pub async fn invalidate_all(&self) {
        let mut cache = self.schemas.write().await;
        let count = cache.len();
        cache.clear();
        info!("Invalidated {} schemas from cache", count);
    }

    /// Get all cached schema names
    pub async fn list_cached(&self) -> Vec<String> {
        let cache = self.schemas.read().await;
        cache.keys().cloned().collect()
    }

    /// Check if schema is cached
    pub async fn is_cached(&self, schema_name: &str) -> bool {
        let cache = self.schemas.read().await;
        cache.contains_key(schema_name)
    }
}

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

    #[tokio::test]
    async fn test_parse_field_type() {
        let registry = SchemaRegistry::new(PathBuf::from("/tmp"));

        let string_def = serde_json::json!({"type": "String"});
        let field_type = registry.parse_type(&string_def).unwrap();
        assert_eq!(field_type, FieldType::String);

        let number_def = serde_json::json!({"type": "Number"});
        let field_type = registry.parse_type(&number_def).unwrap();
        assert_eq!(field_type, FieldType::Number);
    }

    #[tokio::test]
    async fn test_parse_contract_annotation() {
        let registry = SchemaRegistry::new(PathBuf::from("/tmp"));

        let non_empty = serde_json::json!({"name": "std.string.NonEmpty"});
        let contract = registry.parse_contract_annotation(&non_empty);
        assert_eq!(contract, Some(Contract::NonEmpty));

        let min_length = serde_json::json!({
            "name": "std.string.length.min",
            "args": [5]
        });
        let contract = registry.parse_contract_annotation(&min_length);
        assert_eq!(contract, Some(Contract::MinLength(5)));

        let range = serde_json::json!({
            "name": "std.number.between",
            "args": [0.0, 100.0]
        });
        let contract = registry.parse_contract_annotation(&range);
        assert_eq!(
            contract,
            Some(Contract::Range {
                min: 0.0,
                max: 100.0
            })
        );
    }

    #[tokio::test]
    async fn test_schema_source() {
        let inline_source = SchemaSource::Inline(r#"{ name = "test" }"#.to_string());
        assert!(matches!(inline_source, SchemaSource::Inline(_)));

        let file_source = SchemaSource::File(PathBuf::from("test.ncl"));
        assert!(matches!(file_source, SchemaSource::File(_)));
    }
}
