use std::collections::BTreeMap;

use k8s_openapi::api::apps::v1::{Deployment, DeploymentSpec};
use k8s_openapi::api::core::v1::{
    Container, ContainerPort, EnvVar, EnvVarSource, HTTPGetAction, ObjectFieldSelector, PodSpec,
    PodTemplateSpec, Probe, SecretKeySelector, SecurityContext,
};
use k8s_openapi::apimachinery::pkg::api::resource::Quantity;
use k8s_openapi::apimachinery::pkg::apis::meta::v1::LabelSelector;
use k8s_openapi::apimachinery::pkg::util::intstr::IntOrString;
use kube::api::ObjectMeta;
use kube::Resource;

use crate::types::chatservice::ChatService;
use crate::types::minioinstance::MinioInstance;
use crate::types::webservice::{ResourceRequirements, WebService};

const MINIO_IMAGE: &str = "quay.io/minio/minio:latest";

fn to_k8s_resources(
    r: &ResourceRequirements,
) -> k8s_openapi::api::core::v1::ResourceRequirements {
    let mut k8s = k8s_openapi::api::core::v1::ResourceRequirements::default();
    if let Some(requests) = &r.requests {
        let mut map = BTreeMap::new();
        if let Some(cpu) = &requests.cpu {
            map.insert("cpu".to_string(), Quantity(cpu.clone()));
        }
        if let Some(memory) = &requests.memory {
            map.insert("memory".to_string(), Quantity(memory.clone()));
        }
        if !map.is_empty() {
            k8s.requests = Some(map);
        }
    }
    if let Some(limits) = &r.limits {
        let mut map = BTreeMap::new();
        if let Some(cpu) = &limits.cpu {
            map.insert("cpu".to_string(), Quantity(cpu.clone()));
        }
        if let Some(memory) = &limits.memory {
            map.insert("memory".to_string(), Quantity(memory.clone()));
        }
        if !map.is_empty() {
            k8s.limits = Some(map);
        }
    }
    k8s
}

fn env_plain(name: &str, value: &str) -> EnvVar {
    EnvVar {
        name: name.to_string(),
        value: Some(value.to_string()),
        ..Default::default()
    }
}

fn env_secret(name: &str, secret_name: &str, key: &str) -> EnvVar {
    EnvVar {
        name: name.to_string(),
        value_from: Some(EnvVarSource {
            secret_key_ref: Some(SecretKeySelector {
                name: secret_name.to_string(),
                key: key.to_string(),
                optional: Some(false),
            }),
            ..Default::default()
        }),
        ..Default::default()
    }
}

fn env_secret_optional(name: &str, secret_name: &str, key: &str) -> EnvVar {
    EnvVar {
        name: name.to_string(),
        value_from: Some(EnvVarSource {
            secret_key_ref: Some(SecretKeySelector {
                name: secret_name.to_string(),
                key: key.to_string(),
                optional: Some(true),
            }),
            ..Default::default()
        }),
        ..Default::default()
    }
}

fn env_field_ref(name: &str, field_path: &str) -> EnvVar {
    EnvVar {
        name: name.to_string(),
        value_from: Some(EnvVarSource {
            field_ref: Some(ObjectFieldSelector {
                field_path: field_path.to_string(),
                ..Default::default()
            }),
            ..Default::default()
        }),
        ..Default::default()
    }
}

pub fn build_web_deployment(ws: &WebService) -> Deployment {
    let name = ws.metadata.name.clone().unwrap();
    let ns = ws.metadata.namespace.clone().unwrap();
    let oref = ws.controller_owner_ref(&()).unwrap();
    let port = ws.spec.port.unwrap_or(8080);

    let labels: BTreeMap<String, String> = [
        ("app".to_string(), name.clone()),
        (
            "app.kubernetes.io/managed-by".to_string(),
            "platform-operator".to_string(),
        ),
    ]
    .into();

    let selector_labels: BTreeMap<String, String> = [("app".to_string(), name.clone())].into();

    let mut env: Vec<EnvVar> = Vec::new();

    if let Some(db) = &ws.spec.database {
        let prefix = db
            .env_prefix
            .as_deref()
            .unwrap_or("DB");
        env.push(env_plain(&format!("{prefix}_HOST"), &db.host));
        env.push(env_plain(&format!("{prefix}_PORT"), &db.port.to_string()));
        env.push(env_plain(&format!("{prefix}_NAME"), &db.name));
        env.push(env_secret(&format!("{prefix}_USER"), &db.secret_ref, "username"));
        env.push(env_secret(&format!("{prefix}_PASS"), &db.secret_ref, "password"));
    }

    if let Some(oidc) = &ws.spec.oidc {
        env.push(env_plain("OIDC_ISSUER_URL", &oidc.issuer_url));
        env.push(env_secret("OIDC_CLIENT_ID", &oidc.secret_ref, "client-id"));
        env.push(env_secret(
            "OIDC_CLIENT_SECRET",
            &oidc.secret_ref,
            "client-secret",
        ));
        env.push(env_plain("OIDC_REDIRECT_URL", &oidc.redirect_url));
    }

    if let Some(s3) = &ws.spec.s3 {
        env.push(env_plain("S3_ENDPOINT", &s3.endpoint));
        env.push(env_plain("S3_BUCKET", &s3.bucket));
        env.push(env_secret(
            "S3_ACCESS_KEY",
            &s3.credentials_secret,
            "root-user",
        ));
        env.push(env_secret(
            "S3_SECRET_KEY",
            &s3.credentials_secret,
            "root-password",
        ));
    }

    if let Some(stripe) = &ws.spec.stripe {
        env.push(env_secret_optional(
            "STRIPE_SECRET_KEY",
            &stripe.secret_ref,
            "secret-key",
        ));
        env.push(env_secret_optional(
            "STRIPE_PRICE_ID",
            &stripe.secret_ref,
            "price-id",
        ));
        env.push(env_secret_optional(
            "STRIPE_WEBHOOK_SECRET",
            &stripe.secret_ref,
            "webhook-secret",
        ));
    }

    if ws.spec.service_account.is_some() {
        env.push(env_field_ref("NAMESPACE", "metadata.namespace"));
    }

    let resources = ws.spec.resources.as_ref().map(to_k8s_resources);

    let container = Container {
        name: name.clone(),
        image: Some(ws.spec.image.clone()),
        ports: Some(vec![ContainerPort {
            container_port: port,
            name: Some("http".to_string()),
            ..Default::default()
        }]),
        env: if env.is_empty() { None } else { Some(env) },
        resources,
        readiness_probe: Some(Probe {
            http_get: Some(HTTPGetAction {
                path: Some("/health".to_string()),
                port: IntOrString::Int(port),
                ..Default::default()
            }),
            initial_delay_seconds: Some(5),
            ..Default::default()
        }),
        security_context: Some(SecurityContext {
            run_as_non_root: Some(true),
            allow_privilege_escalation: Some(false),
            ..Default::default()
        }),
        ..Default::default()
    };

    Deployment {
        metadata: ObjectMeta {
            name: Some(name.clone()),
            namespace: Some(ns),
            labels: Some(labels),
            owner_references: Some(vec![oref]),
            ..Default::default()
        },
        spec: Some(DeploymentSpec {
            replicas: Some(1),
            selector: LabelSelector {
                match_labels: Some(selector_labels),
                ..Default::default()
            },
            template: PodTemplateSpec {
                metadata: Some(ObjectMeta {
                    labels: Some([("app".to_string(), name.clone())].into()),
                    ..Default::default()
                }),
                spec: Some(PodSpec {
                    service_account_name: ws.spec.service_account.clone(),
                    containers: vec![container],
                    ..Default::default()
                }),
            },
            ..Default::default()
        }),
        ..Default::default()
    }
}

pub fn build_chat_deployment(cs: &ChatService) -> Deployment {
    let name = cs.metadata.name.clone().unwrap();
    let ns = cs.metadata.namespace.clone().unwrap();
    let oref = cs.controller_owner_ref(&()).unwrap();
    let port = cs.spec.port.unwrap_or(8080);

    let labels: BTreeMap<String, String> = [
        ("app".to_string(), name.clone()),
        (
            "app.kubernetes.io/managed-by".to_string(),
            "platform-operator".to_string(),
        ),
    ]
    .into();

    let selector_labels: BTreeMap<String, String> = [("app".to_string(), name.clone())].into();

    let resources = cs.spec.resources.as_ref().map(to_k8s_resources);

    let container = Container {
        name: name.clone(),
        image: Some(cs.spec.image.clone()),
        ports: Some(vec![ContainerPort {
            container_port: port,
            name: Some("http".to_string()),
            ..Default::default()
        }]),
        resources,
        security_context: Some(SecurityContext {
            run_as_non_root: Some(true),
            allow_privilege_escalation: Some(false),
            ..Default::default()
        }),
        ..Default::default()
    };

    Deployment {
        metadata: ObjectMeta {
            name: Some(name.clone()),
            namespace: Some(ns),
            labels: Some(labels),
            owner_references: Some(vec![oref]),
            ..Default::default()
        },
        spec: Some(DeploymentSpec {
            replicas: Some(1),
            selector: LabelSelector {
                match_labels: Some(selector_labels),
                ..Default::default()
            },
            template: PodTemplateSpec {
                metadata: Some(ObjectMeta {
                    labels: Some([("app".to_string(), name.clone())].into()),
                    ..Default::default()
                }),
                spec: Some(PodSpec {
                    containers: vec![container],
                    ..Default::default()
                }),
            },
            ..Default::default()
        }),
        ..Default::default()
    }
}

pub fn build_minio_deployment(mi: &MinioInstance) -> Deployment {
    let name = mi.metadata.name.clone().unwrap();
    let ns = mi.metadata.namespace.clone().unwrap();
    let oref = mi.controller_owner_ref(&()).unwrap();

    let labels: BTreeMap<String, String> = [
        ("app".to_string(), name.clone()),
        (
            "app.kubernetes.io/managed-by".to_string(),
            "platform-operator".to_string(),
        ),
    ]
    .into();

    let selector_labels: BTreeMap<String, String> = [("app".to_string(), name.clone())].into();

    let resources = mi.spec.resources.as_ref().map(to_k8s_resources);

    let env = vec![
        env_secret("MINIO_ROOT_USER", &mi.spec.credentials_secret, "root-user"),
        env_secret(
            "MINIO_ROOT_PASSWORD",
            &mi.spec.credentials_secret,
            "root-password",
        ),
    ];

    let container = Container {
        name: "minio".to_string(),
        image: Some(MINIO_IMAGE.to_string()),
        command: Some(vec!["minio".to_string()]),
        args: Some(vec!["server".to_string(), "/data".to_string()]),
        ports: Some(vec![
            ContainerPort {
                container_port: 9000,
                name: Some("api".to_string()),
                ..Default::default()
            },
            ContainerPort {
                container_port: 9001,
                name: Some("console".to_string()),
                ..Default::default()
            },
        ]),
        env: Some(env),
        resources,
        volume_mounts: Some(vec![k8s_openapi::api::core::v1::VolumeMount {
            name: "data".to_string(),
            mount_path: "/data".to_string(),
            ..Default::default()
        }]),
        security_context: Some(SecurityContext {
            run_as_non_root: Some(true),
            allow_privilege_escalation: Some(false),
            ..Default::default()
        }),
        ..Default::default()
    };

    Deployment {
        metadata: ObjectMeta {
            name: Some(name.clone()),
            namespace: Some(ns),
            labels: Some(labels),
            owner_references: Some(vec![oref]),
            ..Default::default()
        },
        spec: Some(DeploymentSpec {
            replicas: Some(1),
            selector: LabelSelector {
                match_labels: Some(selector_labels),
                ..Default::default()
            },
            template: PodTemplateSpec {
                metadata: Some(ObjectMeta {
                    labels: Some([("app".to_string(), name.clone())].into()),
                    ..Default::default()
                }),
                spec: Some(PodSpec {
                    containers: vec![container],
                    volumes: Some(vec![k8s_openapi::api::core::v1::Volume {
                        name: "data".to_string(),
                        persistent_volume_claim: Some(
                            k8s_openapi::api::core::v1::PersistentVolumeClaimVolumeSource {
                                claim_name: format!("{name}-data"),
                                ..Default::default()
                            },
                        ),
                        ..Default::default()
                    }]),
                    ..Default::default()
                }),
            },
            ..Default::default()
        }),
        ..Default::default()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::testutil::{
        test_chatservice, test_minioinstance, test_webservice, test_webservice_minimal,
    };

    #[test]
    fn web_deployment_uses_image_from_spec() {
        let dep = build_web_deployment(&test_webservice());
        let containers = dep.spec.unwrap().template.spec.unwrap().containers;
        assert!(containers[0].image.as_ref().unwrap().contains("paste"));
    }

    #[test]
    fn web_deployment_has_db_env_vars() {
        let dep = build_web_deployment(&test_webservice());
        let containers = dep.spec.unwrap().template.spec.unwrap().containers;
        let env = containers[0].env.as_ref().unwrap();
        let names: Vec<&str> = env.iter().map(|e| e.name.as_str()).collect();
        assert!(names.contains(&"PASTE_DB_HOST"));
        assert!(names.contains(&"PASTE_DB_PORT"));
        assert!(names.contains(&"PASTE_DB_NAME"));
        assert!(names.contains(&"PASTE_DB_USER"));
        assert!(names.contains(&"PASTE_DB_PASS"));
    }

    #[test]
    fn web_deployment_has_oidc_env_vars() {
        let dep = build_web_deployment(&test_webservice());
        let containers = dep.spec.unwrap().template.spec.unwrap().containers;
        let env = containers[0].env.as_ref().unwrap();
        let names: Vec<&str> = env.iter().map(|e| e.name.as_str()).collect();
        assert!(names.contains(&"OIDC_ISSUER_URL"));
        assert!(names.contains(&"OIDC_CLIENT_ID"));
        assert!(names.contains(&"OIDC_CLIENT_SECRET"));
        assert!(names.contains(&"OIDC_REDIRECT_URL"));
    }

    #[test]
    fn web_deployment_db_user_from_secret() {
        let dep = build_web_deployment(&test_webservice());
        let containers = dep.spec.unwrap().template.spec.unwrap().containers;
        let env = containers[0].env.as_ref().unwrap();
        let db_user = env.iter().find(|e| e.name == "PASTE_DB_USER").unwrap();
        let secret_ref = db_user
            .value_from
            .as_ref()
            .unwrap()
            .secret_key_ref
            .as_ref()
            .unwrap();
        assert_eq!(secret_ref.name, "paste-db-app");
        assert_eq!(secret_ref.key, "username");
    }

    #[test]
    fn web_deployment_minimal_has_no_env() {
        let dep = build_web_deployment(&test_webservice_minimal());
        let containers = dep.spec.unwrap().template.spec.unwrap().containers;
        assert!(containers[0].env.is_none());
    }

    #[test]
    fn web_deployment_selector_uses_app_label() {
        let dep = build_web_deployment(&test_webservice());
        let selector = dep.spec.unwrap().selector.match_labels.unwrap();
        assert_eq!(selector["app"], "txt");
        assert!(!selector.contains_key("app.kubernetes.io/managed-by"));
    }

    #[test]
    fn web_deployment_has_readiness_probe() {
        let dep = build_web_deployment(&test_webservice());
        let containers = dep.spec.unwrap().template.spec.unwrap().containers;
        let probe = containers[0].readiness_probe.as_ref().unwrap();
        let http = probe.http_get.as_ref().unwrap();
        assert_eq!(http.path, Some("/health".to_string()));
    }

    #[test]
    fn web_deployment_has_owner_reference() {
        let dep = build_web_deployment(&test_webservice());
        let orefs = dep.metadata.owner_references.unwrap();
        assert_eq!(orefs[0].name, "txt");
    }

    #[test]
    fn web_deployment_has_resources() {
        let dep = build_web_deployment(&test_webservice());
        let containers = dep.spec.unwrap().template.spec.unwrap().containers;
        let res = containers[0].resources.as_ref().unwrap();
        let requests = res.requests.as_ref().unwrap();
        assert_eq!(requests["memory"], Quantity("64Mi".to_string()));
    }

    #[test]
    fn chat_deployment_has_no_env() {
        let dep = build_chat_deployment(&test_chatservice());
        let containers = dep.spec.unwrap().template.spec.unwrap().containers;
        assert!(containers[0].env.is_none());
    }

    #[test]
    fn chat_deployment_has_no_readiness_probe() {
        let dep = build_chat_deployment(&test_chatservice());
        let containers = dep.spec.unwrap().template.spec.unwrap().containers;
        assert!(containers[0].readiness_probe.is_none());
    }

    #[test]
    fn minio_deployment_uses_minio_image() {
        let dep = build_minio_deployment(&test_minioinstance());
        let containers = dep.spec.unwrap().template.spec.unwrap().containers;
        assert_eq!(containers[0].image.as_ref().unwrap(), MINIO_IMAGE);
    }

    #[test]
    fn minio_deployment_mounts_pvc() {
        let dep = build_minio_deployment(&test_minioinstance());
        let pod_spec = dep.spec.unwrap().template.spec.unwrap();
        let volumes = pod_spec.volumes.unwrap();
        let data_vol = volumes.iter().find(|v| v.name == "data").unwrap();
        assert_eq!(
            data_vol
                .persistent_volume_claim
                .as_ref()
                .unwrap()
                .claim_name,
            "minio-data"
        );
    }

    #[test]
    fn minio_deployment_has_credential_env() {
        let dep = build_minio_deployment(&test_minioinstance());
        let containers = dep.spec.unwrap().template.spec.unwrap().containers;
        let env = containers[0].env.as_ref().unwrap();
        let names: Vec<&str> = env.iter().map(|e| e.name.as_str()).collect();
        assert!(names.contains(&"MINIO_ROOT_USER"));
        assert!(names.contains(&"MINIO_ROOT_PASSWORD"));
    }

    #[test]
    fn minio_deployment_has_two_ports() {
        let dep = build_minio_deployment(&test_minioinstance());
        let containers = dep.spec.unwrap().template.spec.unwrap().containers;
        let ports = containers[0].ports.as_ref().unwrap();
        assert_eq!(ports.len(), 2);
        assert_eq!(ports[0].container_port, 9000);
        assert_eq!(ports[1].container_port, 9001);
    }

    #[test]
    fn minio_deployment_has_owner_reference() {
        let dep = build_minio_deployment(&test_minioinstance());
        let orefs = dep.metadata.owner_references.unwrap();
        assert_eq!(orefs[0].name, "minio");
    }
}
