//! Storage layer for custom XBRL concept mappings.
//!
//! Handles loading, saving, importing, and exporting mapping configurations.

use std::path::PathBuf;

use serde_json;

use super::types::{MappingError, MappingsConfig};
use crate::error::AppError;

/// Configuration file name
const MAPPINGS_FILE: &str = "mappings.json";
/// Backup directory name
const BACKUPS_DIR: &str = "backups";
/// Maximum number of backups to keep
const MAX_BACKUPS: usize = 10;

/// Storage handler for mappings configuration.
pub struct MappingsStorage {
    /// Path to the mappings configuration file
    config_path: PathBuf,
    /// Path to the backups directory
    backups_path: PathBuf,
}

impl MappingsStorage {
    /// Create a new storage handler.
    ///
    /// # Arguments
    /// * `app_data_dir` - The Tauri application data directory
    pub fn new(app_data_dir: PathBuf) -> Result<Self, MappingError> {
        let mappings_dir = app_data_dir.join("mappings");
        let config_path = mappings_dir.join(MAPPINGS_FILE);
        let backups_path = mappings_dir.join(BACKUPS_DIR);

        // Ensure directories exist
        std::fs::create_dir_all(&mappings_dir).map_err(|e| MappingError::Storage {
            reason: format!("Failed to create mappings directory: {}", e),
        })?;
        std::fs::create_dir_all(&backups_path).map_err(|e| MappingError::Storage {
            reason: format!("Failed to create backups directory: {}", e),
        })?;

        Ok(Self {
            config_path,
            backups_path,
        })
    }

    /// Load the mappings configuration from disk.
    pub fn load(&self) -> Result<MappingsConfig, MappingError> {
        if !self.config_path.exists() {
            // Return default config if file doesn't exist
            return Ok(MappingsConfig::default());
        }

        let content =
            std::fs::read_to_string(&self.config_path).map_err(|e| MappingError::Storage {
                reason: format!("Failed to read mappings file: {}", e),
            })?;

        serde_json::from_str(&content).map_err(|e| MappingError::Serialization {
            reason: format!("Failed to parse mappings JSON: {}", e),
        })
    }

    /// Save the mappings configuration to disk.
    ///
    /// Creates a backup before overwriting the existing file.
    pub fn save(&self, config: &MappingsConfig) -> Result<(), MappingError> {
        // Create backup if file exists
        if self.config_path.exists() {
            self.create_backup()?;
        }

        // Update timestamp
        let mut config_to_save = config.clone();
        config_to_save.last_updated = chrono::Utc::now().to_rfc3339();

        // Serialize to JSON with pretty printing
        let json = serde_json::to_string_pretty(&config_to_save).map_err(|e| {
            MappingError::Serialization {
                reason: format!("Failed to serialize mappings: {}", e),
            }
        })?;

        // Write to temporary file first (atomic write)
        let temp_path = self.config_path.with_extension("tmp");
        std::fs::write(&temp_path, json).map_err(|e| MappingError::Storage {
            reason: format!("Failed to write mappings file: {}", e),
        })?;

        // Atomic rename
        std::fs::rename(&temp_path, &self.config_path).map_err(|e| MappingError::Storage {
            reason: format!("Failed to save mappings file: {}", e),
        })?;

        Ok(())
    }

    /// Create a backup of the current mappings file.
    fn create_backup(&self) -> Result<(), MappingError> {
        if !self.config_path.exists() {
            return Ok(());
        }

        let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S");
        let backup_name = format!("mappings_{}.json", timestamp);
        let backup_path = self.backups_path.join(&backup_name);

        std::fs::copy(&self.config_path, &backup_path).map_err(|e| MappingError::Storage {
            reason: format!("Failed to create backup: {}", e),
        })?;

        // Clean up old backups
        self.cleanup_old_backups()?;

        Ok(())
    }

    /// Remove old backups, keeping only the most recent ones.
    fn cleanup_old_backups(&self) -> Result<(), MappingError> {
        let mut backups = std::fs::read_dir(&self.backups_path)
            .map_err(|e| MappingError::Storage {
                reason: format!("Failed to read backups directory: {}", e),
            })?
            .filter_map(Result::ok)
            .filter(|entry| entry.file_name().to_string_lossy().starts_with("mappings_"))
            .collect::<Vec<_>>();

        // Sort by modification time (newest first)
        backups.sort_by(|a, b| {
            let time_a = a.metadata().and_then(|m| m.modified()).ok();
            let time_b = b.metadata().and_then(|m| m.modified()).ok();
            time_b.cmp(&time_a)
        });

        // Remove old backups beyond the limit
        for backup in backups.into_iter().skip(MAX_BACKUPS) {
            if let Err(e) = std::fs::remove_file(backup.path()) {
                eprintln!("failed to remove old backup {:?}: {e}", backup.path());
            }
        }

        Ok(())
    }

    /// Import mappings from a JSON string.
    pub fn import(&self, json: &str) -> Result<MappingsConfig, MappingError> {
        let config: MappingsConfig =
            serde_json::from_str(json).map_err(|e| MappingError::Serialization {
                reason: format!("Failed to parse import JSON: {}", e),
            })?;

        // Validate the imported config
        self.validate_config(&config)?;

        Ok(config)
    }

    /// Export the current mappings configuration to a JSON string.
    pub fn export(&self) -> Result<String, MappingError> {
        let config = self.load()?;
        serde_json::to_string_pretty(&config).map_err(|e| MappingError::Serialization {
            reason: format!("Failed to serialize mappings: {}", e),
        })
    }

    /// Validate a mappings configuration.
    fn validate_config(&self, config: &MappingsConfig) -> Result<(), MappingError> {
        // Check version compatibility
        if config.version > 1 {
            return Err(MappingError::Invalid {
                reason: format!("Unsupported version: {}", config.version),
            });
        }

        // Validate each mapping
        for mapping in &config.mappings {
            if mapping.id.is_empty() {
                return Err(MappingError::Invalid {
                    reason: "Mapping has empty ID".to_string(),
                });
            }

            // Only validate concept mappings
            if let Some(custom_concept) = mapping.custom_concept() {
                if custom_concept.taxonomy.is_empty() {
                    return Err(MappingError::Invalid {
                        reason: "Custom concept has empty taxonomy".to_string(),
                    });
                }

                if custom_concept.concept.is_empty() {
                    return Err(MappingError::Invalid {
                        reason: "Custom concept has empty concept name".to_string(),
                    });
                }
            }
        }

        Ok(())
    }

    #[cfg(test)]
    pub fn config_path(&self) -> &PathBuf {
        &self.config_path
    }

    #[cfg(test)]
    pub fn backups_path(&self) -> &PathBuf {
        &self.backups_path
    }
}

impl From<MappingsStorage> for AppError {
    fn from(_error: MappingsStorage) -> Self {
        AppError::Mappings(MappingError::Storage {
            reason: "Mappings storage initialization failed".to_string(),
        })
    }
}

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

    fn create_test_storage() -> (MappingsStorage, TempDir) {
        let temp_dir = TempDir::new().unwrap();
        let storage = MappingsStorage::new(temp_dir.path().to_path_buf()).unwrap();
        (storage, temp_dir)
    }

    #[test]
    fn test_load_returns_default_when_file_not_exists() {
        let (storage, _temp) = create_test_storage();
        let config = storage.load().unwrap();
        assert_eq!(config.version, 1);
        assert!(config.mappings.is_empty());
    }

    #[test]
    fn test_storage_uses_single_mappings_directory_layer() {
        let (storage, temp) = create_test_storage();
        assert_eq!(
            storage.config_path(),
            &temp.path().join("mappings").join("mappings.json")
        );
        assert_eq!(
            storage.backups_path(),
            &temp.path().join("mappings").join("backups")
        );
    }

    #[test]
    fn test_save_and_load() {
        let (storage, _temp) = create_test_storage();
        let config = MappingsConfig::default();
        storage.save(&config).unwrap();

        let loaded = storage.load().unwrap();
        assert_eq!(loaded.version, 1);
        assert!(loaded.mappings.is_empty());
    }

    #[test]
    fn test_import_valid_json() {
        let (storage, _temp) = create_test_storage();
        let json = r#"{
            "version": 1,
            "lastUpdated": "2026-04-12T10:30:00Z",
            "mappings": []
        }"#;

        let config = storage.import(json).unwrap();
        assert_eq!(config.version, 1);
        assert!(config.mappings.is_empty());
    }

    #[test]
    fn test_import_invalid_json() {
        let (storage, _temp) = create_test_storage();
        let json = "invalid json";

        let result = storage.import(json);
        assert!(result.is_err());
    }

    #[test]
    fn test_export() {
        let (storage, _temp) = create_test_storage();
        let config = MappingsConfig::default();
        storage.save(&config).unwrap();

        let exported = storage.export().unwrap();
        assert!(exported.contains("\"version\": 1"));
        assert!(exported.contains("\"lastUpdated\""));
    }

    #[test]
    fn test_backup_created_on_save() {
        let (storage, _temp) = create_test_storage();

        // Initial save
        let config = MappingsConfig::default();
        storage.save(&config).unwrap();

        // Wait to ensure different timestamp
        std::thread::sleep(std::time::Duration::from_millis(10));

        // Second save should create backup
        storage.save(&config).unwrap();

        let backups = std::fs::read_dir(storage.backups_path())
            .unwrap()
            .filter_map(Result::ok)
            .filter(|entry| entry.file_name().to_string_lossy().starts_with("mappings_"))
            .count();

        assert!(backups >= 1);
    }

    #[test]
    fn test_validate_config_rejects_unsupported_version() {
        let (storage, _temp) = create_test_storage();
        let json = r#"{
            "version": 999,
            "lastUpdated": "2026-04-12T10:30:00Z",
            "mappings": []
        }"#;

        let result = storage.import(json);
        assert!(result.is_err());
    }

    #[test]
    fn test_import_accepts_legacy_snake_case_keys() {
        let (storage, _temp) = create_test_storage();
        let json = r#"{
            "version": 1,
            "last_updated": "2026-04-12T10:30:00Z",
            "mappings": [{
                "id": "2bfd7d10-0a8b-49d5-99c1-388f22d8df8f",
                "target_field": "total_revenues",
                "custom_concept": {
                    "taxonomy": "custom",
                    "concept": "TestRevenue",
                    "unit_family": "currency"
                },
                "scope": {
                    "ticker": "AAPL",
                    "period_start": "2024-01-01",
                    "period_end": "2024-12-31"
                },
                "enabled": true
            }]
        }"#;

        let config = storage.import(json).unwrap();
        assert_eq!(config.mappings.len(), 1);
        assert_eq!(config.mappings[0].scope.ticker.as_deref(), Some("AAPL"));
    }

    #[test]
    fn test_validate_config_rejects_empty_id() {
        let (storage, _temp) = create_test_storage();
        let json = r#"{
            "version": 1,
            "lastUpdated": "2026-04-12T10:30:00Z",
            "mappings": [{
                "id": "",
                "targetField": "total_revenues",
                "customConcept": {
                    "taxonomy": "custom",
                    "concept": "Test",
                    "unitFamily": "currency"
                },
                "scope": {},
                "enabled": true
            }]
        }"#;

        let result = storage.import(json);
        assert!(result.is_err());
    }
}
