package ack

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

	ossv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/oss/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/bucket/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"
	"gomodules.xyz/jsonpatch/v2"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type BucketLifecycleRules struct {
	*task.BaseTask
}

func NewBucketLifecycleRules(ctx *task.Context) *BucketLifecycleRules {
	return &BucketLifecycleRules{BaseTask: &task.BaseTask{Context: ctx}}
}

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

func (t *BucketLifecycleRules) Create(ctx context.Context) deploy.Result {
	t.BeginAt = util.Pointer(time.Now())
	bucket := &ossv1alpha1.Bucket{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.Bucket.Status.Bucket.Name,
		},
	}
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(bucket), bucket); err != nil {
		return deploy.Error(fmt.Errorf("failed to get OSS Bucket resource: %w", err))
	}

	objectPatch := func(obj client.Object) *jsonpatch.Operation {
		return &jsonpatch.Operation{
			Operation: "replace",
			Path:      "/spec/forProvider/lifecycleRule",
			Value:     makeLifecycleRules(t.Bucket.Spec.GetAllLifecycleRules()),
		}
	}
	// Notice: we can not use Apply here because the lifecycle rule is a part of the bucket CR, not a standalone CR.
	//         If we use Apply, we will override the last apply configuration which is managed by bucket task.
	if err := k8s.PatchObject(ctx, t.Local, bucket, objectPatch); err != nil {
		return deploy.Error(fmt.Errorf("failed to patch OSS Bucket LifecycleRule: %w", err))
	}

	return deploy.Done()

}

func (t *BucketLifecycleRules) Delete(ctx context.Context) deploy.Result {
	// no delete logic since lifecycle rules is part of bucket CR, doesn't has a standalone CR
	return deploy.Done()
}

func makeLifecycleRules(rulesDef []v1alpha1.BucketLifecycleRule) []ossv1alpha1.LifecycleRuleParameters {
	rules := make([]ossv1alpha1.LifecycleRuleParameters, 0, len(rulesDef))
	// OSS rejects lifecycle rules that have the same filter and enabled state,
	// even when they configure different actions such as expiration and abort
	// multipart upload. Keep CSNG's action-level rules intact and collapse them
	// only in the ACK renderer so other providers can keep their own semantics.
	ruleIndexByFilter := map[string]int{}
	unmergeableRuleFilters := map[string]bool{}
	for _, def := range rulesDef {
		rule := makeLifecycleRule(def)
		key := lifecycleRuleFilterKey(def)
		if idx, ok := ruleIndexByFilter[key]; ok && !unmergeableRuleFilters[key] {
			if mergeLifecycleRuleActions(&rules[idx], rule) {
				continue
			}
			unmergeableRuleFilters[key] = true
		}

		if _, ok := ruleIndexByFilter[key]; !ok {
			ruleIndexByFilter[key] = len(rules)
		}
		rules = append(rules, rule)
	}
	return rules
}

func makeLifecycleRule(def v1alpha1.BucketLifecycleRule) ossv1alpha1.LifecycleRuleParameters {
	rule := ossv1alpha1.LifecycleRuleParameters{
		ID:      ptr.To(def.Id),
		Enabled: ptr.To(def.Enabled),
	}

	if def.PrefixFilter != "" {
		rule.Prefix = ptr.To(def.PrefixFilter)
	}

	if len(def.TagsFilter) > 0 {
		rule.Tags = make(map[string]*string)
		for _, tag := range def.TagsFilter {
			rule.Tags[tag.Key] = ptr.To(tag.Value)
		}
	}

	if def.ExpirationDays > 0 {
		rule.Expiration = []ossv1alpha1.ExpirationParameters{
			{Days: ptr.To(float64(def.ExpirationDays))},
		}
	}

	if def.IncompleteUploadExpirationDays > 0 {
		rule.AbortMultipartUpload = []ossv1alpha1.AbortMultipartUploadParameters{
			{Days: ptr.To(float64(def.IncompleteUploadExpirationDays))},
		}
	}

	return rule
}

func lifecycleRuleFilterKey(def v1alpha1.BucketLifecycleRule) string {
	// Enabled is included because OSS has only one Enabled value per lifecycle
	// rule. Merging mixed enabled states would implicitly enable or disable an
	// action-level rule and change the business intent.
	type tagFilter struct {
		Key   string `json:"key"`
		Value string `json:"value"`
	}
	type filterKey struct {
		Enabled bool        `json:"enabled"`
		Prefix  string      `json:"prefix"`
		Tags    []tagFilter `json:"tags"`
	}

	tags := make([]tagFilter, 0, len(def.TagsFilter))
	for _, tag := range def.TagsFilter {
		tags = append(tags, tagFilter{
			Key:   tag.Key,
			Value: tag.Value,
		})
	}
	sort.Slice(tags, func(i, j int) bool {
		if tags[i].Key == tags[j].Key {
			return tags[i].Value < tags[j].Value
		}
		return tags[i].Key < tags[j].Key
	})

	key, err := json.Marshal(filterKey{
		Enabled: def.Enabled,
		Prefix:  def.PrefixFilter,
		Tags:    tags,
	})
	if err != nil {
		// The key contains only strings and booleans, so marshaling should not fail.
		panic(fmt.Sprintf("marshal lifecycle rule filter key: %v", err))
	}
	return string(key)
}

func mergeLifecycleRuleActions(dst *ossv1alpha1.LifecycleRuleParameters, src ossv1alpha1.LifecycleRuleParameters) bool {
	// Only complementary actions can be merged safely. Same-action duplicates
	// may carry different rule IDs or day values, so keep them separate and let
	// OSS surface the unsupported duplicate-filter configuration.
	if len(dst.Expiration) > 0 && len(src.Expiration) > 0 {
		return false
	}
	if len(dst.AbortMultipartUpload) > 0 && len(src.AbortMultipartUpload) > 0 {
		return false
	}

	if len(src.Expiration) > 0 {
		dst.Expiration = src.Expiration
	}
	if len(src.AbortMultipartUpload) > 0 {
		dst.AbortMultipartUpload = src.AbortMultipartUpload
	}
	return true
}
