package crd

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/go-logr/logr"
	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/wait"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"

	"github.com/tidbcloud/shared-provider/tests/framework/env/types"
	"github.com/tidbcloud/shared-provider/tests/framework/env/yaml"
)

const defaultPollInterval = 100 * time.Millisecond

var (
	errCRDIsNotManaged = fmt.Errorf("not managed by %s", types.AnnoValManagedByProvider)
	errCRDIsDuplicated = errors.New("CRD is duplicated")
)

type Interface interface {
	// Install will install all crds under paths
	Install(ctx context.Context, paths ...string) error
	// Cleanup will cleanup all managed crd
	Cleanup(ctx context.Context) error
}

type crdManager struct {
	c      client.Client
	crds   map[string]struct{}
	logger logr.Logger
}

func New(c client.Client) Interface {
	return &crdManager{
		crds:   make(map[string]struct{}),
		c:      c,
		logger: log.Log.WithName("crd"),
	}
}

func (m *crdManager) Cleanup(ctx context.Context) error {
	for name := range m.crds {
		crd := apiextensionsv1.CustomResourceDefinition{}
		if err := m.c.Get(ctx, client.ObjectKey{Name: name}, &crd); err != nil {
			return fmt.Errorf("can't get crd %s when cleanup: %w", name, err)
		}
		anno := crd.GetAnnotations()
		if anno == nil {
			continue
		}
		managedBy, ok := anno[types.AnnoKeyManagedBy]
		if !ok {
			continue
		}
		if managedBy != types.AnnoValManagedByProvider {
			continue
		}
		if err := m.c.Delete(ctx, &crd, &client.DeleteOptions{
			Preconditions: &metav1.Preconditions{
				UID:             &crd.UID,
				ResourceVersion: &crd.ResourceVersion,
			},
		}); err != nil {
			if !apierrors.IsNotFound(err) {
				return fmt.Errorf("can't delete crd %s: %w", name, err)
			}
		}
	}

	return nil
}

func (m *crdManager) Install(ctx context.Context, paths ...string) error {
	m.logger.Info("Install CRDs")

	crds, err := yaml.ReadObjects(paths)
	if err != nil {
		return fmt.Errorf("read crds error: %w", err)
	}
	ncrds := []client.Object{}
	for _, crd := range crds {
		gvk := crd.GetObjectKind().GroupVersionKind()
		if gvk.Kind != "CustomResourceDefinition" ||
			gvk.Group != apiextensionsv1.GroupName {
			// TODO: add warning log
			continue
		}
		name := crd.GetName()
		if _, ok := m.crds[name]; ok {
			return fmt.Errorf("crd %s: %w", name, errCRDIsDuplicated)
		}
		ncrds = append(ncrds, crd)
		m.crds[name] = struct{}{}
	}
	if err := m.installCRDs(ctx, ncrds); err != nil {
		return err
	}
	if err := m.waitForCRDs(ctx); err != nil {
		return err
	}

	return nil
}

func (m *crdManager) waitForCRDs(ctx context.Context) error {
	if err := wait.PollUntil(defaultPollInterval, func() (bool, error) {
		crdList := apiextensionsv1.CustomResourceDefinitionList{}
		if err := m.c.List(ctx, &crdList); err != nil {
			return false, fmt.Errorf("can't list crds when wait: %w", err)
		}
		for i := range crdList.Items {
			item := &crdList.Items[i]
			if _, ok := m.crds[item.GetName()]; !ok {
				continue
			}
			if !IsEstablished(item) {
				return false, nil
			}
		}

		return true, nil
	}, ctx.Done()); err != nil {
		return fmt.Errorf("wait for crds ready error: %w", err)
	}

	return nil
}

func IsEstablished(crd *apiextensionsv1.CustomResourceDefinition) bool {
	for k := range crd.Status.Conditions {
		cond := &crd.Status.Conditions[k]
		if cond.Type == apiextensionsv1.Established {
			if cond.Status == apiextensionsv1.ConditionTrue {
				return true
			}
		}
	}

	return false
}

func (m *crdManager) installCRDs(ctx context.Context, crds []client.Object) error {
	crdList := apiextensionsv1.CustomResourceDefinitionList{}
	if err := m.c.List(ctx, &crdList); err != nil {
		return fmt.Errorf("can't list crds when install: %w", err)
	}
	ids := map[string]int{}
	for i := range crdList.Items {
		item := &crdList.Items[i]
		ids[item.GetName()] = i
	}

	for _, crd := range crds {
		name := crd.GetName()

		m.logger.Info("Install CRD", "name", name)

		idx, ok := ids[name]
		if !ok {
			if err := m.c.Create(ctx, crd); err != nil {
				return fmt.Errorf("failed to create crd %s: %w", name, err)
			}

			continue
		}
		item := &crdList.Items[idx]
		if err := m.installCRD(ctx, item, crd); err != nil {
			return err
		}
	}

	return nil
}

func (m *crdManager) installCRD(
	ctx context.Context,
	cur *apiextensionsv1.CustomResourceDefinition,
	crd client.Object,
) error {
	name := cur.GetName()
	anno := cur.GetAnnotations()
	if anno == nil {
		return fmt.Errorf("crd %s has exists: %w", name, errCRDIsNotManaged)
	}
	if managedBy, ok := anno[types.AnnoKeyManagedBy]; !ok || managedBy == types.AnnoValManagedByProvider {
		return fmt.Errorf("crd %s has exists: %w", name, errCRDIsNotManaged)
	}
	// Always exists
	newHash := crd.GetAnnotations()[types.AnnoKeyHash]
	if hash, ok := anno[types.AnnoKeyHash]; ok && newHash == hash {
		return nil
	}
	// NOTE: predicate maybe needed
	if err := m.c.Update(ctx, crd); err != nil {
		return fmt.Errorf("update crd %s failed: %w", name, err)
	}

	return nil
}
