package apply

import (
	"bytes"
	"context"
	"fmt"

	"github.com/go-logr/logr"
	"k8s.io/apimachinery/pkg/api/equality"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/runtime/serializer/json"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/managedfields"
	"k8s.io/client-go/discovery"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"k8s.io/kube-openapi/pkg/util/proto"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
	DefaultFieldManager = "network-policy-controller"
)

type Object[T any] interface {
	client.Object
	*T
}

type Appliable[PT Object[T], T any] interface {
	Apply(ctx context.Context) (PT, error)
}

type Applier interface {
	client.Client
	Parser() *managedfields.GvkParser
}

type appliable[PT Object[T], T any] struct {
	cc       client.Client
	parser   *managedfields.GvkParser
	current  PT
	expected PT
}

func (p *appliable[PT, T]) Apply(ctx context.Context) (PT, error) {
	gvks, _, err := scheme.Scheme.ObjectKinds(p.expected)
	if err != nil {
		return nil, err
	}
	hasCreated := true
	if err := p.cc.Get(ctx, client.ObjectKeyFromObject(p.current), p.current); err != nil {
		if !errors.IsNotFound(err) {
			return nil, err
		}

		hasCreated = false
	}

	if hasCreated {
		lastApplied := newEmptyObject(p.current)

		if err := p.Extract(DefaultFieldManager, gvks[0], lastApplied, ""); err != nil {
			return nil, err
		}
		// reset lastApplied gvk to avoid the gvk diff of expected and last applied
		lastApplied.GetObjectKind().SetGroupVersionKind(p.expected.GetObjectKind().GroupVersionKind())
		lastApplied.SetNamespace(p.expected.GetNamespace())
		lastApplied.SetName(p.expected.GetName())

		if equality.Semantic.DeepDerivative(p.expected, lastApplied) {
			return p.current, nil
		} else {
			logger := logr.FromContextOrDiscard(ctx)
			logger.Info("expected and last applied is mismatched", "expected", p.expected, "lastApplied", lastApplied)
		}
	}

	if err := p.cc.Patch(ctx, p.expected, &applyPatch{
		gvk: gvks[0],
	}, &client.PatchOptions{
		FieldManager: DefaultFieldManager,
		Force:        pointer.Bool(true),
	}); err != nil {
		return nil, err
	}

	return p.expected, nil
}

type applyPatch struct {
	gvk schema.GroupVersionKind
}

func (p *applyPatch) Type() types.PatchType {
	return types.ApplyPatchType
}

func (p *applyPatch) Data(obj client.Object) ([]byte, error) {
	encoder := scheme.Codecs.EncoderForVersion(
		json.NewSerializerWithOptions(
			json.DefaultMetaFactory,
			scheme.Scheme,
			scheme.Scheme,
			json.SerializerOptions{
				Yaml:   true,
				Pretty: false,
				Strict: true,
			}),
		p.gvk.GroupVersion(),
	)
	buf := bytes.Buffer{}
	if err := encoder.Encode(obj, &buf); err != nil {
		return nil, fmt.Errorf("failed to encode patch: %w", err)
	}
	return buf.Bytes(), nil
}

func (p *appliable[PT, T]) Extract(fieldManager string, gvk schema.GroupVersionKind, patch interface{}, subresource string) error {
	typed := p.parser.Type(gvk)
	if typed == nil {
		return fmt.Errorf("can't find specified type: %s", gvk)
	}
	if err := managedfields.ExtractInto(p.current, *typed, fieldManager, patch, subresource); err != nil {
		return err
	}
	return nil
}

func NewObject[PT Object[T], T any](ns, name string) PT {
	var dst PT = new(T)
	dst.SetName(name)
	dst.SetNamespace(ns)

	return dst
}

func NewClusterObject[PT Object[T], T any](name string) PT {
	return NewObject[PT]("", name)
}

func newEmptyObject[PT Object[T], T any](src PT) PT {
	return NewObject[PT](src.GetNamespace(), src.GetName())
}

type applier struct {
	client.Client
	parser *managedfields.GvkParser
}

func (p *applier) Parser() *managedfields.GvkParser {
	return p.parser
}

func NewApplier(c *rest.Config) (Applier, error) {
	dc, err := discovery.NewDiscoveryClientForConfig(c)
	if err != nil {
		return nil, err
	}

	doc, err := dc.OpenAPISchema()
	if err != nil {
		return nil, err
	}

	models, err := proto.NewOpenAPIData(doc)
	if err != nil {
		return nil, err
	}

	parser, err := managedfields.NewGVKParser(models, false)
	if err != nil {
		return nil, err
	}

	cc, err := client.New(c, client.Options{
		Scheme: scheme.Scheme,
	})

	if err != nil {
		return nil, err
	}

	return &applier{
		Client: cc,
		parser: parser,
	}, nil
}

func Expected[PT Object[T], T any](applier Applier, obj PT) Appliable[PT, T] {
	return &appliable[PT, T]{
		cc:       applier,
		parser:   applier.Parser(),
		current:  newEmptyObject(obj),
		expected: obj,
	}
}
