use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::time::Duration;

// ---------------------------------------------------------------------------
// Config file (TOML): behavioral settings only — paths are always computed
// ---------------------------------------------------------------------------

/// Settings that can be overridden via `config.toml` at user or project level.
/// All fields are optional so that a sparse config file can override just
/// what's needed.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct ConfigOverrides {
    pub search: Option<SearchConfig>,
    pub agents: Option<AgentConfig>,
    pub knowledge: Option<KnowledgeConfig>,
}

// ---------------------------------------------------------------------------
// FurnaceConfig: fully-resolved runtime configuration
// ---------------------------------------------------------------------------

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FurnaceConfig {
    pub workspace_root: PathBuf,

    /// User-level directory: `~/.furnace/`
    /// Holds global defaults: user recipes, telemetry, user config.toml.
    pub user_dir: PathBuf,

    /// Project-level directory: `<workspace>/.furnace/`
    /// Holds project-specific data: databases, project recipes, project config.toml.
    pub project_dir: PathBuf,

    /// True if the project directory did not exist before this run.
    /// Used to trigger first-run initialization (e.g., init scroll).
    #[serde(skip)]
    pub is_first_run: bool,

    pub search: SearchConfig,
    pub database: DatabaseConfig,
    pub agents: AgentConfig,

    /// Project-level recipes directory (checked first).
    pub recipes_dir: PathBuf,

    /// User-level recipes directory (fallback).
    pub user_recipes_dir: PathBuf,

    /// Directory for exported `.scroll` files: `<workspace>/.furnace/scrolls/`
    pub scrolls_dir: PathBuf,

    pub knowledge: Option<KnowledgeConfig>,
}

impl Default for FurnaceConfig {
    fn default() -> Self {
        let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
        let user_dir = home.join(".furnace");
        let workspace_root =
            std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        let project_dir = workspace_root.join(".furnace");

        Self {
            workspace_root,
            user_dir: user_dir.clone(),
            project_dir: project_dir.clone(),
            is_first_run: false,
            search: SearchConfig::default(),
            database: DatabaseConfig {
                sqlite_path: project_dir.join("furnace.db"),
                kuzu_path: project_dir.join("graph"),
                lancedb_path: project_dir.join("vectors"),
            },
            agents: AgentConfig::default(),
            recipes_dir: project_dir.join("recipes"),
            user_recipes_dir: user_dir.join("recipes"),
            scrolls_dir: project_dir.join("scrolls"),
            knowledge: None,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SearchConfig {
    pub cache_ttl: Duration,
    pub max_cache_entries: usize,
    pub daily_budget: f64,
    pub tier1_timeout: Duration,
    pub tier2_timeout: Duration,
    pub tier3_timeout: Duration,
    pub min_relevance_score: f64,
    pub max_results_per_tier: usize,
    /// Cost per Tier 1 (vector) search in dollars. Default: 0.0 (free).
    pub tier1_cost: f64,
    /// Cost per Tier 2 (hybrid) search in dollars. Default: 0.001.
    pub tier2_cost: f64,
    /// Cost per Tier 3 (agentic) search in dollars. Default: 0.01.
    pub tier3_cost: f64,
}

impl Default for SearchConfig {
    fn default() -> Self {
        Self {
            cache_ttl: Duration::from_secs(300),
            max_cache_entries: 1000,
            daily_budget: 1.0,
            tier1_timeout: Duration::from_millis(100),
            tier2_timeout: Duration::from_secs(1),
            tier3_timeout: Duration::from_secs(5),
            min_relevance_score: 0.5,
            max_results_per_tier: 20,
            tier1_cost: 0.0,
            tier2_cost: 0.001,
            tier3_cost: 0.01,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DatabaseConfig {
    pub sqlite_path: PathBuf,
    pub kuzu_path: PathBuf,
    pub lancedb_path: PathBuf,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct AgentConfig {
    pub max_concurrent_tasks: usize,
    pub task_timeout: Duration,
    pub max_retries: usize,
}

impl Default for AgentConfig {
    fn default() -> Self {
        Self {
            max_concurrent_tasks: 5,
            task_timeout: Duration::from_secs(300),
            max_retries: 3,
        }
    }
}

/// Configuration for external knowledge base integrations.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeConfig {
    /// Knowledge base source connections.
    pub sources: Vec<KnowledgeSourceConfig>,
    /// Whether to automatically blend KB results into search. Default: true.
    #[serde(default = "default_auto_search")]
    pub auto_search: bool,
    /// Maximum results per KB source in auto-search. Default: 5.
    #[serde(default = "default_max_kb_results")]
    pub max_kb_results: usize,
}

fn default_auto_search() -> bool {
    true
}

fn default_max_kb_results() -> usize {
    5
}

/// Configuration for a single knowledge base source.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeSourceConfig {
    /// Source type identifier (e.g., "ChromaDB", "Pinecone").
    pub source: String,
    /// Command to spawn the KB MCP server.
    pub command: String,
    /// Arguments for the command.
    #[serde(default)]
    pub args: Vec<String>,
    /// Environment variables to set.
    #[serde(default)]
    pub env_vars: Vec<(String, String)>,
    /// Default collection to query.
    pub collection: Option<String>,
    /// Glob pattern for matching multiple collections.
    pub collection_pattern: Option<String>,
}

impl FurnaceConfig {
    /// Load configuration with two-tier directory resolution:
    ///
    /// 1. Compute paths from `workspace_root` (cwd) and `~/.furnace/`
    /// 2. Read `<workspace>/.furnace/config.toml` if it exists, else
    ///    `~/.furnace/config.toml`, else use defaults
    /// 3. Databases always live in project dir (`<workspace>/.furnace/`)
    /// 4. Recipes cascade: project dir first, then user dir
    pub fn load() -> anyhow::Result<Self> {
        let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
        let user_dir = home.join(".furnace");
        let workspace_root =
            std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
        let project_dir = workspace_root.join(".furnace");

        let is_first_run = !project_dir.exists();

        // Read config overrides: project-level wins over user-level.
        let overrides = Self::load_overrides(&project_dir, &user_dir);

        Ok(Self {
            workspace_root,
            user_dir: user_dir.clone(),
            project_dir: project_dir.clone(),
            is_first_run,
            search: overrides.search.unwrap_or_default(),
            database: DatabaseConfig {
                sqlite_path: project_dir.join("furnace.db"),
                kuzu_path: project_dir.join("graph"),
                lancedb_path: project_dir.join("vectors"),
            },
            agents: overrides.agents.unwrap_or_default(),
            recipes_dir: project_dir.join("recipes"),
            user_recipes_dir: user_dir.join("recipes"),
            scrolls_dir: project_dir.join("scrolls"),
            knowledge: overrides.knowledge,
        })
    }

    /// Read config overrides from project-level then user-level TOML.
    fn load_overrides(project_dir: &Path, user_dir: &Path) -> ConfigOverrides {
        // Project-level takes priority.
        let project_config = project_dir.join("config.toml");
        if project_config.exists() {
            if let Ok(content) = std::fs::read_to_string(&project_config) {
                if let Ok(overrides) = toml::from_str::<ConfigOverrides>(&content) {
                    return overrides;
                }
            }
        }

        // Fall back to user-level.
        let user_config = user_dir.join("config.toml");
        if user_config.exists() {
            if let Ok(content) = std::fs::read_to_string(&user_config) {
                if let Ok(overrides) = toml::from_str::<ConfigOverrides>(&content) {
                    return overrides;
                }
            }
        }

        ConfigOverrides::default()
    }

    /// Generate a default config.toml at the given path if it doesn't exist.
    /// Called on first run to create the source-of-truth config file.
    pub fn write_default_config(path: &std::path::Path) -> anyhow::Result<()> {
        if path.exists() {
            return Ok(());
        }
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent)?;
        }
        let default_config = r#"# FURNACE Configuration
# Project-level config overrides user-level (~/.furnace/config.toml).

[search]
cache_ttl = 300              # seconds
daily_budget = 1.0           # dollars per day
tier1_timeout = 100          # ms
tier2_timeout = 1000         # ms
tier3_timeout = 5000         # ms
min_relevance_score = 0.5
max_results_per_tier = 20
tier1_cost = 0.0             # $ per search
tier2_cost = 0.001
tier3_cost = 0.01

[agents]
max_concurrent_tasks = 5
task_timeout = 300           # seconds
max_retries = 3

# [knowledge]
# auto_search = true         # blend KB results into code search
# max_kb_results = 5
#
# [[knowledge.sources]]
# source = "ChromaDB"
# command = "uvx"
# args = ["chroma-mcp-server"]
# collection = "design-docs"
"#;
        std::fs::write(path, default_config)?;
        Ok(())
    }

    /// Create all required directories at both user and project level.
    pub fn ensure_dirs(&self) -> anyhow::Result<()> {
        self.ensure_user_dirs()?;
        self.ensure_project_dirs()?;
        Ok(())
    }

    /// Create user-level directories only (`~/.furnace/`).
    ///
    /// Safe to call at startup — does not touch the project directory.
    pub fn ensure_user_dirs(&self) -> anyhow::Result<()> {
        std::fs::create_dir_all(&self.user_dir)?;
        std::fs::create_dir_all(&self.user_recipes_dir)?;
        Ok(())
    }

    /// Create project-level directories (`<workspace>/.furnace/`).
    ///
    /// Deferred until the first tool call so that `.furnace/` is not
    /// created in the user's workspace before they explicitly use FURNACE.
    pub fn ensure_project_dirs(&self) -> anyhow::Result<()> {
        std::fs::create_dir_all(&self.project_dir)?;
        // LanceDB path must be a directory.
        std::fs::create_dir_all(&self.database.lancedb_path)?;
        // Kuzu path is NOT created here — kuzu manages it as a file (not a
        // directory) in v0.11+. KuzuGraph::new() creates the parent dir.
        std::fs::create_dir_all(&self.recipes_dir)?;
        std::fs::create_dir_all(&self.scrolls_dir)?;
        Ok(())
    }
}

// ===========================================================================
// Tests
// ===========================================================================

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

    #[test]
    fn test_default_paths_are_project_level() {
        let config = FurnaceConfig::default();
        // Database paths should be under project_dir, not user_dir
        assert!(
            config.database.sqlite_path.starts_with(&config.project_dir),
            "SQLite should be under project_dir"
        );
        assert!(
            config.database.kuzu_path.starts_with(&config.project_dir),
            "Kuzu should be under project_dir"
        );
        assert!(
            config.database.lancedb_path.starts_with(&config.project_dir),
            "LanceDB should be under project_dir"
        );
    }

    #[test]
    fn test_recipes_dirs_are_separate() {
        let config = FurnaceConfig::default();
        assert!(
            config.recipes_dir.starts_with(&config.project_dir),
            "Project recipes should be under project_dir"
        );
        assert!(
            config.user_recipes_dir.starts_with(&config.user_dir),
            "User recipes should be under user_dir"
        );
        assert_ne!(config.recipes_dir, config.user_recipes_dir);
    }

    #[test]
    fn test_ensure_dirs_creates_all() {
        let tmp = tempfile::tempdir().unwrap();
        let user_dir = tmp.path().join("user_furnace");
        let project_dir = tmp.path().join("workspace").join(".furnace");

        let config = FurnaceConfig {
            workspace_root: tmp.path().join("workspace"),
            user_dir: user_dir.clone(),
            project_dir: project_dir.clone(),
            is_first_run: true,
            search: SearchConfig::default(),
            database: DatabaseConfig {
                sqlite_path: project_dir.join("furnace.db"),
                kuzu_path: project_dir.join("graph"),
                lancedb_path: project_dir.join("vectors"),
            },
            agents: AgentConfig::default(),
            recipes_dir: project_dir.join("recipes"),
            user_recipes_dir: user_dir.join("recipes"),
            scrolls_dir: project_dir.join("scrolls"),
            knowledge: None,
        };

        config.ensure_dirs().unwrap();

        assert!(user_dir.exists());
        assert!(user_dir.join("recipes").exists());
        assert!(project_dir.exists());
        // graph is NOT created by ensure_dirs — kuzu manages it as a file
        assert!(project_dir.exists());
        assert!(project_dir.join("vectors").exists());
        assert!(project_dir.join("recipes").exists());
        assert!(project_dir.join("scrolls").exists());
    }

    #[test]
    fn test_config_overrides_from_project_toml() {
        let tmp = tempfile::tempdir().unwrap();
        let project_dir = tmp.path().join("project");
        let user_dir = tmp.path().join("user");
        std::fs::create_dir_all(&project_dir).unwrap();
        std::fs::create_dir_all(&user_dir).unwrap();

        // Write project config with custom search settings
        let project_toml = r#"
[search]
daily_budget = 5.0
max_results_per_tier = 50
"#;
        std::fs::write(project_dir.join("config.toml"), project_toml).unwrap();

        let overrides = FurnaceConfig::load_overrides(
            &project_dir.to_path_buf(),
            &user_dir.to_path_buf(),
        );

        let search = overrides.search.unwrap();
        assert_eq!(search.daily_budget, 5.0);
        assert_eq!(search.max_results_per_tier, 50);
    }

    #[test]
    fn test_config_overrides_falls_back_to_user() {
        let tmp = tempfile::tempdir().unwrap();
        let project_dir = tmp.path().join("project");
        let user_dir = tmp.path().join("user");
        // Only create user dir — no project config
        std::fs::create_dir_all(&user_dir).unwrap();

        let user_toml = r#"
[agents]
max_concurrent_tasks = 10
"#;
        std::fs::write(user_dir.join("config.toml"), user_toml).unwrap();

        let overrides = FurnaceConfig::load_overrides(
            &project_dir.to_path_buf(),
            &user_dir.to_path_buf(),
        );

        let agents = overrides.agents.unwrap();
        assert_eq!(agents.max_concurrent_tasks, 10);
    }

    #[test]
    fn test_config_overrides_returns_default_when_no_files() {
        let tmp = tempfile::tempdir().unwrap();
        let project_dir = tmp.path().join("no_project");
        let user_dir = tmp.path().join("no_user");

        let overrides = FurnaceConfig::load_overrides(
            &project_dir.to_path_buf(),
            &user_dir.to_path_buf(),
        );

        assert!(overrides.search.is_none());
        assert!(overrides.agents.is_none());
        assert!(overrides.knowledge.is_none());
    }
}
