//! Plugin Configuration — Phase-92 (T92.4)
//!
//! Handles loading, parsing, and saving of `.plugins.json` configuration files.
//!
//! # File Format
//!
//! ```json
//! {
//!   "version": "1.0",
//!   "plugins": [
//!     {
//!       "name": "pageindex-aifastdb",
//!       "type": "indexer",
//!       "endpoint": "http://localhost:8800",
//!       "enabled": true,
//!       "auto_start": false,
//!       "config": {
//!         "timeout_secs": 60
//!       }
//!     }
//!   ]
//! }
//! ```

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

use serde::{Deserialize, Serialize};

use super::types::{PluginError, PluginResult, PluginType};

/// Top-level `.plugins.json` file structure.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginsConfig {
    /// Configuration file format version.
    #[serde(default = "default_config_version")]
    pub version: String,

    /// List of plugin entries.
    #[serde(default)]
    pub plugins: Vec<PluginEntry>,
}

fn default_config_version() -> String {
    "1.0".to_string()
}

impl Default for PluginsConfig {
    fn default() -> Self {
        Self {
            version: default_config_version(),
            plugins: Vec::new(),
        }
    }
}

impl PluginsConfig {
    /// Create an empty configuration.
    pub fn new() -> Self {
        Self::default()
    }

    /// Load from a file path.
    pub fn load(path: &Path) -> PluginResult<Self> {
        if !path.exists() {
            return Ok(Self::default());
        }

        let content = std::fs::read_to_string(path)?;
        let config: Self = serde_json::from_str(&content)
            .map_err(|e| PluginError::ConfigError(format!(
                "Failed to parse {}: {}", path.display(), e
            )))?;

        Ok(config)
    }

    /// Save to a file path.
    pub fn save(&self, path: &Path) -> PluginResult<()> {
        let json = serde_json::to_string_pretty(self)?;

        // Ensure parent directory exists
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent)?;
        }

        std::fs::write(path, json)?;
        Ok(())
    }

    /// Add a plugin entry.
    pub fn add_plugin(&mut self, entry: PluginEntry) {
        // Replace existing entry with same name
        self.plugins.retain(|p| p.name != entry.name);
        self.plugins.push(entry);
    }

    /// Remove a plugin entry by name.
    pub fn remove_plugin(&mut self, name: &str) -> bool {
        let initial_len = self.plugins.len();
        self.plugins.retain(|p| p.name != name);
        self.plugins.len() < initial_len
    }

    /// Get a plugin entry by name.
    pub fn get_plugin(&self, name: &str) -> Option<&PluginEntry> {
        self.plugins.iter().find(|p| p.name == name)
    }

    /// Get enabled plugin entries.
    pub fn enabled_plugins(&self) -> Vec<&PluginEntry> {
        self.plugins.iter().filter(|p| p.enabled).collect()
    }
}

/// A single plugin entry in the configuration file.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PluginEntry {
    /// Plugin name (must match the plugin's manifest name).
    pub name: String,

    /// Plugin type.
    #[serde(rename = "type")]
    pub plugin_type: PluginType,

    /// HTTP endpoint URL.
    pub endpoint: String,

    /// Whether the plugin is enabled.
    #[serde(default = "default_true")]
    pub enabled: bool,

    /// Whether to automatically start/connect the plugin on server startup.
    #[serde(default)]
    pub auto_start: bool,

    /// Plugin-specific configuration overrides.
    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
    pub config: HashMap<String, serde_json::Value>,
}

fn default_true() -> bool {
    true
}

impl PluginEntry {
    /// Create a new plugin entry with minimal fields.
    pub fn new(name: impl Into<String>, plugin_type: PluginType, endpoint: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            plugin_type,
            endpoint: endpoint.into(),
            enabled: true,
            auto_start: false,
            config: HashMap::new(),
        }
    }

    /// Set auto_start.
    pub fn with_auto_start(mut self, auto_start: bool) -> Self {
        self.auto_start = auto_start;
        self
    }

    /// Add a configuration value.
    pub fn with_config(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
        self.config.insert(key.into(), value);
        self
    }

    /// Get the configured timeout (or None to use default).
    pub fn timeout_secs(&self) -> Option<u64> {
        self.config
            .get("timeout_secs")
            .and_then(|v| v.as_u64())
    }
}

// ============================================================================
// PluginConfigWatcher (file change detection)
// ============================================================================

/// Watches `.plugins.json` for changes and provides hot-reload capability.
///
/// This is a polling-based watcher that checks the file's modification time.
pub struct PluginConfigWatcher {
    /// Path to the config file.
    path: PathBuf,

    /// Last known modification time.
    last_modified: Option<std::time::SystemTime>,
}

impl PluginConfigWatcher {
    /// Create a new watcher for the given config file path.
    pub fn new(path: impl Into<PathBuf>) -> Self {
        Self {
            path: path.into(),
            last_modified: None,
        }
    }

    /// Check if the config file has been modified since the last check.
    ///
    /// Returns `Some(PluginsConfig)` if the file was modified, `None` otherwise.
    pub fn check_for_changes(&mut self) -> Option<PluginsConfig> {
        let metadata = std::fs::metadata(&self.path).ok()?;
        let modified = metadata.modified().ok()?;

        if self.last_modified.as_ref() == Some(&modified) {
            return None; // No change
        }

        self.last_modified = Some(modified);

        match PluginsConfig::load(&self.path) {
            Ok(config) => {
                tracing::info!(
                    path = %self.path.display(),
                    plugins = config.plugins.len(),
                    "Plugin config file changed, reloading"
                );
                Some(config)
            }
            Err(e) => {
                tracing::warn!(
                    path = %self.path.display(),
                    error = %e,
                    "Failed to reload plugin config file"
                );
                None
            }
        }
    }

    /// Get the config file path.
    pub fn path(&self) -> &Path {
        &self.path
    }
}

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

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

    #[test]
    fn test_plugins_config_default() {
        let config = PluginsConfig::default();
        assert_eq!(config.version, "1.0");
        assert!(config.plugins.is_empty());
    }

    #[test]
    fn test_plugins_config_serialization() {
        let mut config = PluginsConfig::new();
        config.add_plugin(PluginEntry::new(
            "pageindex",
            PluginType::Indexer,
            "http://localhost:8800",
        ));

        let json = serde_json::to_string_pretty(&config).unwrap();
        assert!(json.contains("pageindex"));
        assert!(json.contains("http://localhost:8800"));

        let deserialized: PluginsConfig = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized.plugins.len(), 1);
        assert_eq!(deserialized.plugins[0].name, "pageindex");
    }

    #[test]
    fn test_plugins_config_add_remove() {
        let mut config = PluginsConfig::new();

        config.add_plugin(PluginEntry::new("p1", PluginType::Indexer, "http://a:8800"));
        config.add_plugin(PluginEntry::new("p2", PluginType::Processor, "http://b:8800"));
        assert_eq!(config.plugins.len(), 2);

        // Adding same name replaces
        config.add_plugin(PluginEntry::new("p1", PluginType::Custom, "http://c:8800"));
        assert_eq!(config.plugins.len(), 2);
        assert_eq!(config.get_plugin("p1").unwrap().endpoint, "http://c:8800");

        // Remove
        assert!(config.remove_plugin("p1"));
        assert_eq!(config.plugins.len(), 1);
        assert!(!config.remove_plugin("p1")); // Already removed
    }

    #[test]
    fn test_plugins_config_load_nonexistent() {
        let config = PluginsConfig::load(Path::new("/nonexistent/.plugins.json")).unwrap();
        assert!(config.plugins.is_empty());
    }

    #[test]
    fn test_plugins_config_save_and_load() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join(".plugins.json");

        let mut config = PluginsConfig::new();
        config.add_plugin(
            PluginEntry::new("test", PluginType::Indexer, "http://localhost:9000")
                .with_auto_start(true)
                .with_config("timeout_secs", serde_json::json!(60)),
        );

        config.save(&path).unwrap();

        let loaded = PluginsConfig::load(&path).unwrap();
        assert_eq!(loaded.plugins.len(), 1);
        assert_eq!(loaded.plugins[0].name, "test");
        assert!(loaded.plugins[0].auto_start);
        assert_eq!(loaded.plugins[0].timeout_secs(), Some(60));
    }

    #[test]
    fn test_plugin_entry_defaults() {
        let entry = PluginEntry::new("p", PluginType::Custom, "http://x:8000");
        assert!(entry.enabled);
        assert!(!entry.auto_start);
        assert!(entry.config.is_empty());
    }

    #[test]
    fn test_enabled_plugins_filter() {
        let mut config = PluginsConfig::new();
        config.add_plugin(PluginEntry::new("p1", PluginType::Indexer, "http://a:8800"));
        config.add_plugin({
            let mut e = PluginEntry::new("p2", PluginType::Processor, "http://b:8800");
            e.enabled = false;
            e
        });

        let enabled = config.enabled_plugins();
        assert_eq!(enabled.len(), 1);
        assert_eq!(enabled[0].name, "p1");
    }

    #[test]
    fn test_config_watcher_no_file() {
        let mut watcher = PluginConfigWatcher::new("/nonexistent/.plugins.json");
        assert!(watcher.check_for_changes().is_none());
    }

    #[test]
    fn test_config_watcher_detects_changes() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join(".plugins.json");

        let mut watcher = PluginConfigWatcher::new(&path);

        // No file yet
        assert!(watcher.check_for_changes().is_none());

        // Create file
        let config = PluginsConfig::new();
        config.save(&path).unwrap();

        // First check should detect the new file
        let result = watcher.check_for_changes();
        assert!(result.is_some());

        // Second check without changes should return None
        let result = watcher.check_for_changes();
        assert!(result.is_none());
    }
}
