package ack

import (
	"context"
	"fmt"
	"strings"

	pcav1beta1 "github.com/cert-manager/aws-privateca-issuer/pkg/api/v1beta1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	iamv1beta1 "github.com/upbound/provider-aws/apis/iam/v1beta1"
	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"

	"github.com/tidbcloud/infra-provider/regional-server/pkg/applier"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/applier/mutatefuncs"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/config"
	"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/helm"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/namer"
)

const (
	clusterIssuerName  = "cluster-issuer"
	serviceAccountName = "aws-pca-issuer"
)

type PCAIssuerConfig struct {
	config.InfraComponentConfig
	awsRoleArn string
}

func (c *PCAIssuerConfig) ToHelmValues() map[string]interface{} {
	values := c.InfraComponentConfig.ToHelmValues()
	if values["env"] == nil {
		values["env"] = make(map[string]interface{})
	}
	envValues, ok := values["env"].(map[string]interface{})
	if ok {
		// set the awsRoleArn created in this task
		envValues["AWS_ROLE_ARN"] = c.awsRoleArn
	}
	return values
}

// PCAIssuer deploys the AWS Private CA Issuer via Helm chart.
// It creates IAM resources and creates an AWS PCA ClusterIssuer.
type PCAIssuer struct {
	*task.BaseTask
	iamCRName      string
	policyDocument string
	config         *PCAIssuerConfig
}

func NewPCAIssuer(ctx *task.Context) *PCAIssuer {
	return &PCAIssuer{
		BaseTask:  &task.BaseTask{Context: ctx},
		iamCRName: namer.UniqueIdentifier(ctx.APPBox.ObjectMeta, namer.WithPrefix("pca-issuer"), namer.WithLimit(namer.AWSIAMRoleNameLimit)),
		config: &PCAIssuerConfig{
			InfraComponentConfig: ctx.InfraTmplConfig.PCAIssuer,
		},
	}
}

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

func (t *PCAIssuer) Create(ctx context.Context) deploy.Result {
	caIdentifier, region, enabled, err := t.PCAIdentifierAndRegion()
	if err != nil {
		return deploy.Error(fmt.Errorf("failed to resolve PCA config: %w", err))
	}
	if !enabled {
		return deploy.Done()
	}

	if f := t.PCAIssuerPolicyTemplateFile(); f != "" {
		t.policyDocument, err = getPolicyDocument(f, nil)
		if err != nil {
			return deploy.Error(fmt.Errorf("failed to get PCA issuer policy document: %w", err))
		}
	}

	if _, err := applier.NewApplier(t.Local, mutatefuncs.MutateUpboundIAMRole).
		Expected(t.NewIAMRole(false, t.Config.AWSCrossplaneProviderConfig)).
		Apply(ctx); err != nil {
		return deploy.Error(err)
	}
	if _, err := applier.NewApplier(t.Local, mutatefuncs.MutateUpboundIAMPolicy).
		Expected(t.NewIAMPolicy(false)).
		Apply(ctx); err != nil {
		return deploy.Error(err)
	}
	if _, err := applier.NewApplier(t.Local, mutatefuncs.MutateUpboundIAMRolePolicyAttachment).
		Expected(t.NewIAMRolePolicyAttachment(false)).
		Apply(ctx); err != nil {
		return deploy.Error(err)
	}

	t.config.awsRoleArn = fmt.Sprintf("arn:aws:iam::%s:role/%s", t.Config.AWSCrossplaneProviderConfig, t.iamCRName)
	if err := helm.Apply(ctx, t.Remote, t.config); err != nil {
		return deploy.Error(fmt.Errorf("failed to apply PCA Issuer helm chart: %w", err))
	}

	remoteCli, err := t.Remote.GetClient(ctx)
	if err != nil {
		return deploy.Error(fmt.Errorf("failed to get remote client: %w", err))
	}
	issuer := &pcav1beta1.AWSPCAClusterIssuer{
		ObjectMeta: metav1.ObjectMeta{
			Name: clusterIssuerName,
		},
	}
	if _, err = controllerutil.CreateOrUpdate(ctx, remoteCli, issuer, func() error {
		// Only set Arn and Region if they are not already set.
		// Changing these values would cause different PCA to be used for signing certificates.
		if issuer.Spec.Arn == "" {
			issuer.Spec.Arn = caIdentifier
		}
		if issuer.Spec.Region == "" {
			issuer.Spec.Region = region
		}
		return nil
	}); err != nil {
		return deploy.Error(fmt.Errorf("failed to create or update AWS PCA ClusterIssuer: %w", err))
	}
	return deploy.Done()
}

func (t *PCAIssuer) PostCreate(ctx context.Context) deploy.Result {
	_, _, enabled, err := t.PCAIdentifierAndRegion()
	if err != nil {
		return deploy.Error(fmt.Errorf("failed to resolve PCA config: %w", err))
	}
	if !enabled {
		return deploy.Done()
	}
	remoteCli, err := t.Remote.GetClient(ctx)
	if err != nil {
		return deploy.Error(fmt.Errorf("failed to get remote client: %w", err))
	}
	var issuer pcav1beta1.AWSPCAClusterIssuer
	if err := remoteCli.Get(ctx, client.ObjectKey{Name: clusterIssuerName}, &issuer); err != nil {
		return deploy.Error(fmt.Errorf("failed to get AWS PCA ClusterIssuer: %w", err))
	}
	for _, v := range issuer.Status.Conditions {
		if v.Type == "Ready" && v.Status == "True" {
			return deploy.Done()
		}
	}
	return deploy.Error(fmt.Errorf("AWS PCA ClusterIssuer is not ready yet"))
}

func (t *PCAIssuer) Delete(ctx context.Context) deploy.Result {
	if err := controllers.DeleteUpboundResource(ctx, t.Local, t.NewIAMRolePolicyAttachment(true)); err != nil {
		return deploy.Error(err)
	}
	if err := controllers.DeleteUpboundResource(ctx, t.Local, t.NewIAMPolicy(true)); err != nil {
		return deploy.Error(err)
	}
	if err := controllers.DeleteUpboundResource(ctx, t.Local, t.NewIAMRole(true, "")); err != nil {
		return deploy.Error(err)
	}
	return deploy.Done()
}

func (t *PCAIssuer) NewIAMRole(onlyMeta bool, account string) *iamv1beta1.Role {
	role := &iamv1beta1.Role{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.iamCRName,
		},
	}
	if onlyMeta {
		return role
	}

	url := strings.TrimPrefix(t.Cluster.GetOIDCURL(), "https://")
	role.Spec = iamv1beta1.RoleSpec{
		ForProvider: iamv1beta1.RoleParameters{
			AssumeRolePolicy: ptr.To(fmt.Sprintf(assumeRolePolicyDocumentTemplate,
				account,
				url, url,
				t.InfraTmplConfig.PCAIssuer.GetNamespace(),
				serviceAccountName,
			)),
			Description: ptr.To(fmt.Sprintf("role for aws-privateca-issuer in ack cluster %s", t.iamCRName)),
		},
		ResourceSpec: xpv1.ResourceSpec{
			ProviderConfigReference: &xpv1.Reference{Name: t.Config.AWSCrossplaneProviderConfig},
			ManagementPolicies:      controllers.GetResourceManagementPolicies(),
		},
	}
	return role
}

func (t *PCAIssuer) NewIAMPolicy(onlyMeta bool) *iamv1beta1.Policy {
	policy := &iamv1beta1.Policy{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.iamCRName,
		},
	}
	if onlyMeta {
		return policy
	}
	policy.Spec = iamv1beta1.PolicySpec{
		ForProvider: iamv1beta1.PolicyParameters{
			Policy:      ptr.To(t.policyDocument),
			Description: ptr.To(fmt.Sprintf("policy for aws-privateca-issuer in ack cluster %s", t.iamCRName)),
		},
		ResourceSpec: xpv1.ResourceSpec{
			ProviderConfigReference: &xpv1.Reference{Name: t.Config.AWSCrossplaneProviderConfig},
			ManagementPolicies:      controllers.GetResourceManagementPolicies(),
		},
	}
	return policy
}

func (t *PCAIssuer) NewIAMRolePolicyAttachment(onlyMeta bool) *iamv1beta1.RolePolicyAttachment {
	rp := &iamv1beta1.RolePolicyAttachment{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.iamCRName,
		},
	}
	if onlyMeta {
		return rp
	}

	rp.Spec = iamv1beta1.RolePolicyAttachmentSpec{
		ForProvider: iamv1beta1.RolePolicyAttachmentParameters{
			RoleRef: &xpv1.Reference{
				Name: t.iamCRName,
			},
			PolicyArnRef: &xpv1.Reference{
				Name: t.iamCRName,
			},
		},
		ResourceSpec: xpv1.ResourceSpec{
			ProviderConfigReference: &xpv1.Reference{Name: t.Config.AWSCrossplaneProviderConfig},
			ManagementPolicies:      controllers.GetResourceManagementPolicies(),
		},
	}
	return rp
}
