package ack

import (
	"context"
	"fmt"
	"strings"

	ecsv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/ecs/v1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"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/appbox/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/namer"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/util"
)

const (
	// sgRefLabelKey is a label used for grouping SecurityGroupRule belong to a specific SecurityGroup
	sgRefLabelKey = "sg-reference"
)

type securityGroup struct {
	*task.BaseTask
}

func NewSecurityGroup(ctx *task.Context) deploy.Task {
	return &securityGroup{
		BaseTask: &task.BaseTask{Context: ctx},
	}
}

func (t *securityGroup) Name() string {
	return "SecurityGroup"
}

func (t *securityGroup) Create(ctx context.Context) deploy.Result {
	sgName := t.getSecurityGroupName()
	tags := controllers.ConvertToCloudTags(t.APPBox.Labels, consts.ProviderTypeACK)
	labels := t.APPBox.Labels
	sg := &ecsv1alpha1.SecurityGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name:   sgName,
			Labels: labels,
		},
		Spec: ecsv1alpha1.SecurityGroupSpec{
			ForProvider: ecsv1alpha1.SecurityGroupParameters{
				SecurityGroupName: ptr.To(sgName),
				Description:       ptr.To("Security group for api gateway"),
				Region:            ptr.To(t.APPBox.Spec.Region),
				VPCID:             ptr.To(t.VPC.Status.VPC.VPCID),
				Tags:              util.ToMapPointer(tags),
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
		},
	}
	if err := k8s.Apply(ctx, t.Local, sg); err != nil {
		return deploy.Panic(err)
	}

	// Reconcile security group rules using the "apply first, cleanup later" pattern
	// to prevent service interruption during configuration updates:
	//   - Desired rules are applied first to ensure new configurations are active
	//   - Stale rules are cleaned up after to remove obsolete configurations
	// This ensures continuous service availability when PublicAccessCidrs configuration
	// is updated (e.g., IPs are added or removed).
	desiredRules := t.buildSecurityGroupRules()
	desiredNames := make(map[string]bool, len(desiredRules))
	for _, rule := range desiredRules {
		desiredNames[rule.Name] = true
		if err := k8s.Apply(ctx, t.Local, &rule); err != nil {
			return deploy.Error(err)
		}
	}

	// Cleanup stale rules after desired rules are applied
	if err := t.reconcileSecurityGroupRules(ctx, desiredNames); err != nil {
		return deploy.Error(err)
	}

	return deploy.Done()
}

func (t *securityGroup) PostCreate(ctx context.Context) deploy.Result {
	sgName := t.getSecurityGroupName()
	sg := &ecsv1alpha1.SecurityGroup{
		ObjectMeta: metav1.ObjectMeta{Name: sgName},
	}
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(sg), sg); err != nil {
		return deploy.Error(err)
	}
	if !controllers.IsResourceReady(sg.Status.ResourceStatus) {
		return deploy.Error(fmt.Errorf("apigateway security group not ready: %s", sgName))
	}
	ruleList := &ecsv1alpha1.SecurityGroupRuleList{}
	matchLabels := client.MatchingLabels{sgRefLabelKey: sgName}
	if err := t.Local.List(ctx, ruleList, matchLabels); err != nil {
		return deploy.Error(err)
	}
	for _, rule := range ruleList.Items {
		if !controllers.IsResourceReady(rule.Status.ResourceStatus) {
			return deploy.Error(fmt.Errorf("security group rule not ready: %s", rule.Name))
		}
	}
	return deploy.Done()
}

func (t *securityGroup) Delete(ctx context.Context) deploy.Result {
	sgName := t.getSecurityGroupName()
	ruleList := &ecsv1alpha1.SecurityGroupRuleList{}
	matchLabels := client.MatchingLabels{sgRefLabelKey: sgName}
	if err := t.Local.List(ctx, ruleList, matchLabels); err != nil {
		return deploy.Error(err)
	}
	for _, rule := range ruleList.Items {
		if err := controllers.DeleteUpboundResource(ctx, t.Local, &rule); err != nil {
			return deploy.Error(err)
		}
	}
	sg := &ecsv1alpha1.SecurityGroup{
		ObjectMeta: metav1.ObjectMeta{Name: sgName},
	}
	return deploy.ErrorOrDone(controllers.DeleteUpboundResource(ctx, t.Local, sg))
}

func (t *securityGroup) getSecurityGroupName() string {
	return controllers.UniqueIdentifier(t.APPBox.ObjectMeta, namer.WithPrefix(t.Config.ResourceNamePrefix+"-apigateway"))
}

func (t *securityGroup) getSecurityGroupRuleName(cidr string, port int) string {
	ip := strings.Split(cidr, "/")[0] // remove the /xx suffix if exists, it'sip always /32 or /0
	sip := strings.ReplaceAll(ip, ".", "-")
	prefix := fmt.Sprintf("%s-apigateway-%s-%d", t.Config.ResourceNamePrefix, sip, port)
	return controllers.UniqueIdentifier(t.APPBox.ObjectMeta, namer.WithPrefix(prefix))
}

func (t *securityGroup) buildSecurityGroupRules() []ecsv1alpha1.SecurityGroupRule {
	sgName := t.getSecurityGroupName()
	labels := util.MergeMaps(t.APPBox.Labels, map[string]string{sgRefLabelKey: sgName})
	rules := []ecsv1alpha1.SecurityGroupRule{
		{ // default deny all with lower priority
			ObjectMeta: metav1.ObjectMeta{
				Name:   fmt.Sprintf("%s-deny", t.getSecurityGroupName()),
				Labels: labels,
			},
			Spec: ecsv1alpha1.SecurityGroupRuleSpec{
				ForProvider: ecsv1alpha1.SecurityGroupRuleParameters{
					SecurityGroupIDRef: &xpv1.Reference{Name: t.getSecurityGroupName()},
					CidrIP:             ptr.To("0.0.0.0/0"),
					Priority:           ptr.To(float64(100)),
					IPProtocol:         ptr.To("all"),
					Policy:             ptr.To("drop"),
					PortRange:          ptr.To("-1/-1"),
					Region:             ptr.To(t.APPBox.Spec.Region),
					Type:               ptr.To("ingress"),
				},
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
					ManagementPolicies:      controllers.GetResourceManagementPolicies(),
				},
			},
		},
	}
	for _, cidr := range t.Config.APPBoxConfig.ApiGateway.PublicAccessCidrs {
		rule1 := ecsv1alpha1.SecurityGroupRule{
			ObjectMeta: metav1.ObjectMeta{
				Name:   t.getSecurityGroupRuleName(cidr, 80),
				Labels: labels,
			},
			Spec: ecsv1alpha1.SecurityGroupRuleSpec{
				ForProvider: ecsv1alpha1.SecurityGroupRuleParameters{
					SecurityGroupIDRef: &xpv1.Reference{Name: t.getSecurityGroupName()},
					CidrIP:             ptr.To(cidr),
					IPProtocol:         ptr.To("tcp"),
					PortRange:          ptr.To("80/80"),
					Region:             ptr.To(t.APPBox.Spec.Region),
					Type:               ptr.To("ingress"),
				},
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
					ManagementPolicies:      controllers.GetResourceManagementPolicies(),
				},
			},
		}
		rule2 := ecsv1alpha1.SecurityGroupRule{
			ObjectMeta: metav1.ObjectMeta{
				Name:   t.getSecurityGroupRuleName(cidr, 443),
				Labels: labels,
			},
			Spec: ecsv1alpha1.SecurityGroupRuleSpec{
				ForProvider: ecsv1alpha1.SecurityGroupRuleParameters{
					SecurityGroupIDRef: &xpv1.Reference{Name: t.getSecurityGroupName()},
					CidrIP:             ptr.To(cidr),
					IPProtocol:         ptr.To("tcp"),
					PortRange:          ptr.To("443/443"),
					Region:             ptr.To(t.APPBox.Spec.Region),
					Type:               ptr.To("ingress"),
				},
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
					ManagementPolicies:      controllers.GetResourceManagementPolicies(),
				},
			},
		}
		rules = append(rules, rule1, rule2)
	}
	return rules
}

// reconcileSecurityGroupRules removes security group rules not in the desired set.
func (t *securityGroup) reconcileSecurityGroupRules(ctx context.Context, desiredNames map[string]bool) error {
	sgName := t.getSecurityGroupName()
	ruleList := &ecsv1alpha1.SecurityGroupRuleList{}
	opts := k8s.ListOpts[*ecsv1alpha1.SecurityGroupRule]{
		ObjectList: ruleList,
		Unpack: func() []*ecsv1alpha1.SecurityGroupRule {
			return k8s.MapPtr(ruleList.Items)
		},
	}

	return opts.ReconcileByDesiredNamesWithDelete(
		ctx,
		t.Local,
		desiredNames,
		controllers.DeleteUpboundResource,
		client.MatchingLabels{sgRefLabelKey: sgName},
	)
}
