//! Deployment manager implementation.

use super::spec::{ContainerSpec, DeploymentSpec, DeploymentStrategy, VolumeSource};
use super::status::DeploymentStatus;
use crate::config::{DeploymentOptions, RollbackOptions};
use crate::error::{Error, Result};
use k8s_openapi::api::apps::v1::{
    Deployment, DeploymentSpec as K8sDeploymentSpec, DeploymentStrategy as K8sDeploymentStrategy,
    RollingUpdateDeployment,
};
use k8s_openapi::api::core::v1::{
    ConfigMapVolumeSource, Container, ContainerPort, EmptyDirVolumeSource, EnvVar,
    HostPathVolumeSource, KeyToPath as K8sKeyToPath, LocalObjectReference,
    PersistentVolumeClaimVolumeSource, Pod, PodSpec as K8sPodSpec, PodTemplateSpec,
    ResourceRequirements as K8sResourceRequirements, SecretVolumeSource, Volume as K8sVolume,
};
use k8s_openapi::apimachinery::pkg::api::resource::Quantity;
use k8s_openapi::apimachinery::pkg::apis::meta::v1::{LabelSelector, ObjectMeta};
use k8s_openapi::apimachinery::pkg::util::intstr::IntOrString;
use kube::Client;
use kube::api::{Api, DeleteParams, ListParams, Patch, PatchParams, PostParams};
use std::collections::BTreeMap;
use std::time::Duration;
use tracing::{debug, info, warn};

/// Manager for Kubernetes Deployment operations.
///
/// # Examples
///
/// ```no_run
/// use lmrc_kubernetes::{Client, ClientConfig};
/// use lmrc_kubernetes::deployment::{DeploymentSpec, ContainerSpec};
/// use lmrc_kubernetes::config::DeploymentOptions;
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// let config = ClientConfig::infer().await?;
/// let client = Client::new(config, "default").await?;
/// let manager = client.deployments();
///
/// // Create a deployment
/// let container = ContainerSpec::new("app", "nginx:1.21")
///     .with_port(80);
/// let spec = DeploymentSpec::new("my-app")
///     .with_replicas(3)
///     .with_container(container);
///
/// let opts = DeploymentOptions::new().timeout_secs(300);
/// manager.create(&spec, &opts).await?;
/// # Ok(())
/// # }
/// ```
#[derive(Clone)]
pub struct DeploymentManager {
    client: Client,
    namespace: String,
    field_manager: String,
}

impl DeploymentManager {
    /// Create a new deployment manager.
    pub(crate) fn new(client: Client, namespace: String, field_manager: String) -> Self {
        Self {
            client,
            namespace,
            field_manager,
        }
    }

    /// Create a new deployment.
    ///
    /// # Arguments
    ///
    /// * `spec` - The deployment specification
    /// * `options` - Deployment options (timeout, wait, etc.)
    ///
    /// # Errors
    ///
    /// Returns an error if the deployment cannot be created or does not become ready.
    pub async fn create(&self, spec: &DeploymentSpec, options: &DeploymentOptions) -> Result<()> {
        let api: Api<Deployment> = Api::namespaced(self.client.clone(), &self.namespace);
        let k8s_deployment = self.spec_to_k8s(spec)?;

        info!("Creating deployment: {}", spec.name());

        api.create(&PostParams::default(), &k8s_deployment)
            .await
            .map_err(|e| Error::deployment(format!("Failed to create deployment: {}", e)))?;

        if options.wait {
            self.wait_for_ready(spec.name(), options.timeout_secs)
                .await?;
        }

        info!("Deployment {} created successfully", spec.name());
        Ok(())
    }

    /// Update an existing deployment or create if it doesn't exist.
    ///
    /// This uses server-side apply for seamless updates.
    pub async fn apply(&self, spec: &DeploymentSpec, options: &DeploymentOptions) -> Result<()> {
        let api: Api<Deployment> = Api::namespaced(self.client.clone(), &self.namespace);
        let k8s_deployment = self.spec_to_k8s(spec)?;

        info!("Applying deployment: {}", spec.name());

        let patch = Patch::Apply(&k8s_deployment);
        let mut params = PatchParams::apply(&self.field_manager);
        if options.force {
            params = params.force();
        }

        api.patch(spec.name(), &params, &patch)
            .await
            .map_err(|e| Error::deployment(format!("Failed to apply deployment: {}", e)))?;

        if options.wait {
            self.wait_for_ready(spec.name(), options.timeout_secs)
                .await?;
        }

        info!("Deployment {} applied successfully", spec.name());
        Ok(())
    }

    /// Get the status of a deployment.
    pub async fn status(&self, name: &str) -> Result<DeploymentStatus> {
        let api: Api<Deployment> = Api::namespaced(self.client.clone(), &self.namespace);

        let deployment = api
            .get(name)
            .await
            .map_err(|_e| Error::not_found("Deployment", name, &self.namespace))?;

        let status = deployment
            .status
            .ok_or_else(|| Error::deployment(format!("Deployment {} has no status", name)))?;

        Ok(DeploymentStatus {
            replicas: status.replicas.unwrap_or(0),
            ready_replicas: status.ready_replicas.unwrap_or(0),
            available_replicas: status.available_replicas.unwrap_or(0),
            updated_replicas: status.updated_replicas.unwrap_or(0),
            unavailable_replicas: status.unavailable_replicas.unwrap_or(0),
            observed_generation: status.observed_generation,
        })
    }

    /// Wait for a deployment to be ready.
    ///
    /// # Arguments
    ///
    /// * `name` - Name of the deployment
    /// * `timeout_secs` - Timeout in seconds
    pub async fn wait_for_ready(&self, name: &str, timeout_secs: u64) -> Result<()> {
        let api: Api<Deployment> = Api::namespaced(self.client.clone(), &self.namespace);
        let pods_api: Api<Pod> = Api::namespaced(self.client.clone(), &self.namespace);

        info!(
            "Waiting for deployment {} to be ready (timeout: {}s)",
            name, timeout_secs
        );

        let start = std::time::Instant::now();
        let timeout = Duration::from_secs(timeout_secs);

        loop {
            if start.elapsed() > timeout {
                return Err(Error::timeout(
                    format!("deployment/{}", name),
                    timeout_secs,
                    "Deployment did not become ready in time".to_string(),
                ));
            }

            // Check deployment status
            match api.get(name).await {
                Ok(deployment) => {
                    if let Some(status) = &deployment.status {
                        let ready = status.ready_replicas.unwrap_or(0);
                        let desired = deployment
                            .spec
                            .as_ref()
                            .and_then(|s| s.replicas)
                            .unwrap_or(1);

                        debug!("Deployment {}: {}/{} replicas ready", name, ready, desired);

                        if ready >= desired && desired > 0 {
                            info!("Deployment {} is ready!", name);
                            return Ok(());
                        }
                    }
                }
                Err(e) => {
                    warn!("Error checking deployment status: {}", e);
                }
            }

            // Check for pod errors
            self.check_pod_errors(name, &pods_api).await?;

            tokio::time::sleep(Duration::from_secs(5)).await;
        }
    }

    /// Scale a deployment to a specific number of replicas.
    pub async fn scale(&self, name: &str, replicas: i32) -> Result<()> {
        let api: Api<Deployment> = Api::namespaced(self.client.clone(), &self.namespace);

        info!("Scaling deployment {} to {} replicas", name, replicas);

        let mut deployment = api
            .get(name)
            .await
            .map_err(|_| Error::not_found("Deployment", name, &self.namespace))?;

        if let Some(ref mut spec) = deployment.spec {
            spec.replicas = Some(replicas);
        }

        let patch = Patch::Apply(&deployment);
        let params = PatchParams::apply(&self.field_manager);

        api.patch(name, &params, &patch)
            .await
            .map_err(|e| Error::scaling(format!("Failed to scale deployment: {}", e)))?;

        info!("Deployment {} scaled successfully", name);
        Ok(())
    }

    /// Rollback a deployment to a previous revision.
    pub async fn rollback(&self, name: &str, options: &RollbackOptions) -> Result<()> {
        info!("Rolling back deployment: {}", name);

        // For rollback, we use kubectl because the kube-rs library doesn't have
        // direct support for rollout undo. In production, you might want to
        // implement this using ReplicaSets directly.
        let mut cmd = std::process::Command::new("kubectl");
        cmd.arg("rollout")
            .arg("undo")
            .arg("deployment")
            .arg(name)
            .arg("-n")
            .arg(&self.namespace);

        if let Some(revision) = options.revision {
            cmd.arg(format!("--to-revision={}", revision));
        }

        let output = cmd
            .output()
            .map_err(|e| Error::rollback(format!("Failed to execute rollback: {}", e)))?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            return Err(Error::rollback(format!("Rollback failed: {}", stderr)));
        }

        // Wait for rollback to complete
        self.wait_for_ready(name, options.timeout_secs).await?;

        info!("Deployment {} rolled back successfully", name);
        Ok(())
    }

    /// Delete a deployment.
    pub async fn delete(&self, name: &str) -> Result<()> {
        let api: Api<Deployment> = Api::namespaced(self.client.clone(), &self.namespace);

        info!("Deleting deployment: {}", name);

        api.delete(name, &DeleteParams::default())
            .await
            .map_err(|e| Error::deployment(format!("Failed to delete deployment: {}", e)))?;

        info!("Deployment {} deleted", name);
        Ok(())
    }

    /// List all deployments in the namespace.
    pub async fn list(&self) -> Result<Vec<String>> {
        let api: Api<Deployment> = Api::namespaced(self.client.clone(), &self.namespace);

        let list = api
            .list(&ListParams::default())
            .await
            .map_err(|e| Error::deployment(format!("Failed to list deployments: {}", e)))?;

        Ok(list
            .items
            .iter()
            .filter_map(|d| d.metadata.name.clone())
            .collect())
    }

    /// Check if a deployment exists.
    pub async fn exists(&self, name: &str) -> Result<bool> {
        let api: Api<Deployment> = Api::namespaced(self.client.clone(), &self.namespace);
        Ok(api.get(name).await.is_ok())
    }

    // Helper methods

    fn spec_to_k8s(&self, spec: &DeploymentSpec) -> Result<Deployment> {
        let labels = spec.labels().clone();
        let mut labels_btree = BTreeMap::new();
        for (k, v) in labels.iter() {
            labels_btree.insert(k.clone(), v.clone());
        }

        // Convert containers
        let containers: Vec<Container> = spec
            .containers()
            .iter()
            .map(|c| self.container_to_k8s(c))
            .collect::<Result<Vec<_>>>()?;

        if containers.is_empty() {
            return Err(Error::invalid_spec(
                "Deployment must have at least one container",
            ));
        }

        // Convert init containers
        let init_containers: Vec<Container> = spec
            .init_containers()
            .iter()
            .map(|c| self.container_to_k8s(c))
            .collect::<Result<Vec<_>>>()?;

        // Convert image pull secrets
        let image_pull_secrets: Vec<LocalObjectReference> = spec
            .image_pull_secrets()
            .iter()
            .map(|s| LocalObjectReference {
                name: s.to_string(),
            })
            .collect();

        // Convert volumes
        let volumes: Vec<K8sVolume> = spec
            .volumes()
            .iter()
            .map(|v| match &v.source {
                VolumeSource::ConfigMap {
                    name,
                    default_mode,
                    items,
                } => K8sVolume {
                    name: v.name.clone(),
                    config_map: Some(ConfigMapVolumeSource {
                        name: name.clone(),
                        default_mode: *default_mode,
                        items: if items.is_empty() {
                            None
                        } else {
                            Some(
                                items
                                    .iter()
                                    .map(|item| K8sKeyToPath {
                                        key: item.key.clone(),
                                        path: item.path.clone(),
                                        mode: item.mode,
                                    })
                                    .collect(),
                            )
                        },
                        ..Default::default()
                    }),
                    ..Default::default()
                },
                VolumeSource::Secret {
                    secret_name,
                    default_mode,
                    items,
                } => K8sVolume {
                    name: v.name.clone(),
                    secret: Some(SecretVolumeSource {
                        secret_name: Some(secret_name.clone()),
                        default_mode: *default_mode,
                        items: if items.is_empty() {
                            None
                        } else {
                            Some(
                                items
                                    .iter()
                                    .map(|item| K8sKeyToPath {
                                        key: item.key.clone(),
                                        path: item.path.clone(),
                                        mode: item.mode,
                                    })
                                    .collect(),
                            )
                        },
                        ..Default::default()
                    }),
                    ..Default::default()
                },
                VolumeSource::EmptyDir { medium, size_limit } => K8sVolume {
                    name: v.name.clone(),
                    empty_dir: Some(EmptyDirVolumeSource {
                        medium: medium.clone(),
                        size_limit: size_limit.as_ref().map(|s| Quantity(s.clone())),
                    }),
                    ..Default::default()
                },
                VolumeSource::HostPath { path, path_type } => K8sVolume {
                    name: v.name.clone(),
                    host_path: Some(HostPathVolumeSource {
                        path: path.clone(),
                        type_: path_type.clone(),
                    }),
                    ..Default::default()
                },
                VolumeSource::PersistentVolumeClaim {
                    claim_name,
                    read_only,
                } => K8sVolume {
                    name: v.name.clone(),
                    persistent_volume_claim: Some(PersistentVolumeClaimVolumeSource {
                        claim_name: claim_name.clone(),
                        read_only: Some(*read_only),
                    }),
                    ..Default::default()
                },
            })
            .collect();

        let pod_spec = K8sPodSpec {
            containers,
            init_containers: if init_containers.is_empty() {
                None
            } else {
                Some(init_containers)
            },
            image_pull_secrets: if image_pull_secrets.is_empty() {
                None
            } else {
                Some(image_pull_secrets)
            },
            service_account_name: spec.service_account().map(|s| s.to_string()),
            volumes: if volumes.is_empty() {
                None
            } else {
                Some(volumes)
            },
            ..Default::default()
        };

        let pod_template = PodTemplateSpec {
            metadata: Some(ObjectMeta {
                labels: Some(labels_btree.clone()),
                ..Default::default()
            }),
            spec: Some(pod_spec),
        };

        let strategy = match spec.strategy() {
            DeploymentStrategy::Recreate => Some(K8sDeploymentStrategy {
                type_: Some("Recreate".to_string()),
                ..Default::default()
            }),
            DeploymentStrategy::RollingUpdate {
                max_surge,
                max_unavailable,
            } => Some(K8sDeploymentStrategy {
                type_: Some("RollingUpdate".to_string()),
                rolling_update: Some(RollingUpdateDeployment {
                    max_surge: max_surge.as_ref().map(|s| IntOrString::String(s.clone())),
                    max_unavailable: max_unavailable
                        .as_ref()
                        .map(|s| IntOrString::String(s.clone())),
                }),
            }),
        };

        let deployment_spec = K8sDeploymentSpec {
            replicas: Some(spec.replicas()),
            selector: LabelSelector {
                match_labels: Some(labels_btree.clone()),
                ..Default::default()
            },
            template: pod_template,
            strategy,
            ..Default::default()
        };

        Ok(Deployment {
            metadata: ObjectMeta {
                name: Some(spec.name().to_string()),
                namespace: Some(self.namespace.clone()),
                labels: Some(labels_btree),
                ..Default::default()
            },
            spec: Some(deployment_spec),
            ..Default::default()
        })
    }

    fn container_to_k8s(&self, spec: &ContainerSpec) -> Result<Container> {
        let ports: Vec<ContainerPort> = spec
            .ports()
            .iter()
            .map(|&port| ContainerPort {
                container_port: port,
                name: Some("http".to_string()),
                ..Default::default()
            })
            .collect();

        let env: Vec<EnvVar> = spec
            .env()
            .iter()
            .map(|(k, v)| EnvVar {
                name: k.clone(),
                value: Some(v.clone()),
                ..Default::default()
            })
            .collect();

        let resources = spec.resources().map(|r| {
            let mut requests = BTreeMap::new();
            let mut limits = BTreeMap::new();

            if let Some(cpu) = r.get_cpu_request() {
                requests.insert("cpu".to_string(), Quantity(cpu.to_string()));
            }
            if let Some(mem) = r.get_memory_request() {
                requests.insert("memory".to_string(), Quantity(mem.to_string()));
            }
            if let Some(cpu) = r.get_cpu_limit() {
                limits.insert("cpu".to_string(), Quantity(cpu.to_string()));
            }
            if let Some(mem) = r.get_memory_limit() {
                limits.insert("memory".to_string(), Quantity(mem.to_string()));
            }

            K8sResourceRequirements {
                requests: if requests.is_empty() {
                    None
                } else {
                    Some(requests)
                },
                limits: if limits.is_empty() {
                    None
                } else {
                    Some(limits)
                },
                ..Default::default()
            }
        });

        // Convert probes
        use crate::deployment::ProbeSpec;
        use k8s_openapi::api::core::v1::{ExecAction, HTTPGetAction, Probe, TCPSocketAction};

        let convert_probe = |probe_spec: &ProbeSpec| -> Probe {
            let mut probe = Probe {
                initial_delay_seconds: Some(probe_spec.initial_delay_seconds),
                period_seconds: Some(probe_spec.period_seconds),
                timeout_seconds: Some(probe_spec.timeout_seconds),
                success_threshold: Some(probe_spec.success_threshold),
                failure_threshold: Some(probe_spec.failure_threshold),
                ..Default::default()
            };

            if let Some(ref http) = probe_spec.http_get {
                probe.http_get = Some(HTTPGetAction {
                    path: Some(http.path.clone()),
                    port: IntOrString::Int(http.port),
                    ..Default::default()
                });
            } else if let Some(ref exec) = probe_spec.exec {
                probe.exec = Some(ExecAction {
                    command: Some(exec.command.clone()),
                });
            } else if let Some(ref tcp) = probe_spec.tcp_socket {
                probe.tcp_socket = Some(TCPSocketAction {
                    port: IntOrString::Int(tcp.port),
                    ..Default::default()
                });
            }

            probe
        };

        let liveness_probe = spec.liveness_probe().map(convert_probe);
        let readiness_probe = spec.readiness_probe().map(convert_probe);
        let startup_probe = spec.startup_probe().map(convert_probe);

        // Convert volume mounts
        use k8s_openapi::api::core::v1::VolumeMount as K8sVolumeMount;
        let volume_mounts: Vec<K8sVolumeMount> = spec
            .volume_mounts()
            .iter()
            .map(|vm| K8sVolumeMount {
                name: vm.name.clone(),
                mount_path: vm.mount_path.clone(),
                read_only: Some(vm.read_only),
                sub_path: vm.sub_path.clone(),
                ..Default::default()
            })
            .collect();

        Ok(Container {
            name: spec.name().to_string(),
            image: Some(spec.image().to_string()),
            ports: if ports.is_empty() { None } else { Some(ports) },
            env: if env.is_empty() { None } else { Some(env) },
            resources,
            liveness_probe,
            readiness_probe,
            startup_probe,
            volume_mounts: if volume_mounts.is_empty() {
                None
            } else {
                Some(volume_mounts)
            },
            command: spec.command().map(|c| c.to_vec()),
            args: spec.args().map(|a| a.to_vec()),
            ..Default::default()
        })
    }

    async fn check_pod_errors(&self, deployment_name: &str, pods_api: &Api<Pod>) -> Result<()> {
        let label_selector = format!("app={}", deployment_name);
        let lp = ListParams::default().labels(&label_selector);

        if let Ok(pod_list) = pods_api.list(&lp).await {
            for pod in pod_list.items {
                let pod_name = pod.metadata.name.unwrap_or_default();

                if let Some(status) = &pod.status {
                    // Check pod phase
                    if let Some(phase) = &status.phase
                        && phase == "Failed"
                    {
                        let reason = status.reason.as_deref().unwrap_or("Unknown");
                        let message = status.message.as_deref().unwrap_or("No details");
                        return Err(Error::pod_failed(
                            pod_name,
                            format!("{}: {}", reason, message),
                        ));
                    }

                    // Check container statuses
                    if let Some(container_statuses) = &status.container_statuses {
                        for cs in container_statuses {
                            if let Some(waiting) =
                                &cs.state.as_ref().and_then(|s| s.waiting.as_ref())
                            {
                                let reason = waiting.reason.as_deref().unwrap_or("Unknown");

                                // Image pull errors
                                if matches!(
                                    reason,
                                    "ImagePullBackOff"
                                        | "ErrImagePull"
                                        | "InvalidImageName"
                                        | "ImageInspectError"
                                ) {
                                    let msg = waiting.message.as_deref().unwrap_or("No details");
                                    return Err(Error::image_pull(
                                        cs.image.as_str(),
                                        format!("{}: {}", reason, msg),
                                    ));
                                }

                                // Container creation errors
                                if matches!(
                                    reason,
                                    "CreateContainerConfigError" | "CreateContainerError"
                                ) {
                                    let msg = waiting.message.as_deref().unwrap_or("No details");
                                    return Err(Error::deployment(format!(
                                        "Container {} failed to create: {}: {}",
                                        cs.name, reason, msg
                                    )));
                                }

                                // Crash loop detection
                                if reason == "CrashLoopBackOff"
                                    && cs.restart_count >= 3
                                    && let Some(terminated) =
                                        &cs.last_state.as_ref().and_then(|s| s.terminated.as_ref())
                                {
                                    return Err(Error::container_crashed(
                                        &pod_name,
                                        &cs.name,
                                        terminated.exit_code,
                                        cs.restart_count,
                                    ));
                                }
                            }
                        }
                    }
                }
            }
        }

        Ok(())
    }
}
