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"
	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/replay/task"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
)

var (
	//go:embed replay_node_policy.json
	replayNodePolicy []byte
)

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 {
	return t.createNodeRole(ctx)
}

func (t *NodeRole) PostCreate(ctx context.Context) deploy.Result {
	// check the role is ready
	roleName := t.getAWSNodeRoleName()
	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 is not ready", "roleName", roleName)
		return deploy.Error(fmt.Errorf("role is not ready")).WithRequeuAfter(time.Second * 20)
	}

	// write back to replay status
	t.Replay.Status.AWSNodeRoleARN = role.Status.AtProvider.ARN
	return deploy.Done()
}

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)
	}

	// AWS managed policies ARNs
	managedPolicies := []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",
	}

	// 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(managedPolicies, policyARN)
		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.Error(fmt.Errorf("role or policy is not ready")).WithRequeuAfter(time.Second * 20)
}

func (t *NodeRole) deleteNodeRole(ctx context.Context) error {
	// No explicit deletion needed here.
	// IAM resources (Role, Policy, RolePolicyAttachment) are created with
	// ProviderConfigReference, and will be automatically cleaned up when
	// ProviderConfig is deleted via ProviderConfigUsage tracking.
	//
	// See provider_config.go:deleteAll() for the actual cleanup logic.
	//
	// This approach:
	// 1. Prevents duplicate deletion errors
	// 2. Handles shared resources correctly (when multiple Replay CRs use same role)
	// 3. Ensures proper cleanup order (NodeRole -> ... -> ProviderConfig)
	return nil
}

func (t *NodeRole) getAWSNodeRoleName() string {
	return fmt.Sprintf("replay-%s-%s", t.RootConfig.Network.Region, t.Replay.Labels[cloudv1.ProjectTagKey])
}

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 replay 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 := fmt.Sprintf("%s-node-policy", 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(replayNodePolicy),
			},
			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 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"
