package aws

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"time"

	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	kmsv1beta1 "github.com/upbound/provider-aws/apis/kms/v1beta1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"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/key/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/util"
)

type Key struct {
	*task.BaseTask
}

func NewKey(ctx *task.Context) *Key {
	return &Key{&task.BaseTask{Context: ctx}}
}

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

func (t *Key) Create(ctx context.Context) deploy.Result {
	t.BeginAt = util.Pointer(time.Now())
	name := task.GenerateKeyName(t.Key, t.Config.ResourceNamePrefix)

	tags := controllers.ConvertToCloudTags(t.Key.Labels, consts.ProviderTypeEKS)
	tags = util.BuildAWSTagWithName(tags, t.Key.Name)
	tags = util.MergeMaps(tags, t.Config.AdditionalCloudTags)

	managerRole := fmt.Sprintf("arn:aws:iam::%s:role/%s", t.Key.Spec.AccountRef.AccountID, t.Key.Spec.AccountRef.Role)

	userRoles, buildUserRolesResult := t.Context.BuildTrustedNodeRoleARNs(ctx)

	keyPolicy, err := buildKeyPolicy(
		t.Key.Spec.AccountRef.AccountID,
		[]string{managerRole},
		userRoles,
	)
	if err != nil {
		return deploy.Panic(err)
	}

	key := kmsv1beta1.Key{
		ObjectMeta: metav1.ObjectMeta{
			Name:   name,
			Labels: t.Key.Labels,
		},
		Spec: kmsv1beta1.KeySpec{
			ForProvider: kmsv1beta1.KeyParameters{
				Region:            ptr.To(t.Key.Spec.Region),
				Description:       ptr.To(fmt.Sprintf("KMS key for %s/%s", t.Key.Namespace, t.Key.Name)),
				KeyUsage:          ptr.To("ENCRYPT_DECRYPT"),
				IsEnabled:         ptr.To(true),
				EnableKeyRotation: ptr.To(true),
				Tags:              util.ToMapPointer(tags),
				Policy:            ptr.To(keyPolicy),
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
		},
	}

	if err := k8s.Apply(ctx, t.Local, &key); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	if buildUserRolesResult != nil {
		return *buildUserRolesResult
	}
	return deploy.Done()
}

func (t *Key) PostCreate(ctx context.Context) deploy.Result {
	name := task.GenerateKeyName(t.Key, t.Config.ResourceNamePrefix)
	key := &kmsv1beta1.Key{}

	if err := t.Local.Get(ctx, types.NamespacedName{Name: name}, key); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	if !controllers.IsResourceReady(key.Status.ResourceStatus) {
		t.Key.Status.Phase = v1alpha1.KeyPhaseCreating
		return deploy.Panic(errors.New("key is not ready")).WithRequeuAfter(time.Second * 30)
	}

	t.Key.Status.ID = ptr.Deref(key.Status.AtProvider.Arn, "")
	t.Key.Status.Phase = v1alpha1.KeyPhaseActive

	return deploy.Done()
}

func (t *Key) Delete(ctx context.Context) deploy.Result {
	name := task.GenerateKeyName(t.Key, t.Config.ResourceNamePrefix)
	key := &kmsv1beta1.Key{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}

	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(key), key); client.IgnoreNotFound(err) != nil {
		return deploy.Error(err)
	}

	if err := controllers.DeleteUpboundResource(ctx, t.Local, key); err != nil {
		return deploy.Panic(err).WithRequeuAfter(time.Second * 30)
	}

	return deploy.Done()
}

func (t *Key) PostDelete(ctx context.Context) deploy.Result {
	return deploy.ErrorOrDone(k8s.GetDeletedObject(ctx, t.Local, types.NamespacedName{Name: task.GenerateKeyName(t.Key, t.Config.ResourceNamePrefix)}, &kmsv1beta1.Key{}))
}

// buildKeyPolicy creates a key policy that allows the trusted roles to use the key
func buildKeyPolicy(account string, managerRoles, userRoles []string) (string, error) {
	policy := Policy{
		Version: "2012-10-17",
		Statement: []Statement{
			{ // avoid key unmanagable
				Sid:    "Enable IAM User Permissions",
				Effect: "Allow",
				Principal: map[string][]string{
					"AWS": {fmt.Sprintf("arn:aws:iam::%s:root", account)},
				},
				Action:   []string{"kms:*"},
				Resource: "*",
			},
			{
				Sid:    "Allow manager roles to manage the key",
				Effect: "Allow",
				Principal: map[string][]string{
					"AWS": managerRoles,
				},
				Action: []string{
					"kms:Create*",
					"kms:Describe*",
					"kms:Enable*",
					"kms:List*",
					"kms:Put*",
					"kms:Update*",
					"kms:Revoke*",
					"kms:Disable*",
					"kms:Get*",
					"kms:Delete*",
					"kms:TagResource",
					"kms:UntagResource",
					"kms:ScheduleKeyDeletion",
					"kms:CancelKeyDeletion",
				},
				Resource: "*",
			},
		},
	}

	if len(userRoles) != 0 {
		policy.Statement = append(policy.Statement, Statement{
			Sid:    "Allow user roles to use the key for encryption & decryption",
			Effect: "Allow",
			Principal: map[string][]string{
				"AWS": userRoles,
			},
			Action: []string{
				"kms:Encrypt",
				"kms:Decrypt",
				"kms:ReEncrypt*",
				"kms:GenerateDataKey*",
				"kms:DescribeKey",
			},
			Resource: "*",
		})
	}

	policyJSON, err := json.Marshal(policy)
	if err != nil {
		return "", fmt.Errorf("failed to marshal key policy: %w", err)
	}
	return string(policyJSON), nil
}

type Policy struct {
	Version   string      `json:"Version"`
	Statement []Statement `json:"Statement"`
}

// Statement represents an individual permission block
type Statement struct {
	Sid       string              `json:"Sid,omitempty"`
	Effect    string              `json:"Effect"`
	Principal map[string][]string `json:"Principal"`
	Action    []string            `json:"Action"`
	Resource  string              `json:"Resource"`
}
