package ack

import (
	"context"
	"fmt"
	"strings"

	cs "github.com/alibabacloud-go/cs-20151215/v5/client"
	ackv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/ack/v1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/infra-provider/regional-server/pkg/cloud/aliyun"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/appbox/task"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/deploy"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/k8s"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/namer"
	authv1alpha1 "github.com/tidbcloud/infra-provider/regional-server/pkg/ramauthenticator/apis/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/util"
)

const (
	// AnnACKNonBootstrappedKey indicates whether the ack enables non-bootstrapped ram roles
	AnnACKNonBootstrappedRAMEnabledKey = "tidbcloud.com/ack-non-bootstrapped-ram-enabled"
)

type permissions struct {
	*task.BaseTask
	aliyunCli aliyun.Client
}

func NewPermissions(ctx *task.Context, aliyunCli aliyun.Client) deploy.Task {
	return &permissions{
		BaseTask:  &task.BaseTask{Context: ctx},
		aliyunCli: aliyunCli,
	}
}

func (t *permissions) Name() string {
	return "Permissions"
}

// crossplane-provider only supports ak/sk authentication at the moment
// We have to grant dataplane-manager role as admin
// then this role can use static kubeconfig to create RAMIdentityMapping
// then the mapping roles can use dynamic kubeconfig
func (t *permissions) Create(ctx context.Context) deploy.Result {
	clusterId := t.APPBox.Status.KubernetesClusterID
	labels := util.MergeMaps(t.Namer.Labels(), t.APPBox.Labels)
	ackName := t.K8sName()
	labels = util.MergeMaps(labels, map[string]string{ackRefLabelKey: ackName})
	dataplaneAccountID := t.VPC.Spec.AccountRef.AccountID
	// First grant the IAM roles kubernetes admin permissions
	for _, entry := range t.Config.APPBoxConfig.ClusterAdmins {
		if !strings.Contains(entry.IAMRoleARN, dataplaneAccountID) {
			// ACK does not support cross-account role
			t.Logger.Info("skip cross-account role", "roleARN", entry.IAMRoleARN)
			continue
		}
		crName := t.getPermissionName(entry.ID)
		perm := &ackv1alpha1.KubernetesPermissions{
			ObjectMeta: metav1.ObjectMeta{
				Name:   crName,
				Labels: labels,
			},
			Spec: ackv1alpha1.KubernetesPermissionsSpec{
				ForProvider: ackv1alpha1.KubernetesPermissionsParameters{
					Permissions: []ackv1alpha1.PermissionsParameters{
						{
							Cluster:   ptr.To(clusterId),
							IsRAMRole: ptr.To(true),
							RoleName:  ptr.To("admin"),
							RoleType:  ptr.To("cluster"),
						},
					},
					Region: ptr.To(t.APPBox.Spec.Region),
					UID:    ptr.To(entry.ID),
				},
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
					ManagementPolicies:      controllers.GetResourceManagementPolicies(),
				},
			},
		}
		if err := k8s.Apply(ctx, t.Local, perm); err != nil {
			return deploy.Panic(err)
		}
	}

	// Then create IAMIdentityMapping in dataplane k8s
	var remoteCli client.Client
	var err error
	if ramEnabled, ok := t.APPBox.Annotations[AnnACKNonBootstrappedRAMEnabledKey]; ok && ramEnabled == "true" {
		remoteCli, err = t.Remote.GetClient(ctx)
	} else {
		remoteCli, err = t.getRemoteCliFromStaticKubeconfig()
	}
	if err != nil {
		return deploy.Panic(err)
	}
	for _, entry := range t.Config.APPBoxConfig.ClusterAdmins {
		if entry.InDataAccount && !strings.Contains(entry.IAMRoleARN, dataplaneAccountID) {
			// skip cross-account dataplane role
			continue
		}
		splits := strings.Split(entry.IAMRoleARN, "/")
		username := splits[len(splits)-1] // use role name as username
		crName := t.getCRName(username)
		mapping := &authv1alpha1.RAMIdentityMapping{
			ObjectMeta: metav1.ObjectMeta{
				Name:   crName,
				Labels: labels,
			},
			Spec: authv1alpha1.RAMIdentityMappingSpec{
				ARN:      entry.IAMRoleARN,
				Username: username,
				Groups:   []string{"system:masters"},
			},
		}
		if err := k8s.Apply(ctx, remoteCli, mapping); err != nil {
			return deploy.Panic(err)
		}
	}
	return deploy.Done()
}

func (t *permissions) PostCreate(ctx context.Context) deploy.Result {
	t.APPBox.Annotations[AnnACKNonBootstrappedRAMEnabledKey] = "true"
	return deploy.Done()
}

// we don't need to delete the RAMIdentityMapping objects in dataplane k8s
func (t *permissions) Delete(ctx context.Context) deploy.Result {
	// delete KubernetesPermission
	permissions := new(ackv1alpha1.KubernetesPermissionsList)
	selector := client.MatchingLabels{namer.AppBoxSelectorKey: t.APPBox.Name}
	if err := t.Local.List(ctx, permissions, selector); err != nil {
		return deploy.Error(err)
	}
	for _, perm := range permissions.Items {
		if err := controllers.DeleteUpboundResource(ctx, t.Local, &perm); err != nil {
			return deploy.Error(err)
		}
	}
	return deploy.Done()
}

func (t *permissions) PostDelete(ctx context.Context) deploy.Result {
	return deploy.Done()
}

func (t *permissions) getRemoteCliFromStaticKubeconfig() (client.Client, error) {
	assumedRole := aliyun.AssumedRole{
		RoleARN: fmt.Sprintf("acs:ram::%s:role/%s", t.VPC.Spec.AccountRef.AccountID, t.Config.DataPlaneRoleName),
	}
	req := cs.DescribeClusterUserKubeconfigRequest{
		TemporaryDurationMinutes: ptr.To(int64(15)),
	}
	csCli, err := t.aliyunCli.CS(&assumedRole, aliyun.WithRegion(t.APPBox.Spec.Region))
	if err != nil {
		return nil, err
	}
	clusterId := t.APPBox.Status.KubernetesClusterID
	resp, err := csCli.DescribeClusterUserKubeconfig(&clusterId, &req)
	if err != nil {
		return nil, err
	}
	kubeconfig := resp.Body.Config
	// build kubeclient from kubeconfig
	kubecfg, err := clientcmd.Load([]byte(*kubeconfig))
	if err != nil {
		return nil, err
	}
	restcfg, err := clientcmd.NewDefaultClientConfig(*kubecfg, &clientcmd.ConfigOverrides{}).ClientConfig()
	if err != nil {
		return nil, err
	}
	// build kube client from restcfg
	scheme := runtime.NewScheme()
	utilruntime.Must(authv1alpha1.AddToScheme(scheme))
	return client.New(restcfg, client.Options{Scheme: scheme})
}

func (t *permissions) getCRName(prefix string) string {
	namespacedName := t.APPBox.Namespace + "-" + t.APPBox.Name
	return namer.NewName(namespacedName, prefix, "", -1)
}

// The name must start with a letter or underscore and may contain only letters
// We can't specify the resource name via spec, resource name is inherited from CR name
// For AppBox creation triggered by csng, its name is unique
// A RAM role uid is only added once in an ACK cluster, so the generated name is also unique
func (t *permissions) getPermissionName(uid string) string {
	return "p" + t.APPBox.Name + uid
}
