package eks

import (
	"bytes"
	"context"
	"crypto/md5"
	"fmt"
	"html/template"
	"time"

	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"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"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/util"
	iamv1beta1 "github.com/upbound/provider-aws/apis/iam/v1beta1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
	roleRefLabelKey = "role-reference"
	trustPolicy     = `{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Principal": {
				"Service": "ec2.amazonaws.com"
			},
			"Action": "sts:AssumeRole"
		}
	]
}`
)

// NodeRole task will create a role for the node and attach the policy to the role.
type NodeRole struct {
	*task.BaseTask
}

func NewNodeRole(ctx *task.Context) deploy.Task {
	return &NodeRole{BaseTask: &task.BaseTask{Context: ctx}}
}

func (t *NodeRole) Name() string {
	return "NodeRole"
}

func (t *NodeRole) Create(ctx context.Context) deploy.Result {
	t.BeginAt = util.Pointer(time.Now())
	var nodeRoleARN string
	var err error
	if roleArn := t.getStaticRoleArn(); roleArn != "" {
		nodeRoleARN = roleArn
	} else {
		nodeRoleARN, err = t.ensureNodeRole(ctx)
		if err != nil {
			return deploy.Panic(err)
		}
	}

	if t.APPBox.Status.IAM.NodeRoleARN != nodeRoleARN {
		t.APPBox.Status.IAM.NodeRoleARN = nodeRoleARN
		return deploy.Panic(nil).WithRequeuAfter(0)
	}

	return deploy.Done()
}

func (t *NodeRole) ensureNodeRole(ctx context.Context) (string, error) {
	roleName := controllers.UniqueIdentifier(t.APPBox.ObjectMeta, namer.WithPrefix(t.Config.ResourceNamePrefix))
	tags := controllers.ConvertToCloudTags(t.APPBox.Labels, cloudv1.ProviderTypeEKS)
	tags = util.MergeMaps(tags, t.Config.AdditionalCloudTags)
	tags = util.BuildAWSTagWithName(tags, roleName)

	// Create role
	role := &iamv1beta1.Role{
		ObjectMeta: metav1.ObjectMeta{
			Name:   roleName,
			Labels: t.APPBox.Labels,
		},
		Spec: iamv1beta1.RoleSpec{
			ForProvider: iamv1beta1.RoleParameters{
				Description:      ptr.To(fmt.Sprintf("Node role for %s", t.APPBox.Name)),
				AssumeRolePolicy: ptr.To(trustPolicy),
				Tags:             util.ToMapPointer(tags),
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
		},
	}
	if err := k8s.Apply(ctx, t.Local, role); err != nil {
		return "", err
	}

	// Attach managed policies
	for _, policy := range t.Config.APPBoxConfig.AdminNodeRoleConfig.ManagedPolicies {
		labels := util.MergeMaps(t.APPBox.Labels, map[string]string{roleRefLabelKey: roleName})
		attachment := &iamv1beta1.RolePolicyAttachment{
			ObjectMeta: metav1.ObjectMeta{
				Name:   policyAttachmentName(roleName, policy),
				Labels: labels,
			},
			Spec: iamv1beta1.RolePolicyAttachmentSpec{
				ForProvider: iamv1beta1.RolePolicyAttachmentParameters{
					RoleRef:   &xpv1.Reference{Name: roleName},
					PolicyArn: &policy,
				},
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
					ManagementPolicies:      controllers.GetResourceManagementPolicies(),
				},
			},
		}
		if err := k8s.Apply(ctx, t.Local, attachment); err != nil {
			return "", err
		}
	}

	// Create inline policies
	type inlinePolicyConfig struct {
		name string
		file string
		data map[string]string
	}
	// the total inline policy has a limit of 10240 character
	inlinePolicyConfigs := []inlinePolicyConfig{
		{
			name: "load-balancer-controller",
			file: t.Config.APPBoxConfig.AdminNodeRoleConfig.LoadBalancerControllerPolicyTemplateFile,
			data: nil,
		},
		{
			name: "cluster-autoscaler",
			file: t.Config.APPBoxConfig.AdminNodeRoleConfig.ClusterAutoscalerPolicyTemplateFile,
			data: map[string]string{"ClusterName": t.APPBox.Status.KubernetesClusterName},
		},
		{
			name: "cert-manager",
			file: t.Config.APPBoxConfig.AdminNodeRoleConfig.CertManagerPolicyTemplateFile,
			data: nil,
		},
		{
			name: "tidb-cluster",
			file: t.Config.APPBoxConfig.AdminNodeRoleConfig.TidbClusterPolicyTemplateFile,
			data: nil,
		},
		{
			name: "o11y-agent",
			file: t.Config.APPBoxConfig.AdminNodeRoleConfig.O11yAgentPolicyTemplateFile,
			data: nil,
		},
	}
	if t.Config.APPBoxConfig.AdminNodeRoleConfig.ExternalDNSPolicyTemplateFile != "" {
		inlinePolicyConfigs = append(inlinePolicyConfigs, inlinePolicyConfig{
			name: "external-dns",
			file: t.Config.APPBoxConfig.AdminNodeRoleConfig.ExternalDNSPolicyTemplateFile,
			data: map[string]string{"ZoneID": t.Config.DNSProviderConfig.AwsRoute53.ZoneId},
		})
	}

	for _, c := range inlinePolicyConfigs {
		document, err := getInlinePolicyDocument(c.file, c.data)
		if err != nil {
			return "", err
		}
		prefix := namer.WithPrefix(t.Config.ResourceNamePrefix + "-" + c.name)
		policyName := controllers.UniqueIdentifier(t.APPBox.ObjectMeta, prefix)
		labels := util.MergeMaps(t.APPBox.Labels, map[string]string{roleRefLabelKey: roleName})
		// create inline rolePolicy
		rolePolicy := &iamv1beta1.RolePolicy{
			ObjectMeta: metav1.ObjectMeta{
				Name:   policyName,
				Labels: labels,
			},
			Spec: iamv1beta1.RolePolicySpec{
				ForProvider: iamv1beta1.RolePolicyParameters{
					Policy:  ptr.To(document),
					RoleRef: &xpv1.Reference{Name: roleName},
				},
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
					ManagementPolicies:      controllers.GetResourceManagementPolicies(),
				},
			},
		}
		if err := k8s.Apply(ctx, t.Local, rolePolicy); err != nil {
			return "", err
		}
	}

	if role.Status.AtProvider.Arn == nil {
		return "", fmt.Errorf("node role is not ready")
	}
	nodeRoleARN := *role.Status.AtProvider.Arn

	// Create InstanceProfile
	instanceProfile := &iamv1beta1.InstanceProfile{
		ObjectMeta: metav1.ObjectMeta{
			Name:   roleName,
			Labels: t.APPBox.Labels,
		},
		Spec: iamv1beta1.InstanceProfileSpec{
			ForProvider: iamv1beta1.InstanceProfileParameters{
				Role: ptr.To(roleName),
				Tags: util.ToMapPointer(tags),
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
		},
	}
	if err := k8s.Apply(ctx, t.Local, instanceProfile); err != nil {
		return "", err
	}
	return nodeRoleARN, nil
}

func (t *NodeRole) PostCreate(ctx context.Context) deploy.Result {
	// do nothing, since we need to wait for the role to be ready in Create()
	return deploy.Done()
}

// Delete will delete all role policy attachments and the role itself.
// Must delete the role attachments before deleting the role.
func (t *NodeRole) Delete(ctx context.Context) deploy.Result {
	if t.getStaticRoleArn() != "" {
		// use static node role, no resources need to be deleted
		return deploy.Done()
	}

	name := controllers.UniqueIdentifier(t.APPBox.ObjectMeta, namer.WithPrefix(t.Config.ResourceNamePrefix))
	matchLabels := client.MatchingLabels{roleRefLabelKey: name}

	// Delete InstanceProfile
	instanceProfile := &iamv1beta1.InstanceProfile{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(instanceProfile), instanceProfile); client.IgnoreNotFound(err) != nil {
		return deploy.Error(err)
	}
	instanceProfile.Spec.ResourceSpec.ManagementPolicies = xpv1.ManagementPolicies{xpv1.ManagementActionAll}
	if err := k8s.PatchObject(ctx, t.Local, instanceProfile, controllers.ManagementPoliciesPatch); client.IgnoreNotFound(err) != nil {
		return deploy.Error(err)
	}
	if err := client.IgnoreNotFound(t.Local.Delete(ctx, instanceProfile)); err != nil {
		return deploy.Panic(err)
	}

	// Delete inline policies
	inlinePolicyList := &iamv1beta1.RolePolicyList{}
	if err := t.Local.List(ctx, inlinePolicyList, matchLabels); err != nil {
		return deploy.Error(err)
	}
	for _, policy := range inlinePolicyList.Items {
		policy.Spec.ResourceSpec.ManagementPolicies = xpv1.ManagementPolicies{xpv1.ManagementActionAll}
		if err := k8s.PatchObject(ctx, t.Local, &policy, controllers.ManagementPoliciesPatch); client.IgnoreNotFound(err) != nil {
			return deploy.Error(err)
		}
	}
	if err := client.IgnoreNotFound(t.Local.DeleteAllOf(ctx, &iamv1beta1.RolePolicy{}, matchLabels)); err != nil {
		return deploy.Panic(err)
	}

	// Detach managed policies
	attachmentList := &iamv1beta1.RolePolicyAttachmentList{}
	if err := t.Local.List(ctx, attachmentList, matchLabels); err != nil {
		return deploy.Error(err)
	}
	for _, attachment := range attachmentList.Items {
		attachment.Spec.ResourceSpec.ManagementPolicies = xpv1.ManagementPolicies{xpv1.ManagementActionAll}
		if err := k8s.PatchObject(ctx, t.Local, &attachment, controllers.ManagementPoliciesPatch); client.IgnoreNotFound(err) != nil {
			return deploy.Error(err)
		}
	}
	if err := client.IgnoreNotFound(t.Local.DeleteAllOf(ctx, &iamv1beta1.RolePolicyAttachment{}, matchLabels)); err != nil {
		return deploy.Panic(err)
	}

	// Delete role
	role := &iamv1beta1.Role{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(role), role); client.IgnoreNotFound(err) != nil {
		return deploy.Error(err)
	}
	role.Spec.ResourceSpec.ManagementPolicies = xpv1.ManagementPolicies{xpv1.ManagementActionAll}
	if err := k8s.PatchObject(ctx, t.Local, role, controllers.ManagementPoliciesPatch); client.IgnoreNotFound(err) != nil {
		return deploy.Error(err)
	}
	if err := client.IgnoreNotFound(t.Local.Delete(ctx, role)); err != nil {
		return deploy.Panic(err)
	}

	return deploy.Done()
}

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

func getInlinePolicyDocument(policyTemplateFile string, data map[string]string) (string, error) {
	tmpl, err := template.ParseFiles(policyTemplateFile)
	if err != nil {
		return "", fmt.Errorf("failed to parse policy template file %s: %w", policyTemplateFile, err)
	}

	var buf bytes.Buffer
	if err := tmpl.Execute(&buf, data); err != nil {
		return "", fmt.Errorf("failed to execute policy template: %w", err)
	}

	return buf.String(), nil
}

func policyAttachmentName(name, policyARN string) string {
	hash := fmt.Sprintf("%x", md5.Sum([]byte(policyARN)))[:8]
	return fmt.Sprintf("%s-%s", name, hash)
}

func (t *NodeRole) getStaticRoleArn() string {
	if t.Config.BYOC.NodeRoleName != "" {
		return "arn:aws:iam::" + t.AccountID() + ":role/" + t.Config.BYOC.NodeRoleName
	}
	return ""
}
