package gke

import (
	"context"
	"fmt"
	"time"

	gkev1beta2 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta2"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"google.golang.org/api/container/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/pointer"

	"github.com/tidbcloud/eks-provider/controllers/network/task"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/applier"
	"github.com/tidbcloud/infra-provider/pkg/applier/mutatefuncs"
	"github.com/tidbcloud/infra-provider/pkg/cloud/gcp"
	"github.com/tidbcloud/infra-provider/pkg/config"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/util"
)

const (
	gkeRequeueAfter                         = time.Minute
	maintenanceExclusionOperationAnnotation = "operation.container.cloud.pingcap.com/maintenance-exclusion"
	defaultMaintenanceExclusionName         = "tidbcloud"
)

type GKE struct {
	*task.BaseTask
	gcpCli gcp.Client
}

func NewGKE(ctx *task.Context, gcpCli gcp.Client) *GKE {
	return &GKE{
		BaseTask: &task.BaseTask{Context: ctx},
		gcpCli:   gcpCli,
	}
}

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

func (t *GKE) Create(ctx context.Context) deploy.Result {
	t.BeginAt = util.Pointer(time.Now())
	name := namer.UniqueIdentifier(t.Network.ObjectMeta)
	tags := cloudv1.TagsFromLabels(t.Network.Labels)

	endpointPublicAccess := true

	// We don't do version update in network controller
	// so use the static global GKE default version specified in configuration
	// version changes will be ignored for existing gke cluster by calling t.ignoreChanges
	version := t.Config.Network.GetK8sVersion(t.GetTenantID(), t.GetProjectID())
	if version == "" {
		return deploy.Panic(fmt.Errorf("no version specified in configuration"))
	}

	util.SetAnnotation(t.Network, consts.AnnotationKeyDataplaneName, t.gkeName())

	cluster := &gkev1beta2.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name:   t.gkeName(),
			Labels: t.Namer.Labels(),
		},
		Spec: gkev1beta2.ClusterSpec{
			ForProvider: gkev1beta2.ClusterParameters{
				Location:              t.Network.Spec.Region,
				InitialClusterVersion: pointer.String(version),
				ResourceLabels:        tags,
				NetworkRef:            &xpv1.Reference{Name: name},
				SubnetworkRef:         &xpv1.Reference{Name: fmt.Sprintf("%s-nodes", name)},
				DefaultMaxPodsConstraint: &gkev1beta2.MaxPodsConstraint{
					MaxPodsPerNode: *t.GKENetworkSpec.MaxPodsPerNode,
				},
				IPAllocationPolicy: &gkev1beta2.IPAllocationPolicy{
					ClusterIpv4CidrBlock:  pointer.String(t.GKECIDRDispatcher.PodIPv4CIDRBlock()),
					ServicesIpv4CidrBlock: pointer.String(t.GKECIDRDispatcher.ServiceIPv4CIDRBlock()),
					UseIPAliases:          pointer.Bool(true),
				},
				PrivateClusterConfig: &gkev1beta2.PrivateClusterConfigSpec{
					EnablePrivateEndpoint: pointer.Bool(true),
					EnablePrivateNodes:    pointer.Bool(true),
					MasterIpv4CidrBlock:   pointer.String(t.GKECIDRDispatcher.MasterIPv4CIDRBlock()),
				},
				MasterAuthorizedNetworksConfig: &gkev1beta2.MasterAuthorizedNetworksConfig{
					Enabled: pointer.Bool(endpointPublicAccess),
				},
				ReleaseChannel: &gkev1beta2.ReleaseChannel{
					// Auto-upgrade must be true in nodepool when it is set to others
					Channel: "UNSPECIFIED",
				},
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: name},
			},
		},
	}

	if config.FeatureEnabled(t.Config.FeatureGates, config.DisableGKELogging, t.GetTenantID(), t.GetProjectID()) {
		cluster.Spec.ForProvider.LoggingService = pointer.String("none")
	}
	if config.FeatureEnabled(t.Config.FeatureGates, config.DisableGKEMonitoring, t.GetTenantID(), t.GetProjectID()) {
		cluster.Spec.ForProvider.MonitoringService = pointer.String("none")
	}

	// crossplane-gcp will write-back MaintenancePolicy if the field in spec is empty and the real cluster has one.
	// Also, it has a bug(not set fingerprint when sending request) and can't update maintenance policy with spec.
	// To workaroud its write-back mechanism, set MaintenancePolicy in spec and send correct update request later with cli.
	if t.isConfigMaintenanceExclusion() {
		cluster.Spec.ForProvider.MaintenancePolicy = &gkev1beta2.MaintenancePolicySpec{
			Window: gkev1beta2.MaintenanceWindowSpec{
				MaintenanceExclusions: map[string]gkev1beta2.TimeWindow{
					// Only no_upgrades scope can be specified to no channel clusters
					defaultMaintenanceExclusionName: {
						StartTime: t.Config.Network.GKEMaintenanceExclusionTimeWindow.StartTime.Format(time.RFC3339),
						EndTime:   t.Config.Network.GKEMaintenanceExclusionTimeWindow.EndTime.Format(time.RFC3339),
					},
				},
			},
		}
	}

	if endpointPublicAccess {
		cidrs := []*gkev1beta2.CidrBlock{}
		for _, cidr := range t.Config.Network.ApiserverPublicAccessCidrs {
			cidrs = append(cidrs, &gkev1beta2.CidrBlock{
				CidrBlock: cidr,
			})
		}
		cluster.Spec.ForProvider.PrivateClusterConfig.EnablePrivateEndpoint = pointer.Bool(false)
		cluster.Spec.ForProvider.MasterAuthorizedNetworksConfig.CidrBlocks = cidrs
	}

	if err := t.ignoreChanges(ctx, cluster); err != nil {
		return deploy.Panic(err)
	}

	if _, err := applier.NewApplier(t.Local, mutatefuncs.MutateGKE).
		Expected(cluster).
		Apply(ctx); err != nil {
		return deploy.Panic(err)
	}

	return deploy.Done()
}

func (t *GKE) gkeName() string {
	return namer.LegacyBuildClusterNameByNetwork(t.Network)
}

func (t *GKE) isConfigMaintenanceExclusion() bool {
	if t.Config.Network.GKEMaintenanceExclusionTimeWindow.StartTime == nil ||
		t.Config.Network.GKEMaintenanceExclusionTimeWindow.EndTime == nil {
		return false
	}
	return true
}

// crossplane-gcp has a bug(not set fingerprint when sending request) to update maintenance policy, set it with gcp-client
func (t *GKE) setMaintenanceExclusion(ctx context.Context) error {
	// Validated in validateGKEMaintenanceExclusionConfig in cluster-controller/internal/cmd/common.go
	if !t.isConfigMaintenanceExclusion() {
		return nil
	}
	if opName := util.GetAnnotation(t.Network, maintenanceExclusionOperationAnnotation); len(opName) > 0 {
		done, err := t.gcpCli.IsContainerOperationDone(ctx, opName)
		if err != nil {
			return err
		}
		if !done {
			return fmt.Errorf("waiting gke operation done, operation: %s", opName)
		}
		// if done
		delete(t.Network.Annotations, maintenanceExclusionOperationAnnotation)
		return nil
	}

	opName, err := t.gcpCli.SetClusterMaintenanceExclusion(ctx,
		t.GKENetworkSpec.ProjectID,
		t.Network.Spec.Region,
		t.gkeName(),
		defaultMaintenanceExclusionName,
		&container.TimeWindow{
			StartTime: t.Config.Network.GKEMaintenanceExclusionTimeWindow.StartTime.Format(time.RFC3339),
			EndTime:   t.Config.Network.GKEMaintenanceExclusionTimeWindow.EndTime.Format(time.RFC3339),
			MaintenanceExclusionOptions: &container.MaintenanceExclusionOptions{
				// Only no_upgrades scope can be specified to no channel clusters
				Scope: "NO_UPGRADES",
			},
		},
	)
	if err != nil {
		return err
	}
	if len(opName) > 0 {
		util.SetAnnotation(t.Network, maintenanceExclusionOperationAnnotation, opName)
		return fmt.Errorf("waiting gke operation done, operation: %s", opName)
	}

	return nil
}

// ignore some fields changes for existing clusters
func (t *GKE) ignoreChanges(ctx context.Context, newCluster *gkev1beta2.Cluster) error {
	key := types.NamespacedName{
		Name: namer.LegacyBuildClusterNameByNetwork(t.Network),
	}
	var c gkev1beta2.Cluster
	err := t.Local.Get(ctx, key, &c)
	if err != nil {
		if apierrors.IsNotFound(err) {
			return nil
		}
		return err
	}
	// ignore gke version change
	// otherwise all gke clusters will be updated at the same time once we change the global version
	newCluster.Spec.ForProvider.InitialClusterVersion = c.Spec.ForProvider.InitialClusterVersion
	newCluster.Spec.ForProvider.LoggingService = c.Spec.ForProvider.LoggingService
	newCluster.Spec.ForProvider.MonitoringService = c.Spec.ForProvider.MonitoringService
	// ignore pod cidr change, see https://pingcap.feishu.cn/wiki/HbDlwu8GFitnHnkqtWFcG5JDnHe for more detail
	newCluster.Spec.ForProvider.IPAllocationPolicy.ClusterIpv4CidrBlock = c.Spec.ForProvider.IPAllocationPolicy.ClusterIpv4CidrBlock
	newCluster.Spec.ForProvider.IPAllocationPolicy.ServicesIpv4CidrBlock = c.Spec.ForProvider.IPAllocationPolicy.ServicesIpv4CidrBlock
	newCluster.Spec.ForProvider.PrivateClusterConfig.MasterIpv4CidrBlock = c.Spec.ForProvider.PrivateClusterConfig.MasterIpv4CidrBlock
	return nil
}

func (t *GKE) PostCreate(ctx context.Context) (res deploy.Result) {
	key := types.NamespacedName{Name: namer.LegacyBuildClusterNameByNetwork(t.Network)}
	var c gkev1beta2.Cluster
	err := t.Local.Get(ctx, key, &c)
	if err != nil {
		return deploy.Panic(err)
	}
	status := c.Status.AtProvider.Status
	if status != gkev1beta2.ClusterStateRunning {
		return deploy.Panic(fmt.Errorf("gke cluster %s status not ready: %v", c.Name, status)).WithRequeuAfter(gkeRequeueAfter)
	}
	if err := t.setMaintenanceExclusion(ctx); err != nil {
		return deploy.Error(fmt.Errorf("failed to set maintenance exclusion: %v", err))
	}
	t.Cluster = task.NewGKEClusterIdentifier(&c)
	return deploy.Done()
}

func (t *GKE) Delete(ctx context.Context) deploy.Result {
	cluster := &gkev1beta2.Cluster{
		ObjectMeta: metav1.ObjectMeta{
			Name: namer.LegacyBuildClusterNameByNetwork(t.Network),
		},
	}
	if err := k8s.DeleteObject(ctx, t.Local, cluster); err != nil {
		return deploy.Error(err)
	}
	if err := k8s.PatchObject(ctx, t.Local, cluster, k8s.RemoveCrossplaneExternalCreatePending, k8s.DeletionTrigger); err != nil {
		return deploy.Error(err)
	}
	return deploy.Done()
}

func (t *GKE) PostDelete(ctx context.Context) deploy.Result {
	key := types.NamespacedName{
		Name: namer.LegacyBuildClusterNameByNetwork(t.Network),
	}
	var c gkev1beta2.Cluster
	return deploy.ErrorOrDone(k8s.GetDeletedObject(ctx, t.Local, key, &c))
}
