use derive_new::new;
use k8s_openapi::{ClusterResourceScope, NamespaceResourceScope};
use kube::{
    Api, Client, Config, Error, Resource,
    api::{Patch, PatchParams},
    config::{KubeConfigOptions, Kubeconfig},
};
use log::{debug, error, trace};
use serde::de::DeserializeOwned;

#[derive(new)]
pub struct K8sClient {
    client: Client,
}

impl K8sClient {
    pub async fn try_default() -> Result<Self, Error> {
        Ok(Self {
            client: Client::try_default().await?,
        })
    }

    /// Apply a resource in namespace
    ///
    /// See `kubectl apply` for more information on the expected behavior of this function
    pub async fn apply<K>(&self, resource: &K, namespace: Option<&str>) -> Result<K, Error>
    where
        K: Resource + Clone + std::fmt::Debug + DeserializeOwned + serde::Serialize,
        <K as Resource>::Scope: ApplyStrategy<K>,
        <K as kube::Resource>::DynamicType: Default,
    {
        debug!(
            "Applying resource {:?} with ns {:?}",
            resource.meta().name,
            namespace
        );
        trace!(
            "{:#}",
            serde_json::to_value(resource).unwrap_or(serde_json::Value::Null)
        );

        let api: Api<K> =
            <<K as Resource>::Scope as ApplyStrategy<K>>::get_api(&self.client, namespace);
        // api.create(&PostParams::default(), &resource).await
        let patch_params = PatchParams::apply("harmony");
        let name = resource
            .meta()
            .name
            .as_ref()
            .expect("K8s Resource should have a name");
        api.patch(name, &patch_params, &Patch::Apply(resource))
            .await
    }

    pub async fn apply_many<K>(&self, resource: &Vec<K>, ns: Option<&str>) -> Result<Vec<K>, Error>
    where
        K: Resource + Clone + std::fmt::Debug + DeserializeOwned + serde::Serialize,
        <K as Resource>::Scope: ApplyStrategy<K>,
        <K as kube::Resource>::DynamicType: Default,
    {
        let mut result = Vec::new();
        for r in resource.iter() {
            result.push(self.apply(r, ns).await?);
        }

        Ok(result)
    }

    pub(crate) async fn from_kubeconfig(path: &str) -> Option<K8sClient> {
        let k = match Kubeconfig::read_from(path) {
            Ok(k) => k,
            Err(e) => {
                error!("Failed to load kubeconfig from {path} : {e}");
                return None;
            }
        };
        Some(K8sClient::new(
            Client::try_from(
                Config::from_custom_kubeconfig(k, &KubeConfigOptions::default())
                    .await
                    .unwrap(),
            )
            .unwrap(),
        ))
    }
}

pub trait ApplyStrategy<K: Resource> {
    fn get_api(client: &Client, ns: Option<&str>) -> Api<K>;
}

/// Implementation for all resources that are cluster-scoped.
/// It will always use `Api::all` and ignore the namespace parameter.
impl<K> ApplyStrategy<K> for ClusterResourceScope
where
    K: Resource<Scope = ClusterResourceScope>,
    <K as kube::Resource>::DynamicType: Default,
{
    fn get_api(client: &Client, _ns: Option<&str>) -> Api<K> {
        Api::all(client.clone())
    }
}

/// Implementation for all resources that are namespace-scoped.
/// It will use `Api::namespaced` if a namespace is provided, otherwise
/// it falls back to the default namespace configured in your kubeconfig.
impl<K> ApplyStrategy<K> for NamespaceResourceScope
where
    K: Resource<Scope = NamespaceResourceScope>,
    <K as kube::Resource>::DynamicType: Default,
{
    fn get_api(client: &Client, ns: Option<&str>) -> Api<K> {
        match ns {
            Some(ns) => Api::namespaced(client.clone(), ns),
            None => Api::default_namespaced(client.clone()),
        }
    }
}
