package client

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/rancher/lasso/pkg/mapper"
	"github.com/rancher/lasso/pkg/scheme"
	"k8s.io/apimachinery/pkg/api/meta"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/runtime/serializer"
	"k8s.io/client-go/discovery"
	"k8s.io/client-go/rest"
)

type SharedClientFactoryOptions struct {
	Mapper meta.RESTMapper
	Scheme *runtime.Scheme
}

type SharedClientFactory interface {
	ForKind(gvk schema.GroupVersionKind) (*Client, error)
	ForResource(gvr schema.GroupVersionResource, namespaced bool) (*Client, error)
	ForResourceKind(gvr schema.GroupVersionResource, kind string, namespaced bool) *Client
	NewObjects(gvk schema.GroupVersionKind) (runtime.Object, runtime.Object, error)
	GVKForObject(obj runtime.Object) (schema.GroupVersionKind, error)
	GVKForResource(gvr schema.GroupVersionResource) (schema.GroupVersionKind, error)
	IsNamespaced(gvk schema.GroupVersionKind) (bool, error)
	ResourceForGVK(gvk schema.GroupVersionKind) (schema.GroupVersionResource, bool, error)
	IsHealthy(ctx context.Context) bool
}

type sharedClientFactory struct {
	createLock sync.RWMutex
	discovery  discovery.DiscoveryInterface
	clients    map[schema.GroupVersionResource]*Client
	timeout    time.Duration
	rest       rest.Interface
	config     *rest.Config

	Mapper meta.RESTMapper
	Scheme *runtime.Scheme
}

func NewSharedClientFactoryForConfig(config *rest.Config) (SharedClientFactory, error) {
	return NewSharedClientFactory(config, nil)
}

func NewSharedClientFactory(config *rest.Config, opts *SharedClientFactoryOptions) (_ SharedClientFactory, err error) {
	opts, err = applyDefaults(config, opts)
	if err != nil {
		return nil, err
	}

	config, timeout := populateConfig(opts.Scheme, config)
	rest, err := rest.UnversionedRESTClientFor(config)
	if err != nil {
		return nil, err
	}

	discovery, err := discovery.NewDiscoveryClientForConfig(config)
	if err != nil {
		return nil, err
	}

	return &sharedClientFactory{
		timeout:   timeout,
		clients:   map[schema.GroupVersionResource]*Client{},
		Scheme:    opts.Scheme,
		Mapper:    opts.Mapper,
		rest:      rest,
		config:    config,
		discovery: discovery,
	}, nil
}

func applyDefaults(config *rest.Config, opts *SharedClientFactoryOptions) (*SharedClientFactoryOptions, error) {
	var newOpts SharedClientFactoryOptions
	if opts != nil {
		newOpts = *opts
	}

	if newOpts.Scheme == nil {
		newOpts.Scheme = scheme.All
	}

	if newOpts.Mapper == nil {
		mapperOpt, err := mapper.New(config)
		if err != nil {
			return nil, err
		}
		newOpts.Mapper = mapperOpt
	}

	return &newOpts, nil
}

func (s *sharedClientFactory) GVKForResource(gvr schema.GroupVersionResource) (schema.GroupVersionKind, error) {
	return s.Mapper.KindFor(gvr)
}

func (s *sharedClientFactory) IsHealthy(ctx context.Context) bool {
	_, err := s.rest.Get().AbsPath("/version").Do(ctx).Raw()
	return err == nil
}

func (s *sharedClientFactory) IsNamespaced(gvk schema.GroupVersionKind) (bool, error) {
	mapping, err := s.Mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
	if err != nil {
		return false, err
	}

	return IsNamespaced(mapping.Resource, s.Mapper)
}

func (s *sharedClientFactory) ResourceForGVK(gvk schema.GroupVersionKind) (schema.GroupVersionResource, bool, error) {
	mapping, err := s.Mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
	if err != nil {
		return schema.GroupVersionResource{}, false, err
	}

	nsed, err := IsNamespaced(mapping.Resource, s.Mapper)
	if err != nil {
		return schema.GroupVersionResource{}, false, err
	}

	return mapping.Resource, nsed, nil
}

func (s *sharedClientFactory) GVKForObject(obj runtime.Object) (schema.GroupVersionKind, error) {
	gvks, _, err := s.Scheme.ObjectKinds(obj)
	if err != nil {
		return schema.GroupVersionKind{}, err
	}
	if len(gvks) == 0 {
		return schema.GroupVersionKind{}, fmt.Errorf("failed to find schema.GroupVersionKind for %T", obj)
	}
	return gvks[0], nil
}

func (s *sharedClientFactory) NewObjects(gvk schema.GroupVersionKind) (runtime.Object, runtime.Object, error) {
	obj, err := s.Scheme.New(gvk)
	if runtime.IsNotRegisteredError(err) {
		return &unstructured.Unstructured{}, &unstructured.UnstructuredList{}, nil
	} else if err != nil {
		return nil, nil, err
	}

	objList, err := s.Scheme.New(schema.GroupVersionKind{
		Group:   gvk.Group,
		Version: gvk.Version,
		Kind:    gvk.Kind + "List",
	})
	return obj, objList, err
}

func (s *sharedClientFactory) ForKind(gvk schema.GroupVersionKind) (*Client, error) {
	gvr, nsed, err := s.ResourceForGVK(gvk)
	if err != nil {
		return nil, err
	}

	return s.ForResourceKind(gvr, gvk.Kind, nsed), nil
}

func (s *sharedClientFactory) ForResource(gvr schema.GroupVersionResource, namespaced bool) (*Client, error) {
	gvk, err := s.GVKForResource(gvr)
	if err != nil {
		return nil, err
	}
	return s.ForResourceKind(gvr, gvk.Kind, namespaced), nil
}

func (s *sharedClientFactory) ForResourceKind(gvr schema.GroupVersionResource, kind string, namespaced bool) *Client {
	client := s.getClient(gvr)
	if client != nil {
		return client
	}

	s.createLock.Lock()
	defer s.createLock.Unlock()

	client = s.clients[gvr]
	if client != nil {
		return client
	}

	client = NewClient(gvr, kind, namespaced, s.rest, s.timeout)
	if s.config != nil {
		client.Config = *s.config
	}
	s.clients[gvr] = client
	return client
}

func (s *sharedClientFactory) getClient(gvr schema.GroupVersionResource) *Client {
	s.createLock.RLock()
	defer s.createLock.RUnlock()
	return s.clients[gvr]
}

func populateConfig(scheme *runtime.Scheme, config *rest.Config) (*rest.Config, time.Duration) {
	config = rest.CopyConfig(config)
	config.NegotiatedSerializer = unstructuredNegotiator{
		NegotiatedSerializer: serializer.NewCodecFactory(scheme).WithoutConversion(),
	}
	if config.UserAgent == "" {
		config.UserAgent = rest.DefaultKubernetesUserAgent()
	}
	timeout := config.Timeout
	config.Timeout = 0
	return config, timeout
}

type unstructuredNegotiator struct {
	runtime.NegotiatedSerializer
}

func (u unstructuredNegotiator) DecoderToVersion(serializer runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder {
	result := u.NegotiatedSerializer.DecoderToVersion(serializer, gv)
	return unstructuredDecoder{
		Decoder: result,
	}
}

type unstructuredDecoder struct {
	runtime.Decoder
}

func (u unstructuredDecoder) Decode(data []byte, defaults *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
	obj, gvk, err := u.Decoder.Decode(data, defaults, into)
	if into == nil && runtime.IsNotRegisteredError(err) {
		return u.Decode(data, defaults, &unstructured.Unstructured{})
	}
	return obj, gvk, err
}
