package tools

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"

	"cloud.o-forge.io/core/oc-lib/logs"
	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"
	"k8s.io/apimachinery/pkg/api/resource"
	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
	Host string
	CA   string
	Cert string
	Data string
}

func NewDynamicClient(host string, ca string, cert string, data string) (*dynamic.DynamicClient, error) {
	decodedCa, _ := base64.StdEncoding.DecodeString(ca)
	decodedCert, _ := base64.StdEncoding.DecodeString(cert)
	decodedKey, _ := base64.StdEncoding.DecodeString(data)

	config := &rest.Config{
		Host: host,
		TLSClientConfig: rest.TLSClientConfig{
			CAData:   []byte(decodedCa),
			CertData: []byte(decodedCert),
			KeyData:  []byte(decodedKey),
		},
	}

	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(host string, ca string, cert string, data string) (*KubernetesService, error) {
	decodedCa, _ := base64.StdEncoding.DecodeString(ca)
	decodedCert, _ := base64.StdEncoding.DecodeString(cert)
	decodedKey, _ := base64.StdEncoding.DecodeString(data)

	config := &rest.Config{
		Host: host,
		TLSClientConfig: rest.TLSClientConfig{
			CAData:   []byte(decodedCa),
			CertData: []byte(decodedCert),
			KeyData:  []byte(decodedKey),
		},
	}

	// Create clientset
	clientset, err := kubernetes.NewForConfig(config)
	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,
		Host: host,
		CA:   ca,
		Cert: cert,
		Data: data,
	}, 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,
			Annotations: map[string]string{
				"multicluster.admiralty.io/elect": "",
			},
		},
	}
	// Create the namespace
	fmt.Println("Creating namespace...")
	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
}

// ProvisionExecutionNamespace creates the full Argo execution environment for a
// namespace: namespace, service-account, role and role-binding. Idempotent — if
// the namespace already exists the call is a no-op.
func (k *KubernetesService) ProvisionExecutionNamespace(ctx context.Context, ns string) error {
	existing, _ := k.GetNamespace(ctx, ns)
	if existing != nil {
		return nil
	}
	if err := k.CreateNamespace(ctx, ns); err != nil && !strings.Contains(err.Error(), "already exists") {
		return err
	}
	if err := k.CreateServiceAccount(ctx, ns); err != nil && !strings.Contains(err.Error(), "already exists") {
		return err
	}
	role := "argo-role"
	if err := k.CreateRole(ctx, ns, role,
		[][]string{{"coordination.k8s.io"}, {""}, {""}, {"multicluster.admiralty.io"}, {"argoproj.io"}},
		[][]string{{"leases"}, {"secrets"}, {"pods"}, {"podchaperons"}, {"workflowtaskresults"}},
		[][]string{{"get", "create", "update"}, {"get"}, {"patch"}, {"get", "list", "watch", "create", "update", "patch", "delete"}, {"create", "patch"}},
	); err != nil {
		return err
	}
	return k.CreateRoleBinding(ctx, ns, "argo-role-binding", role)
}

// TeardownExecutionNamespace deletes the namespace and lets Kubernetes cascade
// the deletion of all contained resources (SA, Role, RoleBinding, pods…).
func (k *KubernetesService) TeardownExecutionNamespace(ctx context.Context, ns string) error {
	if err := k.Set.CoreV1().Namespaces().Delete(ctx, ns, metav1.DeleteOptions{}); err != nil {
		return errors.New("error deleting namespace " + ns + ": " + err.Error())
	}
	return nil
}

func (k *KubernetesService) DeleteNamespace(ctx context.Context, ns string, f func()) error {
	targetGVR := schema.GroupVersionResource{
		Group:    "multicluster.admiralty.io",
		Version:  "v1alpha1",
		Resource: "targets",
	}

	// Delete the Target
	dyn, err := NewDynamicClient(k.Host, k.CA, k.Cert, k.Data)
	if err != nil {
		return err
	}

	err = dyn.Resource(targetGVR).Namespace(ns).Delete(context.TODO(), "target-"+ns, metav1.DeleteOptions{})
	if err != nil {
		return err
	}
	err = k.Set.CoreV1().ServiceAccounts(ns).Delete(context.TODO(), "sa-"+ns, metav1.DeleteOptions{})
	if err != nil {
		return err
	}
	// Delete the namespace
	if err := k.Set.CoreV1().Namespaces().Delete(ctx, ns, metav1.DeleteOptions{}); err != nil {
		return errors.New("Error deleting namespace: " + err.Error())
	}
	f()
	// monitor.StreamRegistry.Cancel(ns)
	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
	}

	var targetDict map[string]interface{}
	err = json.Unmarshal(resp, &targetDict)
	if err != nil {
		return nil, err
	}

	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, peerId string) ([]byte, error) {
	exists, err := k.GetKubeconfigSecret(context, executionId, peerId)
	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
	}

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

	res, err := dynamicClientApply(k.Host, k.CA, k.Cert, k.Data, executionId, targetName, gvrTargets, context, target)
	if err != nil {
		return nil, errors.New("Error when trying to apply Target definition :" + err.Error())
	}

	return res, 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) {

	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,
		},
	}

	res, err := dynamicClientApply(k.Host, k.CA, k.Cert, k.Data, executionId, "source-"+executionId, gvrSources, context, source)
	if err != nil {
		return nil, errors.New("Error when trying to apply Source definition :" + err.Error())
	}

	return res, 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, peerId string) ([]byte, error) {
	config, err := base64.StdEncoding.DecodeString(kubeconfig)
	// config, err := base64.RawStdEncoding.DecodeString(kubeconfig)
	if err != nil {
		fmt.Println("Error while encoding kubeconfig")
		return nil, err
	}

	secretApplyConfig := apply.Secret("kube-secret-"+GetConcatenatedName(peerId, executionId),
		executionId).
		WithData(map[string][]byte{
			"config": config,
		},
		)
	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)
		return nil, err
	}

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

func (k *KubernetesService) GetKubeconfigSecret(context context.Context, executionId string, peerId string) ([]byte, error) {
	resp, err := k.Set.CoreV1().
		Secrets(executionId).
		Get(context, "kube-secret-"+GetConcatenatedName(peerId, 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)
		return nil, err
	}

	data, err := json.Marshal(resp)

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

	return data, nil
}

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

	return []byte{}, nil
}

func (k *KubernetesService) GetNamespace(context context.Context, executionID string) (*v1.Namespace, error) {
	resp, err := k.Set.CoreV1().Namespaces().Get(context, executionID, metav1.GetOptions{})
	if apierrors.IsNotFound(err) {
		return nil, nil
	}
	if err != nil {
		logger := logs.GetLogger()
		logger.Error().Msg("An error occured when trying to get namespace " + executionID + " : " + err.Error())
		return nil, err
	}

	return resp, 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 dynamicClientApply(host string, ca string, cert string, data string, executionId string, resourceName string, resourceDefinition schema.GroupVersionResource, ctx context.Context, object map[string]interface{}) ([]byte, error) {
	cli, err := NewDynamicClient(host, ca, cert, data)
	if err != nil {
		return nil, errors.New("Could not retrieve dynamic client when creating Admiralty Source : " + err.Error())
	}

	res, err := cli.Resource(resourceDefinition).
		Namespace(executionId).
		Apply(ctx,
			resourceName,
			&unstructured.Unstructured{Object: object},
			metav1.ApplyOptions{
				FieldManager: "kubectl-client-side-apply",
			},
		)
	if err != nil {
		fmt.Println("Error from k8s API when applying "+fmt.Sprintf("%v", object)+" 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

}

func (k *KubernetesService) CheckHealth() error {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	// Check API server connectivity
	_, err := k.Set.ServerVersion()
	if err != nil {
		return fmt.Errorf("API server unreachable: %v", err)
	}

	// Check nodes status
	nodes, err := k.Set.CoreV1().Nodes().List(ctx, metav1.ListOptions{})
	if err != nil {
		return fmt.Errorf("failed to list nodes: %v", err)
	}
	for _, node := range nodes.Items {
		for _, condition := range node.Status.Conditions {
			if condition.Type == "Ready" && condition.Status != "True" {
				return fmt.Errorf("node %s not ready", node.Name)
			}
		}
	}

	// Optional: Check if all pods in kube-system are running
	pods, err := k.Set.CoreV1().Pods("kube-system").List(ctx, metav1.ListOptions{})
	if err != nil {
		return fmt.Errorf("failed to list pods: %v", err)
	}
	for _, pod := range pods.Items {
		if pod.Status.Phase != "Running" && pod.Status.Phase != "Succeeded" {
			return fmt.Errorf("pod %s in namespace kube-system is %s", pod.Name, pod.Status.Phase)
		}
	}
	return 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, peerId string) (*v1.Node, error) {
	concatenatedName := GetConcatenatedName(peerId, executionID)

	res, err := k.Set.CoreV1().
		Nodes().
		List(
			context,
			metav1.ListOptions{},
		)
	if err != nil {
		fmt.Println("Error getting the list of nodes from k8s API")
		return nil, err
	}

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

	return nil, nil
}

func (k *KubernetesService) CreateSecret(context context.Context, minioId string, executionID string, access string, secret string) error {

	data := map[string][]byte{
		"access-key": []byte(access),
		"secret-key": []byte(secret),
	}

	s := v1.Secret{
		Type: v1.SecretTypeOpaque,
		Data: data,
		ObjectMeta: metav1.ObjectMeta{
			Name: minioId + "-secret-s3",
		},
	}

	_, err := k.Set.CoreV1().Secrets(executionID).Create(context, &s, metav1.CreateOptions{})
	if err != nil {
		logger := logs.GetLogger()
		logger.Error().Msg("An error happened when creating the secret holding minio credentials in namespace " + executionID + " : " + err.Error())
		return err
	}

	return nil
}

// DeleteSecretsByLabel deletes all Secrets in the given namespace matching labelSelector.
// Used by oc-datacenter to clean up ephemeral source-presigned Secrets after workflow completion.
func (k *KubernetesService) DeleteSecretsByLabel(ctx context.Context, namespace, labelSelector string) error {
	return k.Set.CoreV1().Secrets(namespace).DeleteCollection(
		ctx,
		metav1.DeleteOptions{},
		metav1.ListOptions{LabelSelector: labelSelector},
	)
}

// CreatePVC creates a static PersistentVolume + PersistentVolumeClaim in the given namespace.
// Static provisioning (no StorageClass) avoids the WaitForFirstConsumer deadlock
// with Admiralty virtual nodes — the PVC binds immediately.
func (k *KubernetesService) CreatePVC(ctx context.Context, name, namespace, storageSize string) error {
	storageClassName := ""
	pv := &v1.PersistentVolume{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
		Spec: v1.PersistentVolumeSpec{
			Capacity: v1.ResourceList{
				v1.ResourceStorage: resource.MustParse(storageSize),
			},
			AccessModes:                   []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce},
			StorageClassName:              storageClassName,
			PersistentVolumeReclaimPolicy: v1.PersistentVolumeReclaimDelete,
			PersistentVolumeSource: v1.PersistentVolumeSource{
				HostPath: &v1.HostPathVolumeSource{
					Path: "/var/lib/oc-storage/" + name,
					Type: func() *v1.HostPathType { t := v1.HostPathDirectoryOrCreate; return &t }(),
				},
			},
			ClaimRef: &v1.ObjectReference{
				Namespace: namespace,
				Name:      name,
			},
		},
	}
	_, err := k.Set.CoreV1().PersistentVolumes().Create(ctx, pv, metav1.CreateOptions{})
	if err != nil && !apierrors.IsAlreadyExists(err) {
		return fmt.Errorf("CreatePV %s: %w", name, err)
	}

	pvc := &v1.PersistentVolumeClaim{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
		},
		Spec: v1.PersistentVolumeClaimSpec{
			AccessModes:      []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce},
			StorageClassName: &storageClassName,
			VolumeName:       name,
			Resources: v1.VolumeResourceRequirements{
				Requests: v1.ResourceList{
					v1.ResourceStorage: resource.MustParse(storageSize),
				},
			},
		},
	}
	_, err = k.Set.CoreV1().PersistentVolumeClaims(namespace).Create(ctx, pvc, metav1.CreateOptions{})
	if err != nil && !apierrors.IsAlreadyExists(err) {
		return fmt.Errorf("CreatePVC %s/%s: %w", namespace, name, err)
	}
	return nil
}

// DeletePVC deletes a PersistentVolumeClaim and its associated PersistentVolume.
func (k *KubernetesService) DeletePVC(ctx context.Context, name, namespace string) error {
	err := k.Set.CoreV1().PersistentVolumeClaims(namespace).Delete(ctx, name, metav1.DeleteOptions{})
	if err != nil && !apierrors.IsNotFound(err) {
		return fmt.Errorf("DeletePVC %s/%s: %w", namespace, name, err)
	}
	err = k.Set.CoreV1().PersistentVolumes().Delete(ctx, name, metav1.DeleteOptions{})
	if err != nil && !apierrors.IsNotFound(err) {
		return fmt.Errorf("DeletePV %s: %w", name, err)
	}
	return nil
}

// ============== ADMIRALTY ==============
// Returns a concatenation of the peerId and namespace in order for
// kubernetes ressources to have a unique name, under 63 characters
// and yet identify which peer they are created for
func GetConcatenatedName(peerId string, namespace string) string {
	s := strings.Split(namespace, "-")[:2]
	n := s[0] + "-" + s[1]

	return peerId + "-" + n
}
