package infrastructure

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"oc-datacenter/conf"
	"strings"

	authv1 "k8s.io/api/authentication/v1"
	v1 "k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	apply "k8s.io/client-go/applyconfigurations/core/v1"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
)

var gvrSources = schema.GroupVersionResource{Group: "multicluster.admiralty.io", Version: "v1alpha1", Resource: "sources"}
var gvrTargets = schema.GroupVersionResource{Group: "multicluster.admiralty.io", Version: "v1alpha1", Resource: "targets"}

type KubernetesService struct {
	Set 			*kubernetes.Clientset
}

func NewDynamicClient() (*dynamic.DynamicClient, error) {
	config := &rest.Config{
		Host: conf.GetConfig().KubeHost + ":" + conf.GetConfig().KubePort,
		TLSClientConfig: rest.TLSClientConfig{
			CAData:   []byte(conf.GetConfig().KubeCA),
			CertData: []byte(conf.GetConfig().KubeCert),
			KeyData:  []byte(conf.GetConfig().KubeData),
		},
	}

	dynamicClient, err := dynamic.NewForConfig(config)
	if err != nil {
		return nil, errors.New("Error creating Dynamic client: " + err.Error())
	}
	if dynamicClient == nil {
		return nil, errors.New("Error creating Dynamic client: dynamicClient is nil")
	}

	return dynamicClient, nil
}

func NewKubernetesService() (Infrastructure, error) {
	config := &rest.Config{
		Host: conf.GetConfig().KubeHost + ":" + conf.GetConfig().KubePort,
		TLSClientConfig: rest.TLSClientConfig{
			CAData:   []byte(conf.GetConfig().KubeCA),
			CertData: []byte(conf.GetConfig().KubeCert),
			KeyData:  []byte(conf.GetConfig().KubeData),
		},
	}
	
	// Create clientset
	clientset, err := kubernetes.NewForConfig(config)
	fmt.Println("NewForConfig", clientset, err)
	if err != nil {
		return nil, errors.New("Error creating Kubernetes client: " + err.Error())
	}
	if clientset == nil {
		return nil, errors.New("Error creating Kubernetes client: clientset is nil")
	}


	return &KubernetesService{
		Set: clientset,
	}, nil
}

func NewRemoteKubernetesService(url string, ca string, cert string, key string) (Infrastructure, error) {
	decodedCa, _ := base64.StdEncoding.DecodeString(ca)
	decodedCert, _ := base64.StdEncoding.DecodeString(cert)
	decodedKey, _ := base64.StdEncoding.DecodeString(key)

	config := &rest.Config{
		Host: url + ":6443",
		TLSClientConfig: rest.TLSClientConfig{
			CAData:   decodedCa,
			CertData: decodedCert,
			KeyData:  decodedKey,
		},
	}
	// Create clientset
	clientset, err := kubernetes.NewForConfig(config)
	fmt.Println("NewForConfig", clientset, err)
	if err != nil {
		return nil, errors.New("Error creating Kubernetes client: " + err.Error())
	}
	if clientset == nil {
		return nil, errors.New("Error creating Kubernetes client: clientset is nil")
	}

	return &KubernetesService{
		Set: clientset,
	}, nil
}

func (k *KubernetesService) CreateNamespace(ctx context.Context, ns string) error {
	// Define the namespace
	fmt.Println("ExecutionID in CreateNamespace() : ", ns)
	namespace := &v1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: ns,
			Labels: map[string]string{
				"multicluster-scheduler":"enabled",
			},
		},
	}
	// Create the namespace
	fmt.Println("Creating namespace...", k.Set)
	if _, err := k.Set.CoreV1().Namespaces().Create(ctx, namespace, metav1.CreateOptions{}); err != nil {
		return errors.New("Error creating namespace: " + err.Error())
	}
	fmt.Println("Namespace created successfully!")
	return nil
}

func (k *KubernetesService) CreateServiceAccount(ctx context.Context, ns string) error {
	// Create the ServiceAccount object
	serviceAccount := &v1.ServiceAccount{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "sa-" + ns,
			Namespace: ns,
		},
	}
	// Create the ServiceAccount in the specified namespace
	_, err := k.Set.CoreV1().ServiceAccounts(ns).Create(ctx, serviceAccount, metav1.CreateOptions{})
	if err != nil {
		return errors.New("Failed to create ServiceAccount: " + err.Error())
	}
	return nil
}

func (k *KubernetesService) CreateRole(ctx context.Context, ns string, role string, groups [][]string, resources [][]string, verbs [][]string) error {
	// Create the Role object
	if len(groups) != len(resources) || len(resources) != len(verbs) {
		return errors.New("Invalid input: groups, resources, and verbs must have the same length")
	}
	rules := []rbacv1.PolicyRule{}
	for i, group := range groups {
		rules = append(rules, rbacv1.PolicyRule{
			APIGroups: group,
			Resources: resources[i],
			Verbs:     verbs[i],
		})
	}
	r := &rbacv1.Role{
		ObjectMeta: metav1.ObjectMeta{
			Name:      role,
			Namespace: ns,
		},
		Rules: rules,
	}
	// Create the Role in the specified namespace
	_, err := k.Set.RbacV1().Roles(ns).Create(ctx, r, metav1.CreateOptions{})
	if err != nil {
		return errors.New("Failed to create Role: " + err.Error())
	}
	return nil
}

func (k *KubernetesService) CreateRoleBinding(ctx context.Context, ns string, roleBinding string, role string) error {
	// Create the RoleBinding object
	rb := &rbacv1.RoleBinding{
		ObjectMeta: metav1.ObjectMeta{
			Name:      roleBinding,
			Namespace: ns,
		},
		Subjects: []rbacv1.Subject{
			{
				Kind:      "ServiceAccount",
				Name:      "sa-" + ns,
				Namespace: ns,
			},
		},
		RoleRef: rbacv1.RoleRef{
			Kind:     "Role",
			Name:     role,
			APIGroup: "rbac.authorization.k8s.io",
		},
	}
	// Create the RoleBinding in the specified namespace
	_, err := k.Set.RbacV1().RoleBindings(ns).Create(ctx, rb, metav1.CreateOptions{})
	if err != nil {
		return errors.New("Failed to create RoleBinding: " + err.Error())
	}
	return nil
}

func (k *KubernetesService) DeleteNamespace(ctx context.Context, ns string) error {
	// Delete the namespace
	if err := k.Set.CoreV1().Namespaces().Delete(ctx, ns, metav1.DeleteOptions{}); err != nil {
		return errors.New("Error deleting namespace: " + err.Error())
	}
	fmt.Println("Namespace deleted successfully!")
	return nil
}

// Returns the string representing the token generated for the serviceAccount
// in the namespace identified by the value `ns` with the name sa-`ns`, which is valid for
// `duration` seconds
func (k *KubernetesService) GenerateToken(ctx context.Context, ns string, duration int) (string, error) {
	// Define TokenRequest (valid for 1 hour)
	d := int64(duration)
	tokenRequest := &authv1.TokenRequest{
		Spec: authv1.TokenRequestSpec{
			ExpirationSeconds: &d, // 1 hour validity
		},
	}
	// Generate the token
	token, err := k.Set.CoreV1().
		ServiceAccounts(ns).
		CreateToken(ctx, "sa-"+ns, tokenRequest, metav1.CreateOptions{})
	if err != nil {
		return "", errors.New("Failed to create token for ServiceAccount: " + err.Error())
	}
	return token.Status.Token, nil
}



// Needs refactoring :
//  - Retrieving the metada (in a method that Unmarshall the part of the json in a metadata object)
func (k *KubernetesService) GetTargets(ctx context.Context) ([]string,error){

	var listTargets []string
	resp, err := getCDRapiKube(*k.Set, ctx,"/apis/multicluster.admiralty.io/v1alpha1/targets")
	if err != nil {
		return nil,err
	}

	fmt.Println(string(resp))
	var targetDict map[string]interface{}
	err = json.Unmarshal(resp,&targetDict)
	if err != nil {
		fmt.Println("TODO: handle the error when unmarshalling k8s API response")
		return nil, err
	}

	b, _ := json.MarshalIndent(targetDict,""," ")
	fmt.Println(string(b))

	data := targetDict["items"].([]interface{})

	for _, item := range data {
		var metadata metav1.ObjectMeta
		item := item.(map[string]interface{})
		byteMetada, err := json.Marshal(item["metadata"])
		
		if err != nil {
			fmt.Println("Error while Marshalling metadata field")
			return nil,err
		}
		err = json.Unmarshal(byteMetada,&metadata)
		if err != nil {
			fmt.Println("Error while Unmarshalling metadata field to the library object")
			return nil,err
		}

		listTargets = append(listTargets, metadata.Name)
	}

	return listTargets,nil

}

// Admiralty Target allows a cluster to deploy pods to remote cluster 
//
// The remote  cluster must :
//
// - have declared a Source resource
//
// - have declared the same namespace as the one where the pods are created in the local cluster
//
// - have delcared a serviceAccount with sufficient permission to create pods 
func (k *KubernetesService) CreateAdmiraltyTarget(context context.Context,executionId string)([]byte,error){
	exists, err := k.GetKubeconfigSecret(context,executionId)
	if err != nil {
		fmt.Println("Error verifying kube-secret before creating target")
		return nil, err
	}

	if exists == nil {
		fmt.Println("Target needs to be binded to a secret in namespace ",executionId)
		return nil, nil	// Maybe we could create a wrapper for errors and add more info to have
	}
	
	// var targetManifest string 
	// var tpl bytes.Buffer
	// tmpl, err := template.New("target").
	// 	Parse("{\"apiVersion\": \"multicluster.admiralty.io/v1alpha1\", \"kind\": \"Target\", \"metadata\": {\"name\": \"target-{{.ExecutionId}}\"}, \"spec\": { \"kubeconfigSecret\" :{\"name\": \"kube-secret-{{.ExecutionId}}\"}} }")
	// if err != nil {
	// 	fmt.Println("Error creating the template for the target Manifest")
	// 	return nil, err
	// }

	// err = tmpl.Execute(&tpl, map[string]string{"ExecutionId":executionId})
	// targetManifest = tpl.String()
	
	// resp, err := putCDRapiKube(
	// 	*k.Set,
	// 	context,
	// 	"/apis/multicluster.admiralty.io/v1alpha1/namespaces/"+ executionId +"/targets",
	// 	[]byte(targetManifest),
	// 	map[string]string{"fieldManager":"kubectl-client-side-apply"},
	// 	map[string]string{"fieldValidation":"Strict"},
	// )

	target := map[string]interface{}{
        "apiVersion": "multicluster.admiralty.io/v1alpha1",
        "kind":       "Target",
        "metadata": map[string]interface{}{
            "name":      "target-"+executionId,
            "namespace": executionId,
        },
        "spec": map[string]interface{}{
            "kubeconfigSecret": map[string]string{
				"name" : "kube-secret-"+executionId,
			},
        },
    }

	body, err := json.Marshal(target)
	if err != nil {
		fmt.Println("Error creating the body from the source Manifest")
		return nil, err
	}

	cli, err := NewDynamicClient()
	if err != nil {
		return nil, errors.New("Could not retrieve dynamic client when creating Admiralty Source : " + err.Error())
	}

	res, err := cli.Resource(gvrTargets).
					Namespace(executionId).
					Apply(context, 
						"source-"+executionId,
						&unstructured.Unstructured{Object: target},
						metav1.ApplyOptions{
							FieldManager: "kubectl-client-side-apply", 
						},
					)
	if err != nil {
		fmt.Println("Error from k8s API when applying " + string(body) + " to " + gvrSources.String() + " : " , err)
		return nil,err
	}

	resByte, err := json.Marshal(res) 
	if err != nil {
		// fmt.Println("Error trying to create a Source on remote cluster : ", err , " : ", res)
		return nil, err
	}

	return resByte, nil
}


// Admiralty Source allows a cluster to receive pods from a remote cluster
//
// The source must be associated to a serviceAccount, which will execute the pods locally.
// This serviceAccount must have sufficient permission to create and patch pods
//
// This method is temporary to implement the use of Admiralty, but must be edited
// to rather contact the oc-datacenter from the remote cluster to create the source
// locally and retrieve the token for the serviceAccount
func (k *KubernetesService) CreateAdmiraltySource(context context.Context,executionId string) ([]byte, error) {
	// var sourceManifest string 
	// var tpl bytes.Buffer
	// tmpl, err := template.New("source").
	// 	Parse("{\"apiVersion\": \"multicluster.admiralty.io/v1alpha1\", \"kind\": \"Source\", \"metadata\": {\"name\": \"source-{{.ExecutionId}}\"}, \"spec\": {\"serviceAccountName\": \"sa-{{.ExecutionId}}\"} }")
	// if err != nil {
	// 	fmt.Println("Error creating the template for the source Manifest")
	// 	return nil, err
	// }

	source := map[string]interface{}{
        "apiVersion": "multicluster.admiralty.io/v1alpha1",
        "kind":       "Source",
        "metadata": map[string]interface{}{
            "name":      "source-"+executionId,
            "namespace": executionId,
        },
        "spec": map[string]interface{}{
            "serviceAccountName": "sa-"+executionId,
        },
    }

	body, err := json.Marshal(source)
	if err != nil {
		fmt.Println("Error creating the body from the source Manifest")
		return nil, err
	}

	// err = tmpl.Execute(&tpl, map[string]string{"ExecutionId":executionId})
	// sourceManifest = tpl.String()
	// resp, err := putCDRapiKube(
	// 	*k.Set,
	// 	context,
	// 	"/apis/multicluster.admiralty.io/v1alpha1/namespaces/"+ executionId +"/sources",
	// 	[]byte(sourceManifest),
	// 	map[string]string{"fieldManager":"kubectl-client-side-apply"},
	// 	map[string]string{"fieldValidation":"Strict"},
	// )

	// params := []map[string]string{
	// 	{"fieldManager":"kubectl-client-side-apply"},
	// 	{"fieldValidation":"Strict"},
	// }
	
	cli, err := NewDynamicClient()
	if err != nil {
		return nil, errors.New("Could not retrieve dynamic client when creating Admiralty Source : " + err.Error())
	}


	res, err := cli.Resource(gvrSources).
					Namespace(executionId).
					Apply(context, 
						"source-"+executionId,
						&unstructured.Unstructured{Object: source},
						metav1.ApplyOptions{
							FieldManager: "kubectl-client-side-apply", 
						},
					)
	if err != nil {
		fmt.Println("Error from k8s API when applying " + string(body) + " to " + gvrSources.String() + " : " , err)
		return nil,err
	}


	// We can add more info to the log with the content of resp if not nil
	resByte, err := json.Marshal(res) 
	if err != nil {
		// fmt.Println("Error trying to create a Source on remote cluster : ", err , " : ", res)
		return nil, err
	}

	return resByte, nil
}

// Create a secret from a kubeconfing. Use it to create the secret binded to an Admiralty
// target, which must contain the serviceAccount's token value
func (k *KubernetesService) CreateKubeconfigSecret(context context.Context,kubeconfig string, executionId string) ([]byte, error) {
	config, err := base64.StdEncoding.DecodeString(kubeconfig)
	// config, err := base64.RawStdEncoding.DecodeString(kubeconfig)
	if err != nil {
		fmt.Println("Error while encoding kubeconfig")
		fmt.Println(err)
		return nil, err
	}

	secretApplyConfig := apply.Secret("kube-secret-" + executionId,
									executionId).
								WithData(map[string][]byte{
										"config": config,
										},
									)



	// exists, err := k.GetKubeconfigSecret(context,executionId)
	// if err != nil {
	// 	fmt.Println("Error verifying if kube secret exists in namespace ", executionId)
	// 	return nil, err
	// }
	// if exists != nil {
	// 	fmt.Println("kube-secret already exists in namespace", executionId)
	// 	fmt.Println("Overriding existing kube-secret with a newer resource")
	// 	// TODO : implement DeleteKubeConfigSecret(executionID)
	// 	deleted, err := k.DeleteKubeConfigSecret(executionId)
	// 	_ = deleted
	// 	_ = err
	// }


	resp, err := k.Set.CoreV1().
				Secrets(executionId).
				Apply(context,
					secretApplyConfig,
					metav1.ApplyOptions{
						FieldManager: "admiralty-manager",
				})
				
	if err != nil {
		fmt.Println("Error while trying to contact API to get secret kube-secret-"+executionId)
		fmt.Println(err)
		return nil, err
	}

	data, err := json.Marshal(resp)
	if err != nil {
		fmt.Println("Couldn't marshal resp from : ", data)
		fmt.Println(err)
		return nil, err
	}
	return data, nil
} 

func (k *KubernetesService) GetKubeconfigSecret(context context.Context,executionId string) ([]byte, error) {
	resp, err := k.Set.CoreV1().
						Secrets(executionId).
						Get(context,"kube-secret-"+executionId,metav1.GetOptions{})

	if err != nil {
		if(apierrors.IsNotFound(err)){
			fmt.Println("kube-secret not found for execution", executionId)
			return nil, nil
		}
		fmt.Println("Error while trying to contact API to get secret kube-secret-"+executionId)
		fmt.Println(err)
		return nil, err
	}

	data, err := json.Marshal(resp)

	if err != nil {
		fmt.Println("Couldn't marshal resp from : ", data)
		fmt.Println(err)
		return nil, err
	}

	return data, nil
}

func (k *KubernetesService) DeleteKubeConfigSecret(executionID string) ([]byte, error){

	return []byte{}, nil
}

func getCDRapiKube(client kubernetes.Clientset, ctx context.Context, path string) ([]byte,error) {
	resp, err := client.RESTClient().Get().
	AbsPath(path).
	DoRaw(ctx) // from https://stackoverflow.com/questions/60764908/how-to-access-kubernetes-crd-using-client-go

	if err != nil {
		fmt.Println("Error from k8s API when getting " + path + " : " , err)
		return nil,err
	}

	return resp, nil
}

func putCDRapiKube(client kubernetes.Clientset, ctx context.Context, path string, body []byte, params ...map[string]string) ([]byte, error){
	req := client.RESTClient().
				Post().
				AbsPath(path).
				Body(body)

	for _, param := range params {
		for k,v := range param {
			req = req.Param(k,v)
		}
	}
	
	resp, err := req.DoRaw(ctx)

	if err != nil {
		fmt.Println("Error from k8s API when posting " + string(body) + " to " + path + " : " , err)
		return nil,err
	}

	return resp, nil
}

// Returns the Kubernetes' Node object corresponding to the executionID if it exists on this host
//
// The node is created when an admiralty Target (on host) can connect to an admiralty Source (on remote)
func (k *KubernetesService) GetOneNode(context context.Context,executionID string) (*v1.Node, error) {
	res, err := k.Set.CoreV1().
						Nodes().
						List(
							context, 
							metav1.ListOptions{},
						)
	if err != nil {
		fmt.Println("Error getting the list of nodes from k8s API")
		fmt.Println(err)
		return nil, err
	}

	for _, node := range res.Items {
		if isNode := strings.Contains(node.Name,"admiralty-"+executionID+"-target-"+executionID+"-"); isNode {
			return &node, nil
		}
	}

	return nil, nil
}