//! Container builder for ergonomic container creation.

use crate::DockerClient;
use crate::containers::ContainerRef;
use crate::error::{DockerError, Result};
use bollard::container::*;
use bollard::models::{
    ContainerCreateBody, EndpointSettings, HostConfig, NetworkingConfig, PortBinding,
};
use std::collections::HashMap;
use tracing::info;

/// Builder for creating containers with a fluent API.
pub struct ContainerBuilder<'a> {
    client: &'a DockerClient,
    image: String,
    name: Option<String>,
    config: ContainerCreateBody,
    host_config: HostConfig,
}

impl<'a> ContainerBuilder<'a> {
    pub(crate) fn new(client: &'a DockerClient, image: impl Into<String>) -> Self {
        let image = image.into();
        Self {
            client,
            config: ContainerCreateBody {
                image: Some(image.clone()),
                ..Default::default()
            },
            host_config: HostConfig::default(),
            name: None,
            image,
        }
    }

    /// Set the container name.
    pub fn name(mut self, name: impl Into<String>) -> Self {
        self.name = Some(name.into());
        self
    }

    /// Add an environment variable.
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use lmrc_docker::DockerClient;
    /// # async fn example(client: &DockerClient) -> Result<(), Box<dyn std::error::Error>> {
    /// client.containers()
    ///     .create("nginx:latest")
    ///     .env("ENV", "production")
    ///     .env("DEBUG", "false")
    ///     .build()
    ///     .await?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        let env_var = format!("{}={}", key.into(), value.into());
        if let Some(ref mut env) = self.config.env {
            env.push(env_var);
        } else {
            self.config.env = Some(vec![env_var]);
        }
        self
    }

    /// Map a port from host to container.
    ///
    /// # Arguments
    ///
    /// * `host_port` - Port on the host
    /// * `container_port` - Port in the container
    /// * `protocol` - Protocol ("tcp" or "udp")
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use lmrc_docker::DockerClient;
    /// # async fn example(client: &DockerClient) -> Result<(), Box<dyn std::error::Error>> {
    /// client.containers()
    ///     .create("nginx:latest")
    ///     .port(8080, 80, "tcp")
    ///     .port(8443, 443, "tcp")
    ///     .build()
    ///     .await?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn port(mut self, host_port: u16, container_port: u16, protocol: &str) -> Self {
        // Add exposed port
        let port_key = format!("{}/{}", container_port, protocol);
        if let Some(ref mut exposed_ports) = self.config.exposed_ports {
            exposed_ports.insert(port_key.clone(), HashMap::new());
        } else {
            let mut exposed = HashMap::new();
            exposed.insert(port_key.clone(), HashMap::new());
            self.config.exposed_ports = Some(exposed);
        }

        // Add port binding
        let binding = vec![PortBinding {
            host_ip: Some("0.0.0.0".to_string()),
            host_port: Some(host_port.to_string()),
        }];

        if let Some(ref mut port_bindings) = self.host_config.port_bindings {
            port_bindings.insert(port_key, Some(binding));
        } else {
            let mut bindings = HashMap::new();
            bindings.insert(port_key, Some(binding));
            self.host_config.port_bindings = Some(bindings);
        }

        self
    }

    /// Mount a volume.
    ///
    /// # Arguments
    ///
    /// * `host_path` - Path on the host
    /// * `container_path` - Path in the container
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use lmrc_docker::DockerClient;
    /// # async fn example(client: &DockerClient) -> Result<(), Box<dyn std::error::Error>> {
    /// client.containers()
    ///     .create("nginx:latest")
    ///     .volume("/host/data", "/app/data")
    ///     .build()
    ///     .await?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn volume(
        mut self,
        host_path: impl Into<String>,
        container_path: impl Into<String>,
    ) -> Self {
        let binding = format!("{}:{}", host_path.into(), container_path.into());
        if let Some(ref mut binds) = self.host_config.binds {
            binds.push(binding);
        } else {
            self.host_config.binds = Some(vec![binding]);
        }
        self
    }

    /// Connect to a network.
    pub fn network(mut self, network: impl Into<String>) -> Self {
        let network = network.into();
        let endpoint_config = EndpointSettings::default();

        let mut endpoints = HashMap::new();
        endpoints.insert(network, endpoint_config);

        if let Some(ref mut networking_config) = self.config.networking_config {
            if let Some(ref mut endpoints_config) = networking_config.endpoints_config {
                endpoints_config.extend(endpoints);
            } else {
                networking_config.endpoints_config = Some(endpoints);
            }
        } else {
            self.config.networking_config = Some(NetworkingConfig {
                endpoints_config: Some(endpoints),
            });
        }

        self
    }

    /// Set the command to run in the container.
    ///
    /// # Example
    ///
    /// ```no_run
    /// # use lmrc_docker::DockerClient;
    /// # async fn example(client: &DockerClient) -> Result<(), Box<dyn std::error::Error>> {
    /// client.containers()
    ///     .create("alpine:latest")
    ///     .cmd(vec!["echo", "hello"])
    ///     .build()
    ///     .await?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn cmd(mut self, cmd: Vec<impl Into<String>>) -> Self {
        self.config.cmd = Some(cmd.into_iter().map(|s| s.into()).collect());
        self
    }

    /// Set the entrypoint.
    pub fn entrypoint(mut self, entrypoint: Vec<impl Into<String>>) -> Self {
        self.config.entrypoint = Some(entrypoint.into_iter().map(|s| s.into()).collect());
        self
    }

    /// Set the working directory.
    pub fn working_dir(mut self, dir: impl Into<String>) -> Self {
        self.config.working_dir = Some(dir.into());
        self
    }

    /// Set the restart policy to "always".
    pub fn restart_always(mut self) -> Self {
        self.host_config.restart_policy = Some(bollard::models::RestartPolicy {
            name: Some(bollard::models::RestartPolicyNameEnum::ALWAYS),
            maximum_retry_count: None,
        });
        self
    }

    /// Set the restart policy to "unless-stopped".
    pub fn restart_unless_stopped(mut self) -> Self {
        self.host_config.restart_policy = Some(bollard::models::RestartPolicy {
            name: Some(bollard::models::RestartPolicyNameEnum::UNLESS_STOPPED),
            maximum_retry_count: None,
        });
        self
    }

    /// Set the restart policy to "on-failure" with optional retry count.
    pub fn restart_on_failure(mut self, max_retries: Option<i64>) -> Self {
        self.host_config.restart_policy = Some(bollard::models::RestartPolicy {
            name: Some(bollard::models::RestartPolicyNameEnum::ON_FAILURE),
            maximum_retry_count: max_retries,
        });
        self
    }

    /// Set memory limit in bytes.
    pub fn memory(mut self, bytes: i64) -> Self {
        self.host_config.memory = Some(bytes);
        self
    }

    /// Set CPU shares (relative weight).
    pub fn cpu_shares(mut self, shares: i64) -> Self {
        self.host_config.cpu_shares = Some(shares);
        self
    }

    /// Add a label.
    pub fn label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        if self.config.labels.is_none() {
            self.config.labels = Some(HashMap::new());
        }
        if let Some(ref mut labels) = self.config.labels {
            labels.insert(key.into(), value.into());
        }
        self
    }

    /// Enable auto-remove (container is deleted when it stops).
    pub fn auto_remove(mut self, enable: bool) -> Self {
        self.host_config.auto_remove = Some(enable);
        self
    }

    /// Enable privileged mode.
    pub fn privileged(mut self, enable: bool) -> Self {
        self.host_config.privileged = Some(enable);
        self
    }

    /// Build and create the container.
    ///
    /// Returns a [`ContainerRef`] that can be used to interact with the container.
    pub async fn build(self) -> Result<ContainerRef<'a>> {
        info!("Creating container from image: {}", self.image);

        let options = CreateContainerOptions {
            name: self.name.as_deref().unwrap_or(""),
            platform: None,
        };

        let config = ContainerCreateBody {
            host_config: Some(self.host_config),
            ..self.config
        };

        let response = self
            .client
            .docker
            .create_container(Some(options), config)
            .await
            .map_err(|e| {
                DockerError::ContainerOperationFailed(format!("Failed to create: {}", e))
            })?;

        info!("Container created with ID: {}", response.id);

        Ok(ContainerRef::new(self.client, response.id))
    }
}

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

    #[test]
    fn test_builder_env() {
        let client = DockerClient::new().unwrap();
        let builder = ContainerBuilder::new(&client, "alpine:latest")
            .env("KEY1", "value1")
            .env("KEY2", "value2");

        assert_eq!(builder.config.env.as_ref().unwrap().len(), 2);
        assert!(
            builder
                .config
                .env
                .as_ref()
                .unwrap()
                .contains(&"KEY1=value1".to_string())
        );
        assert!(
            builder
                .config
                .env
                .as_ref()
                .unwrap()
                .contains(&"KEY2=value2".to_string())
        );
    }

    #[test]
    fn test_builder_name() {
        let client = DockerClient::new().unwrap();
        let builder = ContainerBuilder::new(&client, "alpine:latest").name("test-container");

        assert_eq!(builder.name, Some("test-container".to_string()));
    }

    #[test]
    fn test_builder_port() {
        let client = DockerClient::new().unwrap();
        let builder = ContainerBuilder::new(&client, "nginx:latest").port(8080, 80, "tcp");

        assert!(builder.config.exposed_ports.is_some());
        assert!(builder.host_config.port_bindings.is_some());

        let bindings = builder.host_config.port_bindings.as_ref().unwrap();
        assert!(bindings.contains_key("80/tcp"));
    }

    #[test]
    fn test_builder_volume() {
        let client = DockerClient::new().unwrap();
        let builder =
            ContainerBuilder::new(&client, "alpine:latest").volume("/host/path", "/container/path");

        assert!(builder.host_config.binds.is_some());
        let binds = builder.host_config.binds.as_ref().unwrap();
        assert_eq!(binds.len(), 1);
        assert_eq!(binds[0], "/host/path:/container/path");
    }

    #[test]
    fn test_builder_cmd() {
        let client = DockerClient::new().unwrap();
        let builder = ContainerBuilder::new(&client, "alpine:latest").cmd(vec!["echo", "hello"]);

        assert!(builder.config.cmd.is_some());
        let cmd = builder.config.cmd.as_ref().unwrap();
        assert_eq!(cmd.len(), 2);
        assert_eq!(cmd[0], "echo");
        assert_eq!(cmd[1], "hello");
    }

    #[test]
    fn test_builder_working_dir() {
        let client = DockerClient::new().unwrap();
        let builder = ContainerBuilder::new(&client, "alpine:latest").working_dir("/app");

        assert_eq!(builder.config.working_dir, Some("/app".to_string()));
    }

    #[test]
    fn test_builder_restart_policies() {
        let client = DockerClient::new().unwrap();

        let builder_always = ContainerBuilder::new(&client, "alpine:latest").restart_always();
        assert!(builder_always.host_config.restart_policy.is_some());

        let builder_unless =
            ContainerBuilder::new(&client, "alpine:latest").restart_unless_stopped();
        assert!(builder_unless.host_config.restart_policy.is_some());

        let builder_failure =
            ContainerBuilder::new(&client, "alpine:latest").restart_on_failure(Some(5));
        assert!(builder_failure.host_config.restart_policy.is_some());
        assert_eq!(
            builder_failure
                .host_config
                .restart_policy
                .as_ref()
                .unwrap()
                .maximum_retry_count,
            Some(5)
        );
    }

    #[test]
    fn test_builder_memory() {
        let client = DockerClient::new().unwrap();
        let builder = ContainerBuilder::new(&client, "alpine:latest").memory(512 * 1024 * 1024);

        assert_eq!(builder.host_config.memory, Some(512 * 1024 * 1024));
    }

    #[test]
    fn test_builder_cpu_shares() {
        let client = DockerClient::new().unwrap();
        let builder = ContainerBuilder::new(&client, "alpine:latest").cpu_shares(512);

        assert_eq!(builder.host_config.cpu_shares, Some(512));
    }

    #[test]
    fn test_builder_labels() {
        let client = DockerClient::new().unwrap();
        let builder = ContainerBuilder::new(&client, "alpine:latest")
            .label("env", "test")
            .label("version", "1.0");

        assert!(builder.config.labels.is_some());
        let labels = builder.config.labels.as_ref().unwrap();
        assert_eq!(labels.get("env"), Some(&"test".to_string()));
        assert_eq!(labels.get("version"), Some(&"1.0".to_string()));
    }

    #[test]
    fn test_builder_auto_remove() {
        let client = DockerClient::new().unwrap();
        let builder = ContainerBuilder::new(&client, "alpine:latest").auto_remove(true);

        assert_eq!(builder.host_config.auto_remove, Some(true));
    }

    #[test]
    fn test_builder_privileged() {
        let client = DockerClient::new().unwrap();
        let builder = ContainerBuilder::new(&client, "alpine:latest").privileged(true);

        assert_eq!(builder.host_config.privileged, Some(true));
    }

    #[test]
    fn test_builder_chaining() {
        let client = DockerClient::new().unwrap();
        let builder = ContainerBuilder::new(&client, "nginx:latest")
            .name("web-server")
            .env("ENV", "production")
            .port(8080, 80, "tcp")
            .volume("/data", "/app/data")
            .memory(512 * 1024 * 1024)
            .restart_always()
            .label("app", "web");

        assert_eq!(builder.name, Some("web-server".to_string()));
        assert!(builder.config.env.is_some());
        assert!(builder.config.exposed_ports.is_some());
        assert!(builder.host_config.binds.is_some());
        assert!(builder.host_config.memory.is_some());
        assert!(builder.host_config.restart_policy.is_some());
        assert!(builder.config.labels.is_some());
    }
}
