package controllers

import (
	"context"
	"flag"
	"fmt"
	stdlog "log"
	"strings"
	"sync"

	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/consts"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/k8s"
	pkgk8s "github.com/tidbcloud/infra-provider/regional-server/pkg/k8s"
	"helm.sh/helm/v3/pkg/action"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/cli-runtime/pkg/genericclioptions"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/manager"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
)

var (
	injectedKubeconfig = flag.String("injected-dataplane-kubeconfig", "", "Injected kubeconfig for development")
)

type APIReaderGetter interface {
	GetAPIReader(context.Context) (client.Reader, error)
	GetHelmConfig(context.Context) (*action.Configuration, error)
}

// TODO: make a higher abstraction to combine with the existing network_manager
// Manager holds the dataplane k8s controller-runtime manager
// It's used for reconciler to build dataplane k8s client cache
type Manager struct {
	scheme      *runtime.Scheme
	localClient client.Client

	lock     sync.Mutex
	appBoxes map[client.ObjectKey]*APPBoxContext

	errors chan error
}

type APPBoxContext struct {
	Manager   manager.Manager
	actionCfg *action.Configuration

	cancel context.CancelFunc
}

// NewManager creates a new manager.
func NewManager(cli client.Client, scheme *runtime.Scheme) *Manager {
	return &Manager{
		scheme:      scheme,
		localClient: cli,
		appBoxes:    make(map[client.ObjectKey]*APPBoxContext),
		errors:      make(chan error, 1),
	}
}

func (m *Manager) ClientGetter(ctx context.Context, name client.ObjectKey) (k8s.ClientGetter, error) {
	m.lock.Lock()
	defer m.lock.Unlock()
	appBoxCtx, ok := m.appBoxes[name]
	if !ok {
		return nil, fmt.Errorf("appbox %s unregistered", name)
	}
	return appBoxCtx, nil
}

func (m *Manager) APIReaderGetter(ctx context.Context, name client.ObjectKey) (APIReaderGetter, error) {
	m.lock.Lock()
	defer m.lock.Unlock()
	appBoxCtx, ok := m.appBoxes[name]
	if !ok {
		return nil, fmt.Errorf("appbox %s unregistered", name)
	}
	return appBoxCtx, nil
}

// appboxContext implements ClientGetter
var _ k8s.ClientGetter = &APPBoxContext{}

func (c *APPBoxContext) GetClient(ctx context.Context) (client.Client, error) {
	// Wait for cache to be synced before returning client
	// This prevents "the cache is not started, can not read objects" errors
	if !c.Manager.GetCache().WaitForCacheSync(ctx) {
		return nil, fmt.Errorf("cache sync failed or context cancelled")
	}
	return c.Manager.GetClient(), nil
}

func (c *APPBoxContext) GetAPIReader(ctx context.Context) (client.Reader, error) {
	return c.Manager.GetAPIReader(), nil
}

func (c *APPBoxContext) GetHelmConfig(ctx context.Context) (*action.Configuration, error) {
	if c.actionCfg != nil {
		// use cached action.Configuration to avoid initialize a action.Configuration every time
		// reset the capabilities to nil to avoid the api version cache
		c.actionCfg.Capabilities = nil
		return c.actionCfg, nil
	}
	restCfg := c.Manager.GetConfig()
	var actionCfg action.Configuration
	// https://github.com/helm/helm/issues/11047
	// helm client can't use rest.Config directly
	// The Manager is constructed from rest.Config and rest.Config is created from configFlags
	// So here we just reverse the process to construct configFlags from rest.Config
	configFlags := genericclioptions.NewConfigFlags(false)
	configFlags.APIServer = &restCfg.Host
	configFlags.Insecure = pointer.Bool(true)
	configFlags.WrapConfigFn = func(_ *rest.Config) *rest.Config {
		return restCfg
	}
	err := actionCfg.Init(
		configFlags,
		k8s.HelmNamespace,
		"configmap", // use configmap as the driver
		stdlog.Printf,
	)
	if err != nil {
		return nil, err
	}
	c.actionCfg = &actionCfg
	return c.actionCfg, nil
}

// Errors return the errors channel.
// If some error happens, it means some appbox manage fails to run
// we should better just exit.
func (m *Manager) Errors() <-chan error {
	return m.errors
}

// Register registers a running appbox with the manager.
// It is safe to call this method multiple times with same appbox.
func (m *Manager) Register(ctx context.Context, appBox *v1alpha1.APPBox) error {
	nkey := client.ObjectKeyFromObject(appBox)
	m.lock.Lock()
	defer m.lock.Unlock()
	_, ok := m.appBoxes[nkey]
	if ok {
		return nil
	}

	logger := log.FromContext(ctx)
	logger.Info("registering to appbox manager", "appbox", nkey)

	vpc := &v1alpha1.VPC{
		ObjectMeta: metav1.ObjectMeta{
			Name:      appBox.Spec.VPCRef.Name,
			Namespace: appBox.Spec.VPCRef.Namespace,
		},
	}
	if err := m.localClient.Get(ctx, client.ObjectKeyFromObject(vpc), vpc); err != nil {
		return err
	}
	cfg, err := ConfigFromAPPBox(ctx, m.localClient, appBox, m.scheme, vpc.Spec.AccountRef)
	if err != nil {
		if apierrors.IsNotFound(err) &&
			strings.Contains(err.Error(), "Cluster.eks.aws.crossplane.io") ||
			strings.Contains(err.Error(), "Cluster.container.gcp.crossplane.io") {
			logger.Info("Dataplane K8s cluster is not found, skip appbox manager registration")
			return nil
		}
		return err
	}

	mgr, err := manager.New(cfg, manager.Options{Scheme: m.scheme, LeaderElection: false, Metrics: metricsserver.Options{BindAddress: "0"}})
	if err != nil {
		logger.Error(err, "new mgr failed", "appbox", nkey, "kubeconfig", cfg.String())
		return err
	}

	nctx, cancel := context.WithCancel(context.Background())
	appboxContext := &APPBoxContext{
		Manager: mgr,
		cancel:  cancel,
	}

	go func() {
		logger.Info("start manager", "appbox", nkey)
		err := mgr.Start(nctx)
		if err != nil {
			m.errors <- err
		}
	}()
	m.appBoxes[nkey] = appboxContext

	return nil
}

// Deregister deregisters a appbox with the manager.
// When we delete or hibernates a appbox, we need to deregister it from cache
// It's safe to deregister the appbox multiple times
func (m *Manager) Deregister(ctx context.Context, appBox *v1alpha1.APPBox) {
	nkey := client.ObjectKeyFromObject(appBox)
	m.lock.Lock()
	defer m.lock.Unlock()

	nctx, ok := m.appBoxes[nkey]
	if !ok {
		return
	}

	log.FromContext(ctx).Info("deregistering to appbox manager", "appbox", nkey)

	nctx.cancel()
	delete(m.appBoxes, nkey)
}

// Close the manager.
func (m *Manager) Close() {
	m.lock.Lock()
	defer m.lock.Unlock()

	for _, nctx := range m.appBoxes {
		nctx.cancel()
	}
}

// ConfigFromAPPBox retrieves a k8s rest.Config from the given appBox (dataplane)
// The runtime.Scheme is used for dataplane apiserver interaction
func ConfigFromAPPBox(ctx context.Context, localClient client.Client, appBox *v1alpha1.APPBox, scheme *runtime.Scheme, acctRef *v1alpha1.AccountReference) (*rest.Config, error) {
	opts := client.Options{
		Scheme: scheme,
	}
	var g k8s.ClientManager
	if *injectedKubeconfig != "" {
		conf, err := clientcmd.BuildConfigFromFlags("", *injectedKubeconfig)
		if err != nil {
			return nil, err
		}
		return conf, nil
	}

	// If kubeconfig is set in the appbox status, use it.
	// Currently, only AKS is set kubeconfig in the status.
	if appBox.Status.KubeConfigSecretRef != nil {
		secretRef := appBox.Status.KubeConfigSecretRef
		kubeconfig, err := k8s.GetDataplaneKubeConfigFromMetaSecret(ctx, localClient, secretRef)
		if err != nil {
			return nil, fmt.Errorf("failed to get kubeconfig from secret %s/%s, err: %w", secretRef.Namespace, secretRef.Name, err)
		}
		cfg, err := k8s.NewRESTConfigForKubeConfig(kubeconfig)
		if err != nil {
			return nil, err
		}
		return cfg, nil
	}

	switch provider := appBox.Spec.Provider; provider {
	case consts.ProviderTypeEKS:
		g = pkgk8s.NewEKSClientManager(localClient, opts)
	case consts.ProviderTypeACK:
		assumeRoleARN := fmt.Sprintf("acs:ram::%s:role/%s", acctRef.AccountID, acctRef.Role)
		g = pkgk8s.NewACKClientManager(localClient, opts, assumeRoleARN)
	default:
		return nil, fmt.Errorf("unknown provider: %s", provider)
	}
	key := types.NamespacedName{
		Name: appBox.GetClusterName(),
	}
	cfg, err := g.GetConfig(ctx, key)
	if err != nil {
		return nil, err
	}

	return cfg, nil
}

var _ k8s.ClientGetter = &MockClientGetter{}

type MockClientGetter struct {
	client.Client
	Config *action.Configuration
}

func (m *MockClientGetter) GetClient(context.Context) (client.Client, error) {
	return m.Client, nil
}
func (m *MockClientGetter) GetHelmConfig(context.Context) (*action.Configuration, error) {
	return m.Config, nil
}
