package eks

import (
	"context"
	"crypto/md5"
	_ "embed"
	"encoding/json"
	"fmt"
	"time"

	"github.com/crossplane-contrib/provider-aws/apis/iam/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	"github.com/tidbcloud/eks-provider/controllers/imports/task"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
)

var (
	// AWS managed policies ARNs
	managedPolicies4ImportRole = []string{
		"arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly",
		"arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy",
		"arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy",
		"arn:aws:iam::aws:policy/AmazonRoute53ResolverFullAccess",
		"arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore",
		"arn:aws:iam::aws:policy/AWSDirectConnectFullAccess",
	}
	expectedRoleAttachmentCount = len(managedPolicies4ImportRole) + 1 // managed policies + custom node policy
)

var (
	//go:embed dm_node_policy.json
	dmNodePolicy []byte
)

type NodeRole struct {
	*task.BaseTask
	Now func() time.Time
}

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

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

func (t *NodeRole) Create(ctx context.Context) deploy.Result {
	return t.createNodeRole(ctx)
}

const (
	iamReadyPropagationWait = time.Minute
	iamReadyRequeueInterval = 20 * time.Second
)

func (t *NodeRole) PostCreate(ctx context.Context) deploy.Result {
	roleName := t.getAWSNodeRoleName()

	// fetch role
	role := &v1beta1.Role{
		ObjectMeta: metav1.ObjectMeta{Name: roleName},
	}
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(role), role); err != nil {
		t.Logger.Error(err, "failed to get role", "roleName", roleName)
		return deploy.Error(err)
	}
	if role.Status.AtProvider.ARN == "" {
		t.Logger.Info("role arn not ready", "roleName", roleName)
		return deploy.Wait(iamReadyRequeueInterval, "role %s ARN is not ready", roleName)
	}

	// list attachments
	attachments := &v1beta1.RolePolicyAttachmentList{}
	if err := t.Local.List(ctx, attachments, client.MatchingLabels{RoleRefLabelKey: roleName}); err != nil {
		t.Logger.Error(err, "failed to list role policy attachments", "roleName", roleName)
		return deploy.Error(err)
	}
	if len(attachments.Items) < expectedRoleAttachmentCount {
		t.Logger.Info("attachments count not match", "have", len(attachments.Items), "want", expectedRoleAttachmentCount)
		return deploy.Wait(iamReadyRequeueInterval, "role %s attachments are not ready: have %d, want %d", roleName, len(attachments.Items), expectedRoleAttachmentCount)
	}
	for _, a := range attachments.Items {
		if !isReadyConditionTrue(a.Status.Conditions) {
			t.Logger.Info("attachment not ready", "name", a.Name)
			return deploy.Wait(iamReadyRequeueInterval, "role policy attachment %s is not ready", a.Name)
		}
	}

	// compute latest transition time among attachments.
	latest := latestReadyTransition(attachments.Items)
	if latest.IsZero() {
		t.Logger.Info("attachment ready transition is zero, waiting for transition timestamp", "roleName", roleName)
		return deploy.Wait(iamReadyRequeueInterval, "role %s attachment ready transition is unavailable", roleName)
	}
	if t.Now().Sub(latest) < iamReadyPropagationWait {
		t.Logger.Info("waiting propagation cooldown", "since", t.Now().Sub(latest))
		return deploy.Wait(iamReadyRequeueInterval, "role %s is waiting for IAM propagation", roleName)
	}

	// all good, write back ARN
	t.Import.Status.AWSNodeRoleARN = role.Status.AtProvider.ARN
	return deploy.Done()
}

// helpers for readiness timestamps

func isReadyConditionTrue(conds []xpv1.Condition) bool {
	for _, c := range conds {
		if c.Type == xpv1.TypeReady && c.Status == v1.ConditionTrue {
			return true
		}
	}
	return false
}

func getReadyTransition(conds []xpv1.Condition) time.Time {
	var latest time.Time
	for _, c := range conds {
		if c.Type == xpv1.TypeReady && c.Status == v1.ConditionTrue {
			if c.LastTransitionTime.IsZero() {
				continue
			}
			if c.LastTransitionTime.Time.After(latest) {
				latest = c.LastTransitionTime.Time
			}
		}
	}
	return latest
}

func latestReadyTransition(attachments []v1beta1.RolePolicyAttachment) time.Time {
	var latest time.Time
	for _, a := range attachments {
		aT := getReadyTransition(a.Status.Conditions)
		if aT.After(latest) {
			latest = aT
		}
	}
	return latest
}

func (t *NodeRole) Delete(ctx context.Context) deploy.Result {
	// if err := t.deleteNodeRole(ctx); err != nil {
	// 	return deploy.Error(err)
	// }
	return deploy.Done()
}

func (t *NodeRole) createNodeRole(ctx context.Context) deploy.Result {
	roleName := t.getAWSNodeRoleName()
	trustPolicy, err := t.getTrustPolicy()
	if err != nil {
		t.Logger.Error(err, "failed to get trust policy")
		return deploy.Error(err)
	}

	role, roleReady, err := t.createRole(ctx, roleName, trustPolicy)
	if err != nil {
		t.Logger.Error(err, "failed to create role", "roleName", roleName)
		return deploy.Error(err)
	}

	// Create custom node policy
	policyARN, policyReady, err := t.createNodeRolePolicy(ctx, roleName)
	if err != nil {
		t.Logger.Error(err, "failed to create node role policy", "roleName", roleName)
		return deploy.Error(err)
	}

	if roleReady && policyReady {
		// Append custom policy to managed policies
		allPolicies := append([]string{policyARN}, managedPolicies4ImportRole...)
		if err := t.createRoleAttachment(ctx, allPolicies, role); err != nil {
			t.Logger.Error(err, "failed to create role attachment", "roleName", roleName)
			return deploy.Error(err)
		}
		return deploy.Done()
	}

	return deploy.Wait(iamReadyRequeueInterval, "role or policy is not ready")
}

// func (t *NodeRole) deleteNodeRole(ctx context.Context) error {
// 	roleName := t.getAWSNodeRoleName()

// 	if err := client.IgnoreNotFound(t.Local.DeleteAllOf(ctx,
// 		&v1beta1.RolePolicyAttachment{}, client.MatchingLabels{RoleRefLabelKey: roleName})); err != nil {
// 		return fmt.Errorf("delete rolePolicyAttachment: %w", err)
// 	}

// 	if err := client.IgnoreNotFound(t.Local.DeleteAllOf(ctx,
// 		&v1beta1.Policy{}, client.MatchingLabels{RoleRefLabelKey: roleName})); err != nil {
// 		return fmt.Errorf("delete policy: %w", err)
// 	}

// 	role := &v1beta1.Role{
// 		ObjectMeta: metav1.ObjectMeta{
// 			Name: roleName,
// 		},
// 	}
// 	if err := client.IgnoreNotFound(t.Local.Delete(ctx, role)); err != nil {
// 		return fmt.Errorf("delete IAM role: %w", err)
// 	}

// 	return nil
// }

func (t *NodeRole) getAWSNodeRoleName() string {
	var projectID string
	if t.Cluster != nil {
		projectID = t.Cluster.ProjectId
	} else {
		projectID = t.ProjectID
	}
	return fmt.Sprintf("import-%s-%s", t.Config.Network.Region, projectID)
}

func (t *NodeRole) getTrustPolicy() (string, error) {
	trustPolicy := map[string]interface{}{
		"Version": "2012-10-17",
		"Statement": []map[string]interface{}{
			{
				"Effect": "Allow",
				"Principal": map[string]interface{}{
					"Service": "ec2.amazonaws.com",
				},
				"Action": []string{"sts:AssumeRole"},
			},
		},
	}

	policyBytes, err := json.Marshal(trustPolicy)
	if err != nil {
		return "", fmt.Errorf("marshal trust policy: %w", err)
	}

	return string(policyBytes), nil
}

func (t *NodeRole) createRole(ctx context.Context, roleName, trustPolicy string) (*v1beta1.Role, bool, error) {
	role := &v1beta1.Role{
		ObjectMeta: metav1.ObjectMeta{
			Name: roleName,
		},
		Spec: v1beta1.RoleSpec{
			ForProvider: v1beta1.RoleParameters{
				Description:              pointer.String("Node role for DM clusters"),
				AssumeRolePolicyDocument: trustPolicy,
			},
			ResourceSpec: xpv1.ResourceSpec{
				DeletionPolicy:          xpv1.DeletionDelete,
				ProviderConfigReference: &xpv1.Reference{Name: t.GetEKSProviderConfigName()},
			},
		},
	}

	err := k8s.Apply(ctx, t.Local, role)
	if err != nil {
		return role, false, err
	}

	return role, role.Status.AtProvider.ARN != "", nil
}

func (t *NodeRole) createNodeRolePolicy(ctx context.Context, roleName string) (string, bool, error) {
	policyName := policyName(roleName)
	policy := &v1beta1.Policy{
		ObjectMeta: metav1.ObjectMeta{
			Name:   policyName,
			Labels: map[string]string{RoleRefLabelKey: roleName},
		},
		Spec: v1beta1.PolicySpec{
			ForProvider: v1beta1.PolicyParameters{
				Description: pointer.String("Node role permissions for DM clusters"),
				Name:        policyName,
				Document:    string(dmNodePolicy),
			},
			ResourceSpec: xpv1.ResourceSpec{
				DeletionPolicy:          xpv1.DeletionDelete,
				ProviderConfigReference: &xpv1.Reference{Name: t.GetEKSProviderConfigName()},
			},
		},
	}

	_, err := controllerutil.CreateOrUpdate(ctx, t.Local, policy, func() error {
		return nil
	})
	if err != nil {
		return "", false, err
	}

	return policy.Status.AtProvider.ARN, policy.Status.AtProvider.ARN != "", nil
}

func (t *NodeRole) createRoleAttachment(ctx context.Context, policyARNs []string, role *v1beta1.Role) error {
	for _, policyARN := range policyARNs {
		attachment := &v1beta1.RolePolicyAttachment{
			ObjectMeta: metav1.ObjectMeta{
				Name:   policyAttachmentName(role.Name, policyARN),
				Labels: map[string]string{RoleRefLabelKey: role.Name},
			},
			Spec: v1beta1.RolePolicyAttachmentSpec{
				ForProvider: v1beta1.RolePolicyAttachmentParameters{
					RoleNameRef: &xpv1.Reference{
						Name: role.Name,
					},
					PolicyARN: policyARN,
				},
				ResourceSpec: xpv1.ResourceSpec{
					DeletionPolicy:          xpv1.DeletionDelete,
					ProviderConfigReference: &xpv1.Reference{Name: t.GetEKSProviderConfigName()},
				},
			},
		}

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

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

	return nil
}

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

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

const RoleRefLabelKey = "role-reference"
