package ack

import (
	"context"
	"fmt"
	"strings"

	cs "github.com/alibabacloud-go/cs-20151215/v5/client"
	ackv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/ack/v1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/cloud/aliyun"
	"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/util"
)

const (
	// ackRefLabelKey is a label used for grouping resources belong to a specific ack cluster
	ackRefLabelKey = "ack-reference"
)

// bootstrapAddons is the addons that must be installed at ACK cluster bootstrap
var bootstrapAddons = map[string]struct{}{
	"kube-apiserver":           {},
	"cloud-controller-manager": {},
	"managed-kube-proxy":       {},
	"kube-controller-manager":  {},
	"ack-scheduler":            {},
	"ack-goatscaler":           {},
	"ack-ram-authenticator":    {},
	"terway-controlplane":      {}, // CNI needs to be installed at bootstrap
	"terway-eniip":             {},
}

// addonsWithoutVersion is the addons that should not specify version
// to allow ACK to use the version that matches the cluster version
var addonsWithoutVersion = map[string]struct{}{
	"kube-apiserver":          {},
	"kube-controller-manager": {},
}

type ack struct {
	*task.BaseTask
	aliyunCli aliyun.Client
}

func NewACK(ctx *task.Context, aliyunCli aliyun.Client) deploy.Task {
	return &ack{
		BaseTask:  &task.BaseTask{Context: ctx},
		aliyunCli: aliyunCli,
	}
}

func (t *ack) Name() string {
	return "ACK"
}

// controlPlaneVswitches determines which vswitches to use for ACK control plane.
//
// Background: ACK clusters use Classic Load Balancer (CLB) for API server access.
// When IPv4Gateway is enabled in the VPC, the CLB must be in a public vswitch.
// This is required in most Alibaba Cloud regions and will be universal in the future.
//
// Behavior:
// - Existing clusters: Preserve existing configuration (immutable, protects production)
// - New clusters: Use public vswitches (PublicSubnets) for all regions
//
// Pod vswitches (PodVswitchIds) always use NodeSubnets regardless.
func (t *ack) controlPlaneVswitches(ctx context.Context) ([]string, error) {
	// Check if cluster already exists (immutability protection)
	ackName := t.K8sName()
	cluster := &ackv1alpha1.ManagedKubernetes{
		ObjectMeta: metav1.ObjectMeta{Name: ackName},
	}

	err := t.Local.Get(ctx, client.ObjectKeyFromObject(cluster), cluster)

	if err == nil {
		// Cluster exists - preserve existing vswitches (immutable)
		// This protects production clusters in ap-southeast-1
		t.Logger.Info("ACK cluster exists, preserving existing vswitch configuration")
		vswitches := []string{}
		for _, vswitch := range cluster.Spec.ForProvider.VswitchIds {
			if vswitch != nil {
				vswitches = append(vswitches, *vswitch)
			}
		}
		return vswitches, nil
	} else if !apierrors.IsNotFound(err) {
		return nil, fmt.Errorf("failed to check existing cluster: %w", err)
	}

	// New cluster - always use public vswitches
	t.Logger.Info("Creating new ACK cluster with public vswitches for control plane")

	vswitches := []string{}
	for _, subnet := range t.VPC.Status.PublicSubnets {
		if subnet.SubnetID == "" {
			return nil, fmt.Errorf("public subnet %s id is empty", subnet.Name)
		}
		vswitches = append(vswitches, subnet.SubnetID)
	}

	if len(vswitches) == 0 {
		return nil, fmt.Errorf("no public subnets available for control plane")
	}

	return vswitches, nil
}

func (t *ack) Create(ctx context.Context) deploy.Result {
	ackName := t.K8sName()
	providerConfigName := t.ProviderConfigName()
	tags := controllers.ConvertToCloudTags(t.APPBox.Labels, consts.ProviderTypeACK)
	tags = util.MergeMaps(tags, t.Config.AdditionalCloudTags)
	version := t.APPBox.Spec.KubernetesConfig.Version

	// Resolve the full ACK version
	fullVersion, err := t.resolveACKVersion(ctx, version)
	if err != nil {
		return deploy.Panic(fmt.Errorf("failed to resolve ACK version: %w", err))
	}
	t.Logger.Info("Resolved ACK version", "requested", version, "resolved", fullVersion)

	labels := util.MergeMaps(t.Namer.Labels(), t.APPBox.Labels)
	labels = util.MergeMaps(labels, tags)

	// Pod vswitches - always use node subnets
	podVswitches := []string{}
	for _, subnet := range t.VPC.Status.NodeSubnets {
		if subnet.SubnetID == "" {
			return deploy.Panic(fmt.Errorf("node subnet %s id is empty", subnet.Name))
		}
		podVswitches = append(podVswitches, subnet.SubnetID)
	}

	// Control plane vswitches - existence-based selection (public for new, preserve for existing)
	controlPlaneVswitches, err := t.controlPlaneVswitches(ctx)
	if err != nil {
		return deploy.Panic(fmt.Errorf("failed to determine control plane vswitches: %w", err))
	}

	serviceCidr := t.APPBox.Spec.KubernetesConfig.ServiceCidr
	if serviceCidr == "" {
		serviceCidr = t.Config.APPBoxConfig.K8sServiceCidr
	}
	if serviceCidr == "" {
		t.Logger.Info("No service CIDR is configured, use 172.20.0.0/16 as default value")
		serviceCidr = "172.20.0.0/16"
	}
	var terwayConfigured bool
	addons := []ackv1alpha1.ManagedKubernetesAddonsInitParameters{}
	for _, conf := range t.AddonTmplConfig.ACK[t.APPBox.Spec.KubernetesConfig.Version] {
		if conf.AddonName == "terway-eniip" {
			terwayConfigured = true
		}
		if _, ok := bootstrapAddons[conf.AddonName]; !ok {
			continue
		}
		addon := ackv1alpha1.ManagedKubernetesAddonsInitParameters{
			Name:   &conf.AddonName,
			Config: conf.Values,
		}
		if _, skip := addonsWithoutVersion[conf.AddonName]; !skip {
			addon.Version = &conf.AddonVersion
		}
		addons = append(addons, addon)
	}
	if !terwayConfigured { // ensure terway-eniip always installed at bootstrap
		addons = append(addons, ackv1alpha1.ManagedKubernetesAddonsInitParameters{Name: ptr.To("terway-eniip")})
	}
	cluster := &ackv1alpha1.ManagedKubernetes{
		ObjectMeta: metav1.ObjectMeta{
			Name:   ackName,
			Labels: labels,
		},
		Spec: ackv1alpha1.ManagedKubernetesSpec{
			InitProvider: ackv1alpha1.ManagedKubernetesInitParameters{
				ServiceCidr: ptr.To(serviceCidr), // Set service CIDR in initProvider since it's immutable
				Addons:      addons,
			},
			ForProvider: ackv1alpha1.ManagedKubernetesParameters{
				Name:               ptr.To(ackName),
				ClusterSpec:        ptr.To("ack.pro.small"),
				ProxyMode:          ptr.To("ipvs"),
				NodeCidrMask:       ptr.To(float64(28)),                        // max-pods 2^(32-28)-1
				NewNATGateway:      ptr.To(false),                              // we manually manage the NAT
				PodVswitchIds:      util.ToSlicePointer(podVswitches),          // use node vswitches for pods
				VswitchIds:         util.ToSlicePointer(controlPlaneVswitches), // control plane: public for new, preserve for existing
				Region:             ptr.To(t.APPBox.Spec.Region),
				Version:            ptr.To(fullVersion),
				SlbInternetEnabled: ptr.To(true), // ACL will be added after cluster is created
				Tags:               util.ToMapPointer(tags),
				// It will create a OIDC issuer, then we can use the service account token to do cross-cloud authentication.
				EnableRrsa: ptr.To(true),
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: providerConfigName},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
		},
	}
	return deploy.PanicOrDone(k8s.Apply(ctx, t.Local, cluster))
}

func (t *ack) PostCreate(ctx context.Context) deploy.Result {
	ackName := t.K8sName()
	cluster := &ackv1alpha1.ManagedKubernetes{
		ObjectMeta: metav1.ObjectMeta{
			Name: ackName,
		},
	}
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(cluster), cluster); err != nil {
		return deploy.Panic(err)
	}

	t.Cluster = task.NewACKClusterIdentifier(cluster)
	t.APPBox.Status.KubernetesClusterName = ptr.Deref(cluster.Status.AtProvider.Name, "")
	t.APPBox.Status.KubernetesClusterID = ptr.Deref(cluster.Status.AtProvider.ID, "")

	// Log vswitch configuration for troubleshooting
	if len(cluster.Spec.ForProvider.VswitchIds) > 0 {
		vswitchIDs := []string{}
		for _, v := range cluster.Spec.ForProvider.VswitchIds {
			if v != nil {
				vswitchIDs = append(vswitchIDs, *v)
			}
		}
		t.Logger.Info("ACK cluster control plane configuration",
			"controlPlaneVswitches", vswitchIDs)
	}

	condition := cluster.Status.GetCondition(xpv1.TypeReady)
	if condition.Type == xpv1.TypeReady {
		if condition.Status != corev1.ConditionTrue {
			t.APPBox.Status.Conditions.SetCondition(metav1.Condition{
				Type:               v1alpha1.APPBoxConditionK8SReady,
				Status:             metav1.ConditionStatus(condition.Status),
				Reason:             "ACKClusterNotReady",
				Message:            "ACK cluster is not ready",
				LastTransitionTime: metav1.Now(),
			})
			return deploy.Error(fmt.Errorf("ack cluster status not ready: %s", condition.Message))
		}
		t.APPBox.Status.Conditions.SetCondition(metav1.Condition{
			Type:               v1alpha1.APPBoxConditionK8SReady,
			Status:             metav1.ConditionTrue,
			Reason:             "ACKClusterReady",
			Message:            "ACK cluster is ready",
			LastTransitionTime: metav1.Now(),
		})
		return deploy.Done()
	}
	return deploy.Panic(fmt.Errorf("ACK cluster is not ready"))
}

func (t *ack) Delete(ctx context.Context) deploy.Result {
	ackName := t.K8sName()
	cluster := &ackv1alpha1.ManagedKubernetes{
		ObjectMeta: metav1.ObjectMeta{
			Name: ackName,
		},
	}
	return deploy.PanicOrDone(controllers.DeleteUpboundResource(ctx, t.Local, cluster))
}

func (t *ack) PostDelete(ctx context.Context) deploy.Result {
	key := types.NamespacedName{
		Name: t.K8sName(),
	}
	var cluster ackv1alpha1.ManagedKubernetes
	return deploy.ErrorOrDone(k8s.GetDeletedObject(ctx, t.Local, key, &cluster))
}

// resolveACKVersion resolves the full ACK version based on the user-provided version string
// If the version is already complete (e.g., "1.34.3-aliyun.1"), it returns it directly
// Otherwise, it queries available versions and matches the latest one with the same major.minor prefix
func (t *ack) resolveACKVersion(ctx context.Context, requestedVersion string) (string, error) {
	// If version looks complete (contains "-aliyun."), use it directly
	if strings.Contains(requestedVersion, "-aliyun.") {
		return requestedVersion, nil
	}

	// Get all available ACK versions
	metadata, err := t.getKubernetesVersionMetadata(ctx, "ManagedKubernetes", "", "creatable")
	if err != nil {
		return "", fmt.Errorf("failed to get kubernetes version metadata: %w", err)
	}

	// Extract all available versions
	var availableVersions []string
	for _, meta := range metadata {
		if meta.Version != nil && meta.Creatable != nil && *meta.Creatable {
			availableVersions = append(availableVersions, *meta.Version)
		}
	}

	if len(availableVersions) == 0 {
		return "", fmt.Errorf("no available ACK versions found")
	}

	// Match the requested version prefix with available versions
	// Find the first matching version (versions are usually returned in descending order)
	for _, version := range availableVersions {
		if strings.HasPrefix(version, requestedVersion+".") || strings.HasPrefix(version, requestedVersion+"-") {
			return version, nil
		}
	}

	return "", fmt.Errorf("no matching ACK version found for requested version %s, available versions: %v", requestedVersion, availableVersions)
}

// getKubernetesVersionMetadata retrieves available ACK version metadata from Alibaba Cloud CS service
func (t *ack) getKubernetesVersionMetadata(_ context.Context, clusterType, kubernetesVersion, mode string) ([]*cs.DescribeKubernetesVersionMetadataResponseBody, error) {
	assumedRole := aliyun.AssumedRole{
		RoleARN: fmt.Sprintf("acs:ram::%s:role/%s", t.VPC.Spec.AccountRef.AccountID, t.Config.DataPlaneRoleName),
	}

	csCli, err := t.aliyunCli.CS(&assumedRole, aliyun.WithRegion(t.APPBox.Spec.Region))
	if err != nil {
		return nil, err
	}

	req := &cs.DescribeKubernetesVersionMetadataRequest{
		Region:      ptr.To(t.APPBox.Spec.Region),
		ClusterType: ptr.To(clusterType),
	}
	if kubernetesVersion != "" {
		req.KubernetesVersion = ptr.To(kubernetesVersion)
	}
	if mode != "" {
		req.Mode = ptr.To(mode)
	}

	resp, err := csCli.DescribeKubernetesVersionMetadata(req)
	if err != nil {
		return nil, err
	}

	return resp.Body, nil
}
