use tracing::{info, error};
use crate::error::{AptOstreeError, AptOstreeResult};
use crate::ostree::OstreeManager;
use serde_json::{json, Value};
use serde::{Serialize, Deserialize};
use std::path::{Path, PathBuf};
use std::collections::HashMap;
use tokio::fs;
use tokio::process::Command;
use chrono::{DateTime, Utc};

/// OCI image configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OciConfig {
    pub architecture: String,
    pub os: String,
    pub created: DateTime<Utc>,
    pub author: Option<String>,
    pub config: OciImageConfig,
    pub rootfs: OciRootfs,
    pub history: Vec<OciHistory>,
}

/// OCI image config
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OciImageConfig {
    pub user: Option<String>,
    pub working_dir: Option<String>,
    pub env: Vec<String>,
    pub entrypoint: Option<Vec<String>>,
    pub cmd: Option<Vec<String>>,
    pub volumes: HashMap<String, Value>,
    pub exposed_ports: HashMap<String, Value>,
    pub labels: HashMap<String, String>,
}

/// OCI rootfs
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OciRootfs {
    pub diff_ids: Vec<String>,
    pub r#type: String,
}

/// OCI history
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OciHistory {
    pub created: DateTime<Utc>,
    pub author: Option<String>,
    pub created_by: Option<String>,
    pub comment: Option<String>,
    pub empty_layer: Option<bool>,
}

/// OCI manifest
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OciManifest {
    #[serde(rename = "schemaVersion")]
    pub schema_version: u32,
    pub config: OciDescriptor,
    pub layers: Vec<OciDescriptor>,
    pub annotations: Option<HashMap<String, String>>,
}

/// OCI descriptor
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OciDescriptor {
    #[serde(rename = "mediaType")]
    pub media_type: String,
    pub digest: String,
    pub size: u64,
    pub annotations: Option<HashMap<String, String>>,
}

/// OCI index
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OciIndex {
    #[serde(rename = "schemaVersion")]
    pub schema_version: u32,
    pub manifests: Vec<OciIndexManifest>,
    pub annotations: Option<HashMap<String, String>>,
}

/// OCI index manifest
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OciIndexManifest {
    #[serde(rename = "mediaType")]
    pub media_type: String,
    pub digest: String,
    pub size: u64,
    pub platform: Option<OciPlatform>,
    pub annotations: Option<HashMap<String, String>>,
}

/// OCI platform
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OciPlatform {
    pub architecture: String,
    pub os: String,
    pub os_version: Option<String>,
    pub os_features: Option<Vec<String>>,
    pub variant: Option<String>,
}

/// OCI image builder options
#[derive(Debug, Clone)]
pub struct OciBuildOptions {
    pub format: String,
    pub labels: HashMap<String, String>,
    pub entrypoint: Option<Vec<String>>,
    pub cmd: Option<Vec<String>>,
    pub user: Option<String>,
    pub working_dir: Option<String>,
    pub env: Vec<String>,
    pub exposed_ports: Vec<String>,
    pub volumes: Vec<String>,
    pub max_layers: usize,
    pub compression: String,
    pub platform: Option<String>,
}

impl Default for OciBuildOptions {
    fn default() -> Self {
        Self {
            format: "oci".to_string(),
            labels: HashMap::new(),
            entrypoint: None,
            cmd: Some(vec!["/bin/bash".to_string()]),
            user: Some("root".to_string()),
            working_dir: Some("/".to_string()),
            env: vec![
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_string(),
                "DEBIAN_FRONTEND=noninteractive".to_string(),
            ],
            exposed_ports: Vec::new(),
            volumes: Vec::new(),
            max_layers: 64,
            compression: "gzip".to_string(),
            platform: None,
        }
    }
}

/// OCI image builder
pub struct OciImageBuilder {
    ostree_manager: OstreeManager,
    temp_dir: PathBuf,
    options: OciBuildOptions,
}

impl OciImageBuilder {
    /// Create a new OCI image builder
    pub async fn new(options: OciBuildOptions) -> AptOstreeResult<Self> {
        Self::new_with_repo(options, "/var/lib/apt-ostree/repo").await
    }

    /// Create a new OCI image builder with custom repository path
    pub async fn new_with_repo(options: OciBuildOptions, repo_path: &str) -> AptOstreeResult<Self> {
        let ostree_manager = OstreeManager::new(repo_path)?;
        let temp_dir = std::env::temp_dir().join(format!("apt-ostree-oci-{}", chrono::Utc::now().timestamp()));
        fs::create_dir_all(&temp_dir).await?;
        
        Ok(Self {
            ostree_manager,
            temp_dir,
            options,
        })
    }

    /// Build OCI image from OSTree commit
    pub async fn build_image_from_commit(
        &self,
        source: &str,
        output_name: &str,
    ) -> AptOstreeResult<String> {
        info!("Building OCI image from source: {} -> {} ({})", source, output_name, self.options.format);
        
        // Create output directory
        let output_dir = self.temp_dir.join("output");
        fs::create_dir_all(&output_dir).await?;
        
        // Step 1: Checkout OSTree commit to temporary directory
        let checkout_dir = self.temp_dir.join("checkout");
        fs::create_dir_all(&checkout_dir).await?;
        
        info!("Checking out OSTree commit: {}", source);
        self.checkout_commit(source, &checkout_dir).await?;
        
        // Step 2: Create filesystem layer
        info!("Creating filesystem layer");
        let layer_path = self.create_filesystem_layer(&checkout_dir).await?;
        
        // Step 3: Generate OCI configuration
        info!("Generating OCI configuration");
        let config = self.generate_oci_config(source).await?;
        let config_path = self.write_oci_config(&config, &output_dir).await?;
        
        // Step 4: Copy layer to output directory
        let output_layer_path = output_dir.join("layer.tar.gz");
        fs::copy(&layer_path, &output_layer_path).await?;
        
        // Step 5: Generate OCI manifest
        info!("Generating OCI manifest");
        let manifest = self.generate_oci_manifest(&config_path, &output_layer_path).await?;
        let manifest_path = self.write_oci_manifest(&manifest, &output_dir).await?;
        
        // Step 6: Create final image
        info!("Creating final image");
        let final_path = self.create_final_image(&output_dir, output_name).await?;
        
        info!("OCI image created successfully: {}", final_path);
        Ok(final_path)
    }

    /// Checkout OSTree commit to directory
    async fn checkout_commit(&self, source: &str, checkout_dir: &Path) -> AptOstreeResult<()> {
        info!("Checking out commit {} to {}", source, checkout_dir.display());
        
        // Remove checkout directory if it exists
        if checkout_dir.exists() {
            fs::remove_dir_all(checkout_dir).await?;
        }
        
        // Use the actual OSTree library to checkout
        let repo = ostree::Repo::new_for_path(&self.ostree_manager.get_repo_path());
        
        // Try to checkout using ostree command
        let output = Command::new("ostree")
            .args(&[
                "checkout",
                "--repo", 
                self.ostree_manager.get_repo_path_str(),
                source,
                checkout_dir.to_str().unwrap()
            ])
            .output()
            .await?;
        
        if output.status.success() {
            info!("Successfully checked out {} to {}", source, checkout_dir.display());
            Ok(())
        } else {
            let error_msg = String::from_utf8_lossy(&output.stderr);
            error!("Failed to checkout {}: {}", source, error_msg);
            Err(AptOstreeError::InvalidArgument(
                format!("Failed to checkout source: {} - {}", source, error_msg)
            ))
        }
    }

    /// Create filesystem layer from checkout directory
    async fn create_filesystem_layer(&self, checkout_dir: &Path) -> AptOstreeResult<PathBuf> {
        let layer_path = self.temp_dir.join("layer.tar.gz");
        
        // Create tar archive of the filesystem
        let output = Command::new("tar")
            .args(&[
                "-czf", 
                layer_path.to_str().unwrap(),
                "-C", 
                checkout_dir.to_str().unwrap(),
                "."
            ])
            .output()
            .await?;
        
        if !output.status.success() {
            return Err(AptOstreeError::SystemError(
                format!("Failed to create filesystem layer: {}", String::from_utf8_lossy(&output.stderr))
            ));
        }
        
        Ok(layer_path)
    }

    /// Generate OCI configuration
    async fn generate_oci_config(&self, source: &str) -> AptOstreeResult<OciConfig> {
        let now = Utc::now();
        
        // Build labels
        let mut labels = self.options.labels.clone();
        labels.insert("org.aptostree.source".to_string(), source.to_string());
        labels.insert("org.aptostree.created".to_string(), now.to_rfc3339());
        labels.insert("org.aptostree.version".to_string(), env!("CARGO_PKG_VERSION").to_string());
        labels.insert("org.opencontainers.image.created".to_string(), now.to_rfc3339());
        labels.insert("org.opencontainers.image.source".to_string(), source.to_string());
        
        // Build exposed ports
        let mut exposed_ports = HashMap::new();
        for port in &self.options.exposed_ports {
            exposed_ports.insert(port.clone(), json!({}));
        }
        
        // Build volumes
        let mut volumes = HashMap::new();
        for volume in &self.options.volumes {
            volumes.insert(volume.clone(), json!({}));
        }
        
        let config = OciConfig {
            architecture: self.options.platform.as_deref().unwrap_or("amd64").to_string(),
            os: "linux".to_string(),
            created: now,
            author: Some("apt-ostree".to_string()),
            config: OciImageConfig {
                user: self.options.user.clone(),
                working_dir: self.options.working_dir.clone(),
                env: self.options.env.clone(),
                entrypoint: self.options.entrypoint.clone(),
                cmd: self.options.cmd.clone(),
                volumes,
                exposed_ports,
                labels,
            },
            rootfs: OciRootfs {
                diff_ids: vec!["sha256:placeholder".to_string()], // Will be updated with actual digest
                r#type: "layers".to_string(),
            },
            history: vec![OciHistory {
                created: now,
                author: Some("apt-ostree".to_string()),
                created_by: Some(format!("apt-ostree compose build-image {}", source)),
                comment: Some("Created by apt-ostree".to_string()),
                empty_layer: Some(false),
            }],
        };
        
        Ok(config)
    }

    /// Write OCI configuration to file
    async fn write_oci_config(&self, config: &OciConfig, output_dir: &Path) -> AptOstreeResult<PathBuf> {
        let config_path = output_dir.join("config.json");
        let config_json = serde_json::to_string_pretty(config)?;
        fs::write(&config_path, config_json).await?;
        Ok(config_path)
    }

    /// Generate OCI manifest
    async fn generate_oci_manifest(&self, config_path: &Path, layer_path: &Path) -> AptOstreeResult<OciManifest> {
        // Calculate layer digest and size
        let layer_content = fs::read(layer_path).await?;
        let layer_digest = format!("sha256:{}", sha256::digest(&layer_content));
        let layer_size = layer_content.len() as u64;
        
        // Calculate config digest and size
        let config_content = fs::read(config_path).await?;
        let config_digest = format!("sha256:{}", sha256::digest(&config_content));
        let config_size = config_content.len() as u64;
        
        let manifest = OciManifest {
            schema_version: 2,
            config: OciDescriptor {
                media_type: "application/vnd.oci.image.config.v1+json".to_string(),
                digest: config_digest,
                size: config_size,
                annotations: None,
            },
            layers: vec![OciDescriptor {
                media_type: "application/vnd.oci.image.layer.v1.tar+gzip".to_string(),
                digest: layer_digest,
                size: layer_size,
                annotations: None,
            }],
            annotations: {
                let mut annotations = HashMap::new();
                annotations.insert("org.aptostree.created".to_string(), Utc::now().to_rfc3339());
                Some(annotations)
            },
        };
        
        Ok(manifest)
    }

    /// Write OCI manifest to file
    async fn write_oci_manifest(&self, manifest: &OciManifest, output_dir: &Path) -> AptOstreeResult<PathBuf> {
        let manifest_path = output_dir.join("manifest.json");
        let manifest_json = serde_json::to_string_pretty(manifest)?;
        fs::write(&manifest_path, manifest_json).await?;
        Ok(manifest_path)
    }

    /// Create final image in specified format
    async fn create_final_image(&self, output_dir: &Path, output_name: &str) -> AptOstreeResult<String> {
        let final_path = PathBuf::from(output_name);
        
        match self.options.format.to_lowercase().as_str() {
            "oci" => {
                // For OCI format, create a directory structure
                let oci_dir = final_path.with_extension("oci");
                fs::create_dir_all(&oci_dir).await?;
                
                // Create blobs directory
                let blobs_dir = oci_dir.join("blobs").join("sha256");
                fs::create_dir_all(&blobs_dir).await?;
                
                // Copy config
                let config_content = fs::read(output_dir.join("config.json")).await?;
                let config_digest = format!("sha256:{}", sha256::digest(&config_content));
                let config_blob_path = blobs_dir.join(&config_digest[7..]); // Remove "sha256:" prefix
                fs::write(&config_blob_path, config_content).await?;
                
                // Copy layer
                let layer_content = fs::read(output_dir.join("layer.tar.gz")).await?;
                let layer_digest = format!("sha256:{}", sha256::digest(&layer_content));
                let layer_blob_path = blobs_dir.join(&layer_digest[7..]); // Remove "sha256:" prefix
                fs::write(&layer_blob_path, layer_content).await?;
                
                // Copy manifest
                let manifest_content = fs::read(output_dir.join("manifest.json")).await?;
                let manifest_digest = format!("sha256:{}", sha256::digest(&manifest_content));
                let manifest_size = manifest_content.len() as u64;
                let manifest_blob_path = blobs_dir.join(&manifest_digest[7..]); // Remove "sha256:" prefix
                fs::write(&manifest_blob_path, manifest_content).await?;
                
                let index = OciIndex {
                    schema_version: 2,
                    manifests: vec![OciIndexManifest {
                        media_type: "application/vnd.oci.image.manifest.v1+json".to_string(),
                        digest: manifest_digest,
                        size: manifest_size,
                        platform: Some(OciPlatform {
                            architecture: self.options.platform.as_deref().unwrap_or("amd64").to_string(),
                            os: "linux".to_string(),
                            os_version: None,
                            os_features: None,
                            variant: None,
                        }),
                        annotations: {
                            let mut annotations = HashMap::new();
                            annotations.insert("org.opencontainers.image.ref.name".to_string(), output_name.to_string());
                            Some(annotations)
                        },
                    }],
                    annotations: None,
                };
                
                fs::write(oci_dir.join("index.json"), serde_json::to_string_pretty(&index)?).await?;
                
                Ok(oci_dir.to_string_lossy().to_string())
            },
            "docker" => {
                // For Docker format, create a tar archive
                let docker_path = final_path.with_extension("tar");
                
                let output = Command::new("tar")
                    .args(&["-cf", docker_path.to_str().unwrap(), "-C", output_dir.to_str().unwrap(), "."])
                    .output()
                    .await?;
                
                if !output.status.success() {
                    return Err(AptOstreeError::SystemError(
                        format!("Failed to create Docker image: {}", String::from_utf8_lossy(&output.stderr))
                    ));
                }
                
                Ok(docker_path.to_string_lossy().to_string())
            },
            _ => {
                Err(AptOstreeError::InvalidArgument(
                    format!("Unsupported format: {}", self.options.format)
                ))
            }
        }
    }

    /// Clean up temporary files
    pub async fn cleanup(&self) -> AptOstreeResult<()> {
        if self.temp_dir.exists() {
            fs::remove_dir_all(&self.temp_dir).await?;
        }
        Ok(())
    }
}

/// OCI registry operations
pub struct OciRegistry {
    registry_url: String,
    username: Option<String>,
    password: Option<String>,
}

impl OciRegistry {
    /// Create a new OCI registry client
    pub fn new(registry_url: &str) -> Self {
        Self {
            registry_url: registry_url.to_string(),
            username: None,
            password: None,
        }
    }

    /// Set authentication credentials
    pub fn with_auth(mut self, username: &str, password: &str) -> Self {
        self.username = Some(username.to_string());
        self.password = Some(password.to_string());
        self
    }

    /// Push image to registry
    pub async fn push_image(&self, image_path: &str, tag: &str) -> AptOstreeResult<()> {
        info!("Pushing image to registry: {} -> {}", image_path, tag);
        
        let mut args = vec!["copy".to_string()];
        
        // Add source
        if image_path.ends_with(".oci") {
            args.push("oci:".to_string());
        } else {
            args.push("docker-archive:".to_string());
        }
        args.push(image_path.to_string());
        
        // Add destination
        let destination = format!("docker://{}/{}", self.registry_url, tag);
        args.push(destination);
        
        // Add authentication if provided
        if let (Some(username), Some(password)) = (&self.username, &self.password) {
            args.push("--src-creds".to_string());
            args.push(format!("{}:{}", username, password));
            args.push("--dest-creds".to_string());
            args.push(format!("{}:{}", username, password));
        }
        
        let output = Command::new("skopeo")
            .args(&args)
            .output()
            .await?;
        
        if !output.status.success() {
            return Err(AptOstreeError::SystemError(
                format!("Failed to push image: {}", String::from_utf8_lossy(&output.stderr))
            ));
        }
        
        info!("Successfully pushed image to registry");
        Ok(())
    }

    /// Pull image from registry
    pub async fn pull_image(&self, tag: &str, output_path: &str) -> AptOstreeResult<()> {
        info!("Pulling image from registry: {} -> {}", tag, output_path);
        
        let mut args = vec!["copy".to_string()];
        
        // Add source
        let source = format!("docker://{}/{}", self.registry_url, tag);
        args.push(source);
        
        // Add destination
        if output_path.ends_with(".oci") {
            args.push("oci:".to_string());
        } else {
            args.push("docker-archive:".to_string());
        }
        args.push(output_path.to_string());
        
        // Add authentication if provided
        if let (Some(username), Some(password)) = (&self.username, &self.password) {
            args.push("--src-creds".to_string());
            args.push(format!("{}:{}", username, password));
            args.push("--dest-creds".to_string());
            args.push(format!("{}:{}", username, password));
        }
        
        let output = Command::new("skopeo")
            .args(&args)
            .output()
            .await?;
        
        if !output.status.success() {
            return Err(AptOstreeError::SystemError(
                format!("Failed to pull image: {}", String::from_utf8_lossy(&output.stderr))
            ));
        }
        
        info!("Successfully pulled image from registry");
        Ok(())
    }

    /// Inspect image in registry
    pub async fn inspect_image(&self, tag: &str) -> AptOstreeResult<Value> {
        info!("Inspecting image in registry: {}", tag);
        
        let mut args = vec!["inspect".to_string()];
        let source = format!("docker://{}/{}", self.registry_url, tag);
        args.push(source);
        
        // Add authentication if provided
        if let (Some(username), Some(password)) = (&self.username, &self.password) {
            args.push("--creds".to_string());
            args.push(format!("{}:{}", username, password));
        }
        
        let output = Command::new("skopeo")
            .args(&args)
            .output()
            .await?;
        
        if !output.status.success() {
            return Err(AptOstreeError::SystemError(
                format!("Failed to inspect image: {}", String::from_utf8_lossy(&output.stderr))
            ));
        }
        
        let inspection: Value = serde_json::from_slice(&output.stdout)?;
        Ok(inspection)
    }
}

/// OCI utilities
pub struct OciUtils;

impl OciUtils {
    /// Validate OCI image
    pub async fn validate_image(image_path: &str) -> AptOstreeResult<bool> {
        info!("Validating OCI image: {}", image_path);
        
        let output = Command::new("skopeo")
            .args(&["inspect", image_path])
            .output()
            .await?;
        
        Ok(output.status.success())
    }

    /// Get image information
    pub async fn get_image_info(image_path: &str) -> AptOstreeResult<Value> {
        info!("Getting image information: {}", image_path);
        
        let output = Command::new("skopeo")
            .args(&["inspect", image_path])
            .output()
            .await?;
        
        if !output.status.success() {
            return Err(AptOstreeError::SystemError(
                format!("Failed to get image info: {}", String::from_utf8_lossy(&output.stderr))
            ));
        }
        
        let info: Value = serde_json::from_slice(&output.stdout)?;
        Ok(info)
    }

    /// Convert image format
    pub async fn convert_image(input_path: &str, output_path: &str, format: &str) -> AptOstreeResult<()> {
        info!("Converting image format: {} -> {} ({})", input_path, output_path, format);
        
        let mut args = vec!["copy"];
        
        // Add source
        if input_path.ends_with(".oci") {
            args.push("oci:");
        } else {
            args.push("docker-archive:");
        }
        args.push(input_path);
        
        // Add destination
        match format.to_lowercase().as_str() {
            "oci" => args.push("oci:"),
            "docker" => args.push("docker-archive:"),
            _ => return Err(AptOstreeError::InvalidArgument(format!("Unsupported format: {}", format))),
        }
        args.push(output_path);
        
        let output = Command::new("skopeo")
            .args(&args)
            .output()
            .await?;
        
        if !output.status.success() {
            return Err(AptOstreeError::SystemError(
                format!("Failed to convert image: {}", String::from_utf8_lossy(&output.stderr))
            ));
        }
        
        info!("Successfully converted image format");
        Ok(())
    }
}

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

    #[tokio::test]
    async fn test_oci_build_options_default() {
        let options = OciBuildOptions::default();
        assert_eq!(options.format, "oci");
        assert_eq!(options.max_layers, 64);
        assert_eq!(options.compression, "gzip");
    }

    #[tokio::test]
    async fn test_oci_config_generation() {
        let options = OciBuildOptions::default();
        let builder = OciImageBuilder::new(options).await.unwrap();
        let config = builder.generate_oci_config("test-commit").await.unwrap();
        
        assert_eq!(config.architecture, "amd64");
        assert_eq!(config.os, "linux");
        assert!(config.config.labels.contains_key("org.aptostree.source"));
    }
} 