package keyspacerole

import (
	"context"
	"encoding/json"
	"fmt"
	"regexp"
	"strings"

	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	dmpbv1 "github.com/tidbcloud/cluster-service-ng/api/tidbcloud/dataplane-manager/v1"
	bizerr "github.com/tidbcloud/infra-provider/regional-server/pkg/common/errors"
	"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/util"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/validation"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
	keyspaceNameLabelKey         = "tags.tidbcloud.com/keyspace"
	keyspaceRoleBucketAnnotation = "keyspacerole.tidbcloud.com/bucket-id"
	keyspaceRolePrefixAnnotation = "keyspacerole.tidbcloud.com/allowed-prefix"
	keyspaceRoleExtIDAnnotation  = "keyspacerole.tidbcloud.com/external-id"
	cloudRoleNameLimit           = 64
)

var allowedPrefixRegexp = regexp.MustCompile(`^[A-Za-z0-9._=-]+(?:/[A-Za-z0-9._=-]+)*$`)

func keyspaceRoleResponse(
	dataplaneID, keyspaceName, roleARN string,
	labels, annotations map[string]string,
	status dmpbv1.KeyspaceRole_Status,
	message string,
) *dmpbv1.KeyspaceRole {
	resp := &dmpbv1.KeyspaceRole{
		KeyspaceName: keyspaceName,
		RoleArn:      roleARN,
		Status:       status,
		Message:      message,
		Tags: map[string]string{
			consts.DataplaneLabelKey: dataplaneID,
			keyspaceNameLabelKey:     keyspaceName,
		},
	}
	resp.Tags = util.MergeMaps(resp.Tags, labels)
	if annotations != nil {
		resp.BucketId = annotations[keyspaceRoleBucketAnnotation]
		resp.AllowedPrefix = annotations[keyspaceRolePrefixAnnotation]
		resp.ExternalId = annotations[keyspaceRoleExtIDAnnotation]
	}
	return resp
}

type managedResource struct {
	name   string
	object client.Object
	status xpv1.ResourceStatus
}

func keyspaceRoleStatus(roleARN string, resources ...managedResource) (dmpbv1.KeyspaceRole_Status, string) {
	for _, resource := range resources {
		if resource.object.GetDeletionTimestamp() != nil {
			return dmpbv1.KeyspaceRole_Deleting, "keyspace role is being deleted"
		}
	}
	for _, resource := range resources {
		if failed, message := resourceFailed(resource.status, resource.name); failed {
			return dmpbv1.KeyspaceRole_Failed, message
		}
	}
	if roleARN == "" {
		return dmpbv1.KeyspaceRole_Creating, "keyspace role ARN is not ready"
	}
	for _, resource := range resources {
		if !controllers.IsResourceReady(resource.status) {
			return dmpbv1.KeyspaceRole_Creating, resource.name + " is not ready"
		}
	}
	return dmpbv1.KeyspaceRole_Ready, ""
}

func resourceFailed(status xpv1.ResourceStatus, fallback string) (bool, string) {
	condition := status.GetCondition(xpv1.TypeSynced)
	if condition.Status == corev1.ConditionFalse {
		if condition.Message != "" {
			return true, condition.Message
		}
		if condition.Reason != "" {
			return true, string(condition.Reason)
		}
		return true, fallback + " failed"
	}
	return false, ""
}

func isAWSProvider(provider string) bool {
	return provider == consts.ProviderTypeAWS || provider == consts.ProviderTypeEKS
}

func resourceName(prefix, keyspaceName string) string {
	prefix = strings.Trim(strings.TrimSpace(prefix), "-")
	if prefix == "" {
		prefix = "tidbcloud"
	}
	return fmt.Sprintf("%s-keyspace-%s", prefix, keyspaceName)
}

func validateRoleName(name string) error {
	if len(name) > cloudRoleNameLimit {
		return bizerr.New(bizerr.ParameterInvalid, fmt.Sprintf("generated keyspace role name must not exceed %d characters", cloudRoleNameLimit))
	}
	if errs := validation.IsDNS1123Subdomain(name); len(errs) > 0 {
		return bizerr.New(bizerr.ParameterInvalid, "generated keyspace role name is invalid: "+strings.Join(errs, "; "))
	}
	return nil
}

func rolePolicyName(roleName string) string {
	return roleName + "-policy"
}

func rolePolicyAttachmentName(roleName string) string {
	return roleName + "-policy-attachment"
}

func normalizeS3Prefix(prefix string) string {
	return strings.Trim(strings.TrimSpace(prefix), "/")
}

func validateAllowedPrefix(prefix string) (string, error) {
	prefix = normalizeS3Prefix(prefix)
	if prefix == "" {
		return "", bizerr.New(bizerr.ParameterInvalid, "keyspace_role.allowed_prefix is required")
	}
	if strings.ContainsAny(prefix, "*?") {
		return "", bizerr.New(bizerr.ParameterInvalid, "keyspace_role.allowed_prefix must not contain wildcard characters")
	}
	if !allowedPrefixRegexp.MatchString(prefix) {
		return "", bizerr.New(bizerr.ParameterInvalid, "keyspace_role.allowed_prefix contains unsupported characters")
	}
	return prefix, nil
}

func validateDataplaneOwner(labels map[string]string, dataplaneID string) error {
	if labels[consts.DataplaneLabelKey] != dataplaneID {
		return bizerr.New(bizerr.ResourceNotFound, "keyspace role not found")
	}
	return nil
}

func validateCreateDataplaneOwner(labels map[string]string, dataplaneID string) error {
	if labels[consts.DataplaneLabelKey] != dataplaneID {
		return bizerr.New(bizerr.ResourceConflict, "keyspace role already exists for another dataplane")
	}
	return nil
}

func validateExistingResourceOwner(ctx context.Context, local client.Client, obj client.Object, name, dataplaneID string) error {
	if err := local.Get(ctx, types.NamespacedName{Name: name}, obj); err != nil {
		if apierrors.IsNotFound(err) {
			return nil
		}
		return bizerr.Wrap(bizerr.K8SClientError, "get existing keyspace role resource error", err)
	}
	return validateCreateDataplaneOwner(obj.GetLabels(), dataplaneID)
}

func marshalPolicy(policy map[string]any) (string, error) {
	data, err := json.Marshal(policy)
	if err != nil {
		return "", err
	}
	return string(data), nil
}
