use crate::constants::{
    NAMESPACE, RESOURCE_AGENT_BINDING, RESOURCE_AGENT_ROLE, RESOURCE_AGENT_SERVICE_ACCOUNT,
    TESTSYS, TEST_AGENT_BINDING, TEST_AGENT_ROLE, TEST_AGENT_SERVICE_ACCOUNT,
};
use k8s_openapi::api::core::v1::ServiceAccount;
use k8s_openapi::api::rbac::v1::{ClusterRole, ClusterRoleBinding, PolicyRule, RoleRef, Subject};
use kube::api::ObjectMeta;
use maplit::btreemap;

#[derive(Debug, Clone, Copy)]
pub enum AgentType {
    Test,
    Resource,
}

/// Defines the service account for an agent of type `agent_type`.
pub fn agent_service_account(agent_type: AgentType) -> ServiceAccount {
    ServiceAccount {
        metadata: ObjectMeta {
            name: Some(agent_type.service_account_name()),
            namespace: Some(NAMESPACE.to_string()),
            annotations: Some(btreemap! {
                "kubernetes.io/service-account.name".to_string() => agent_type.service_account_name()
            }),
            ..Default::default()
        },
        ..Default::default()
    }
}

/// Defines the cluster role for an agent of type `agent_type`.
pub fn agent_cluster_role(agent_type: AgentType) -> ClusterRole {
    ClusterRole {
        metadata: ObjectMeta {
            name: Some(agent_type.role_name()),
            namespace: Some(NAMESPACE.to_string()),
            ..Default::default()
        },
        rules: Some(agent_type.policy_rules()),
        ..Default::default()
    }
}

/// Defines the cluster role binding for an agent of type `agent_type`.
pub fn agent_cluster_role_binding(agent_type: AgentType) -> ClusterRoleBinding {
    ClusterRoleBinding {
        metadata: ObjectMeta {
            name: Some(agent_type.binding_name()),
            namespace: Some(NAMESPACE.to_string()),
            ..Default::default()
        },
        role_ref: RoleRef {
            kind: "ClusterRole".to_string(),
            name: agent_type.role_name(),
            api_group: "rbac.authorization.k8s.io".to_string(),
        },
        subjects: Some(vec![Subject {
            kind: "ServiceAccount".to_string(),
            name: agent_type.service_account_name(),
            namespace: Some(NAMESPACE.to_string()),
            ..Default::default()
        }]),
    }
}

impl AgentType {
    fn role_name(&self) -> String {
        match self {
            AgentType::Test => TEST_AGENT_ROLE.to_string(),
            AgentType::Resource => RESOURCE_AGENT_ROLE.to_string(),
        }
    }

    fn service_account_name(&self) -> String {
        match self {
            AgentType::Test => TEST_AGENT_SERVICE_ACCOUNT.to_string(),
            AgentType::Resource => RESOURCE_AGENT_SERVICE_ACCOUNT.to_string(),
        }
    }

    fn binding_name(&self) -> String {
        match self {
            AgentType::Test => TEST_AGENT_BINDING.to_string(),
            AgentType::Resource => RESOURCE_AGENT_BINDING.to_string(),
        }
    }

    fn managed_resource_plural_name(&self) -> &'static str {
        match self {
            AgentType::Test => "tests",
            AgentType::Resource => "resources",
        }
    }

    fn policy_rules(&self) -> Vec<PolicyRule> {
        let managed_resource = self.managed_resource_plural_name().to_string();
        let managed_status = format!("{}/status", managed_resource);

        // TODO - make two policy rules, remove patch/update for `managed_resource` (i.e. status only)
        let mut policy_rules = vec![PolicyRule {
            api_groups: Some(vec![TESTSYS.to_string()]),
            resources: Some(vec![managed_resource, managed_status]),
            verbs: vec!["get", "list", "patch", "update", "watch"]
                .iter()
                .map(|s| s.to_string())
                .collect(),
            ..Default::default()
        }];

        // We need to give the test agent the ability to get a resource's result.
        if matches!(self, &AgentType::Test) {
            policy_rules.push(PolicyRule {
                api_groups: Some(vec![TESTSYS.to_string()]),
                resources: Some(vec![AgentType::Resource
                    .managed_resource_plural_name()
                    .to_string()]),
                verbs: vec!["get".to_string()],
                ..Default::default()
            })
        }

        policy_rules
    }
}