//! CronJob manager implementation.

use crate::cronjob::CronJobSpec;
use crate::deployment::ContainerSpec;
use crate::error::{Error, Result};
use crate::job::JobSpec;
use k8s_openapi::api::batch::v1::CronJob;
use k8s_openapi::api::batch::v1::CronJobSpec as K8sCronJobSpec;
use k8s_openapi::api::batch::v1::{JobSpec as K8sJobSpec, JobTemplateSpec};
use k8s_openapi::api::core::v1::{
    Container, ContainerPort, EnvVar, LocalObjectReference, PodSpec as K8sPodSpec, PodTemplateSpec,
    ResourceRequirements as K8sResourceRequirements,
};
use k8s_openapi::apimachinery::pkg::api::resource::Quantity;
use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
use kube::Client as KubeClient;
use kube::api::{Api, DeleteParams, ListParams, Patch, PatchParams};
use std::collections::BTreeMap;

/// Manager for Kubernetes CronJob operations.
pub struct CronJobManager {
    api: Api<CronJob>,
    namespace: String,
}

impl CronJobManager {
    /// Creates a new CronJobManager.
    ///
    /// # Arguments
    ///
    /// * `client` - The Kubernetes client
    /// * `namespace` - The namespace to operate in
    pub fn new(client: KubeClient, namespace: impl Into<String>) -> Self {
        let namespace = namespace.into();
        let api = Api::namespaced(client, &namespace);
        Self { api, namespace }
    }

    /// Creates or updates a CronJob using server-side apply.
    ///
    /// # Arguments
    ///
    /// * `spec` - The CronJob specification
    ///
    /// # Returns
    ///
    /// Returns the created/updated CronJob name on success.
    pub async fn apply(&self, spec: &CronJobSpec) -> Result<String> {
        // Validate spec
        spec.validate().map_err(Error::ValidationError)?;

        let cronjob = self.spec_to_k8s(spec)?;
        let name = spec.name();

        let params = PatchParams::apply("kubernetes-manager").force();
        let patch = Patch::Apply(&cronjob);

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

        Ok(name.to_string())
    }

    /// Gets a CronJob by name.
    ///
    /// # Arguments
    ///
    /// * `name` - The CronJob name
    ///
    /// # Returns
    ///
    /// Returns the CronJob if found.
    pub async fn get(&self, name: &str) -> Result<CronJob> {
        self.api
            .get(name)
            .await
            .map_err(|_| Error::ResourceNotFound {
                kind: "CronJob".to_string(),
                name: name.to_string(),
                namespace: self.namespace.clone(),
            })
    }

    /// Lists all CronJobs in the namespace.
    ///
    /// # Returns
    ///
    /// Returns a vector of CronJobs.
    pub async fn list(&self) -> Result<Vec<CronJob>> {
        let lp = ListParams::default();
        let list = self
            .api
            .list(&lp)
            .await
            .map_err(|e| Error::CronJobError(format!("Failed to list CronJobs: {}", e)))?;

        Ok(list.items)
    }

    /// Lists CronJobs matching the given label selector.
    ///
    /// # Arguments
    ///
    /// * `label_selector` - The label selector (e.g., "app=batch")
    ///
    /// # Returns
    ///
    /// Returns a vector of matching CronJobs.
    pub async fn list_by_label(&self, label_selector: &str) -> Result<Vec<CronJob>> {
        let lp = ListParams::default().labels(label_selector);
        let list = self
            .api
            .list(&lp)
            .await
            .map_err(|e| Error::CronJobError(format!("Failed to list CronJobs: {}", e)))?;

        Ok(list.items)
    }

    /// Deletes a CronJob by name.
    ///
    /// # Arguments
    ///
    /// * `name` - The CronJob name
    pub async fn delete(&self, name: &str) -> Result<()> {
        let dp = DeleteParams::default();
        self.api.delete(name, &dp).await.map_err(|e| {
            Error::CronJobError(format!("Failed to delete CronJob '{}': {}", name, e))
        })?;

        Ok(())
    }

    /// Checks if a CronJob exists.
    ///
    /// # Arguments
    ///
    /// * `name` - The CronJob name
    ///
    /// # Returns
    ///
    /// Returns `true` if the CronJob exists, `false` otherwise.
    pub async fn exists(&self, name: &str) -> Result<bool> {
        match self.get(name).await {
            Ok(_) => Ok(true),
            Err(Error::ResourceNotFound { .. }) => Ok(false),
            Err(e) => Err(e),
        }
    }

    /// Converts container spec to Kubernetes container.
    fn container_to_k8s(&self, cs: &ContainerSpec) -> Result<Container> {
        let ports: Vec<ContainerPort> = cs
            .ports()
            .iter()
            .map(|&p| ContainerPort {
                container_port: p,
                ..Default::default()
            })
            .collect();

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

        let resources = cs.resources().as_ref().map(|res| {
            let mut requests = BTreeMap::new();
            let mut limits = BTreeMap::new();

            if let Some(cpu_req) = res.get_cpu_request() {
                requests.insert("cpu".to_string(), Quantity(cpu_req.to_string()));
            }
            if let Some(mem_req) = res.get_memory_request() {
                requests.insert("memory".to_string(), Quantity(mem_req.to_string()));
            }
            if let Some(cpu_lim) = res.get_cpu_limit() {
                limits.insert("cpu".to_string(), Quantity(cpu_lim.to_string()));
            }
            if let Some(mem_lim) = res.get_memory_limit() {
                limits.insert("memory".to_string(), Quantity(mem_lim.to_string()));
            }

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

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

    /// Converts JobSpec to Kubernetes job template.
    fn job_template_to_k8s(&self, spec: &JobSpec) -> Result<JobTemplateSpec> {
        let mut labels = BTreeMap::new();
        for (k, v) in spec.labels() {
            labels.insert(k.clone(), v.clone());
        }

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

        // 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();

        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()),
            restart_policy: Some(spec.restart_policy().as_str().to_string()),
            ..Default::default()
        };

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

        let job_spec = K8sJobSpec {
            template: pod_template,
            completions: spec.completions(),
            parallelism: spec.parallelism(),
            backoff_limit: spec.backoff_limit(),
            active_deadline_seconds: spec.active_deadline_seconds(),
            ttl_seconds_after_finished: spec.ttl_seconds_after_finished(),
            ..Default::default()
        };

        Ok(JobTemplateSpec {
            metadata: Some(ObjectMeta {
                labels: Some(labels),
                ..Default::default()
            }),
            spec: Some(job_spec),
        })
    }

    /// Converts CronJobSpec to Kubernetes API CronJob.
    fn spec_to_k8s(&self, spec: &CronJobSpec) -> Result<CronJob> {
        let job_template = match spec.job_template() {
            Some(template) => self.job_template_to_k8s(template)?,
            None => {
                return Err(Error::invalid_spec("CronJob must have a job template"));
            }
        };

        let cronjob_spec = K8sCronJobSpec {
            schedule: spec.schedule().to_string(),
            job_template,
            concurrency_policy: Some(spec.concurrency_policy().as_str().to_string()),
            successful_jobs_history_limit: spec.successful_jobs_history_limit(),
            failed_jobs_history_limit: spec.failed_jobs_history_limit(),
            starting_deadline_seconds: spec.starting_deadline_seconds(),
            suspend: if spec.is_suspended() {
                Some(true)
            } else {
                None
            },
            ..Default::default()
        };

        Ok(CronJob {
            metadata: ObjectMeta {
                name: Some(spec.name().to_string()),
                namespace: Some(self.namespace.clone()),
                ..Default::default()
            },
            spec: Some(cronjob_spec),
            ..Default::default()
        })
    }
}
