package task

import (
	"context"
	"fmt"
	"sort"
	"strings"
	"time"

	"github.com/go-logr/logr"
	opv1alpha1 "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/client-go/tools/record"
	"sigs.k8s.io/controller-runtime/pkg/client"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/config"
	"github.com/tidbcloud/infra-provider/pkg/dataplane"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	clusternamer "github.com/tidbcloud/infra-provider/pkg/namer"
)

const (
	Finalizer = "backup.cloud.pingcap.com/finalizer"
)

type Context struct {
	recorder record.EventRecorder
	logger   logr.Logger

	local  client.Client // controlplane k8s client
	remote client.Client // dataplane k8s client

	backup       *cloudv1.Backup
	cluster      *cloudv1.Cluster
	clusterNamer *clusternamer.Namer

	env   string
	cloud CloudProvider

	// envProvider is the dataplane EnvProvider used for helm operations (e.g., CRRF daemon).
	// It may be nil if only a plain client.Client was obtained.
	envProvider dataplane.EnvProvider
	// brConfig holds the backup/restore controller configuration.
	brConfig *config.BRConfig

	beginAt *time.Time
}

func NewContext(
	recorder record.EventRecorder,
	logger logr.Logger,
	local client.Client,
	remote client.Client,
	backup *cloudv1.Backup,
	cluster *cloudv1.Cluster,
	env string,
	cloud CloudProvider,
) *Context {
	clusterName := backup.Spec.ClusterName
	tidbNs := cloudv1.ClusterPrefixId(clusterName)
	clusterNamer := clusternamer.NewNamer().WithClusterName(clusterName).WithTiDBNamespace(tidbNs)
	return &Context{
		recorder:     recorder,
		logger:       logger,
		local:        local,
		remote:       remote,
		backup:       backup,
		cluster:      cluster,
		clusterNamer: clusterNamer,
		env:          env,
		cloud:        cloud,
	}
}

// WithEnvProvider sets the dataplane EnvProvider on the context for helm operations.
func (c *Context) WithEnvProvider(ep dataplane.EnvProvider) *Context {
	c.envProvider = ep
	return c
}

// WithBRConfig sets the BRConfig on the context.
func (c *Context) WithBRConfig(cfg *config.BRConfig) *Context {
	c.brConfig = cfg
	return c
}

type baseTask struct {
	*Context
}

func (t *baseTask) Name() string {
	return "Base"
}

func (t *baseTask) Category() deploy.TaskCategory {
	return deploy.BRTask
}

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()
}

// MetricsStartTime will return the `BeginAt` (nil by default). For
// Task whose metrics need to be exported, the `BeginAt` should be
// initialized when the `Create` is being called.
func (t *baseTask) MetricsStartTime() *time.Time {
	return t.beginAt
}

func (t *baseTask) getBackupName() string {
	return t.backup.Name
}

func (t *baseTask) getNodePoolName(instanceType, k8sName, k8sVersion string, zones []string) string {
	// sort zones to make the nodepool name consistent
	zs := make([]string, len(zones))
	copy(zs, zones)
	sort.Strings(zs)
	return fmt.Sprintf("np-backup-%s-%s-%s-%s", instanceType, k8sName, k8sVersion, strings.Join(zs, "-"))
}

func (t *baseTask) getTenantID() string {
	return t.backup.Labels[cloudv1.TenantTagKey]
}

func (t *baseTask) getProjectID() string {
	return t.backup.Labels[cloudv1.ProjectTagKey]
}

type CloudProvider interface {
	ProviderConfiger
	NodePooler
	K8sGetter
	StorageOverlayer
}

// ProviderConfiger builds a cloud vendor specific config CR
// This config will be used to interact with the cloud
// When using crossplane, the config is the crossplane ProviderConfig CR
// If not using crossplane, the config might be a ConfigMap or a Secret
// For AWS, providerconfig reuse the cluster providerconfig, and recycle by the cluster provierconfig task
// For GCP/Azure, providerconfig is reuse the cluster providerconfig too, but is dataplane-level.
type ProviderConfiger interface {
	GetProviderConfigName() string
}

// NodePooler builds a vendor specific node pool CR
// For AWS, the nodepool is created in cluster-level, use cluster-level providercofnig, noderole, nodegroup, pod affinity
// For GCP/Azure, the nodepool is created in dataplane-level, use dataplane-level providerconfig, noderole, nodegroup, pod affinity
// nodepool labels and pod affinity is used for node selector
type NodePooler interface {
	BuildNodePool(ctx context.Context, pc string, np *NodePool) ([]client.Object, error)
	GetNodepoolLabels() map[string]string
	GetPodAffinity() *corev1.Affinity
}

type NodePool struct {
	Name           string // this may not be the same as the corresponding CR
	Arch           string
	Region         string
	Zones          []string
	InstanceType   string
	RootDiskSizeGB int64
	Identity       string
	KubeClusterID  string
	NodeSubnets    []string
	PodSubnets     []string
	Taints         []corev1.Taint
	// Labels are k8s node labels
	Labels map[string]string
	// key-value pairs attached to cloud resources
	Tags map[string]string
}

// K8sGetter gets the k8s cluster info
type K8sGetter interface {
	GetKubernetesName() string
	GetKubernetesID() string
	GetKubernetesVersion() string
	GetNodeIdentity() string
	GetNodeZones() []string
	GetNodeSubnets() []string
	GetPodSubnets() []string
}

type StorageOverlayer interface {
	Overlay(*opv1alpha1.Backup) error
	OverlayStatus(*cloudv1.Backup, *opv1alpha1.Backup) error
}
