package ack

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

	ramv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/ram/v1alpha1"
	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"

	"github.com/tidbcloud/infra-provider/regional-server/pkg/consts"
	"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"
)

const (
	roleRefLabelKey          = "role-reference"
	assumeRolePolicyDocument = `{
	"Statement": [
		{
			"Action": "sts:AssumeRole",
			"Effect": "Allow",
			"Principal": {
				"Service": ["ecs.aliyuncs.com"]
			}
		}
	],
	"Version": "1"
}`
)

type nodeRole struct {
	*task.BaseTask
}

func NewNodeRole(c *task.Context) deploy.Task {
	return &nodeRole{
		BaseTask: &task.BaseTask{Context: c},
	}
}

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

func (t *nodeRole) Create(ctx context.Context) deploy.Result {
	roleName := t.getNodeRoleName()
	tags := controllers.ConvertToCloudTags(t.APPBox.Labels, consts.ProviderTypeACK)
	tags = util.MergeMaps(tags, t.Config.AdditionalCloudTags)
	labels := util.MergeMaps(t.Namer.Labels(), t.APPBox.Labels)
	labels = util.MergeMaps(labels, tags)
	ackName := t.K8sName()
	labels = util.MergeMaps(labels, map[string]string{ackRefLabelKey: ackName})
	role := &ramv1alpha1.Role{
		ObjectMeta: metav1.ObjectMeta{
			Name:   roleName,
			Labels: labels,
		},
		Spec: ramv1alpha1.RoleSpec{
			ForProvider: ramv1alpha1.RoleParameters{
				Description:              ptr.To("ack worker node role"),
				RoleName:                 ptr.To(roleName),
				AssumeRolePolicyDocument: ptr.To(assumeRolePolicyDocument),
				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 deploy.Panic(err)
	}

	// ACK can work without explicit permission magically
	type customPolicyConfig struct {
		name string
		file string
		data map[string]string
	}
	customPolicyConfigs := []customPolicyConfig{
		{
			name: "acr-pull",
			file: t.Config.APPBoxConfig.AdminNodeRoleConfig.ContainerRegistryPolicyTemplateFile,
			data: nil,
		},
		{
			name: "tidb-cluster",
			file: t.Config.APPBoxConfig.AdminNodeRoleConfig.TidbClusterPolicyTemplateFile,
			data: nil,
		},
		{
			name: "o11y-agent",
			file: t.Config.APPBoxConfig.AdminNodeRoleConfig.O11yAgentPolicyTemplateFile,
			data: nil,
		},
	}
	// add role name reference label
	labels = util.MergeMaps(labels, map[string]string{roleRefLabelKey: roleName})
	for _, c := range customPolicyConfigs {
		document, err := getPolicyDocument(c.file, c.data)
		if err != nil {
			return deploy.Panic(err)
		}
		prefix := namer.WithPrefix(t.Config.ResourceNamePrefix + "-" + c.name)
		policyName := controllers.UniqueIdentifier(t.APPBox.ObjectMeta, prefix)
		// Use clear variable name to avoid shadowing
		policyTags := maps.Clone(tags)
		policy := &ramv1alpha1.Policy{
			ObjectMeta: metav1.ObjectMeta{
				Name:   policyName,
				Labels: labels,
			},
			Spec: ramv1alpha1.PolicySpec{
				ForProvider: ramv1alpha1.PolicyParameters{
					Description:    ptr.To(fmt.Sprintf("custom policy for %s", c.name)),
					PolicyDocument: ptr.To(document),
					PolicyName:     ptr.To(policyName),
					Tags:           util.ToMapPointer(policyTags),
				},
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
					ManagementPolicies:      controllers.GetResourceManagementPolicies(),
				},
			},
		}
		if err := k8s.Apply(ctx, t.Local, policy); err != nil {
			return deploy.Panic(err)
		}
		attachment := &ramv1alpha1.RolePolicyAttachment{
			ObjectMeta: metav1.ObjectMeta{
				Name:   policyAttachmentName(roleName, policyName),
				Labels: util.MergeMaps(t.APPBox.Labels, map[string]string{roleRefLabelKey: roleName}),
			},
			Spec: ramv1alpha1.RolePolicyAttachmentSpec{
				ForProvider: ramv1alpha1.RolePolicyAttachmentParameters{
					PolicyType:    ptr.To("Custom"),
					PolicyNameRef: &xpv1.Reference{Name: policyName},
					RoleName:      ptr.To(roleName),
				},
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
					ManagementPolicies:      controllers.GetResourceManagementPolicies(),
				},
			},
		}
		if err := k8s.Apply(ctx, t.Local, attachment); err != nil {
			return deploy.Panic(err)
		}
	}
	return deploy.Done()
}

func (t *nodeRole) PostCreate(ctx context.Context) deploy.Result {
	roleName := t.getNodeRoleName()
	role := &ramv1alpha1.Role{
		ObjectMeta: metav1.ObjectMeta{Name: roleName},
	}
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(role), role); err != nil {
		return deploy.Panic(err)
	}
	t.APPBox.Status.IAM.NodeRoleName = roleName
	if arn := role.Status.AtProvider.Arn; arn != nil {
		t.APPBox.Status.IAM.NodeRoleARN = *arn
		return deploy.Done()
	}
	return deploy.Panic(fmt.Errorf("node role is not ready, arn is empty"))
}

func (t *nodeRole) Delete(ctx context.Context) deploy.Result {
	attachmentList := &ramv1alpha1.RolePolicyAttachmentList{}
	matchLabels := client.MatchingLabels{roleRefLabelKey: t.getNodeRoleName()}
	if err := t.Local.List(ctx, attachmentList, matchLabels); err != nil {
		return deploy.Error(err)
	}
	for _, attachment := range attachmentList.Items {
		if err := controllers.DeleteUpboundResource(ctx, t.Local, &attachment); err != nil {
			return deploy.Error(err)
		}
	}
	policyList := &ramv1alpha1.PolicyList{}
	if err := t.Local.List(ctx, policyList, matchLabels); err != nil {
		return deploy.Error(err)
	}
	for _, policy := range policyList.Items {
		if err := controllers.DeleteUpboundResource(ctx, t.Local, &policy); err != nil {
			return deploy.Error(err)
		}
	}
	roleName := t.getNodeRoleName()
	role := &ramv1alpha1.Role{
		ObjectMeta: metav1.ObjectMeta{Name: roleName},
	}
	return deploy.ErrorOrDone(controllers.DeleteUpboundResource(ctx, t.Local, role))
}

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

func getPolicyDocument(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) getNodeRoleName() string {
	return controllers.UniqueIdentifier(t.APPBox.ObjectMeta, namer.WithPrefix(t.Config.ResourceNamePrefix))
}
