//! Image management operations.

use crate::DockerClient;
use crate::error::{DockerError, Result};
use bollard::auth::DockerCredentials;
use bollard::image::*;
use bollard::models::*;
use futures_util::StreamExt;
use std::collections::HashMap;
use tracing::{debug, info, warn};

mod builder;
pub use builder::*;

/// Image operations manager.
pub struct Images<'a> {
    client: &'a DockerClient,
}

impl<'a> Images<'a> {
    pub(crate) fn new(client: &'a DockerClient) -> Self {
        Self { client }
    }

    /// Create a new image builder for building images.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use lmrc_docker::DockerClient;
    /// use std::path::Path;
    ///
    /// #[tokio::main]
    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let client = DockerClient::new()?;
    ///
    ///     client.images()
    ///         .build("my-app:latest")
    ///         .dockerfile("Dockerfile")
    ///         .context(Path::new("."))
    ///         .build_arg("RUNTIME_IMAGE", "alpine:latest")
    ///         .execute()
    ///         .await?;
    ///
    ///     Ok(())
    /// }
    /// ```
    pub fn build(&self, tag: impl Into<String>) -> ImageBuilder<'a> {
        ImageBuilder::new(self.client, tag)
    }

    /// Get a reference to a specific image.
    pub fn get(&self, name_or_id: impl Into<String>) -> ImageRef<'a> {
        ImageRef::new(self.client, name_or_id.into())
    }

    /// List all images.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use lmrc_docker::DockerClient;
    ///
    /// #[tokio::main]
    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let client = DockerClient::new()?;
    ///     let images = client.images().list(true).await?;
    ///     for image in images {
    ///         println!("{:?}", image.repo_tags);
    ///     }
    ///     Ok(())
    /// }
    /// ```
    pub async fn list(&self, all: bool) -> Result<Vec<ImageSummary>> {
        let options = Some(ListImagesOptions::<String> {
            all,
            ..Default::default()
        });

        self.client
            .docker
            .list_images(options)
            .await
            .map_err(|e| DockerError::Other(format!("Failed to list images: {}", e)))
    }

    /// Pull an image from a registry.
    ///
    /// # Arguments
    ///
    /// * `image` - Image name with optional tag (e.g., "nginx:latest")
    /// * `credentials` - Optional Docker registry credentials
    ///
    /// # Example
    ///
    /// ```no_run
    /// use lmrc_docker::DockerClient;
    ///
    /// #[tokio::main]
    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let client = DockerClient::new()?;
    ///     client.images().pull("nginx:latest", None).await?;
    ///     Ok(())
    /// }
    /// ```
    pub async fn pull(
        &self,
        image: impl Into<String>,
        credentials: Option<DockerCredentials>,
    ) -> Result<()> {
        let image = image.into();
        info!("Pulling image: {}", image);

        let options = Some(CreateImageOptions::<String> {
            from_image: image.clone(),
            ..Default::default()
        });

        let mut stream = self.client.docker.create_image(options, None, credentials);

        while let Some(result) = stream.next().await {
            match result {
                Ok(info) => {
                    if let Some(status) = info.status {
                        debug!("Pull status: {}", status);
                    }
                    if let Some(error) = info.error {
                        return Err(DockerError::PullFailed(error));
                    }
                }
                Err(e) => {
                    return Err(DockerError::PullFailed(e.to_string()));
                }
            }
        }

        info!("Successfully pulled image: {}", image);
        Ok(())
    }

    /// Search for images in a registry.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use lmrc_docker::DockerClient;
    ///
    /// #[tokio::main]
    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let client = DockerClient::new()?;
    ///     let results = client.images().search("nginx", None).await?;
    ///     for result in results {
    ///         println!("{}: {}", result.name.unwrap_or_default(), result.description.unwrap_or_default());
    ///     }
    ///     Ok(())
    /// }
    /// ```
    pub async fn search(
        &self,
        term: impl Into<String>,
        limit: Option<i64>,
    ) -> Result<Vec<ImageSearchResponseItem>> {
        let options = SearchImagesOptions {
            term: term.into(),
            limit: limit.map(|l| l as u64),
            ..Default::default()
        };

        self.client
            .docker
            .search_images(options)
            .await
            .map_err(|e| DockerError::Other(format!("Failed to search images: {}", e)))
    }

    /// Prune unused images.
    ///
    /// # Arguments
    ///
    /// * `dangling_only` - If true, only remove dangling images (untagged)
    pub async fn prune(&self, dangling_only: bool) -> Result<ImagePruneResponse> {
        info!("Pruning unused images...");
        let mut filters = HashMap::new();
        if dangling_only {
            filters.insert("dangling", vec!["true"]);
        }

        let options = Some(PruneImagesOptions { filters });

        self.client
            .docker
            .prune_images(options)
            .await
            .map_err(|e| DockerError::Other(format!("Failed to prune images: {}", e)))
    }
}

/// Reference to a specific image.
pub struct ImageRef<'a> {
    client: &'a DockerClient,
    name: String,
}

impl<'a> ImageRef<'a> {
    pub(crate) fn new(client: &'a DockerClient, name: String) -> Self {
        Self { client, name }
    }

    /// Get the image name.
    pub fn name(&self) -> &str {
        &self.name
    }

    /// Inspect the image to get detailed information.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use lmrc_docker::DockerClient;
    ///
    /// #[tokio::main]
    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let client = DockerClient::new()?;
    ///     let info = client.images().get("nginx:latest").inspect().await?;
    ///     println!("Image ID: {:?}", info.id);
    ///     Ok(())
    /// }
    /// ```
    pub async fn inspect(&self) -> Result<ImageInspect> {
        debug!("Inspecting image: {}", self.name);
        self.client
            .docker
            .inspect_image(&self.name)
            .await
            .map_err(|e| DockerError::ImageNotFound(format!("{}: {}", self.name, e)))
    }

    /// Get the history of the image (layer information).
    pub async fn history(&self) -> Result<Vec<HistoryResponseItem>> {
        debug!("Getting history for image: {}", self.name);
        self.client
            .docker
            .image_history(&self.name)
            .await
            .map_err(|e| DockerError::ImageNotFound(format!("{}: {}", self.name, e)))
    }

    /// Tag the image with a new name/tag.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use lmrc_docker::DockerClient;
    ///
    /// #[tokio::main]
    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let client = DockerClient::new()?;
    ///     client.images()
    ///         .get("nginx:latest")
    ///         .tag("my-nginx:v1.0")
    ///         .await?;
    ///     Ok(())
    /// }
    /// ```
    pub async fn tag(&self, new_tag: impl Into<String>) -> Result<()> {
        let new_tag = new_tag.into();
        info!("Tagging image {} as {}", self.name, new_tag);

        // Parse the new tag to extract repo and tag
        let parts: Vec<&str> = new_tag.rsplitn(2, ':').collect();
        let (repo, tag) = if parts.len() == 2 {
            (parts[1], Some(parts[0]))
        } else {
            (new_tag.as_str(), None)
        };

        let options = TagImageOptions {
            repo,
            tag: tag.unwrap_or("latest"),
        };

        self.client
            .docker
            .tag_image(&self.name, Some(options))
            .await
            .map_err(|e| DockerError::Other(format!("Failed to tag image: {}", e)))
    }

    /// Push the image to a registry.
    ///
    /// # Arguments
    ///
    /// * `credentials` - Optional Docker registry credentials
    ///
    /// # Example
    ///
    /// ```no_run
    /// use lmrc_docker::DockerClient;
    ///
    /// #[tokio::main]
    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let client = DockerClient::new()?;
    ///     client.images()
    ///         .get("myregistry.com/my-app:v1.0")
    ///         .push(None)
    ///         .await?;
    ///     Ok(())
    /// }
    /// ```
    pub async fn push(&self, credentials: Option<DockerCredentials>) -> Result<()> {
        info!("Pushing image: {}", self.name);

        // Parse tag from image name
        let tag = self.name.split(':').nth(1).unwrap_or("latest");

        let options = Some(PushImageOptions { tag });

        let mut stream = self
            .client
            .docker
            .push_image(&self.name, options, credentials);

        while let Some(result) = stream.next().await {
            match result {
                Ok(info) => {
                    if let Some(status) = info.status {
                        if status.contains("error") || status.contains("Error") {
                            warn!("Push error: {}", status);
                        } else {
                            debug!("Push status: {}", status);
                        }
                    }
                    if let Some(error) = info.error {
                        return Err(DockerError::PushFailed(error));
                    }
                }
                Err(e) => {
                    return Err(DockerError::PushFailed(e.to_string()));
                }
            }
        }

        info!("Successfully pushed image: {}", self.name);
        Ok(())
    }

    /// Remove the image.
    ///
    /// # Arguments
    ///
    /// * `force` - Force removal even if image is in use
    /// * `noprune` - Do not delete untagged parent images
    pub async fn remove(&self, force: bool, noprune: bool) -> Result<Vec<ImageDeleteResponseItem>> {
        info!("Removing image: {}", self.name);

        let options = Some(RemoveImageOptions { force, noprune });

        self.client
            .docker
            .remove_image(&self.name, options, None)
            .await
            .map_err(|e| DockerError::Other(format!("Failed to remove image: {}", e)))
    }

    /// Export the image as a tar archive.
    pub async fn export(&self) -> Result<Vec<u8>> {
        info!("Exporting image: {}", self.name);

        let mut stream = self.client.docker.export_image(&self.name);
        let mut data = Vec::new();

        while let Some(chunk) = stream.next().await {
            match chunk {
                Ok(bytes) => data.extend_from_slice(&bytes),
                Err(e) => {
                    return Err(DockerError::Other(format!("Failed to export image: {}", e)));
                }
            }
        }

        Ok(data)
    }
}

impl DockerClient {
    /// Access image operations.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use lmrc_docker::DockerClient;
    ///
    /// #[tokio::main]
    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let client = DockerClient::new()?;
    ///     let images = client.images().list(false).await?;
    ///     Ok(())
    /// }
    /// ```
    pub fn images(&self) -> Images<'_> {
        Images::new(self)
    }
}
