//! Configuration management

use crate::error::Result;
use crate::phase::ProjectPhase;
use crate::types::{ProjectMetadata, ProjectMode, ProjectType, ToolMode};
use crate::LifecycleError;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;

/// SBOM format selection
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum SbomFormat {
    /// SPDX format (Software Package Data Exchange)
    #[serde(rename = "spdx")]
    Spdx,
    /// CycloneDX format (Component transparency)
    #[serde(rename = "cyclonedx")]
    CycloneDx,
    /// Both SPDX and CycloneDX formats
    #[serde(rename = "both")]
    #[default]
    Both,
}

impl std::fmt::Display for SbomFormat {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Spdx => write!(f, "spdx"),
            Self::CycloneDx => write!(f, "cyclonedx"),
            Self::Both => write!(f, "both"),
        }
    }
}

/// Checklist configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChecklistConfig {
    /// Default task type for new items (default: feature)
    #[serde(default = "default_task_type")]
    pub default_task_type: String,
    /// Default priority for new items (default: Medium)
    #[serde(default = "default_task_priority")]
    pub default_priority: String,
    /// Task type defaults per phase (optional)
    #[serde(default)]
    pub phase_defaults: std::collections::HashMap<String, ChecklistPhaseDefaults>,
}

/// Per-phase checklist defaults
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChecklistPhaseDefaults {
    /// Default task type for this phase
    #[serde(default)]
    pub default_task_type: Option<String>,
    /// Default priority for this phase
    #[serde(default)]
    pub default_priority: Option<String>,
}

impl Default for ChecklistConfig {
    fn default() -> Self {
        Self {
            default_task_type: "feature".to_string(),
            default_priority: "Medium".to_string(),
            phase_defaults: std::collections::HashMap::new(),
        }
    }
}

/// Default task type
fn default_task_type() -> String {
    "feature".to_string()
}

/// Default task priority
fn default_task_priority() -> String {
    "Medium".to_string()
}

/// SBOM (Software Bill of Materials) configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SbomConfig {
    /// Enabled SBOM generation
    #[serde(default = "default_sbom_enabled")]
    pub enabled: bool,
    /// Format(s) to generate (spdx, cyclonedx, or both)
    #[serde(default)]
    pub format: SbomFormat,
    /// Include transitive dependencies in output
    #[serde(default = "default_sbom_include_transitive")]
    pub include_transitive: bool,
    /// Include dev dependencies
    #[serde(default)]
    pub include_dev: bool,
}

fn default_sbom_enabled() -> bool {
    true
}

fn default_sbom_include_transitive() -> bool {
    true
}

impl Default for SbomConfig {
    fn default() -> Self {
        Self {
            enabled: true,
            format: SbomFormat::Both,
            include_transitive: true,
            include_dev: false,
        }
    }
}

/// Export format selection
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum ExportFormat {
    /// JSON format
    #[serde(rename = "json")]
    #[default]
    Json,
    /// CSV format
    #[serde(rename = "csv")]
    Csv,
    /// TOML format
    #[serde(rename = "toml")]
    Toml,
    /// YAML format
    #[serde(rename = "yaml")]
    Yaml,
    /// Markdown format
    #[serde(rename = "markdown")]
    Markdown,
}

impl std::fmt::Display for ExportFormat {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Json => write!(f, "json"),
            Self::Csv => write!(f, "csv"),
            Self::Toml => write!(f, "toml"),
            Self::Yaml => write!(f, "yaml"),
            Self::Markdown => write!(f, "markdown"),
        }
    }
}

impl ExportFormat {
    /// Parse format from string
    #[allow(clippy::should_implement_trait)]
    pub fn from_str(s: &str) -> crate::error::Result<Self> {
        match s.to_lowercase().as_str() {
            "json" => Ok(Self::Json),
            "csv" => Ok(Self::Csv),
            "toml" => Ok(Self::Toml),
            "yaml" | "yml" => Ok(Self::Yaml),
            "markdown" | "md" => Ok(Self::Markdown),
            _ => Err(crate::LifecycleError::invalid_config(format!(
                "Unknown export format: {}",
                s
            ))),
        }
    }

    /// Get file extension
    pub fn extension(&self) -> &'static str {
        match self {
            Self::Json => "json",
            Self::Csv => "csv",
            Self::Toml => "toml",
            Self::Yaml => "yaml",
            Self::Markdown => "md",
        }
    }
}

/// Project data export configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportConfig {
    /// Default export format (json, csv, toml, yaml, markdown)
    #[serde(default)]
    pub format: ExportFormat,
    /// Default output path with {timestamp} placeholder support
    /// If not set, exports to stdout
    #[serde(default = "default_export_output_path")]
    pub output_path: Option<String>,
    /// Include metadata in exports
    #[serde(default = "default_export_include_metadata")]
    pub include_metadata: bool,
    /// Include tags in exports
    #[serde(default = "default_export_include_tags")]
    pub include_tags: bool,
    /// Pretty-print output (for JSON, YAML, TOML)
    #[serde(default = "default_export_pretty")]
    pub pretty: bool,
}

fn default_export_output_path() -> Option<String> {
    Some("exports/project-{timestamp}.json".to_string())
}

fn default_export_include_metadata() -> bool {
    true
}

fn default_export_include_tags() -> bool {
    true
}

fn default_export_pretty() -> bool {
    true
}

impl Default for ExportConfig {
    fn default() -> Self {
        Self {
            format: ExportFormat::Json,
            output_path: default_export_output_path(),
            include_metadata: true,
            include_tags: true,
            pretty: true,
        }
    }
}

/// Project configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectConfig {
    /// Project metadata
    pub project: ProjectMetadata,
    /// Project type
    pub project_type: ProjectType,
    /// Project mode
    pub mode: ProjectMode,
    /// Primary language
    pub primary_language: String,
    /// Supported languages
    pub languages: Vec<String>,
    /// Current phase
    pub phase: ProjectPhase,
    /// Phase configuration
    pub phases: PhaseConfig,
    /// Tool configurations
    pub tools: ToolsConfig,
    /// Template variables
    pub template_variables: HashMap<String, String>,
    /// Database path (relative to project root)
    #[serde(default = "default_database_path")]
    pub database_path: String,
    /// SBOM configuration
    #[serde(default)]
    pub sbom: SbomConfig,
    /// Export configuration
    #[serde(default)]
    pub export: ExportConfig,
    /// Checklist configuration
    #[serde(default)]
    pub checklist: ChecklistConfig,
}

/// Default database path - relative to config directory
fn default_database_path() -> String {
    "data.db".to_string()
}

/// Phase configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PhaseConfig {
    /// Allowed transitions
    pub allowed_transitions: Vec<(ProjectPhase, ProjectPhase)>,
}

/// Tools configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolsConfig {
    /// Tool configurations by name
    pub tools: HashMap<String, ToolConfigEntry>,
}

/// Tool configuration entry
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolConfigEntry {
    /// Enabled
    pub enabled: bool,
    /// Active phases
    pub phases: Vec<ProjectPhase>,
    /// Tool mode
    pub mode: ToolMode,
    /// Configuration
    #[serde(default)]
    pub config: HashMap<String, serde_json::Value>,
}

impl Default for ProjectConfig {
    fn default() -> Self {
        let now = chrono::Utc::now();
        Self {
            project: ProjectMetadata {
                name: "my-project".to_string(),
                version: "0.1.0".to_string(),
                description: "My awesome project".to_string(),
                authors: vec!["Your Name".to_string()],
                repository: None,
                license: "MIT".to_string(),
                keywords: vec![],
                categories: vec![],
                created: now,
                last_updated: now,
            },
            project_type: ProjectType::RustWeb,
            mode: ProjectMode::Standalone,
            primary_language: "rust".to_string(),
            languages: vec!["rust".to_string()],
            phase: ProjectPhase::Creation,
            phases: PhaseConfig {
                allowed_transitions: vec![],
            },
            tools: ToolsConfig {
                tools: HashMap::new(),
            },
            template_variables: HashMap::new(),
            database_path: default_database_path(),
            sbom: SbomConfig::default(),
            export: ExportConfig::default(),
            checklist: ChecklistConfig::default(),
        }
    }
}

impl ProjectConfig {
    /// Create a new project config
    pub fn new(name: impl Into<String>) -> Self {
        let mut config = Self::default();
        config.project.name = name.into();
        config
    }

    /// Load config from TOML file
    pub fn load_from_file(path: &Path) -> Result<Self> {
        let content = std::fs::read_to_string(path)
            .map_err(|_| LifecycleError::file_not_found(path.to_path_buf()))?;

        Ok(toml::from_str(&content)?)
    }

    /// Save config to TOML file
    pub fn save_to_file(&self, path: &Path) -> Result<()> {
        let content =
            toml::to_string_pretty(self).map_err(|e| LifecycleError::TomlSeError(e.to_string()))?;
        std::fs::write(path, content)?;
        Ok(())
    }

    /// Merge with defaults - preserves existing values, fills in missing sections
    /// This is useful when loading an existing config that may not have all sections
    /// (e.g., old configs without sbom or export sections)
    pub fn merge_with_defaults(self) -> Self {
        // SBOM section: if enabled is not set, use default; if not present in config, add it
        // The serde defaults handle missing fields, so just ensure it's initialized
        // by calling the default constructor which sets all serde(default) fields

        // Export section: same logic - ensure it's initialized
        // Again handled by serde defaults

        // The config was loaded with serde defaults, so missing sections are already filled
        self
    }

    /// Validate configuration
    pub fn validate(&self) -> Result<()> {
        // Validate project name
        if self.project.name.is_empty() {
            return Err(LifecycleError::invalid_config(
                "Project name cannot be empty",
            ));
        }

        // Validate languages
        if self.languages.is_empty() {
            return Err(LifecycleError::invalid_config(
                "At least one language must be specified",
            ));
        }

        // Validate primary language
        if !self.languages.contains(&self.primary_language) {
            return Err(LifecycleError::invalid_config(
                "Primary language must be in languages list",
            ));
        }

        Ok(())
    }
}

/// Config loader
#[derive(Debug)]
pub struct ConfigLoader;

impl ConfigLoader {
    /// Load config from file
    pub fn load(path: &Path) -> Result<ProjectConfig> {
        ProjectConfig::load_from_file(path)
    }

    /// Save config to file
    pub fn save(config: &ProjectConfig, path: &Path) -> Result<()> {
        config.save_to_file(path)
    }
}

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

    #[test]
    fn test_default_config() {
        let config = ProjectConfig::default();
        assert!(!config.project.name.is_empty());
        assert!(!config.languages.is_empty());
        assert!(config.sbom.enabled);
        assert_eq!(config.sbom.format, SbomFormat::Both);
    }

    #[test]
    fn test_new_config() {
        let config = ProjectConfig::new("my-app");
        assert_eq!(config.project.name, "my-app");
    }

    #[test]
    fn test_config_validate() {
        let mut config = ProjectConfig::default();
        assert!(config.validate().is_ok());

        config.project.name = String::new();
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_sbom_format_default() {
        assert_eq!(SbomFormat::default(), SbomFormat::Both);
    }

    #[test]
    fn test_sbom_format_display() {
        assert_eq!(SbomFormat::Spdx.to_string(), "spdx");
        assert_eq!(SbomFormat::CycloneDx.to_string(), "cyclonedx");
        assert_eq!(SbomFormat::Both.to_string(), "both");
    }

    #[test]
    fn test_sbom_config_default() {
        let sbom = SbomConfig::default();
        assert!(sbom.enabled);
        assert_eq!(sbom.format, SbomFormat::Both);
        assert!(sbom.include_transitive);
        assert!(!sbom.include_dev);
    }

    #[test]
    fn test_sbom_format_serde() {
        let toml_spdx = "format = \"spdx\"";
        let config: SbomConfig = toml::from_str(toml_spdx).unwrap();
        assert_eq!(config.format, SbomFormat::Spdx);

        let toml_cyclone = "format = \"cyclonedx\"";
        let config: SbomConfig = toml::from_str(toml_cyclone).unwrap();
        assert_eq!(config.format, SbomFormat::CycloneDx);

        let toml_both = "format = \"both\"";
        let config: SbomConfig = toml::from_str(toml_both).unwrap();
        assert_eq!(config.format, SbomFormat::Both);
    }

    #[test]
    fn test_export_format_default() {
        assert_eq!(ExportFormat::default(), ExportFormat::Json);
    }

    #[test]
    fn test_export_format_display() {
        assert_eq!(ExportFormat::Json.to_string(), "json");
        assert_eq!(ExportFormat::Csv.to_string(), "csv");
        assert_eq!(ExportFormat::Toml.to_string(), "toml");
        assert_eq!(ExportFormat::Yaml.to_string(), "yaml");
        assert_eq!(ExportFormat::Markdown.to_string(), "markdown");
    }

    #[test]
    fn test_export_format_from_str() {
        assert_eq!(ExportFormat::from_str("json").unwrap(), ExportFormat::Json);
        assert_eq!(ExportFormat::from_str("csv").unwrap(), ExportFormat::Csv);
        assert_eq!(ExportFormat::from_str("toml").unwrap(), ExportFormat::Toml);
        assert_eq!(ExportFormat::from_str("yaml").unwrap(), ExportFormat::Yaml);
        assert_eq!(ExportFormat::from_str("yml").unwrap(), ExportFormat::Yaml);
        assert_eq!(
            ExportFormat::from_str("markdown").unwrap(),
            ExportFormat::Markdown
        );
        assert_eq!(
            ExportFormat::from_str("md").unwrap(),
            ExportFormat::Markdown
        );
        assert!(ExportFormat::from_str("invalid").is_err());
    }

    #[test]
    fn test_export_format_extension() {
        assert_eq!(ExportFormat::Json.extension(), "json");
        assert_eq!(ExportFormat::Csv.extension(), "csv");
        assert_eq!(ExportFormat::Toml.extension(), "toml");
        assert_eq!(ExportFormat::Yaml.extension(), "yaml");
        assert_eq!(ExportFormat::Markdown.extension(), "md");
    }

    #[test]
    fn test_export_config_default() {
        let export = ExportConfig::default();
        assert_eq!(export.format, ExportFormat::Json);
        assert!(export.output_path.is_some());
        assert!(export.output_path.as_ref().unwrap().contains("{timestamp}"));
        assert!(export.include_metadata);
        assert!(export.include_tags);
        assert!(export.pretty);
    }

    #[test]
    fn test_export_config_serde() {
        let toml = r#"
format = "csv"
output_path = ".project/exports/projects-{timestamp}.csv"
include_metadata = false
include_tags = true
pretty = false
"#;
        let config: ExportConfig = toml::from_str(toml).unwrap();
        assert_eq!(config.format, ExportFormat::Csv);
        assert_eq!(
            config.output_path,
            Some(".project/exports/projects-{timestamp}.csv".to_string())
        );
        assert!(!config.include_metadata);
        assert!(config.include_tags);
        assert!(!config.pretty);
    }

    #[test]
    fn test_config_includes_export() {
        let config = ProjectConfig::default();
        assert_eq!(config.export.format, ExportFormat::Json);
        assert!(config.export.include_metadata);
    }

    #[test]
    fn test_config_merge_with_defaults() {
        // Create a config
        let config = ProjectConfig::default();
        // Verify merge doesn't break anything
        let merged = config.merge_with_defaults();
        assert_eq!(merged.export.format, ExportFormat::Json);
        assert!(merged.sbom.enabled);
    }

    #[test]
    fn test_partial_config_load_with_defaults() {
        // Create a full config, serialize to TOML, remove export section,
        // then deserialize to verify defaults are applied
        let config = ProjectConfig::default();
        let toml_str = toml::to_string_pretty(&config).unwrap();

        // Remove export section to simulate old config file
        let mut lines: Vec<&str> = toml_str.lines().collect();

        // Find and remove [export] section
        if let Some(export_idx) = lines.iter().position(|line| line.contains("[export]")) {
            // Remove from [export] to next section or end
            let mut remove_to = lines.len();
            for (i, line) in lines[(export_idx + 1)..].iter().enumerate() {
                if line.starts_with('[') {
                    remove_to = export_idx + 1 + i;
                    break;
                }
            }
            lines.drain(export_idx..remove_to);
        }

        let partial_toml = lines.join("\n");

        // Now deserialize partial config - serde defaults should fill missing export section
        let loaded: ProjectConfig = toml::from_str(&partial_toml).unwrap();

        // Verify export section is present with defaults even though we removed it
        assert_eq!(loaded.export.format, ExportFormat::Json);
        assert!(loaded.export.include_metadata);
        assert!(loaded.sbom.enabled);
    }
}
