package cloudprovider

import (
	"encoding/json"
	"errors"
	"io"

	"github.com/k3s-io/k3s/pkg/util"
	"github.com/k3s-io/k3s/pkg/util/logger"
	"github.com/k3s-io/k3s/pkg/version"
	"github.com/rancher/wrangler/v3/pkg/apply"
	"github.com/rancher/wrangler/v3/pkg/generated/controllers/apps"
	appsclient "github.com/rancher/wrangler/v3/pkg/generated/controllers/apps/v1"
	"github.com/rancher/wrangler/v3/pkg/generated/controllers/core"
	coreclient "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1"
	"github.com/rancher/wrangler/v3/pkg/generated/controllers/discovery"
	discoveryclient "github.com/rancher/wrangler/v3/pkg/generated/controllers/discovery/v1"
	"github.com/rancher/wrangler/v3/pkg/generic"
	"github.com/rancher/wrangler/v3/pkg/start"
	"github.com/sirupsen/logrus"
	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/record"
	"k8s.io/client-go/util/workqueue"
	cloudprovider "k8s.io/cloud-provider"
)

// Config describes externally-configurable cloud provider configuration.
// This is normally unmarshalled from a JSON config file.
type Config struct {
	LBDefaultPriorityClassName string `json:"lbDefaultPriorityClassName"`
	LBEnabled                  bool   `json:"lbEnabled"`
	LBImage                    string `json:"lbImage"`
	LBNamespace                string `json:"lbNamespace"`
	NodeEnabled                bool   `json:"nodeEnabled"`
	Rootless                   bool   `json:"rootless"`
}

type k3s struct {
	Config

	client   kubernetes.Interface
	recorder record.EventRecorder

	processor      apply.Apply
	daemonsetCache appsclient.DaemonSetCache
	endpointsCache discoveryclient.EndpointSliceCache
	nodeCache      coreclient.NodeCache
	podCache       coreclient.PodCache
	workqueue      workqueue.RateLimitingInterface
}

var _ cloudprovider.Interface = &k3s{}

func init() {
	cloudprovider.RegisterCloudProvider(version.Program, func(config io.Reader) (cloudprovider.Interface, error) {
		var err error
		k := k3s{
			Config: Config{
				LBDefaultPriorityClassName: DefaultLBPriorityClassName,
				LBEnabled:                  true,
				LBImage:                    DefaultLBImage,
				LBNamespace:                DefaultLBNS,
				NodeEnabled:                true,
			},
		}

		if config != nil {
			var bytes []byte
			bytes, err = io.ReadAll(config)
			if err == nil {
				err = json.Unmarshal(bytes, &k.Config)
			}
		}

		if !k.LBEnabled && !k.NodeEnabled {
			return nil, errors.New("all cloud-provider functionality disabled by config")
		}

		return &k, err
	})
}

func (k *k3s) Initialize(clientBuilder cloudprovider.ControllerClientBuilder, stop <-chan struct{}) {
	ctx := logger.NewContext(wait.ContextForChannel(stop), controllerName)
	config := clientBuilder.ConfigOrDie(controllerName)
	k.client = kubernetes.NewForConfigOrDie(config)

	if k.LBEnabled {
		// Wrangler controller and caches are only needed if the load balancer controller is enabled.
		k.recorder = util.BuildControllerEventRecorder(ctx, k.client, controllerName, meta.NamespaceAll)
		coreFactory := core.NewFactoryFromConfigOrDie(config)
		k.nodeCache = coreFactory.Core().V1().Node().Cache()

		lbCoreFactory := core.NewFactoryFromConfigWithOptionsOrDie(config, &generic.FactoryOptions{Namespace: k.LBNamespace})
		lbAppsFactory := apps.NewFactoryFromConfigWithOptionsOrDie(config, &generic.FactoryOptions{Namespace: k.LBNamespace})
		lbDiscFactory := discovery.NewFactoryFromConfigOrDie(config)

		processor, err := apply.NewForConfig(config)
		if err != nil {
			logrus.Panicf("failed to create apply processor for %s: %v", controllerName, err)
		}
		k.processor = processor.WithDynamicLookup().WithCacheTypes(lbAppsFactory.Apps().V1().DaemonSet())
		k.daemonsetCache = lbAppsFactory.Apps().V1().DaemonSet().Cache()
		k.endpointsCache = lbDiscFactory.Discovery().V1().EndpointSlice().Cache()
		k.podCache = lbCoreFactory.Core().V1().Pod().Cache()
		k.workqueue = workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())

		if err := k.Register(ctx, coreFactory.Core().V1().Node(), lbCoreFactory.Core().V1().Pod(), lbDiscFactory.Discovery().V1().EndpointSlice()); err != nil {
			logrus.Panicf("failed to register %s handlers: %v", controllerName, err)
		}

		if err := start.All(ctx, 1, coreFactory, lbCoreFactory, lbAppsFactory, lbDiscFactory); err != nil {
			logrus.Panicf("failed to start %s controllers: %v", controllerName, err)
		}
	} else {
		// If load-balancer functionality has not been enabled, delete managed daemonsets.
		// This uses the raw kubernetes client, as the controllers are not started when the load balancer controller is disabled.
		if err := k.deleteAllDaemonsets(ctx); err != nil {
			logrus.Panicf("failed to clean up %s daemonsets: %v", controllerName, err)
		}
	}
}

func (k *k3s) Instances() (cloudprovider.Instances, bool) {
	return nil, false
}

func (k *k3s) InstancesV2() (cloudprovider.InstancesV2, bool) {
	return k, k.NodeEnabled
}

func (k *k3s) LoadBalancer() (cloudprovider.LoadBalancer, bool) {
	return k, k.LBEnabled
}

func (k *k3s) Zones() (cloudprovider.Zones, bool) {
	return nil, false
}

func (k *k3s) Clusters() (cloudprovider.Clusters, bool) {
	return nil, false
}

func (k *k3s) Routes() (cloudprovider.Routes, bool) {
	return nil, false
}

func (k *k3s) ProviderName() string {
	return version.Program
}

func (k *k3s) HasClusterID() bool {
	return false
}
