//! Treefile parsing and validation for apt-ostree
//! 
//! This module handles the declarative configuration files that describe
//! how to generate an OSTree commit from a set of Debian packages.

use std::collections::HashMap;
use std::path::Path;
use serde::{Deserialize, Serialize};
use apt_ostree::lib::error::{AptOstreeError, AptOstreeResult};

/// Main treefile structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Treefile {
    /// API version for the treefile format
    #[serde(default = "default_api_version")]
    pub api_version: String,
    
    /// Kind of treefile
    #[serde(default = "default_kind")]
    pub kind: String,
    
    /// Metadata about the tree
    pub metadata: TreefileMetadata,
    
    /// Package configuration
    pub packages: PackageConfig,
    
    /// Repository configuration
    pub repositories: Vec<Repository>,
    
    /// Customizations to apply
    pub customizations: Customizations,
    
    /// Output configuration
    pub output: OutputConfig,
    
    /// Commit message for the generated tree
    #[serde(default = "default_commit_message")]
    pub commit_message: String,
}

fn default_api_version() -> String {
    "apt-ostree/v1".to_string()
}

fn default_kind() -> String {
    "Treefile".to_string()
}

fn default_commit_message() -> String {
    "Composed tree from apt-ostree".to_string()
}

/// Treefile metadata
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TreefileMetadata {
    /// Name of the tree
    pub name: String,
    
    /// Version of the tree
    pub version: String,
    
    /// Description of the tree
    #[serde(default)]
    pub description: Option<String>,
    
    /// Architecture target
    #[serde(default = "default_architecture")]
    pub architecture: String,
    
    /// Base distribution
    #[serde(default = "default_distribution")]
    pub distribution: String,
    
    /// Release codename
    #[serde(default = "default_release")]
    pub release: String,
}

fn default_architecture() -> String {
    "amd64".to_string()
}

fn default_distribution() -> String {
    "debian".to_string()
}

fn default_release() -> String {
    "trixie".to_string()
}

/// Package configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageConfig {
    /// Base packages to always include
    #[serde(default)]
    pub base_packages: Vec<String>,
    
    /// Packages to include
    #[serde(default)]
    pub include: Vec<String>,
    
    /// Packages to exclude
    #[serde(default)]
    pub exclude: Vec<String>,
    
    /// Package groups to include
    #[serde(default)]
    pub groups: Vec<String>,
    
    /// Package overrides
    #[serde(default)]
    pub overrides: Vec<PackageOverride>,
}

/// Package override configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PackageOverride {
    /// Package name
    pub name: String,
    
    /// Override action
    pub action: OverrideAction,
    
    /// Version constraint
    #[serde(default)]
    pub version: Option<String>,
    
    /// Architecture constraint
    #[serde(default)]
    pub architecture: Option<String>,
}

/// Package override actions
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OverrideAction {
    /// Replace the package
    Replace,
    /// Remove the package
    Remove,
    /// Pin to specific version
    Pin,
}

/// Repository configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Repository {
    /// Repository name
    pub name: String,
    
    /// Repository URL
    pub url: String,
    
    /// Repository suite/distribution
    pub suite: String,
    
    /// Repository components
    #[serde(default = "default_components")]
    pub components: Vec<String>,
    
    /// GPG key for repository
    #[serde(default)]
    pub gpg_key: Option<String>,
    
    /// Whether repository is enabled
    #[serde(default = "default_enabled")]
    pub enabled: bool,
}

fn default_components() -> Vec<String> {
    vec!["main".to_string(), "contrib".to_string(), "non-free".to_string()]
}

fn default_enabled() -> bool {
    true
}

/// Customizations to apply to the tree
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Customizations {
    /// File modifications
    #[serde(default)]
    pub files: Vec<FileModification>,
    
    /// Package overrides
    #[serde(default)]
    pub package_overrides: Vec<PackageOverride>,
    
    /// System modifications
    #[serde(default)]
    pub system_modifications: Vec<SystemModification>,
    
    /// Scripts to run
    #[serde(default)]
    pub scripts: Vec<Script>,
}

/// File modification configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileModification {
    /// Path to the file
    pub path: String,
    
    /// Action to perform on the file
    pub action: FileAction,
    
    /// Content for the file (if creating/modifying)
    #[serde(default)]
    pub content: Option<String>,
    
    /// Source file to copy from
    #[serde(default)]
    pub source: Option<String>,
    
    /// File permissions
    #[serde(default = "default_file_permissions")]
    pub permissions: u32,
}

fn default_file_permissions() -> u32 {
    0o644
}

/// File actions
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum FileAction {
    /// Create or modify the file
    Create,
    /// Copy the file from source
    Copy,
    /// Remove the file
    Remove,
    /// Set permissions only
    Chmod,
}

/// System modification configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemModification {
    /// Name of the modification
    pub name: String,
    
    /// Type of modification
    pub modification_type: ModificationType,
    
    /// Configuration data
    pub config: serde_json::Value,
}

/// Modification types
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ModificationType {
    /// User/group management
    Users,
    /// Service configuration
    Services,
    /// Network configuration
    Network,
    /// Security configuration
    Security,
    /// Custom modification
    Custom,
}

/// Script configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Script {
    /// Script name
    pub name: String,
    
    /// Script content
    pub content: String,
    
    /// Script interpreter
    #[serde(default = "default_interpreter")]
    pub interpreter: String,
    
    /// Whether script runs as root
    #[serde(default)]
    pub run_as_root: bool,
    
    /// Script execution order
    #[serde(default)]
    pub order: Option<u32>,
}

fn default_interpreter() -> String {
    "/bin/bash".to_string()
}

/// Output configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputConfig {
    /// Output format
    #[serde(default = "default_output_format")]
    pub format: OutputFormat,
    
    /// Output path
    pub path: String,
    
    /// Image size specification
    #[serde(default)]
    pub size: Option<String>,
    
    /// Compression algorithm
    #[serde(default)]
    pub compression: Option<CompressionAlgorithm>,
}

/// Output formats
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OutputFormat {
    /// Raw disk image
    Raw,
    /// ISO image
    Iso,
    /// QCOW2 image
    Qcow2,
    /// VMDK image
    Vmdk,
    /// OSTree repository
    Ostree,
}

fn default_output_format() -> OutputFormat {
    OutputFormat::Ostree
}

/// Compression algorithms
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CompressionAlgorithm {
    /// Gzip compression
    Gzip,
    /// XZ compression
    Xz,
    /// LZ4 compression
    Lz4,
    /// Zstd compression
    Zstd,
}

/// Parse a treefile from a file path
pub async fn parse_treefile(path: &str) -> AptOstreeResult<Treefile> {
    let content = tokio::fs::read_to_string(path).await
        .map_err(|e| AptOstreeError::System(format!("Failed to read treefile {}: {}", path, e)))?;
    
    parse_treefile_content(&content)
}

/// Parse a treefile from content string
pub fn parse_treefile_content(content: &str) -> AptOstreeResult<Treefile> {
    // Try to detect format
    let format = detect_format(content);
    
    let treefile: Treefile = match format {
        InputFormat::Yaml => serde_yaml::from_str(content)
            .map_err(|e| AptOstreeError::InvalidArgument(format!("Failed to parse YAML treefile: {}", e)))?,
        InputFormat::Json => serde_json::from_str(content)
            .map_err(|e| AptOstreeError::InvalidArgument(format!("Failed to parse JSON treefile: {}", e)))?,
    };
    
    // Validate the treefile
    validate_treefile(&treefile)?;
    
    Ok(treefile)
}

/// Input format detection
#[derive(Debug, Clone)]
enum InputFormat {
    Yaml,
    Json,
}

/// Detect the input format
fn detect_format(content: &str) -> InputFormat {
    let trimmed = content.trim();
    if trimmed.starts_with('{') || trimmed.starts_with('[') {
        InputFormat::Json
    } else {
        InputFormat::Yaml
    }
}

/// Validate a treefile
fn validate_treefile(treefile: &Treefile) -> AptOstreeResult<()> {
    // Check API version
    if treefile.api_version != "apt-ostree/v1" {
        return Err(AptOstreeError::InvalidArgument(
            format!("Unsupported API version: {}", treefile.api_version)
        ));
    }
    
    // Check kind
    if treefile.kind != "Treefile" {
        return Err(AptOstreeError::InvalidArgument(
            format!("Invalid kind: {}", treefile.kind)
        ));
    }
    
    // Validate metadata
    if treefile.metadata.name.is_empty() {
        return Err(AptOstreeError::InvalidArgument("Tree name cannot be empty".to_string()));
    }
    
    if treefile.metadata.version.is_empty() {
        return Err(AptOstreeError::InvalidArgument("Tree version cannot be empty".to_string()));
    }
    
    // Validate repositories
    for repo in &treefile.repositories {
        if repo.name.is_empty() {
            return Err(AptOstreeError::InvalidArgument("Repository name cannot be empty".to_string()));
        }
        
        if repo.url.is_empty() {
            return Err(AptOstreeError::InvalidArgument("Repository URL cannot be empty".to_string()));
        }
        
        if repo.suite.is_empty() {
            return Err(AptOstreeError::InvalidArgument("Repository suite cannot be empty".to_string()));
        }
    }
    
    // Validate packages
    if treefile.packages.include.is_empty() && treefile.packages.groups.is_empty() {
        return Err(AptOstreeError::InvalidArgument(
            "At least one package or group must be specified".to_string()
        ));
    }
    
    Ok(())
}

/// Create a default treefile for testing
pub fn create_default_treefile() -> Treefile {
    Treefile {
        api_version: "apt-ostree/v1".to_string(),
        kind: "Treefile".to_string(),
        metadata: TreefileMetadata {
            name: "debian-silverblue".to_string(),
            version: "13.0".to_string(),
            description: Some("Default Debian Silverblue tree".to_string()),
            architecture: "amd64".to_string(),
            distribution: "debian".to_string(),
            release: "trixie".to_string(),
        },
        packages: PackageConfig {
            base_packages: vec![
                "systemd".to_string(),
                "systemd-sysv".to_string(),
                "dbus".to_string(),
                "polkit".to_string(),
            ],
            include: vec![
                "vim".to_string(),
                "git".to_string(),
                "curl".to_string(),
            ],
            exclude: vec![],
            groups: vec![],
            overrides: vec![],
        },
        repositories: vec![
            Repository {
                name: "debian".to_string(),
                url: "http://deb.debian.org/debian".to_string(),
                suite: "trixie".to_string(),
                components: vec!["main".to_string(), "contrib".to_string(), "non-free".to_string()],
                gpg_key: None,
                enabled: true,
            },
        ],
        customizations: Customizations::default(),
        output: OutputConfig {
            format: OutputFormat::Ostree,
            path: "/srv/ostree/repo".to_string(),
            size: None,
            compression: None,
        },
        commit_message: "Default Debian Silverblue tree".to_string(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_default_treefile() {
        let treefile = create_default_treefile();
        assert_eq!(treefile.api_version, "apt-ostree/v1");
        assert_eq!(treefile.kind, "Treefile");
        assert_eq!(treefile.metadata.name, "debian-silverblue");
        assert_eq!(treefile.metadata.architecture, "amd64");
    }
    
    #[test]
    fn test_parse_yaml_treefile() {
        let yaml_content = r#"
apiVersion: "apt-ostree/v1"
kind: "Treefile"
metadata:
  name: "test-tree"
  version: "1.0"
  architecture: "amd64"
  distribution: "debian"
  release: "trixie"
packages:
  include: ["vim", "git"]
repositories:
  - name: "debian"
    url: "http://deb.debian.org/debian"
    suite: "trixie"
    components: ["main"]
output:
  format: "ostree"
  path: "/tmp/repo"
"#;
        
        let treefile = parse_treefile_content(yaml_content).unwrap();
        assert_eq!(treefile.metadata.name, "test-tree");
        assert_eq!(treefile.packages.include, vec!["vim", "git"]);
    }
    
    #[test]
    fn test_validate_treefile() {
        let mut treefile = create_default_treefile();
        
        // Valid treefile should pass validation
        assert!(validate_treefile(&treefile).is_ok());
        
        // Invalid API version should fail
        treefile.api_version = "invalid/v1".to_string();
        assert!(validate_treefile(&treefile).is_err());
        
        // Reset and test invalid kind
        treefile = create_default_treefile();
        treefile.kind = "Invalid".to_string();
        assert!(validate_treefile(&treefile).is_err());
        
        // Reset and test empty name
        treefile = create_default_treefile();
        treefile.metadata.name = "".to_string();
        assert!(validate_treefile(&treefile).is_err());
    }
}
