package network

import (
	"context"
	"fmt"
	"reflect"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	apiequality "k8s.io/apimachinery/pkg/api/equality"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"sigs.k8s.io/controller-runtime/pkg/client"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/shared-provider/pkg/types"
	"github.com/tidbcloud/shared-provider/pkg/utils/namespace"
)

func (r *Reconciler) syncTiDBOperator(ctx context.Context, net *cloudv1.Network) error {
	ns := namespace.GetOwnedNamespaceNameFromNetwork(net)

	cleaned, err := r.isResourceCleaned(ctx, net)
	if err != nil {
		return err
	}

	if cleaned {
		if err := r.tryToDeleteTiDBOperator(ctx, ns); err != nil {
			return err
		}

		return nil
	}

	if err := r.tryToCreateOrUpdateTiDBOperator(ctx, ns, net); err != nil {
		return err
	}

	return nil
}

// delete running pods to save resources.
func (r *Reconciler) tryToDeleteTiDBOperator(ctx context.Context, ns string) error {
	deploy := r.newTiDBOperatorManagerDeployment(ns)
	if deploy == nil {
		return fmt.Errorf("can't get tidb operator deploy template")
	}
	if err := r.Cache.Get(ctx, client.ObjectKeyFromObject(deploy), deploy); err != nil {
		// TiDBOperator deployment event never trigger resync.
		// If deploy exists and cache is not synced, deployment maybe left
		// until next sync triggered by other events.
		// It's ok because prosibility is very small.
		if apierrors.IsNotFound(err) {
			return nil
		}

		return err
	}
	if err := r.Client.Delete(ctx, deploy); err != nil {
		if !apierrors.IsNotFound(err) {
			return fmt.Errorf("can't delete deployment %v: %w", client.ObjectKeyFromObject(deploy), err)
		}
	}

	return nil
}

func (r *Reconciler) tryToCreateOrUpdateTiDBOperator(ctx context.Context, ns string, net *cloudv1.Network) error {
	provider := net.Spec.Provider

	sa := r.newTiDBOperatorManagerServiceAccount(ns)
	if err := r.createOrUpdateServiceAccount(ctx, sa); err != nil {
		return err
	}

	role := r.newTiDBOperatorManagerRole(ns)
	if err := r.createOrUpdateRole(ctx, role); err != nil {
		return err
	}

	rb := r.newTiDBOperatorManagerRoleBinding(ns)
	if err := r.createOrUpdateRoleBinding(ctx, rb); err != nil {
		return err
	}

	deploy := r.newTiDBOperatorManagerDeployment(ns)
	if err := r.createOrUpdateDeployment(ctx, deploy); err != nil {
		return err
	}

	backupSa := r.newBackupManagerServiceAccount(ns)
	switch provider {
	case types.ProviderTypeAWSSharedTier:
		backupRoleArn := getAWSIamIdentity(r.AWSAccountID,
			getAWSBackupRoleName(net.Spec.Provider, r.KubeClusterName, net.Namespace))
		backupSa.Annotations[types.AnnoKeyEKSRoleARN] = backupRoleArn
	case types.ProviderTypeGCPSharedTier:
		// TODO: GCP support
	default:
		return fmt.Errorf("unknown provider %s", provider)
	}
	if err := r.createOrUpdateServiceAccount(ctx, backupSa); err != nil {
		return err
	}

	backupRole := r.newBackupManagerRole(ns)
	if err := r.createOrUpdateRole(ctx, backupRole); err != nil {
		return err
	}

	backupRoleBinding := r.newBackupManagerRoleBinding(ns)
	if err := r.createOrUpdateRoleBinding(ctx, backupRoleBinding); err != nil {
		return err
	}

	return nil
}

func (r *Reconciler) createOrUpdateServiceAccount(ctx context.Context, expected *corev1.ServiceAccount) error {
	if expected == nil {
		return fmt.Errorf("can't get expected service account template")
	}
	current := &corev1.ServiceAccount{}
	key := client.ObjectKeyFromObject(expected)
	if err := r.Cache.Get(ctx, key, current); err != nil {
		if !apierrors.IsNotFound(err) {
			return fmt.Errorf("can't get service account %s: %w", key, err)
		}
		if err := r.Client.Create(ctx, expected); err != nil {
			return fmt.Errorf("can't create service account %s: %w", key, err)
		}

		return nil
	}

	// TODO: check diff of service account

	return nil
}

func (r *Reconciler) createOrUpdateRole(ctx context.Context, expected *rbacv1.Role) error {
	if expected == nil {
		return fmt.Errorf("can't get expected role template")
	}
	current := &rbacv1.Role{}
	key := client.ObjectKeyFromObject(expected)
	if err := r.Cache.Get(ctx, key, current); err != nil {
		if !apierrors.IsNotFound(err) {
			return fmt.Errorf("can't get role %s: %w", key, err)
		}
		if err := r.Client.Create(ctx, expected); err != nil {
			return fmt.Errorf("can't create role %s: %w", key, err)
		}

		return nil
	}

	if apiequality.Semantic.DeepEqual(&current.Rules, &expected.Rules) {
		return nil
	}

	expected.ResourceVersion = current.ResourceVersion

	if err := r.Client.Update(ctx, expected); err != nil {
		return fmt.Errorf("can't update role %s: %w", key, err)
	}

	return nil
}

func (r *Reconciler) createOrUpdateRoleBinding(ctx context.Context, expected *rbacv1.RoleBinding) error {
	if expected == nil {
		return fmt.Errorf("can't get expected rolebinding template")
	}
	current := &rbacv1.RoleBinding{}
	key := client.ObjectKeyFromObject(expected)
	if err := r.Cache.Get(ctx, key, current); err != nil {
		if !apierrors.IsNotFound(err) {
			return fmt.Errorf("can't get rolebinding %s: %w", key, err)
		}
		if err := r.Client.Create(ctx, expected); err != nil {
			return fmt.Errorf("can't create rolebinding %s: %w", key, err)
		}

		return nil
	}

	if apiequality.Semantic.DeepEqual(&current.Subjects, &expected.Subjects) &&
		apiequality.Semantic.DeepEqual(&current.RoleRef, &expected.RoleRef) {
		return nil
	}

	expected.ResourceVersion = current.ResourceVersion

	if err := r.Client.Update(ctx, expected); err != nil {
		return fmt.Errorf("can't update rolebinding %s: %w", key, err)
	}

	return nil
}

func (r *Reconciler) createOrUpdateDeployment(ctx context.Context, expected *appsv1.Deployment) error {
	if expected == nil {
		return fmt.Errorf("can't get expected deploy template")
	}
	current := &appsv1.Deployment{}
	key := client.ObjectKeyFromObject(expected)
	if err := r.Cache.Get(ctx, key, current); err != nil {
		if !apierrors.IsNotFound(err) {
			return fmt.Errorf("can't get deployment %s: %w", key, err)
		}
		if err := r.Client.Create(ctx, expected); err != nil {
			return fmt.Errorf("can't create deployment %s: %w", key, err)
		}

		return nil
	}

	// TODO: check whether tidb operator deployment can be updated

	expected.ResourceVersion = current.ResourceVersion

	if err := r.Client.Update(ctx, expected); err != nil {
		return fmt.Errorf("can't update deployemnt %s: %w", key, err)
	}

	return nil
}

const (
	tidbOperatorKey      = "tidb-operator"
	controllerManagerKey = "controller-manager"
	backupManagerKey     = "backup-manager"

	serviceAccountKey = "sa"
	roleKey           = "role"
	roleBindingKey    = "rolebinding"
	deploymentKey     = "deploy"
)

// component: controller-manager/backup-manager
// resource: sa/role/rolebinding/deploy
func getTemplateKey(component string, resource string) string {
	return tidbOperatorKey + "." + component + "." + resource
}

func (r *Reconciler) initTiDBOperatorTemplate() error {
	resources := []struct {
		path string
		obj  client.Object
	}{
		{
			path: getTemplateKey(controllerManagerKey, serviceAccountKey),
			obj:  &corev1.ServiceAccount{},
		},
		{
			path: getTemplateKey(controllerManagerKey, roleKey),
			obj:  &rbacv1.Role{},
		},
		{
			path: getTemplateKey(controllerManagerKey, roleBindingKey),
			obj:  &rbacv1.RoleBinding{},
		},
		{
			path: getTemplateKey(controllerManagerKey, deploymentKey),
			obj:  &appsv1.Deployment{},
		},
		{
			path: getTemplateKey(backupManagerKey, serviceAccountKey),
			obj:  &corev1.ServiceAccount{},
		},
		{
			path: getTemplateKey(backupManagerKey, roleKey),
			obj:  &rbacv1.Role{},
		},
		{
			path: getTemplateKey(backupManagerKey, roleBindingKey),
			obj:  &rbacv1.RoleBinding{},
		},
	}

	cm := &corev1.ConfigMap{}
	if err := r.Client.Get(context.TODO(), client.ObjectKey{
		Namespace: r.AddonConfigMapNamespace,
		Name:      r.AddonConfigMapName,
	}, cm); err != nil {
		return fmt.Errorf("can't get configmap %s/%s: %w", r.AddonConfigMapNamespace, r.AddonConfigMapName, err)
	}

	for _, rsc := range resources {
		if err := r.tmpl.Add(rsc.path, rsc.obj); err != nil {
			return fmt.Errorf("can't add template with key %s: %w", rsc.path, err)
		}
	}

	if err := r.tmpl.Flush(cm.Data); err != nil {
		return fmt.Errorf("can't flush templates: %w", err)
	}

	for _, rsc := range resources {
		obj, ok := r.tmpl.Get(rsc.path)
		if !ok {
			return fmt.Errorf("can't get template %s", rsc.path)
		}

		r.tracker.Add(&TypeAndKey{
			Type: reflect.TypeOf(obj),
			Name: obj.GetName(),
		})
	}

	return nil
}

func (r *Reconciler) newTiDBOperatorManagerServiceAccount(ns string) *corev1.ServiceAccount {
	key := getTemplateKey(controllerManagerKey, serviceAccountKey)
	obj, ok := r.tmpl.Get(key)
	if !ok {
		return nil
	}
	// nolint: forcetypeassert // obj will always be serviceaccount
	sa := obj.(*corev1.ServiceAccount)

	sa.Namespace = ns

	return sa
}

func (r *Reconciler) newTiDBOperatorManagerRole(ns string) *rbacv1.Role {
	key := getTemplateKey(controllerManagerKey, roleKey)
	obj, ok := r.tmpl.Get(key)
	if !ok {
		return nil
	}
	// nolint: forcetypeassert // obj will always be role
	role := obj.(*rbacv1.Role)

	role.Namespace = ns

	return role
}

func (r *Reconciler) newTiDBOperatorManagerRoleBinding(ns string) *rbacv1.RoleBinding {
	key := getTemplateKey(controllerManagerKey, roleBindingKey)
	obj, ok := r.tmpl.Get(key)
	if !ok {
		return nil
	}
	// nolint: forcetypeassert // obj will always be rolebinding
	rb := obj.(*rbacv1.RoleBinding)

	rb.Namespace = ns

	return rb
}

func (r *Reconciler) newTiDBOperatorManagerDeployment(ns string) *appsv1.Deployment {
	key := getTemplateKey(controllerManagerKey, deploymentKey)
	obj, ok := r.tmpl.Get(key)
	if !ok {
		return nil
	}
	// nolint: forcetypeassert // obj will always be deployment
	deploy := obj.(*appsv1.Deployment)

	deploy.Namespace = ns

	return deploy
}

func (r *Reconciler) newBackupManagerServiceAccount(ns string) *corev1.ServiceAccount {
	key := getTemplateKey(backupManagerKey, serviceAccountKey)
	obj, ok := r.tmpl.Get(key)
	if !ok {
		return nil
	}
	// nolint: forcetypeassert // obj will always be serviceaccount
	sa := obj.(*corev1.ServiceAccount)

	if sa.Annotations == nil {
		sa.Annotations = map[string]string{}
	}

	sa.Namespace = ns

	return sa
}

func (r *Reconciler) newBackupManagerRole(ns string) *rbacv1.Role {
	key := getTemplateKey(backupManagerKey, roleKey)
	obj, ok := r.tmpl.Get(key)
	if !ok {
		return nil
	}
	// nolint: forcetypeassert // obj will always be role
	role := obj.(*rbacv1.Role)

	role.Namespace = ns

	return role
}

func (r *Reconciler) newBackupManagerRoleBinding(ns string) *rbacv1.RoleBinding {
	key := getTemplateKey(backupManagerKey, roleBindingKey)
	obj, ok := r.tmpl.Get(key)
	if !ok {
		return nil
	}
	// nolint: forcetypeassert // obj will always be rolebinding
	rb := obj.(*rbacv1.RoleBinding)

	rb.Namespace = ns

	return rb
}
