package eks

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

	iamv1alpha1 "github.com/crossplane-contrib/provider-aws/apis/iam/v1alpha1"
	iamv1beta1 "github.com/crossplane-contrib/provider-aws/apis/iam/v1beta1"
	apiv1beta1 "github.com/crossplane-contrib/provider-aws/apis/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	pkgconfig "github.com/tidbcloud/infra-provider/pkg/config"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/util"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/config"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/appbox/task"
)

const (
	nodeRolePolicyName             = "full-manager-node-role-permission"
	awsLBControllerPolicyName      = "aws-load-balancer-controller-permission"
	RoleRefLabelKey                = "role-reference"
	AmazonEKSVPCResourceController = "arn:aws:iam::aws:policy/AmazonEKSVPCResourceController"
)

// NodeRole task will only be executed on the full manager network controller.
// It 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 {
	if t.APPBox.Labels[v1alpha1.IsLegacyKey] == "true" {
		if pkgconfig.FeatureEnabled(t.Config.FeatureGates, pkgconfig.UpdateLegacyNodeRolePolicy, t.APPBox.GetLabels()[v1alpha1.TagTenantKey], t.APPBox.GetLabels()[v1alpha1.TagProjectKey]) {
			return t.updateLegacyNodeRolePolicy(ctx, t.Config.APPBoxConfig.AdminNodeRoleConfig)
		}
		return deploy.Done()
	}
	nodeRoleARN, err := prepareNodeRole(ctx, t.Local, t.APPBox, t.ProviderConfigName(), t.Config.APPBoxConfig.AdminNodeRoleConfig)
	if err != nil {
		return deploy.Panic(err)
	}

	if t.APPBox.Status.IAM.NodeRoleARN != nodeRoleARN {
		old := t.APPBox.Status.IAM.NodeRoleARN
		t.APPBox.Status.IAM.NodeRoleARN = nodeRoleARN
		return deploy.Panic(fmt.Errorf("node role changed, old:%s, new %s",
			old, nodeRoleARN)).WithRequeuAfter(0)
	}

	return deploy.Done()
}

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 {
	name := namer.UniqueIdentifier(t.APPBox.ObjectMeta)
	matchLabels := client.MatchingLabels{RoleRefLabelKey: name}
	if t.APPBox.Labels[v1alpha1.IsLegacyKey] == "true" {
		iamroleName := t.APPBox.Annotations[v1alpha1.AnnotationKeyOwnCrossplaneNameIAMRole]
		if iamroleName == "" {
			return deploy.Panic(fmt.Errorf("appbox is legacy but own-crossplane-name-eks is empty"))
		}
		matchLabels[RoleRefLabelKey] = iamroleName
	}
	if err := client.IgnoreNotFound(t.Local.DeleteAllOf(context.Background(), &iamv1beta1.RolePolicyAttachment{}, matchLabels)); err != nil {
		return deploy.Panic(err)
	}

	if err := client.IgnoreNotFound(t.Local.DeleteAllOf(context.Background(), &iamv1beta1.Policy{}, matchLabels)); err != nil {
		return deploy.Panic(err)
	}

	role := &iamv1beta1.Role{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}
	if t.APPBox.Labels[v1alpha1.IsLegacyKey] == "true" {
		iamroleName := t.APPBox.Annotations[v1alpha1.AnnotationKeyOwnCrossplaneNameIAMRole]
		if iamroleName == "" {
			return deploy.Panic(fmt.Errorf("appbox is legacy but own-crossplane-name-eks is empty"))
		}
		role.Name = iamroleName
	}
	if err := client.IgnoreNotFound(t.Local.Delete(context.Background(), role)); err != nil {
		return deploy.Panic(err)
	}

	instanceProfile := &iamv1alpha1.InstanceProfile{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}
	if t.APPBox.Labels[v1alpha1.IsLegacyKey] == "true" {
		instanceProfileName := t.APPBox.Annotations[v1alpha1.AnnotationKeyOwnCrossplaneNameInstanceProfile]
		if instanceProfileName == "" {
			return deploy.Panic(fmt.Errorf("appbox is legacy but own-crossplane-name-eks is empty"))
		}
		instanceProfile.Name = instanceProfileName
	}
	if err := client.IgnoreNotFound(t.Local.Delete(context.Background(), instanceProfile)); err != nil {
		return deploy.Panic(err)
	}

	return deploy.Done()
}

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

// updateLegacyNodeRolePolicy will update the aws lb controller policy for the legacy node role.
func (t *NodeRole) updateLegacyNodeRolePolicy(ctx context.Context, conf config.AdminNodeRoleConfig) deploy.Result {
	nodeRoleName := t.APPBox.Annotations[v1alpha1.AnnotationKeyOwnCrossplaneNameIAMRole]
	if nodeRoleName == "" {
		return deploy.Panic(fmt.Errorf("appbox is legacy but own-crossplane-name-eks is empty"))
	}

	var policy iamv1beta1.PolicyList
	if err := t.Local.List(ctx, &policy, client.MatchingLabels{RoleRefLabelKey: nodeRoleName}); err != nil {
		return deploy.Error(fmt.Errorf("failed to list iam policies: %w", err))
	}

	for i := range policy.Items {
		p := &policy.Items[i]
		// Only update the aws lb controller policy for now.
		if p.Spec.ForProvider.Name == policyName(nodeRoleName, awsLBControllerPolicyName) {
			lbPolicyDocument, err := util.GetAWSPolicyFromFile(conf.AWSLBControllerPolicyFile)
			if err != nil {
				return deploy.Error(fmt.Errorf("failed to get aws lb controller policy document: %w", err))
			}
			res, err := controllerutil.CreateOrUpdate(ctx, t.Local, p, func() error {
				p.Spec.ForProvider.Document = *ptr.To(lbPolicyDocument)
				return nil
			})
			if err != nil {
				return deploy.Error(fmt.Errorf("failed to update aws lb controller policy: %w", err))
			}
			t.Logger.Info("updated aws lb controller policy", "policy", p.Name, "result", res)
		}
	}

	return deploy.Done()
}

func prepareNodeRole(ctx context.Context, cli client.Client, appBox *v1alpha1.APPBox, providerConfigName string, conf config.AdminNodeRoleConfig) (string, error) {
	config := &apiv1beta1.ProviderConfig{
		ObjectMeta: metav1.ObjectMeta{
			Name: providerConfigName,
		},
	}
	err := cli.Get(ctx, client.ObjectKey{Name: config.Name}, config)
	if err != nil {
		return "", err
	}

	if config.Spec.AssumeRoleARN == nil {
		return "", fmt.Errorf("assume role arn is not set")
	}

	// create node role per network
	role, roleReady, err := createNodeRole(ctx, cli, appBox, providerConfigName, conf)
	if err != nil {
		return "", err
	}

	// create node role policy per network
	policyARN, policyReady, err := createNodeRolePolicy(ctx, cli, appBox, providerConfigName, conf)
	if err != nil {
		return "", err
	}
	awsLBCPolicyARN, awsLBCPolicyReady, err := createPolicyForALBController(ctx, cli, appBox, providerConfigName, conf)
	if err != nil {
		return "", fmt.Errorf("failed to create policy for AWS LB Controller: %v", err)
	}

	var nodeRoleARN string
	if roleReady && policyReady && awsLBCPolicyReady {
		policyARNs := make([]string, 0, 2+len(conf.ManagedPolicies))
		policyARNs = append(policyARNs, policyARN, awsLBCPolicyARN)
		policyARNs = append(policyARNs, conf.ManagedPolicies...)
		err := createRoleAttachment(ctx, cli, appBox, policyARNs, role, providerConfigName)
		if err != nil {
			return "", err
		}

		// Create an instance profile per network for AutoScalingGroups.
		// Must wait for the role to be ready before creating the instance profile,
		// since https://github.com/crossplane-contrib/provider-aws/issues/1493
		if err = createInstanceProfile(ctx, cli, appBox, providerConfigName, role); err != nil {
			return "", err
		}

		nodeRoleARN = role.Status.AtProvider.ARN
	} else {
		return "", fmt.Errorf("node role or policy is not ready")
	}

	return nodeRoleARN, nil
}

func createNodeRole(ctx context.Context, cli client.Client, appBox *v1alpha1.APPBox, providerConfigName string, conf config.AdminNodeRoleConfig) (role *iamv1beta1.Role, ready bool, err error) {
	tags := map[string]string{}
	name := namer.UniqueIdentifier(appBox.ObjectMeta)
	tags = util.AddResourceName(tags, name)
	trustPolicyDocument, err := util.GetAWSPolicyFromFile(conf.TrustPolicyFile)
	if err != nil {
		return nil, false, err
	}

	role = &iamv1beta1.Role{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "iam.aws.crossplane.io/v1beta1",
			Kind:       "Role",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
		Spec: iamv1beta1.RoleSpec{
			ForProvider: iamv1beta1.RoleParameters{
				Description:              ptr.To(fmt.Sprintf("Node role for %s", appBox.Name)),
				AssumeRolePolicyDocument: trustPolicyDocument,
				Tags:                     iamTagFromLabels(tags),
			},
			ResourceSpec: xpv1.ResourceSpec{
				DeletionPolicy:          xpv1.DeletionDelete,
				ProviderConfigReference: &xpv1.Reference{Name: providerConfigName},
			},
		},
	}

	err = k8s.Apply(ctx, cli, role)
	if err != nil {
		return role, false, err
	}
	if role.Status.AtProvider.ARN != "" {
		ready = true
	}

	return role, ready, nil
}

func createPolicyForALBController(ctx context.Context, cli client.Client, appBox *v1alpha1.APPBox, providerConfigName string, conf config.AdminNodeRoleConfig) (policyARN string, ready bool, err error) {
	name := namer.UniqueIdentifier(appBox.ObjectMeta)
	policy := iamv1beta1.Policy{
		ObjectMeta: metav1.ObjectMeta{
			Name:   policyName(name, awsLBControllerPolicyName),
			Labels: map[string]string{RoleRefLabelKey: name},
		},
		Spec: iamv1beta1.PolicySpec{
			ForProvider: iamv1beta1.PolicyParameters{
				Description: ptr.To("AWS LB Controller permissions"),
				Name:        policyName(name, awsLBControllerPolicyName),
			},
			ResourceSpec: xpv1.ResourceSpec{
				DeletionPolicy:          xpv1.DeletionDelete,
				ProviderConfigReference: &xpv1.Reference{Name: providerConfigName},
			},
		},
	}

	lbPolicyDocument, err := util.GetAWSPolicyFromFile(conf.AWSLBControllerPolicyFile)
	if err != nil {
		return "", false, err
	}
	_, err = controllerutil.CreateOrUpdate(ctx, cli, &policy, func() error {
		policy.Spec.ForProvider.Document = *ptr.To(lbPolicyDocument)
		return nil
	})
	if err != nil {
		return
	}
	if policy.Status.AtProvider.ARN == "" {
		ready = false
		return
	}

	ready = true
	return policy.Status.AtProvider.ARN, ready, nil
}

func createInstanceProfile(ctx context.Context, cli client.Client, appBox *v1alpha1.APPBox, providerConfigName string, role *iamv1beta1.Role) error {
	name := namer.UniqueIdentifier(appBox.ObjectMeta)

	instanceProfile := &iamv1alpha1.InstanceProfile{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
		Spec: iamv1alpha1.InstanceProfileSpec{
			ForProvider: iamv1alpha1.InstanceProfileParameters{
				CustomInstanceProfileParameters: iamv1alpha1.CustomInstanceProfileParameters{
					RoleRef: &xpv1.Reference{
						Name: role.Name,
					},
				},
			},
			ResourceSpec: xpv1.ResourceSpec{
				DeletionPolicy:          xpv1.DeletionDelete,
				ProviderConfigReference: &xpv1.Reference{Name: providerConfigName},
			},
		},
	}

	return k8s.Apply(ctx, cli, instanceProfile)
}

func policyName(name, policyName string) string {
	return fmt.Sprintf("%s-%s", name, policyName)
}

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

func createRoleAttachment(ctx context.Context, cli client.Client, appBox *v1alpha1.APPBox, policyARNs []string, role *iamv1beta1.Role, providerConfigName string) (err error) {
	// should use role.Name instead?
	name := namer.UniqueIdentifier(appBox.ObjectMeta)

	for _, policyARN := range policyARNs {
		attachment := &iamv1beta1.RolePolicyAttachment{
			ObjectMeta: metav1.ObjectMeta{
				Name:   policyAttachmentName(name, policyARN),
				Labels: map[string]string{RoleRefLabelKey: name},
			},
			Spec: iamv1beta1.RolePolicyAttachmentSpec{
				ForProvider: iamv1beta1.RolePolicyAttachmentParameters{
					RoleNameRef: &xpv1.Reference{
						Name: role.Name,
					},
					PolicyARN: policyARN,
				},
				ResourceSpec: xpv1.ResourceSpec{
					DeletionPolicy:          xpv1.DeletionDelete,
					ProviderConfigReference: &xpv1.Reference{Name: providerConfigName},
				},
			},
		}

		err = controllerutil.SetOwnerReference(role, attachment, cli.Scheme())
		if err != nil {
			return err
		}

		_, err = controllerutil.CreateOrUpdate(ctx, cli, attachment, func() error {
			return nil
		})
		if err != nil {
			return err
		}
	}

	return nil
}

func createNodeRolePolicy(ctx context.Context, cli client.Client, appBox *v1alpha1.APPBox, providerConfigName string, conf config.AdminNodeRoleConfig) (policyARN string, ready bool, err error) {
	name := namer.UniqueIdentifier(appBox.ObjectMeta)
	policy := iamv1beta1.Policy{
		ObjectMeta: metav1.ObjectMeta{
			Name:   policyName(name, nodeRolePolicyName),
			Labels: map[string]string{RoleRefLabelKey: name},
		},
		Spec: iamv1beta1.PolicySpec{
			ForProvider: iamv1beta1.PolicyParameters{
				Description: ptr.To("Node role permissions"),
				Name:        policyName(name, nodeRolePolicyName),
			},
			ResourceSpec: xpv1.ResourceSpec{
				DeletionPolicy:          xpv1.DeletionDelete,
				ProviderConfigReference: &xpv1.Reference{Name: providerConfigName},
			},
		},
	}

	policyDocument, err := getSelfManagedNodeRolePolicyDocument(appBox.GetLabels()[v1alpha1.TagProjectKey], conf.PolicyTemplateFile)
	if err != nil {
		return "", false, err
	}

	_, err = controllerutil.CreateOrUpdate(ctx, cli, &policy, func() error {
		policy.Spec.ForProvider.Document = *ptr.To(policyDocument)
		return nil
	})
	if err != nil {
		return
	}
	if policy.Status.AtProvider.ARN == "" {
		ready = false
		return
	}

	ready = true
	return policy.Status.AtProvider.ARN, ready, nil
}

func iamTagFromLabels(kv map[string]string) []iamv1beta1.Tag {
	tags := make([]iamv1beta1.Tag, 0, len(kv))
	for k, v := range kv {
		tags = append(tags, iamv1beta1.Tag{Key: k, Value: v})
	}
	sort.Slice(tags, func(i, j int) bool {
		return tags[i].Key < tags[j].Key
	})
	return tags
}

func getSelfManagedNodeRolePolicyDocument(project string, policyTemplateFile string) (string, error) {
	policyTmpl, err := util.GetAWSPolicyFromFile(policyTemplateFile)
	if err != nil {
		return "", err
	}
	var trustPolicyBuffer bytes.Buffer
	projectTag := struct{ Project string }{
		Project: project,
	}
	tmpl := template.Must(template.New("assumeRolePolicy").Parse(policyTmpl))
	if err := tmpl.Execute(&trustPolicyBuffer, projectTag); err != nil {
		return "", err
	}
	return trustPolicyBuffer.String(), nil
}
