package cluster

import (
	"context"
	"crypto/tls"
	"fmt"
	"net/http"
	"time"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"

	opv1alpha1 "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"

	clusterctrl "github.com/tidbcloud/eks-provider/controllers/cluster"
	"github.com/tidbcloud/eks-provider/controllers/utils/configmanager"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/namer"
)

type PDServiceResolver func(cluster *cloudv1.Cluster) types.NamespacedName

var newServiceHTTPEndpoint = clusterctrl.NewServiceHTTPEndpoint

func getCluster(ctx context.Context, cli client.Client, key client.ObjectKey) (*cloudv1.Cluster, error) {
	cluster := &cloudv1.Cluster{}
	err := cli.Get(ctx, key, cluster)
	if err != nil {
		if apierrors.IsNotFound(err) {
			return nil, cloudv1.NewWFAbortedError(err)
		}
		return nil, fmt.Errorf("failed to get cluster %s: %s", key, err)
	}

	return cluster, nil
}

// TODO(ideascf）: move to a common place to share with other actions
//
//nolint:unused
func isClusterModifying(ctx context.Context, cluster *cloudv1.Cluster) bool {
	logger := log.FromContext(ctx).WithValues("cluster", cluster.Name)

	// todo(yiwen): push phase==cloudv1.ClusterPhaseSuspend down to `cluster.Synced()`?
	if isSuspended(cluster) {
		return false
	}

	// Mark cluster as syncing if TiProxy toggle workflow is running.
	if _, ok := cluster.Annotations[consts.AnnoKeyTiProxyToggleWorkflow]; ok {
		logger.Info("cluster is modifiying, cause: has TiProxy toggle workflow annotated")
		return true
	}

	if err := cluster.FullSyncedWithDetail(); err != nil {
		logger.Info("cluster is modifiying", "cause", err.Error())
		// other cases
		return true
	}
	//  cluster is resumed/running
	return false
}

//nolint:unused
func isSuspended(cluster *cloudv1.Cluster) bool {
	return cluster.Status.Phase == cloudv1.ClusterPhaseSuspend && cluster.Suspended() && cluster.Spec.Cluster.SuspendAction != nil && cluster.Spec.Cluster.SuspendAction.SuspendCompute
}

// getTiDBCluster gets the TiDBCluster resource from the remote cluster.
// If groupID is empty or equals the default group ID, it returns the default TiDBCluster (named "db").
// If groupID is an extra group ID, it returns the TiDBCluster for that extra group.
// Returns an error if groupID is neither the default group nor a valid extra group.
func getTiDBCluster(ctx context.Context, cluster *cloudv1.Cluster, client client.Client, groupID string) (*opv1alpha1.TidbCluster, error) {
	namespace := cluster.PrefixedId()
	defaultGroupID := cluster.GetDefaultGroupID()

	// Handle default group or empty groupID (treat as default)
	if groupID == "" || groupID == defaultGroupID {
		tcName := consts.ClusterPrefix
		key := types.NamespacedName{Namespace: namespace, Name: tcName}
		tc := &opv1alpha1.TidbCluster{}
		if err := client.Get(ctx, key, tc); err != nil {
			return nil, fmt.Errorf("get TiDBCluster %s/%s: %w", namespace, tcName, err)
		}
		return tc, nil
	}

	// This is an extra group, get the TiDBCluster name for the extra group
	extraGroup, ok := cluster.GetExtraGroupSpec(groupID)
	if !ok {
		return nil, fmt.Errorf("extra group '%s' not found", groupID)
	}

	// Create a namer for the extra group to get the correct TiDBCluster name
	extraGroupNamer := namer.NewNamer().ForTiDBGroup(cluster, extraGroup, "")
	tcName := extraGroupNamer.GetTiDBName()
	key := types.NamespacedName{Namespace: namespace, Name: tcName}

	tc := &opv1alpha1.TidbCluster{}
	if err := client.Get(ctx, key, tc); err != nil {
		return nil, fmt.Errorf("get TiDBCluster %s/%s: %w", namespace, tcName, err)
	}
	return tc, nil
}

func getStoreIDsByState(ctx context.Context, cluster *cloudv1.Cluster, compType cloudv1.ComponentType, client client.Client, state string) ([]string, error) {
	tc, err := getTiDBCluster(ctx, cluster, client, cluster.GetDefaultGroupID())
	if err != nil {
		return nil, fmt.Errorf("get TiDBCluster: %w", err)
	}

	var storeIDs []string
	switch compType {
	case cloudv1.ComponentTiKV:
		for _, store := range tc.Status.TiKV.Stores {
			if store.State == state {
				storeIDs = append(storeIDs, store.ID)
			}
		}
	case cloudv1.ComponentTiFlash:
		for _, store := range tc.Status.TiFlash.Stores {
			if store.State == state {
				storeIDs = append(storeIDs, store.ID)
			}
		}
	}
	return storeIDs, nil
}

// getPDClient creates a PD client for the cluster.
// pdAuth must be set when the dataplane proxy requires authentication (same as ClusterReconciler.PDAuthFunc).
func getPDClient(ctx context.Context, cluster *cloudv1.Cluster, client client.Client, pdAuth configmanager.PDAuth, pdServiceResolver PDServiceResolver, dryRun bool) (configmanager.DynamicPDClient, error) {
	// Create HTTP client
	httpClient := &http.Client{
		Timeout: 10 * time.Second,
		Transport: &http.Transport{
			TLSClientConfig: &tls.Config{
				//nolint:gosec
				InsecureSkipVerify: true,
			},
		},
	}

	if pdServiceResolver == nil {
		pdServiceResolver = defaultPDServiceResolver
	}
	svcName := pdServiceResolver(cluster)
	pdClient, err := newServiceHTTPEndpoint(client, svcName).
		GetPDClient(ctx, httpClient, pdAuth, cluster.Name, dryRun)
	if err != nil {
		return nil, fmt.Errorf("get PD client: %w", err)
	}

	return pdClient, nil
}

func defaultPDServiceResolver(cluster *cloudv1.Cluster) types.NamespacedName {
	return types.NamespacedName{
		Namespace: "tidb-admin",
		Name:      "proxy-cluster-proxy",
	}
}
