package alicloud

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

	alikmsv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/kms/v1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	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"
)

// defaultPendingWindowInDays is the waiting period (in days) before a KMS key is
// permanently deleted after scheduling deletion. Required by the Alibaba Cloud
// Terraform provider; valid range is 7-366.
const defaultPendingWindowInDays = 7

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

	if t.Config.KMSConfig.InstanceID == "" {
		return deploy.Panic(errors.New("kmsConfig.instanceID is required for ACK CMEK but not configured"))
	}
	if t.Config.ControlPlaneCrossplaneProviderConfig == "" {
		return deploy.Panic(errors.New("controlPlaneCrossplaneProviderConfig is required for ACK CMEK but not configured"))
	}

	name := task.GenerateKeyName(t.Key, t.Config.ResourceNamePrefix)

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

	// AccountRef already points to the KMS account (set by service layer in CreateKey).
	// Node roles from data plane accounts are authorized via cross-account key policy.
	userRoles, buildUserRolesResult := t.Context.BuildTrustedNodeRoleARNs(ctx)

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

	tagsPtr := make(map[string]*string, len(tags))
	for k, v := range tags {
		tagsPtr[k] = ptr.To(v)
	}

	key := alikmsv1alpha1.Key{
		ObjectMeta: metav1.ObjectMeta{
			Name:   name,
			Labels: t.Key.Labels,
		},
		Spec: alikmsv1alpha1.KeySpec{
			ForProvider: alikmsv1alpha1.KeyParameters{
				Region:              ptr.To(t.Key.Spec.Region),
				Description:         ptr.To(fmt.Sprintf("KMS key for %s/%s", t.Key.Namespace, t.Key.Name)),
				KeySpec:             ptr.To("Aliyun_AES_256"),
				KeyUsage:            ptr.To("ENCRYPT/DECRYPT"),
				AutomaticRotation:   ptr.To("Enabled"),
				RotationInterval:    ptr.To("365d"),
				ProtectionLevel:     ptr.To("SOFTWARE"),
				DkmsInstanceID:      ptr.To(t.Config.KMSConfig.InstanceID),
				PendingWindowInDays: ptr.To(float64(defaultPendingWindowInDays)),
				Policy:              ptr.To(keyPolicy),
				Tags:                tagsPtr,
			},
			ResourceSpec: xpv1.ResourceSpec{
				// The key should be created in the same account as the control plane.
				ProviderConfigReference: &xpv1.Reference{Name: t.Config.ControlPlaneCrossplaneProviderConfig},
				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 := &alikmsv1alpha1.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)
	}

	// Use the full ACS ARN from AtProvider.Arn (e.g. "acs:kms:ap-southeast-1:account:key/key-xxx").
	// This matches the AWS key task which uses AtProvider.Arn for the full AWS ARN.
	// Downstream consumers (cse-ctl --cmek-id, getCmekId validation) require the full ARN.
	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 := &alikmsv1alpha1.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)}, &alikmsv1alpha1.Key{}))
}

// buildKeyPolicy creates an Aliyun KMS key policy granting trusted roles access.
// Policy format follows Aliyun KMS key policy specification:
// https://help.aliyun.com/zh/kms/key-management-service/security-and-compliance/overview-of-key-policies
func buildKeyPolicy(accountID string, userRoles []string) (string, error) {
	policy := Policy{
		Version: "1",
		Statement: []Statement{
			{ // Grant full KMS permissions to all RAM identities under the KMS account.
				// This covers the manager role and prevents the key from becoming unmanageable.
				Sid:    "Enable RAM Permissions",
				Effect: "Allow",
				Principal: map[string][]string{
					"RAM": {fmt.Sprintf("acs:ram::%s:*", accountID)},
				},
				Action:   []string{"kms:*"},
				Resource: []string{"*"},
			},
		},
	}

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

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

// Policy represents an Aliyun KMS key policy document.
type Policy struct {
	Version   string      `json:"Version"`
	Statement []Statement `json:"Statement"`
}

// Statement represents an individual permission block in the key policy.
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"`
}
