package eks

import (
	"bytes"
	"context"
	"fmt"
	"html/template"
	"os"

	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"
	"github.com/pingcap/errors"
	"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"
	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"
)

const (
	tiCDCNodeRolePolicyName = "ticdc-node-role-permission"
	defaultTrustPolicy      = `{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": [
        "sts:AssumeRole"
      ]
    },
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "{{.RoleARN}}"
      },
      "Action": [
        "sts:AssumeRole"
      ]
    }
  ]
}`
	defaultRolePolicy = `{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "eks:DescribeCluster",
                "eks:DescribeNodegroup",
                "eks:DescribeUpdate",
                "eks:ListClusters",
                "eks:ListNodegroups",
                "eks:ListUpdates"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "eks:CreateNodegroup",
                "eks:DeleteNodegroup",
                "eks:TagResource",
                "eks:UntagResource",
                "eks:UpdateNodegroupConfig",
                "eks:UpdateNodegroupVersion"
            ],
            "Resource": "arn:aws:eks:*:*:nodegroup/*/*/*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "eks:AccessKubernetesApi",
                "eks:RegisterCluster"
            ],
            "Resource": "arn:aws:eks:*:*:cluster/*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:GetRepositoryPolicy",
                "ecr:DescribeRepositories",
                "ecr:ListImages",
                "ecr:DescribeImages",
                "ecr:BatchGetImage",
                "ecr:GetLifecyclePolicy",
                "ecr:GetLifecyclePolicyPreview",
                "ecr:ListTagsForResource",
                "ecr:DescribeImageScanFindings"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:AssignPrivateIpAddresses",
                "ec2:AttachNetworkInterface",
                "ec2:CreateNetworkInterface",
                "ec2:DeleteNetworkInterface",
                "ec2:DescribeInstances",
                "ec2:DescribeTags",
                "ec2:DescribeNetworkInterfaces",
                "ec2:DescribeInstanceTypes",
                "ec2:DetachNetworkInterface",
                "ec2:ModifyNetworkInterfaceAttribute",
                "ec2:UnassignPrivateIpAddresses"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": "ec2:CreateTags",
            "Resource": "arn:aws:ec2:*:*:network-interface/*"
        },
        {
            "Effect": "Allow",
            "Action": "s3:*",
            "Resource": "*"
        },
        {
            "Sid": "AssumeRoleO11y",
            "Effect": "Allow",
            "Action": "sts:AssumeRole",
            "Resource": [
                "arn:aws:iam::552185537300:role/*",
                "arn:aws:iam::557537366020:role/*"
            ]
        }
    ]
}
`
	roleNameLenLimit = 15
)

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

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

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

func (t *TiCDCNodeRole) Create(ctx context.Context) deploy.Result {
	if t.APPBox.Status.IAM.NodeRoleARN == "" {
		return deploy.Panic(fmt.Errorf("admin role arn is not ready"))
	}
	nodeRoleARN, err := t.prepareTiCDCNodeRole(ctx, t.ProviderConfigName(), t.Config.APPBoxConfig.TiCDCNodeRoleConfig)
	if err != nil {
		return deploy.Panic(err)
	}

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

	return deploy.Done()
}

func (t *TiCDCNodeRole) PostCreate(_ 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 *TiCDCNodeRole) Delete(ctx context.Context) deploy.Result {
	name := t.getTiCDCNodeRoleName()
	matchLabels := client.MatchingLabels{RoleRefLabelKey: name}
	if err := client.IgnoreNotFound(t.Local.DeleteAllOf(ctx, &iamv1beta1.RolePolicyAttachment{}, matchLabels)); err != nil {
		return deploy.Panic(err)
	}

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

	role := &iamv1beta1.Role{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}
	if err := client.IgnoreNotFound(t.Local.Delete(ctx, role)); err != nil {
		return deploy.Panic(err)
	}
	instanceProfile := &iamv1alpha1.InstanceProfile{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}
	if err := client.IgnoreNotFound(t.Local.Delete(context.Background(), instanceProfile)); err != nil {
		return deploy.Panic(err)
	}
	return deploy.Done()
}

func (t *TiCDCNodeRole) PostDelete(_ context.Context) deploy.Result {
	return deploy.Done()
}

func (t *TiCDCNodeRole) prepareTiCDCNodeRole(ctx context.Context,
	providerConfigName string, nodeRoleConfig config.TiCDCNodeRoleConfig) (string, error) {
	providerCfg := &apiv1beta1.ProviderConfig{
		ObjectMeta: metav1.ObjectMeta{
			Name: providerConfigName,
		},
	}
	err := t.Local.Get(ctx, client.ObjectKey{Name: providerCfg.Name}, providerCfg)
	if err != nil {
		return "", err
	}

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

	// create ticdc node role per appbox
	role, roleReady, err := t.createTiCDCNodeRole(ctx, providerConfigName, nodeRoleConfig.TrustPolicyTemplateFile)
	if err != nil {
		return "", err
	}

	// create ticdc node role policy per appbox
	policyARN, policyReady, err := t.createTiCDCNodeRolePolicy(ctx, providerConfigName, nodeRoleConfig)
	if err != nil {
		return "", err
	}

	var nodeRoleARN string
	if roleReady && policyReady {
		policyARNs := make([]string, 0, 1+len(nodeRoleConfig.ManagedPolicies))
		policyARNs = append(policyARNs, policyARN)
		policyARNs = append(policyARNs, nodeRoleConfig.ManagedPolicies...)
		err := t.createTiCDCRoleAttachment(ctx, policyARNs, role, providerConfigName)
		if err != nil {
			return "", err
		}
		if err = t.createInstanceProfile(ctx, providerConfigName, role); err != nil {
			return "", err
		}
		nodeRoleARN = role.Status.AtProvider.ARN
	} else {
		return "", fmt.Errorf("ticdc node role or policy is not ready")
	}

	return nodeRoleARN, nil
}

func (t *TiCDCNodeRole) getTiCDCNodeRoleName() string {
	return fmt.Sprintf("ticdc-%s",
		namer.UniqueIdentifier(t.APPBox.ObjectMeta, namer.WithLimit(roleNameLenLimit)))
}

func (t *TiCDCNodeRole) createTiCDCNodeRole(ctx context.Context,
	providerConfigName string, trustPolicyTemplateFile string) (role *iamv1beta1.Role, ready bool, err error) {
	tags := map[string]string{}
	name := t.getTiCDCNodeRoleName()
	tags = util.AddResourceName(tags, name)
	trustPolicyDocument, err := getTrustPolicy(trustPolicyTemplateFile, t.APPBox.Status.IAM.NodeRoleARN)
	if err != nil {
		return nil, false, err
	}

	role = buildRoleCR(name, trustPolicyDocument, providerConfigName, tags)

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

	return role, ready, nil
}

func (t *TiCDCNodeRole) createTiCDCRoleAttachment(ctx context.Context,
	policyARNs []string, role *iamv1beta1.Role, providerConfigName string) (err error) {
	roleName := role.Name
	for _, policyARN := range policyARNs {
		attachment := buildRoleAttachmentCR(roleName, policyARN, providerConfigName)
		if err = controllerutil.SetOwnerReference(role, attachment, t.Local.Scheme()); err != nil {
			return err
		}
		if _, err = controllerutil.CreateOrUpdate(ctx, t.Local, attachment, func() error {
			return nil
		}); err != nil {
			return err
		}
	}
	return nil
}

func buildRoleCR(
	name string,
	trustPolicy string,
	providerCfg string,
	tags map[string]string) *iamv1beta1.Role {
	return &iamv1beta1.Role{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
		Spec: iamv1beta1.RoleSpec{
			ForProvider: iamv1beta1.RoleParameters{
				Description:              pointer.String("role for ticdc node group"),
				AssumeRolePolicyDocument: trustPolicy,
				Tags:                     iamTagFromLabels(tags),
			},
			ResourceSpec: xpv1.ResourceSpec{
				DeletionPolicy:          xpv1.DeletionDelete,
				ProviderConfigReference: &xpv1.Reference{Name: providerCfg},
			},
		},
	}
}

func buildRoleAttachmentCR(roleName string, policyARN string, providerCfg string) *iamv1beta1.RolePolicyAttachment {
	return &iamv1beta1.RolePolicyAttachment{
		ObjectMeta: metav1.ObjectMeta{
			Name:   policyAttachmentName(roleName, policyARN),
			Labels: map[string]string{RoleRefLabelKey: roleName},
		},
		Spec: iamv1beta1.RolePolicyAttachmentSpec{
			ForProvider: iamv1beta1.RolePolicyAttachmentParameters{
				RoleNameRef: &xpv1.Reference{
					Name: roleName,
				},
				PolicyARN: policyARN,
			},
			ResourceSpec: xpv1.ResourceSpec{
				DeletionPolicy:          xpv1.DeletionDelete,
				ProviderConfigReference: &xpv1.Reference{Name: providerCfg},
			},
		},
	}
}

func buildRolePolicyCR(roleName string, providerCfg string) *iamv1beta1.Policy {
	return &iamv1beta1.Policy{
		ObjectMeta: metav1.ObjectMeta{
			Name:   policyName(roleName, tiCDCNodeRolePolicyName),
			Labels: map[string]string{RoleRefLabelKey: roleName},
		},
		Spec: iamv1beta1.PolicySpec{
			ForProvider: iamv1beta1.PolicyParameters{
				Description: pointer.String("TiCDC node role permissions"),
				Name:        policyName(roleName, tiCDCNodeRolePolicyName),
			},
			ResourceSpec: xpv1.ResourceSpec{
				DeletionPolicy:          xpv1.DeletionDelete,
				ProviderConfigReference: &xpv1.Reference{Name: providerCfg},
			},
		},
	}
}

func (t *TiCDCNodeRole) createTiCDCNodeRolePolicy(ctx context.Context, providerConfigName string,
	nodeRoleConfig config.TiCDCNodeRoleConfig) (policyARN string, ready bool, err error) {
	roleName := t.getTiCDCNodeRoleName()
	policy := buildRolePolicyCR(roleName, providerConfigName)

	policyDocument, err := getTiCDCNodeRolePolicy(nodeRoleConfig.PolicyTemplateFile, t.APPBox.GetLabels()[v1alpha1.TagProjectKey])
	if err != nil {
		return "", false, err
	}

	_, err = controllerutil.CreateOrUpdate(ctx, t.Local, policy, func() error {
		policy.Spec.ForProvider.Document = *pointer.String(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 (t *TiCDCNodeRole) createInstanceProfile(ctx context.Context,
	providerConfigName string,
	role *iamv1beta1.Role) error {
	name := role.Name

	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, t.Local, instanceProfile)
}

func getTrustPolicy(trustPolicyTemplateFile string, nodeRoleARN string) (string, error) {
	adminNodeRole := struct{ RoleARN string }{
		RoleARN: nodeRoleARN,
	}
	if trustPolicyTemplateFile == "" {
		return renderTemplate(defaultTrustPolicy, adminNodeRole)
	}
	return renderTemplateFile(trustPolicyTemplateFile, adminNodeRole)
}

func getTiCDCNodeRolePolicy(policyTemplateFile string, project string) (string, error) {
	if policyTemplateFile == "" {
		return defaultRolePolicy, nil
	}
	projectTag := struct{ Project string }{
		Project: project,
	}
	return renderTemplateFile(policyTemplateFile, projectTag)
}

func renderTemplateFile(templateFile string, dataObj any) (string, error) {
	tmplStr, err := getFileString(templateFile)
	if err != nil {
		return "", err
	}
	return renderTemplate(tmplStr, dataObj)
}

func renderTemplate(tmplStr string, dataObj any) (string, error) {
	var buffer bytes.Buffer
	tmpl, err := template.New("template").Parse(tmplStr)
	if err != nil {
		return "", err
	}
	if err := tmpl.Execute(&buffer, dataObj); err != nil {
		return "", err
	}
	return buffer.String(), nil
}

func getFileString(filePath string) (string, error) {
	data, err := os.ReadFile(filePath)
	if err != nil {
		return "", errors.Wrap(err, fmt.Sprintf("read template file %q", filePath))
	}
	return string(data), nil
}
