package eks

import (
	"context"
	"fmt"
	"strings"

	"k8s.io/utils/pointer"

	"github.com/aws/aws-sdk-go-v2/service/ec2"
	"github.com/aws/aws-sdk-go-v2/service/ec2/types"

	"github.com/tidbcloud/eks-provider/controllers/cluster/task"
	"github.com/tidbcloud/infra-provider/pkg/cloud/aws"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/util"
	"github.com/tidbcloud/infra-provider/util/collections"
)

// ServiceSG creates a security group used by public LBs.
type ServiceSG struct {
	*task.BaseTask
	awsCli       aws.Client
	ingressCIDRs []string
	egressCIDRs  []string
}

var _ deploy.Task = &ServiceSG{}

const (
	duplicateSGRuleError = "InvalidPermission.Duplicate"
	notFoundSGRuleError  = "InvalidGroup.NotFound"
)

func NewServiceSG(ctx *task.Context, awsCli aws.Client) *ServiceSG {
	res := &ServiceSG{BaseTask: &task.BaseTask{Context: ctx}, awsCli: awsCli}
	// allow vpc peering traffic
	res.ingressCIDRs = []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}
	res.egressCIDRs = []string{"0.0.0.0/0"}
	return res
}

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

// retrieveServiceSG returns the dedicated service security group for the cluster
func retrieveServiceSG(ctx context.Context, cli *ec2.Client, namer *namer.Namer) (*types.SecurityGroup, error) {
	// TODO: if hit EC2 API throttling, we could cache SG ID into cluster's annotation to minimize API calls
	res, err := cli.DescribeSecurityGroups(ctx, &ec2.DescribeSecurityGroupsInput{
		Filters: []types.Filter{
			{
				Name:   pointer.String("tag:Name"),
				Values: []string{namer.GetPublicServiceSGName()},
			},
		},
	})
	if err != nil {
		return nil, err
	}
	if len(res.SecurityGroups) == 0 {
		return nil, nil
	}
	if len(res.SecurityGroups) > 1 {
		return nil, fmt.Errorf("found %d service SG with name %s", len(res.SecurityGroups), namer.GetPublicServiceSGName())
	}
	return &res.SecurityGroups[0], nil
}

func (t *ServiceSG) Create(ctx context.Context) deploy.Result {
	// skip if dedicarted SG feature disabled
	if !t.Cluster.FeatureEnabled(consts.ClusterFeatureDedicatedSG) {
		return deploy.Done()
	}
	// since LB controller needs to add ingress rules to this SG, we cannot use crossplane objects
	// otherwise the rules created by LB controller could be reverted by crossplane
	ec2cli := t.awsCli.EC2(t.GetAWSAssumedRoleForSDK())
	sg, err := retrieveServiceSG(ctx, ec2cli, t.Namer)
	if err != nil {
		return deploy.Error(err)
	}
	// create new sg if doesn't exist
	if sg == nil {
		if _, err := ec2cli.CreateSecurityGroup(ctx, &ec2.CreateSecurityGroupInput{
			Description: pointer.String(fmt.Sprintf("dedicated sg for public svc for cluster %s", t.Cluster.Name)),
			GroupName:   pointer.String(t.Namer.GetPublicServiceSGName()),
			VpcId:       pointer.String(t.EnvProvider.GetNetwork()),
			TagSpecifications: []types.TagSpecification{
				{
					ResourceType: types.ResourceTypeSecurityGroup,
					Tags: util.GetAWSEC2Tags(
						collections.MergeMaps(t.Cluster.Labels, map[string]string{"Name": t.Namer.GetPublicServiceSGName()}),
					),
				},
			},
			DryRun: &t.Config.DryRun.Enabled,
		}); err != nil {
			return deploy.Panic(err)
		}
		// trigger immediate retry to add ingress rules
		return deploy.Panic(nil).WithRequeuAfter(0)
	}

	if t.isUpToDate(sg) {
		return deploy.Done()
	}

	// update ingress rules
	sgID := sg.GroupId
	ipProtocol := "-1"
	// add ingress rules
	ingressRules := &ec2.AuthorizeSecurityGroupIngressInput{
		GroupId:       sgID,
		IpPermissions: []types.IpPermission{{}},
		DryRun:        &t.Config.DryRun.Enabled,
	}
	ingressRules.IpPermissions[0].IpProtocol = &ipProtocol
	for _, cidr := range t.ingressCIDRs {
		ingressRules.IpPermissions[0].IpRanges = append(ingressRules.IpPermissions[0].IpRanges, types.IpRange{CidrIp: pointer.String(cidr)})
	}
	if _, err := ec2cli.AuthorizeSecurityGroupIngress(ctx, ingressRules); err != nil && !strings.Contains(err.Error(), duplicateSGRuleError) {
		return deploy.Error(fmt.Errorf("failed authorize security group ingress: %v", err))
	}
	// add egress rules
	egressRules := &ec2.AuthorizeSecurityGroupEgressInput{
		GroupId:       sgID,
		IpPermissions: []types.IpPermission{{}},
		DryRun:        &t.Config.DryRun.Enabled,
	}
	egressRules.IpPermissions[0].IpProtocol = &ipProtocol
	for _, cidr := range t.egressCIDRs {
		egressRules.IpPermissions[0].IpRanges = append(egressRules.IpPermissions[0].IpRanges, types.IpRange{CidrIp: pointer.String(cidr)})
	}
	if _, err := ec2cli.AuthorizeSecurityGroupEgress(ctx, egressRules); err != nil && !strings.Contains(err.Error(), duplicateSGRuleError) {
		return deploy.Error(fmt.Errorf("failed authorize security group egress: %v", err))
	}
	return deploy.Done()
}

func (t *ServiceSG) isUpToDate(sg *types.SecurityGroup) bool {
	for _, ingressCIDR := range t.ingressCIDRs {
		found := false
		for _, ingress := range sg.IpPermissions {
			if ingress.IpProtocol == nil || *ingress.IpProtocol != "-1" {
				continue
			}

			for _, cidr := range ingress.IpRanges {
				if cidr.CidrIp == nil || *cidr.CidrIp != ingressCIDR {
					continue
				}

				found = true
				break
			}

			if found {
				break
			}
		}
		if !found {
			return false
		}
	}

	for _, egressCIDR := range t.egressCIDRs {
		found := false
		for _, egress := range sg.IpPermissionsEgress {
			if egress.IpProtocol == nil || *egress.IpProtocol != "-1" {
				continue
			}

			for _, cidr := range egress.IpRanges {
				if cidr.CidrIp == nil || *cidr.CidrIp != egressCIDR {
					continue
				}

				found = true
				break
			}

			if found {
				break
			}
		}
		if !found {
			return false
		}
	}

	return true
}

func (t *ServiceSG) PostCreate(ctx context.Context) deploy.Result {
	// skip if dedicarted SG feature disabled
	if !t.Cluster.FeatureEnabled(consts.ClusterFeatureDedicatedSG) {
		return deploy.Done()
	}
	ec2cli := t.awsCli.EC2(t.GetAWSAssumedRoleForSDK())
	sg, err := retrieveServiceSG(ctx, ec2cli, t.Namer)
	if err != nil {
		return deploy.Error(err)
	}
	if sg == nil || len(sg.IpPermissions) == 0 || len(sg.IpPermissionsEgress) == 0 {
		return deploy.Error(fmt.Errorf("service group creation for cluster %s not ready", t.Cluster.Name))
	}
	return deploy.Done()
}

func (t *ServiceSG) Delete(ctx context.Context) deploy.Result {
	// skip if dedicarted SG feature disabled
	if !t.Cluster.FeatureEnabled(consts.ClusterFeatureDedicatedSG) {
		return deploy.Done()
	}
	ec2cli := t.awsCli.EC2(t.GetAWSAssumedRoleForSDK())
	sg, err := retrieveServiceSG(ctx, ec2cli, t.Namer)
	if err != nil {
		return deploy.Error(err)
	}
	if sg == nil {
		return deploy.Done()
	}
	if _, err := ec2cli.DeleteSecurityGroup(ctx, &ec2.DeleteSecurityGroupInput{
		GroupId: sg.GroupId,
	}); err != nil && !strings.Contains(err.Error(), notFoundSGRuleError) {
		return deploy.Error(err)
	}
	return deploy.Done()
}
