package task

import (
	"context"
	"fmt"
	"strings"
	"time"
	"unicode"

	"github.com/go-logr/logr"
	"github.com/pingcap/errors"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/config"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/deploy"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/namer"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/intstr"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

const defaultTiDBPort = 4000

type Context struct {
	Local        client.Client
	Logger       logr.Logger
	BeginAt      *time.Time
	Namer        *namer.Namer
	LoadBalancer *v1alpha1.LoadBalancer
	DataPlane    *v1alpha1.DataPlane
	AppBox       *v1alpha1.APPBox
	VPC          *v1alpha1.VPC
	Manager      *controllers.Manager

	Config config.RootConfig
}

var _ deploy.Task = &BaseTask{}

func (c *Context) Default() (*Context, error) {
	return c, nil
}

type BaseTask struct {
	*Context
}

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

func (t *BaseTask) Category() deploy.TaskCategory {
	return deploy.ClusterTask
}

func (t *BaseTask) Create(ctx context.Context) deploy.Result {
	return deploy.Done()
}

func (t *BaseTask) PostCreate(ctx context.Context) deploy.Result {
	return deploy.Done()
}

func (t *BaseTask) Delete(ctx context.Context) deploy.Result {
	return deploy.Done()
}

func (t *BaseTask) PostDelete(ctx context.Context) deploy.Result {
	return deploy.Done()
}

func (t *BaseTask) MetricsStartTime() *time.Time {
	return t.BeginAt
}

func (t *BaseTask) EffectiveCloudTags(provider string) map[string]string {
	var dataPlaneTags map[string]string
	if t.DataPlane != nil {
		dataPlaneTags = t.DataPlane.Spec.CloudTags
	}
	return controllers.EffectiveCloudTags(
		provider,
		t.Config.AdditionalCloudTags,
		t.LoadBalancer.Labels,
		t.LoadBalancer.Spec.Tags,
		dataPlaneTags,
	)
}

func (t *BaseTask) GetRemoteClient(ctx context.Context) (client.Client, error) {
	remote, err := t.Manager.ClientGetter(ctx, client.ObjectKeyFromObject(t.AppBox))
	if err != nil {
		return nil, errors.Trace(err)
	}
	cli, err := remote.GetClient(ctx)
	if err != nil {
		return nil, errors.Trace(err)
	}
	return cli, nil
}

func (t *BaseTask) GetRemoteAPIReader(ctx context.Context) (client.Reader, error) {
	remote, err := t.Manager.APIReaderGetter(ctx, client.ObjectKeyFromObject(t.AppBox))
	if err != nil {
		return nil, errors.Trace(err)
	}
	reader, err := remote.GetAPIReader(ctx)
	if err != nil {
		return nil, errors.Trace(err)
	}
	return reader, nil
}

func (t *BaseTask) BuildBasicService() *corev1.Service {
	svc := t.BuildServiceMeta(t.LoadBalancer)
	ports, err := buildServicePorts(&t.LoadBalancer.Spec)
	if err != nil {
		// Input is normally validated at the gRPC handler, so reaching this
		// branch means the CR was created out-of-band. Fall through with an
		// empty Ports list; the K8s API server rejects the apply and the
		// reconcile loop surfaces the error through its normal retry path.
		t.Logger.Info("build service ports failed, producing empty ports", "error", err.Error())
	}
	svc.Spec = corev1.ServiceSpec{
		Type:                     corev1.ServiceTypeLoadBalancer,
		SessionAffinity:          corev1.ServiceAffinityNone,
		ExternalTrafficPolicy:    corev1.ServiceExternalTrafficPolicyTypeLocal,
		PublishNotReadyAddresses: t.LoadBalancer.Spec.GracefulUnhealthyEnabled,
		Ports:                    ports,
		LoadBalancerSourceRanges: t.LoadBalancer.Spec.IpWhiteList,
		Selector:                 t.LoadBalancer.Spec.Selector,
	}
	return svc
}

// buildServicePorts materializes the LoadBalancerSpec.Ports / .Port contract.
//
//   - Spec.Ports non-empty: one ServicePort per entry; TargetPort defaults to
//     Port when zero; Protocol empty or "TLS" normalizes to TCP (K8s
//     ServicePort.protocol only accepts TCP / UDP / SCTP). Multi-port
//     entries (len > 1) require a non-empty unique Name (K8s rule).
//   - Spec.Ports empty and Spec.Port != 0: legacy byte-for-byte single-port
//     layout (name "mysql-client", targetPort defaultTiDBPort=4000, TCP).
//   - Both empty, or any per-port entry with Port <= 0, duplicate listener
//     ports, or duplicate / missing names in multi-port mode: validation
//     error.
//
// Spec.Port is ignored when Spec.Ports is non-empty; gRPC handlers must reject
// the mixed input, so reaching this function with both set indicates the CR
// was created out-of-band.
func buildServicePorts(spec *v1alpha1.LoadBalancerSpec) ([]corev1.ServicePort, error) {
	if len(spec.Ports) > 0 {
		out := make([]corev1.ServicePort, 0, len(spec.Ports))
		seenPorts := make(map[int32]struct{}, len(spec.Ports))
		seenNames := make(map[string]struct{}, len(spec.Ports))
		multiPort := len(spec.Ports) > 1
		for i, p := range spec.Ports {
			if p.Port <= 0 {
				return nil, fmt.Errorf("loadbalancer: spec.ports[%d].port must be > 0", i)
			}
			if _, dup := seenPorts[p.Port]; dup {
				return nil, fmt.Errorf("loadbalancer: spec.ports contains duplicate port %d", p.Port)
			}
			seenPorts[p.Port] = struct{}{}
			if multiPort {
				if p.Name == "" {
					return nil, fmt.Errorf("loadbalancer: spec.ports[%d].name is required for multi-port LoadBalancers", i)
				}
				if _, dup := seenNames[p.Name]; dup {
					return nil, fmt.Errorf("loadbalancer: spec.ports contains duplicate name %q", p.Name)
				}
				seenNames[p.Name] = struct{}{}
			}
			target := p.TargetPort
			if target == 0 {
				target = p.Port
			}
			out = append(out, corev1.ServicePort{
				Name:       p.Name,
				Port:       p.Port,
				TargetPort: intstr.FromInt32(target),
				Protocol:   normalizeServicePortProtocol(p.Protocol),
			})
		}
		return out, nil
	}
	if spec.Port != 0 {
		return []corev1.ServicePort{
			{
				Name:       "mysql-client",
				Port:       spec.Port,
				TargetPort: intstr.FromInt32(defaultTiDBPort),
				Protocol:   corev1.ProtocolTCP,
			},
		}, nil
	}
	return nil, errors.New("loadbalancer: neither spec.port nor spec.ports is set")
}

// normalizeServicePortProtocol maps LoadBalancerPort.Protocol to the subset
// accepted by K8s ServicePort.protocol. TLS is an LB-listener concept; at the
// K8s Service level the backend remains TCP, so TLS (and the empty string)
// collapse to TCP.
func normalizeServicePortProtocol(proto string) corev1.Protocol {
	switch strings.ToUpper(proto) {
	case "", "TCP", "TLS":
		return corev1.ProtocolTCP
	case "UDP":
		return corev1.ProtocolUDP
	case "SCTP":
		return corev1.ProtocolSCTP
	default:
		return corev1.ProtocolTCP
	}
}

func (t *BaseTask) BuildServiceMeta(lb *v1alpha1.LoadBalancer) *corev1.Service {
	return &corev1.Service{
		ObjectMeta: metav1.ObjectMeta{
			Name:        getServiceName(lb),
			Namespace:   lb.Spec.Namespace,
			Labels:      map[string]string{},
			Annotations: map[string]string{},
		},
	}
}

func (t *BaseTask) GetNLBName(prefix string, meta metav1.ObjectMeta) string {
	return controllers.UniqueIdentifier(meta, namer.WithPrefix(prefix), namer.WithLimit(32))
}

func (t *BaseTask) GetDNSName(lb *v1alpha1.LoadBalancer, publicHostedZoneDomain string) string {
	prefix := t.GetDNSNamePrefix(lb)
	return fmt.Sprintf("%s.%s", prefix, publicHostedZoneDomain)
}

func (t *BaseTask) GetDNSNamePrefix(lb *v1alpha1.LoadBalancer) string {
	prefix := "tidb"
	if lb.Spec.SubIdentity != "" {
		prefix = prefix + "-" + lb.Spec.SubIdentity
	}
	if lb.Spec.Type == v1alpha1.PublicLoadBalancerType {
		return getPublicDNSPrefix(prefix, lb.Spec.Identity)
	} else {
		return getPrivateDNSNamePrefix(prefix, lb.Spec.Identity)
	}
}

func (t *BaseTask) ProviderConfigName() string {
	return t.VPC.Spec.AccountRef.ProviderConfigName
}

func getPublicDNSPrefix(prefix, identity string) string {
	return fmt.Sprintf("%s.%s", prefix, identity)
}

func getPrivateDNSNamePrefix(prefix, identity string) string {
	return fmt.Sprintf("private-%s", getPublicDNSPrefix(prefix, identity))
}

func getServiceName(lb *v1alpha1.LoadBalancer) string {
	if lb.Spec.Type == v1alpha1.PublicLoadBalancerType {
		return getPublicServiceName(lb)
	}
	return getPrivateServiceName(lb)
}

func getPublicServiceName(lb *v1alpha1.LoadBalancer) string {
	if lb.Spec.SubIdentity != "" {
		return fmt.Sprintf("%s-%s-tidb-public", normalizeK8sServiceNamePrefix(lb.Spec.SubIdentity), lb.Spec.Identity)
	}
	return fmt.Sprintf("%s-tidb-public", lb.Spec.Identity)
}

func getPrivateServiceName(lb *v1alpha1.LoadBalancer) string {
	if lb.Spec.SubIdentity != "" {
		return fmt.Sprintf("%s-%s-tidb-private", normalizeK8sServiceNamePrefix(lb.Spec.SubIdentity), lb.Spec.Identity)
	}
	return fmt.Sprintf("%s-tidb-private", lb.Spec.Identity)
}

func normalizeK8sServiceNamePrefix(name string) string {
	r := rune(name[0])
	if unicode.IsDigit(r) {
		return "s-" + name
	}
	return name
}
