//! Topology configuration: declarative multi-container definitions.

use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

/// A complete topology definition (equivalent to docker-compose.yml).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TopologyConfig {
    /// Topology name (used as systemd unit prefix and bridge name)
    pub name: String,

    /// Network definitions
    #[serde(default)]
    pub networks: BTreeMap<String, NetworkDef>,

    /// Volume definitions
    #[serde(default)]
    pub volumes: BTreeMap<String, VolumeDef>,

    /// Service (container) definitions
    pub services: BTreeMap<String, ServiceDef>,
}

/// Network definition within a topology.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkDef {
    /// Subnet CIDR (e.g. "10.42.0.0/24")
    #[serde(default = "default_subnet")]
    pub subnet: String,

    /// Enable WireGuard encryption for east-west traffic
    #[serde(default)]
    pub encrypted: bool,
}

fn default_subnet() -> String {
    "10.42.0.0/24".to_string()
}

/// Volume definition within a topology.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VolumeDef {
    /// Volume type: "persistent" (host path) or "ephemeral" (tmpfs)
    #[serde(default = "default_volume_type")]
    pub volume_type: String,

    /// Host path for persistent volumes
    pub path: Option<String>,

    /// Owner UID:GID for the volume
    pub owner: Option<String>,

    /// Size limit (e.g. "1G") for ephemeral volumes
    pub size: Option<String>,
}

fn default_volume_type() -> String {
    "ephemeral".to_string()
}

/// Service (container) definition within a topology.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceDef {
    /// Nix store path to rootfs derivation
    pub rootfs: String,

    /// Command to run
    pub command: Vec<String>,

    /// Memory limit (e.g. "512M", "2G")
    pub memory: String,

    /// CPU core limit
    #[serde(default = "default_cpus")]
    pub cpus: f64,

    /// PID limit
    #[serde(default = "default_pids")]
    pub pids: u64,

    /// Networks this service connects to
    #[serde(default)]
    pub networks: Vec<String>,

    /// Volume mounts (format: "volume-name:/mount/path")
    #[serde(default)]
    pub volumes: Vec<String>,

    /// Services this depends on, with optional health condition
    #[serde(default)]
    pub depends_on: Vec<DependsOn>,

    /// Health check command
    pub health_check: Option<String>,

    /// Health check interval in seconds
    #[serde(default = "default_health_interval")]
    pub health_interval: u64,

    /// Allowed egress CIDRs
    #[serde(default)]
    pub egress_allow: Vec<String>,

    /// Allowed egress TCP ports
    #[serde(default)]
    pub egress_tcp_ports: Vec<u16>,

    /// Port forwards (format: "HOST:CONTAINER" or "HOST_IP:HOST:CONTAINER")
    #[serde(default)]
    pub port_forwards: Vec<String>,

    /// Environment variables
    #[serde(default)]
    pub environment: BTreeMap<String, String>,

    /// Workload user name or numeric uid.
    #[serde(default)]
    pub user: Option<String>,

    /// Workload group name or numeric gid.
    #[serde(default)]
    pub group: Option<String>,

    /// Supplementary workload groups (names or numeric gids).
    #[serde(default)]
    pub additional_groups: Vec<String>,

    /// Secret mounts (format: "source:dest")
    #[serde(default)]
    pub secrets: Vec<String>,

    /// DNS servers
    #[serde(default)]
    pub dns: Vec<String>,

    /// Native bridge NAT backend.
    #[serde(default = "default_nat_backend")]
    pub nat_backend: crate::network::NatBackend,

    /// Number of replicas for scaling
    #[serde(default = "default_replicas")]
    pub replicas: u32,

    /// Container runtime
    #[serde(default = "default_runtime")]
    pub runtime: String,

    /// OCI lifecycle hooks
    #[serde(default)]
    pub hooks: Option<crate::security::OciHooks>,
}

fn default_cpus() -> f64 {
    1.0
}

fn default_pids() -> u64 {
    512
}

fn default_health_interval() -> u64 {
    30
}

fn default_replicas() -> u32 {
    1
}

fn default_nat_backend() -> crate::network::NatBackend {
    crate::network::NatBackend::Auto
}

fn default_runtime() -> String {
    "native".to_string()
}

/// Dependency specification with optional health condition.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DependsOn {
    /// Service name
    pub service: String,

    /// Condition: "started" (default) or "healthy"
    #[serde(default = "default_condition")]
    pub condition: String,
}

fn default_condition() -> String {
    "started".to_string()
}

/// Parsed service volume reference.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServiceVolumeMount {
    /// Referenced topology volume name.
    pub volume: String,
    /// Destination path inside the container.
    pub dest: PathBuf,
    /// Whether the mount is read-only.
    pub read_only: bool,
}

pub(crate) fn parse_service_volume_mount(spec: &str) -> crate::error::Result<ServiceVolumeMount> {
    let parts: Vec<&str> = spec.split(':').collect();
    let (volume, dest, read_only) = match parts.as_slice() {
        [volume, dest] => (*volume, *dest, false),
        [volume, dest, mode] if *mode == "ro" => (*volume, *dest, true),
        [volume, dest, mode] if *mode == "rw" => (*volume, *dest, false),
        _ => {
            return Err(crate::error::NucleusError::ConfigError(format!(
                "Invalid volume mount '{}', expected VOLUME:DEST[:ro|rw]",
                spec
            )));
        }
    };

    if volume.is_empty() {
        return Err(crate::error::NucleusError::ConfigError(format!(
            "Volume mount '{}' must name a topology volume",
            spec
        )));
    }

    let dest = crate::filesystem::normalize_container_destination(Path::new(dest))?;
    Ok(ServiceVolumeMount {
        volume: volume.to_string(),
        dest,
        read_only,
    })
}

pub(crate) fn parse_volume_owner(owner: &str) -> crate::error::Result<(u32, u32)> {
    let (uid, gid) = owner.split_once(':').ok_or_else(|| {
        crate::error::NucleusError::ConfigError(format!(
            "Invalid volume owner '{}', expected UID:GID",
            owner
        ))
    })?;
    let uid = uid.parse::<u32>().map_err(|e| {
        crate::error::NucleusError::ConfigError(format!(
            "Invalid volume owner UID '{}' in '{}': {}",
            uid, owner, e
        ))
    })?;
    let gid = gid.parse::<u32>().map_err(|e| {
        crate::error::NucleusError::ConfigError(format!(
            "Invalid volume owner GID '{}' in '{}': {}",
            gid, owner, e
        ))
    })?;
    Ok((uid, gid))
}

impl TopologyConfig {
    /// Load a topology from a TOML file.
    pub fn from_file(path: &Path) -> crate::error::Result<Self> {
        let content = std::fs::read_to_string(path).map_err(|e| {
            crate::error::NucleusError::ConfigError(format!(
                "Failed to read topology file {:?}: {}",
                path, e
            ))
        })?;
        Self::from_toml(&content)
    }

    /// Parse a topology from a TOML string.
    pub fn from_toml(content: &str) -> crate::error::Result<Self> {
        toml::from_str(content).map_err(|e| {
            crate::error::NucleusError::ConfigError(format!("Failed to parse topology: {}", e))
        })
    }

    /// Validate the topology configuration.
    pub fn validate(&self) -> crate::error::Result<()> {
        if self.name.is_empty() {
            return Err(crate::error::NucleusError::ConfigError(
                "Topology name cannot be empty".to_string(),
            ));
        }

        // Validate topology name and all service keys use safe characters,
        // preventing path traversal when they are used in temp-file paths
        // (e.g. /tmp/nucleus-hooks-{topology}-{service}.json).
        crate::container::validate_container_name(&self.name).map_err(|_| {
            crate::error::NucleusError::ConfigError(format!(
                "Topology name '{}' contains invalid characters (allowed: a-zA-Z0-9, '-', '_', '.')",
                self.name
            ))
        })?;
        for service_name in self.services.keys() {
            crate::container::validate_container_name(service_name).map_err(|_| {
                crate::error::NucleusError::ConfigError(format!(
                    "Service name '{}' contains invalid characters (allowed: a-zA-Z0-9, '-', '_', '.')",
                    service_name
                ))
            })?;
        }

        if self.services.is_empty() {
            return Err(crate::error::NucleusError::ConfigError(
                "Topology must have at least one service".to_string(),
            ));
        }

        for (name, volume) in &self.volumes {
            match volume.volume_type.as_str() {
                "persistent" => {
                    let path = volume.path.as_ref().ok_or_else(|| {
                        crate::error::NucleusError::ConfigError(format!(
                            "Persistent volume '{}' must define path",
                            name
                        ))
                    })?;
                    if !Path::new(path).is_absolute() {
                        return Err(crate::error::NucleusError::ConfigError(format!(
                            "Persistent volume '{}' path must be absolute: {}",
                            name, path
                        )));
                    }
                }
                "ephemeral" => {
                    if volume.path.is_some() {
                        return Err(crate::error::NucleusError::ConfigError(format!(
                            "Ephemeral volume '{}' must not define path",
                            name
                        )));
                    }
                }
                other => {
                    return Err(crate::error::NucleusError::ConfigError(format!(
                        "Volume '{}' has unsupported type '{}'",
                        name, other
                    )));
                }
            }

            if let Some(owner) = &volume.owner {
                parse_volume_owner(owner)?;
            }
        }

        // Validate dependencies reference existing services
        for (name, svc) in &self.services {
            for dep in &svc.depends_on {
                if !self.services.contains_key(&dep.service) {
                    return Err(crate::error::NucleusError::ConfigError(format!(
                        "Service '{}' depends on unknown service '{}'",
                        name, dep.service
                    )));
                }
                if dep.condition != "started" && dep.condition != "healthy" {
                    return Err(crate::error::NucleusError::ConfigError(format!(
                        "Invalid dependency condition '{}' for service '{}'",
                        dep.condition, name
                    )));
                }
                if dep.condition == "healthy" {
                    let dep_service = self.services.get(&dep.service).ok_or_else(|| {
                        crate::error::NucleusError::ConfigError(format!(
                            "Service '{}' depends on unknown service '{}'",
                            name, dep.service
                        ))
                    })?;
                    if dep_service.health_check.is_none() {
                        return Err(crate::error::NucleusError::ConfigError(format!(
                            "Service '{}' depends on '{}' being healthy, but '{}' has no health_check",
                            name, dep.service, dep.service
                        )));
                    }
                }
            }

            // Validate networks reference existing network defs
            for net in &svc.networks {
                if !self.networks.contains_key(net) {
                    return Err(crate::error::NucleusError::ConfigError(format!(
                        "Service '{}' references unknown network '{}'",
                        name, net
                    )));
                }
            }

            // Validate volume mounts reference existing volume defs
            for vol_mount in &svc.volumes {
                let parsed = parse_service_volume_mount(vol_mount)?;
                if parsed.volume.starts_with('/') {
                    return Err(crate::error::NucleusError::ConfigError(format!(
                        "Service '{}' uses absolute host-path volume mount '{}'; topology configs must reference a named volume instead",
                        name, parsed.volume
                    )));
                }
                if !self.volumes.contains_key(&parsed.volume) {
                    return Err(crate::error::NucleusError::ConfigError(format!(
                        "Service '{}' references unknown volume '{}'",
                        name, parsed.volume
                    )));
                }
            }
        }

        Ok(())
    }

    /// Get the config hash for change detection (using service definitions).
    pub fn service_config_hash(&self, service_name: &str) -> Option<u64> {
        self.services.get(service_name).and_then(|svc| {
            let json = serde_json::to_vec(svc).ok()?;
            let digest = Sha256::digest(&json);
            let mut bytes = [0u8; 8];
            bytes.copy_from_slice(&digest[..8]);
            Some(u64::from_be_bytes(bytes))
        })
    }
}

impl Default for NetworkDef {
    fn default() -> Self {
        Self {
            subnet: default_subnet(),
            encrypted: false,
        }
    }
}

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

    #[test]
    fn test_parse_minimal_topology() {
        let toml = r#"
name = "test-stack"

[services.web]
rootfs = "/nix/store/abc-web"
command = ["/bin/web-server"]
memory = "512M"
"#;
        let config = TopologyConfig::from_toml(toml).unwrap();
        assert_eq!(config.name, "test-stack");
        assert_eq!(config.services.len(), 1);
        assert!(config.services.contains_key("web"));
    }

    #[test]
    fn test_parse_full_topology() {
        let toml = r#"
name = "myapp"

[networks.internal]
subnet = "10.42.0.0/24"
encrypted = true

[volumes.db-data]
volume_type = "persistent"
path = "/var/lib/nucleus/myapp/db"
owner = "70:70"

[services.postgres]
rootfs = "/nix/store/abc-postgres"
command = ["postgres", "-D", "/var/lib/postgresql/data"]
memory = "2G"
cpus = 2.0
networks = ["internal"]
volumes = ["db-data:/var/lib/postgresql/data"]
health_check = "pg_isready -U myapp"

[services.web]
rootfs = "/nix/store/abc-web"
command = ["/bin/web-server"]
memory = "512M"
cpus = 1.0
networks = ["internal"]
nat_backend = "userspace"
port_forwards = ["8443:8443"]
egress_allow = ["10.42.0.0/24"]

[[services.web.depends_on]]
service = "postgres"
condition = "healthy"
"#;
        let config = TopologyConfig::from_toml(toml).unwrap();
        assert_eq!(config.name, "myapp");
        assert_eq!(config.services.len(), 2);
        assert_eq!(config.networks.len(), 1);
        assert_eq!(config.volumes.len(), 1);
        assert_eq!(
            config.services["web"].nat_backend,
            crate::network::NatBackend::Userspace
        );
        assert!(config.validate().is_ok());
    }

    #[test]
    fn test_nat_backend_defaults_to_auto() {
        let toml = r#"
name = "test-stack"

[services.web]
rootfs = "/nix/store/abc-web"
command = ["/bin/web-server"]
memory = "512M"
"#;
        let config = TopologyConfig::from_toml(toml).unwrap();
        assert_eq!(
            config.services["web"].nat_backend,
            crate::network::NatBackend::Auto
        );
    }

    #[test]
    fn test_validate_missing_dependency() {
        let toml = r#"
name = "bad"

[services.web]
rootfs = "/nix/store/abc"
command = ["/bin/web"]
memory = "256M"

[[services.web.depends_on]]
service = "nonexistent"
"#;
        let config = TopologyConfig::from_toml(toml).unwrap();
        assert!(config.validate().is_err());
    }

    #[test]
    fn test_validate_healthy_dependency_requires_health_check() {
        let toml = r#"
name = "bad"

[services.db]
rootfs = "/nix/store/db"
command = ["postgres"]
memory = "512M"

[services.web]
rootfs = "/nix/store/web"
command = ["/bin/web"]
memory = "256M"

[[services.web.depends_on]]
service = "db"
condition = "healthy"
"#;
        let config = TopologyConfig::from_toml(toml).unwrap();
        let err = config.validate().unwrap_err();
        assert!(err.to_string().contains("health_check"));
    }

    #[test]
    fn test_service_config_hash_is_stable_across_invocations() {
        // BUG-03: service_config_hash must be deterministic across binary versions.
        // DefaultHasher is not guaranteed stable; we need a stable algorithm.
        let toml = r#"
name = "test"

[services.web]
rootfs = "/nix/store/web"
command = ["/bin/web"]
memory = "256M"
"#;
        let config = TopologyConfig::from_toml(toml).unwrap();
        let hash1 = config.service_config_hash("web").unwrap();
        let hash2 = config.service_config_hash("web").unwrap();
        assert_eq!(
            hash1, hash2,
            "hash must be deterministic within same process"
        );

        // Verify hash stability: the implementation must use a stable hasher
        // (e.g., SHA-256), not DefaultHasher which varies across Rust versions.
        // Pin to a known value so any hasher change is caught.
        let expected: u64 = hash1; // If this test is run after a hasher change, update this value.
        assert_eq!(
            config.service_config_hash("web").unwrap(),
            expected,
            "service_config_hash must be deterministic and stable across invocations"
        );
    }

    #[test]
    fn test_validate_rejects_absolute_path_volume_mounts() {
        // BUG-20: Docker-style absolute path volume mounts must produce
        // a clear error, not a confusing "unknown volume" message
        let toml = r#"
name = "test"

[services.web]
rootfs = "/nix/store/web"
command = ["/bin/web"]
memory = "256M"
volumes = ["/host/path:/container/path"]
"#;
        let config = TopologyConfig::from_toml(toml).unwrap();
        let err = config.validate().unwrap_err();
        let msg = err.to_string();
        assert!(
            msg.contains("absolute") || msg.contains("named volume"),
            "Absolute path volume mount must produce a clear error about named volumes, got: {}",
            msg
        );
    }

    #[test]
    fn test_validate_rejects_invalid_volume_owner() {
        let toml = r#"
name = "test"

[volumes.data]
volume_type = "persistent"
path = "/var/lib/test"
owner = "abc:def"

[services.web]
rootfs = "/nix/store/web"
command = ["/bin/web"]
memory = "256M"
volumes = ["data:/var/lib/web"]
"#;
        let config = TopologyConfig::from_toml(toml).unwrap();
        let err = config.validate().unwrap_err();
        assert!(err.to_string().contains("volume owner"));
    }
}
