use log::debug;
use serde::Serialize;
use serde_with::skip_serializing_none;
use serde_yaml::Value;

use crate::modules::application::features::CDApplicationConfig;

#[skip_serializing_none]
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Helm {
    pub pass_credentials: Option<bool>,
    pub parameters: Vec<Value>,
    pub file_parameters: Vec<Value>,
    pub release_name: Option<String>,
    pub value_files: Vec<String>,
    pub ignore_missing_value_files: Option<bool>,
    pub values: Option<String>,
    pub values_object: Option<Value>,
    pub skip_crds: Option<bool>,
    pub skip_schema_validation: Option<bool>,
    pub version: Option<String>,
    pub kube_version: Option<String>,
    pub api_versions: Vec<String>,
    pub namespace: Option<String>,
}

#[skip_serializing_none]
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Source {
    // Using string for this because URL enforces a URL scheme at the beginning but Helm, ArgoCD, etc do not, and it can be counterproductive,
    // as the only way I've found to get OCI working isn't by using oci:// but rather no scheme at all
    #[serde(rename = "repoURL")]
    pub repo_url: String,
    pub target_revision: Option<String>,
    pub chart: String,
    pub helm: Helm,
    pub path: String,
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Automated {
    pub prune: bool,
    pub self_heal: bool,
    pub allow_empty: bool,
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Backoff {
    pub duration: String,
    pub factor: u32,
    pub max_duration: String,
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Retry {
    pub limit: u32,
    pub backoff: Backoff,
}

#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SyncPolicy {
    pub automated: Automated,
    pub sync_options: Vec<String>,
    pub retry: Retry,
}

#[skip_serializing_none]
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ArgoApplication {
    pub name: String,
    pub namespace: Option<String>,
    pub project: String,
    pub source: Source,
    pub sync_policy: SyncPolicy,
    pub revision_history_limit: u32,
}

impl Default for ArgoApplication {
    fn default() -> Self {
        Self {
            name: Default::default(),
            namespace: Default::default(),
            project: Default::default(),
            source: Source {
                repo_url: "http://asdf".to_string(),
                target_revision: None,
                chart: "".to_string(),
                helm: Helm {
                    pass_credentials: None,
                    parameters: vec![],
                    file_parameters: vec![],
                    release_name: None,
                    value_files: vec![],
                    ignore_missing_value_files: None,
                    values: None,
                    values_object: None,
                    skip_crds: None,
                    skip_schema_validation: None,
                    version: None,
                    kube_version: None,
                    api_versions: vec![],
                    namespace: None,
                },
                path: "".to_string(),
            },
            sync_policy: SyncPolicy {
                automated: Automated {
                    prune: false,
                    self_heal: false,
                    allow_empty: false,
                },
                sync_options: vec![],
                retry: Retry {
                    limit: 5,
                    backoff: Backoff {
                        duration: "5s".to_string(),
                        factor: 2,
                        max_duration: "3m".to_string(),
                    },
                },
            },
            revision_history_limit: 10,
        }
    }
}

impl From<CDApplicationConfig> for ArgoApplication {
    fn from(value: CDApplicationConfig) -> Self {
        Self {
            name: value.name,
            namespace: Some(value.namespace),
            project: "default".to_string(),
            source: Source {
                repo_url: value.helm_chart_repo_url,
                target_revision: Some(value.version.to_string()),
                chart: value.helm_chart_name.clone(),
                path: value.helm_chart_name,
                helm: Helm {
                    pass_credentials: None,
                    parameters: vec![],
                    file_parameters: vec![],
                    release_name: None,
                    value_files: vec![],
                    ignore_missing_value_files: None,
                    values: None,
                    values_object: value.values_overrides,
                    skip_crds: None,
                    skip_schema_validation: None,
                    version: None,
                    kube_version: None,
                    api_versions: vec![],
                    namespace: None,
                },
            },
            sync_policy: SyncPolicy {
                automated: Automated {
                    prune: false,
                    self_heal: false,
                    allow_empty: true,
                },
                sync_options: vec![],
                retry: Retry {
                    limit: 5,
                    backoff: Backoff {
                        duration: "5s".to_string(),
                        factor: 2,
                        max_duration: "3m".to_string(),
                    },
                },
            },
            ..Self::default()
        }
    }
}

impl ArgoApplication {
    pub fn to_yaml(&self) -> serde_yaml::Value {
        let name = &self.name;
        let namespace = if let Some(ns) = self.namespace.as_ref() {
            ns
        } else {
            "argocd"
        };
        let project = &self.project;

        let yaml_str = format!(
            r#"
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: {name}
  # You'll usually want to add your resources to the argocd namespace.
  namespace: {namespace}
spec:
  # The project the application belongs to.
  project: {project}

  # Destination cluster and namespace to deploy the application
  destination:
    # cluster API URL
    server: https://kubernetes.default.svc
    # or cluster name
    # name: in-cluster
    # The namespace will only be set for namespace-scoped resources that have not set a value for .metadata.namespace
    namespace: {namespace}

"#
        );

        let mut yaml_value: Value =
            serde_yaml::from_str(yaml_str.as_str()).expect("couldn't parse string to YAML");

        let spec = yaml_value
            .get_mut("spec")
            .expect("couldn't get spec from yaml")
            .as_mapping_mut()
            .expect("couldn't unwrap spec as mutable mapping");

        let source =
            serde_yaml::to_value(&self.source).expect("couldn't serialize source to value");
        let sync_policy = serde_yaml::to_value(&self.sync_policy)
            .expect("couldn't serialize sync_policy to value");
        let revision_history_limit = serde_yaml::to_value(self.revision_history_limit)
            .expect("couldn't serialize revision_history_limit to value");

        spec.insert(
            serde_yaml::to_value("source").expect("string to value failed"),
            source,
        );
        spec.insert(
            serde_yaml::to_value("syncPolicy").expect("string to value failed"),
            sync_policy,
        );
        spec.insert(
            serde_yaml::to_value("revisionHistoryLimit")
                .expect("couldn't convert str to yaml value"),
            revision_history_limit,
        );

        debug!("spec: {}", serde_yaml::to_string(spec).unwrap());
        debug!(
            "entire yaml_value: {}",
            serde_yaml::to_string(&yaml_value).unwrap()
        );

        yaml_value
    }
}

#[cfg(test)]
mod tests {
    use pretty_assertions::assert_eq;

    use crate::modules::application::features::{
        ArgoApplication, Automated, Backoff, Helm, Retry, Source, SyncPolicy,
    };

    #[test]
    fn test_argo_application_to_yaml_happy_path() {
        let app = ArgoApplication {
            name: "test".to_string(),
            namespace: Some("test-ns".to_string()),
            project: "test-project".to_string(),
            source: Source {
                repo_url: "http://test".to_string(),
                target_revision: None,
                chart: "test-chart".to_string(),
                helm: Helm {
                    pass_credentials: None,
                    parameters: vec![],
                    file_parameters: vec![],
                    release_name: Some("test-release-neame".to_string()),
                    value_files: vec![],
                    ignore_missing_value_files: None,
                    values: None,
                    values_object: None,
                    skip_crds: None,
                    skip_schema_validation: None,
                    version: None,
                    kube_version: None,
                    api_versions: vec![],
                    namespace: None,
                },
                path: "".to_string(),
            },
            sync_policy: SyncPolicy {
                automated: Automated {
                    prune: false,
                    self_heal: false,
                    allow_empty: false,
                },
                sync_options: vec![],
                retry: Retry {
                    limit: 5,
                    backoff: Backoff {
                        duration: "5s".to_string(),
                        factor: 2,
                        max_duration: "3m".to_string(),
                    },
                },
            },
            revision_history_limit: 10,
        };

        let expected_yaml_output = r#"apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: test
  namespace: test-ns
spec:
  project: test-project
  destination:
    server: https://kubernetes.default.svc
    namespace: test-ns
  source:
    repoURL: http://test
    chart: test-chart
    helm:
      parameters: []
      fileParameters: []
      releaseName: test-release-neame
      valueFiles: []
      apiVersions: []
    path: ''
  syncPolicy:
    automated:
      prune: false
      selfHeal: false
      allowEmpty: false
    syncOptions: []
    retry:
      limit: 5
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m
  revisionHistoryLimit: 10"#;

        assert_eq!(
            expected_yaml_output.trim(),
            serde_yaml::to_string(&app.clone().to_yaml())
                .unwrap()
                .trim()
        );
    }
}
