//! Configuration schema types
//!
//! These structs correspond to the Nickel schemas defined in `schemas/*.ncl`.
//! They are loaded via: Nickel CLI → JSON → serde → Rust structs.

use std::collections::HashMap;
use std::path::PathBuf;

use serde::{Deserialize, Serialize};

/// Main knowledge base configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KbConfig {
    /// Graph metadata
    pub graph: GraphConfig,
    /// Inheritance settings
    pub inheritance: InheritanceConfig,
    /// Storage backend configuration
    pub storage: StorageConfig,
    /// Embedding provider configuration
    pub embeddings: EmbeddingConfig,
    /// Template engine configuration
    pub templates: TemplateConfig,
    /// Query behavior configuration
    pub query: QueryConfig,
    /// MCP server configuration
    #[serde(default)]
    pub mcp: McpConfig,
    /// Sync settings
    #[serde(default)]
    pub sync: SyncConfig,
}

/// Resolve environment variables in paths
fn resolve_env_vars(path: &str) -> PathBuf {
    let resolved = path.replace("$TOOLS_PATH", &resolve_tools_path());
    let resolved = resolved.replace(
        "$HOME",
        &std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()),
    );
    PathBuf::from(resolved)
}

/// Get `TOOLS_PATH` with fallback to `$HOME/Tools`
fn resolve_tools_path() -> String {
    std::env::var("TOOLS_PATH").unwrap_or_else(|_| {
        let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
        format!("{home}/Tools")
    })
}

impl KbConfig {
    /// Resolve all environment variables in paths
    #[must_use]
    pub fn resolve_paths(mut self) -> Self {
        self.inheritance.base = resolve_env_vars(&self.inheritance.base.to_string_lossy());
        self.inheritance.guidelines = self
            .inheritance
            .guidelines
            .iter()
            .map(|p| resolve_env_vars(&p.to_string_lossy()))
            .collect();
        self
    }

    /// Create default configuration for a given graph name
    #[must_use]
    pub fn default_for_graph(name: String) -> Self {
        Self {
            graph: GraphConfig {
                name,
                version: "1.0.0".to_string(),
                description: String::new(),
            },
            inheritance: InheritanceConfig::default(),
            storage: StorageConfig::default(),
            embeddings: EmbeddingConfig::default(),
            templates: TemplateConfig::default(),
            query: QueryConfig::default(),
            mcp: McpConfig::default(),
            sync: SyncConfig::default(),
        }
    }
}

/// Graph metadata configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphConfig {
    /// Graph name/identifier
    pub name: String,
    /// Graph version (semver)
    #[serde(default = "default_version")]
    pub version: String,
    /// Human-readable description
    #[serde(default)]
    pub description: String,
}

fn default_version() -> String {
    "1.0.0".to_string()
}

/// Inheritance configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InheritanceConfig {
    /// Base path for shared KB
    #[serde(default = "default_base_path")]
    pub base: PathBuf,
    /// Additional guideline paths
    #[serde(default)]
    pub guidelines: Vec<PathBuf>,
    /// Override priority (higher = wins)
    #[serde(default = "default_priority")]
    pub priority: u32,
}

impl Default for InheritanceConfig {
    fn default() -> Self {
        Self {
            base: default_base_path(),
            guidelines: Vec::new(),
            priority: default_priority(),
        }
    }
}

fn default_base_path() -> PathBuf {
    if let Ok(tools_path) = std::env::var("TOOLS_PATH") {
        PathBuf::from(tools_path).join(".kogral-shared")
    } else {
        let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
        PathBuf::from(home).join("Tools/.kogral-shared")
    }
}

fn default_priority() -> u32 {
    100
}

/// Storage backend type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum StorageType {
    /// Filesystem storage (git-friendly, Logseq compatible)
    #[default]
    Filesystem,
    /// In-memory storage (development/cache)
    Memory,
}

/// Secondary storage backend type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum SecondaryStorageType {
    /// `SurrealDB` backend
    #[default]
    Surrealdb,
    /// `SQLite` backend
    Sqlite,
}

/// `SurrealDB` engine selector — dispatches via `engine::any::connect(url)`
///
/// Serialized with an internal `engine` tag so Nickel records map directly:
/// `{ engine = "surreal_kv", path = ".kogral/db/graph" }` → `SurrealKv`.
#[cfg(feature = "surrealdb-backend")]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "engine", rename_all = "snake_case")]
pub enum SurrealEngineConfig {
    /// In-memory — tests and ephemeral use only
    Mem,
    /// `SurrealKV` embedded (B-tree) — default for relational/graph data
    SurrealKv {
        /// Filesystem path for the `SurrealKV` database directory
        path: String,
    },
    /// `RocksDB` embedded (LSM) — default for hot/append data (embeddings, sessions)
    RocksDb {
        /// Filesystem path for the `RocksDB` database directory
        path: String,
    },
    /// Remote WebSocket — team and shared deployments
    Ws {
        /// Full WebSocket URL, e.g. `ws://surrealdb.internal:8000`
        url: String,
    },
}

#[cfg(feature = "surrealdb-backend")]
impl SurrealEngineConfig {
    /// Produce the URL string consumed by `surrealdb::engine::any::connect`
    #[must_use]
    pub fn to_url(&self) -> String {
        match self {
            Self::Mem              => "mem://".to_string(),
            Self::SurrealKv { path } => format!("surrealkv://{path}"),
            Self::RocksDb   { path } => format!("rocksdb://{path}"),
            Self::Ws        { url }  => url.clone(),
        }
    }
}

/// Dual-engine `SurrealDB` backend configuration
///
/// `graph` (default: `SurrealKV`) stores nodes, edges, and metadata using a
/// B-tree engine suited for random-access patterns. `hot` (default: `RocksDB`)
/// stores embeddings and session logs using an LSM engine suited for
/// append-heavy workloads.
#[cfg(feature = "surrealdb-backend")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SurrealDbBackendConfig {
    /// Engine for relational/graph data (nodes, edges, metadata)
    #[serde(default = "default_graph_engine")]
    pub graph: SurrealEngineConfig,
    /// Engine for hot data (embeddings, session logs)
    #[serde(default = "default_hot_engine")]
    pub hot: SurrealEngineConfig,
    /// `SurrealDB` namespace shared by both engines
    #[serde(default = "default_surreal_namespace")]
    pub namespace: String,
}

#[cfg(feature = "surrealdb-backend")]
impl Default for SurrealDbBackendConfig {
    fn default() -> Self {
        Self {
            graph: default_graph_engine(),
            hot: default_hot_engine(),
            namespace: default_surreal_namespace(),
        }
    }
}

#[cfg(feature = "surrealdb-backend")]
fn default_graph_engine() -> SurrealEngineConfig {
    SurrealEngineConfig::SurrealKv {
        path: ".kogral/db/graph".to_string(),
    }
}

#[cfg(feature = "surrealdb-backend")]
fn default_hot_engine() -> SurrealEngineConfig {
    SurrealEngineConfig::RocksDb {
        path: ".kogral/db/hot".to_string(),
    }
}

#[cfg(feature = "surrealdb-backend")]
fn default_surreal_namespace() -> String {
    "kogral".to_string()
}

/// Secondary storage configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Default)]
pub struct SecondaryStorageConfig {
    /// Whether secondary storage is enabled
    #[serde(default)]
    pub enabled: bool,
    /// Secondary storage type
    #[serde(rename = "type", default)]
    pub storage_type: SecondaryStorageType,
    /// `SurrealDB` engine configuration (dual hot/cold layout)
    #[cfg(feature = "surrealdb-backend")]
    #[serde(default)]
    pub surrealdb: SurrealDbBackendConfig,
}


/// NATS `JetStream` event publishing configuration
#[cfg(feature = "nats-events")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NatsEventConfig {
    /// NATS server URL (e.g. `nats://localhost:4222`)
    pub url: String,
    /// `JetStream` stream name
    pub stream_name: String,
    /// Durable consumer name
    pub consumer_name: String,
    /// Subjects this stream captures (e.g. `["kogral.>"]`)
    #[serde(default)]
    pub subjects: Vec<String>,
    /// `NKey` seed for signing published messages (optional)
    #[serde(default)]
    pub nkey_seed: Option<String>,
    /// Public `NKeys` whose signatures are trusted
    #[serde(default)]
    pub trusted_nkeys: Vec<String>,
    /// Reject messages that lack valid `NKey` signatures
    #[serde(default)]
    pub require_signed_messages: bool,
}

#[cfg(feature = "nats-events")]
impl From<NatsEventConfig> for platform_nats::NatsConfig {
    fn from(c: NatsEventConfig) -> Self {
        Self {
            url: c.url,
            stream_name: c.stream_name,
            consumer_name: c.consumer_name,
            subjects: c.subjects,
            nkey_seed: c.nkey_seed,
            trusted_nkeys: c.trusted_nkeys,
            require_signed_messages: c.require_signed_messages,
        }
    }
}

/// Storage backend configuration
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct StorageConfig {
    /// Primary storage type
    #[serde(default)]
    pub primary: StorageType,
    /// Secondary storage (optional, for scaling/search)
    #[serde(default)]
    pub secondary: SecondaryStorageConfig,
    /// NATS event publishing (optional)
    #[cfg(feature = "nats-events")]
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub nats: Option<NatsEventConfig>,
}

/// Embedding provider type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum EmbeddingProvider {
    /// `OpenAI` embeddings
    Openai,
    /// Claude embeddings (via API)
    Claude,
    /// Ollama local embeddings
    Ollama,
    /// `FastEmbed` local embeddings
    #[default]
    Fastembed,
}

/// Embedding configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbeddingConfig {
    /// Whether embeddings are enabled
    #[serde(default = "default_true")]
    pub enabled: bool,
    /// Provider selection
    #[serde(default)]
    pub provider: EmbeddingProvider,
    /// Model name
    #[serde(default = "default_embedding_model")]
    pub model: String,
    /// Vector dimensions
    #[serde(default = "default_dimensions")]
    pub dimensions: usize,
    /// Environment variable name for API key
    #[serde(default = "default_api_key_env")]
    pub api_key_env: String,
}

impl Default for EmbeddingConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            provider: EmbeddingProvider::default(),
            model: default_embedding_model(),
            dimensions: default_dimensions(),
            api_key_env: default_api_key_env(),
        }
    }
}

fn default_true() -> bool {
    true
}

fn default_embedding_model() -> String {
    "BAAI/bge-small-en-v1.5".to_string()
}

fn default_dimensions() -> usize {
    384
}

fn default_api_key_env() -> String {
    "OPENAI_API_KEY".to_string()
}

/// Template mappings per node type
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemplateMap {
    /// Note template
    #[serde(default = "default_note_template")]
    pub note: String,
    /// Decision template
    #[serde(default = "default_decision_template")]
    pub decision: String,
    /// Guideline template
    #[serde(default = "default_guideline_template")]
    pub guideline: String,
    /// Pattern template
    #[serde(default = "default_pattern_template")]
    pub pattern: String,
    /// Journal template
    #[serde(default = "default_journal_template")]
    pub journal: String,
    /// Execution template
    #[serde(default = "default_execution_template")]
    pub execution: String,
}

impl Default for TemplateMap {
    fn default() -> Self {
        Self {
            note: default_note_template(),
            decision: default_decision_template(),
            guideline: default_guideline_template(),
            pattern: default_pattern_template(),
            journal: default_journal_template(),
            execution: default_execution_template(),
        }
    }
}

fn default_note_template() -> String {
    "note.md.tera".to_string()
}

fn default_decision_template() -> String {
    "decision.md.tera".to_string()
}

fn default_guideline_template() -> String {
    "guideline.md.tera".to_string()
}

fn default_pattern_template() -> String {
    "pattern.md.tera".to_string()
}

fn default_journal_template() -> String {
    "journal.md.tera".to_string()
}

fn default_execution_template() -> String {
    "execution.md.tera".to_string()
}

/// Export template mappings
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportTemplateMap {
    /// Logseq page export template
    #[serde(default = "default_logseq_page_template")]
    pub logseq_page: String,
    /// Logseq journal export template
    #[serde(default = "default_logseq_journal_template")]
    pub logseq_journal: String,
    /// Summary report template
    #[serde(default = "default_summary_template")]
    pub summary: String,
    /// JSON export template
    #[serde(default = "default_json_template")]
    pub json: String,
}

impl Default for ExportTemplateMap {
    fn default() -> Self {
        Self {
            logseq_page: default_logseq_page_template(),
            logseq_journal: default_logseq_journal_template(),
            summary: default_summary_template(),
            json: default_json_template(),
        }
    }
}

fn default_logseq_page_template() -> String {
    "export/logseq-page.md.tera".to_string()
}

fn default_logseq_journal_template() -> String {
    "export/logseq-journal.md.tera".to_string()
}

fn default_summary_template() -> String {
    "export/summary.md.tera".to_string()
}

fn default_json_template() -> String {
    "export/graph.json.tera".to_string()
}

/// Template engine configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemplateConfig {
    /// Template directory path
    #[serde(default = "default_templates_dir")]
    pub templates_dir: PathBuf,
    /// Node type templates
    #[serde(default)]
    pub templates: TemplateMap,
    /// Export templates
    #[serde(default)]
    pub export: ExportTemplateMap,
    /// Custom template registry
    #[serde(default)]
    pub custom: HashMap<String, String>,
}

impl Default for TemplateConfig {
    fn default() -> Self {
        Self {
            templates_dir: default_templates_dir(),
            templates: TemplateMap::default(),
            export: ExportTemplateMap::default(),
            custom: HashMap::new(),
        }
    }
}

fn default_templates_dir() -> PathBuf {
    PathBuf::from("templates")
}

/// Query behavior configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryConfig {
    /// Minimum similarity threshold for matches (0.0 to 1.0)
    #[serde(default = "default_similarity_threshold")]
    pub similarity_threshold: f32,
    /// Maximum number of results
    #[serde(default = "default_max_results")]
    pub max_results: usize,
    /// Recency weight (higher = prefer more recent results)
    #[serde(default = "default_recency_weight")]
    pub recency_weight: f32,
    /// Whether cross-graph queries are enabled
    #[serde(default = "default_true")]
    pub cross_graph: bool,
}

impl Default for QueryConfig {
    fn default() -> Self {
        Self {
            similarity_threshold: default_similarity_threshold(),
            max_results: default_max_results(),
            recency_weight: default_recency_weight(),
            cross_graph: true,
        }
    }
}

fn default_similarity_threshold() -> f32 {
    0.4
}

fn default_max_results() -> usize {
    10
}

fn default_recency_weight() -> f32 {
    3.0
}

/// MCP transport type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum McpTransport {
    /// Standard I/O transport
    #[default]
    Stdio,
    /// Server-Sent Events transport
    Sse,
}

/// MCP server configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerConfig {
    /// Server name
    #[serde(default = "default_mcp_name")]
    pub name: String,
    /// Server version
    #[serde(default = "default_version")]
    pub version: String,
    /// Transport protocol
    #[serde(default)]
    pub transport: McpTransport,
}

impl Default for McpServerConfig {
    fn default() -> Self {
        Self {
            name: default_mcp_name(),
            version: default_version(),
            transport: McpTransport::default(),
        }
    }
}

fn default_mcp_name() -> String {
    "kogral-mcp".to_string()
}

/// MCP tools configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(clippy::struct_excessive_bools)]
pub struct McpToolsConfig {
    /// Enable search tool
    #[serde(default = "default_true")]
    pub search: bool,
    /// Enable `add_note` tool
    #[serde(default = "default_true")]
    pub add_note: bool,
    /// Enable `add_decision` tool
    #[serde(default = "default_true")]
    pub add_decision: bool,
    /// Enable link tool
    #[serde(default = "default_true")]
    pub link: bool,
    /// Enable `get_guidelines` tool
    #[serde(default = "default_true")]
    pub get_guidelines: bool,
    /// Enable export tool
    #[serde(default = "default_true")]
    pub export: bool,
}

impl Default for McpToolsConfig {
    fn default() -> Self {
        Self {
            search: true,
            add_note: true,
            add_decision: true,
            link: true,
            get_guidelines: true,
            export: true,
        }
    }
}

/// MCP resources configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpResourcesConfig {
    /// Expose project resources
    #[serde(default = "default_true")]
    pub expose_project: bool,
    /// Expose shared resources
    #[serde(default = "default_true")]
    pub expose_shared: bool,
}

impl Default for McpResourcesConfig {
    fn default() -> Self {
        Self {
            expose_project: true,
            expose_shared: true,
        }
    }
}

/// MCP server configuration
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct McpConfig {
    /// Server settings
    #[serde(default)]
    pub server: McpServerConfig,
    /// Tool enablement
    #[serde(default)]
    pub tools: McpToolsConfig,
    /// Resource exposure
    #[serde(default)]
    pub resources: McpResourcesConfig,
}

/// Sync configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyncConfig {
    /// Auto-index filesystem to `SurrealDB`
    #[serde(default = "default_true")]
    pub auto_index: bool,
    /// Paths to watch for changes
    #[serde(default = "default_watch_paths")]
    pub watch_paths: Vec<String>,
    /// Debounce time in milliseconds
    #[serde(default = "default_debounce_ms")]
    pub debounce_ms: u64,
}

impl Default for SyncConfig {
    fn default() -> Self {
        Self {
            auto_index: true,
            watch_paths: default_watch_paths(),
            debounce_ms: default_debounce_ms(),
        }
    }
}

fn default_watch_paths() -> Vec<String> {
    vec![
        "notes".to_string(),
        "decisions".to_string(),
        "guidelines".to_string(),
        "patterns".to_string(),
        "journal".to_string(),
    ]
}

fn default_debounce_ms() -> u64 {
    500
}

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

    #[test]
    fn test_kb_config_default() {
        let config = KbConfig::default_for_graph("test".to_string());
        assert_eq!(config.graph.name, "test");
        assert_eq!(config.graph.version, "1.0.0");
        assert!(config.embeddings.enabled);
        assert_eq!(config.storage.primary, StorageType::Filesystem);
    }

    #[test]
    fn test_storage_type_default() {
        assert_eq!(StorageType::default(), StorageType::Filesystem);
    }

    #[test]
    fn test_embedding_provider_default() {
        assert_eq!(EmbeddingProvider::default(), EmbeddingProvider::Fastembed);
    }

    #[test]
    fn test_query_config_defaults() {
        let config = QueryConfig::default();
        assert_eq!(config.similarity_threshold, 0.4);
        assert_eq!(config.max_results, 10);
        assert_eq!(config.recency_weight, 3.0);
        assert!(config.cross_graph);
    }

    #[test]
    fn test_mcp_config_defaults() {
        let config = McpConfig::default();
        assert_eq!(config.server.name, "kogral-mcp");
        assert_eq!(config.server.transport, McpTransport::Stdio);
        assert!(config.tools.search);
        assert!(config.tools.add_note);
        assert!(config.resources.expose_project);
    }

    #[test]
    fn test_template_map_defaults() {
        let templates = TemplateMap::default();
        assert_eq!(templates.note, "note.md.tera");
        assert_eq!(templates.decision, "decision.md.tera");
        assert_eq!(templates.guideline, "guideline.md.tera");
    }

    #[test]
    fn test_sync_config_defaults() {
        let config = SyncConfig::default();
        assert!(config.auto_index);
        assert_eq!(config.debounce_ms, 500);
        assert_eq!(config.watch_paths.len(), 5);
        assert!(config.watch_paths.contains(&"notes".to_string()));
    }

    #[test]
    fn test_serialization_roundtrip() {
        let config = KbConfig::default_for_graph("test".to_string());
        let json = serde_json::to_string(&config).unwrap();
        let deserialized: KbConfig = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.graph.name, config.graph.name);
    }
}
