package canaryv2

import (
	"context"
	"fmt"
	"os"

	"github.com/tidbcloud/pkgv2/db"
	"github.com/tidbcloud/pkgv2/log"
	"github.com/tidbcloud/pkgv2/logid"
	"github.com/tidbcloud/serverless-service/internal/domain/entity"
	"github.com/tidbcloud/serverless-service/internal/domain/repo"
	"github.com/tidbcloud/serverless-service/internal/infra/manager"
	"go.uber.org/zap"
	"gopkg.in/yaml.v2"
	v1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/util/workqueue"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/controller"
	"sigs.k8s.io/controller-runtime/pkg/event"
	"sigs.k8s.io/controller-runtime/pkg/handler"
	"sigs.k8s.io/controller-runtime/pkg/predicate"
	"sigs.k8s.io/controller-runtime/pkg/source"
)

const (
	cmName        = "serverless-canary"
	keyNameConfig = "config"
)

var (
	namespace = os.Getenv("NAMESPACE")
	empty     = ctrl.Result{}
)

type CanaryConfig struct {
	Clusters []*repo.CanaryCluster   `yaml:"clusters" json:"clusters"`
	Projects []*entity.ProjectCanary `yaml:"projects" json:"projects"`
}

type Reconciler struct {
	manager    *manager.LocalElectionManager
	canaryRepo repo.CanaryRepo
	dbProvider db.TransProvider
}

func NewReconciler(
	mgr *manager.LocalElectionManager,
	canaryRepo repo.CanaryRepo,
	dbProvider db.TransProvider,
) *Reconciler {
	return &Reconciler{
		manager:    mgr,
		canaryRepo: canaryRepo,
		dbProvider: dbProvider,
	}
}

func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
	c, err := controller.New("Canary", mgr, controller.Options{
		Reconciler: r,
	})
	if err != nil {
		return fmt.Errorf("create canary controller: %w", err)
	}

	err = c.Watch(source.Kind(mgr.GetCache(), &v1.ConfigMap{}, &handler.TypedFuncs[*v1.ConfigMap, ctrl.Request]{
		CreateFunc: r.handleCmCreate,
		UpdateFunc: r.handleCmUpdate,
	}, predicate.NewTypedPredicateFuncs[*v1.ConfigMap](
		func(object *v1.ConfigMap) bool {
			return object.GetNamespace() == namespace && object.GetName() == cmName
		},
	)),
	)
	if err != nil {
		return fmt.Errorf("can't not watch %s configmap", cmName)
	}

	log.Info(context.Background(), "canary controller setup successfully")
	return nil
}

func (r *Reconciler) handleCmCreate(
	ctx context.Context,
	event event.TypedCreateEvent[*v1.ConfigMap],
	q workqueue.TypedRateLimitingInterface[ctrl.Request],
) {
	q.Add(ctrl.Request{
		NamespacedName: types.NamespacedName{
			Name:      event.Object.GetName(),
			Namespace: event.Object.GetNamespace(),
		},
	})
}

func (r *Reconciler) handleCmUpdate(
	ctx context.Context,
	event event.TypedUpdateEvent[*v1.ConfigMap],
	q workqueue.TypedRateLimitingInterface[ctrl.Request],
) {
	q.Add(ctrl.Request{
		NamespacedName: types.NamespacedName{
			Name:      event.ObjectNew.GetName(),
			Namespace: event.ObjectNew.GetNamespace(),
		},
	})
}

func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	ctx = logid.WithLogid(ctx, logid.GenLogid())

	cm := &v1.ConfigMap{}
	if err := r.manager.GetClient().Get(ctx, req.NamespacedName, cm); err != nil {
		if apierrors.IsNotFound(err) {
			return empty, nil
		}
		log.Error(ctx, "fail to get canary configmap", zap.Error(err))
		return empty, err
	}

	canaryConfig := &CanaryConfig{}
	err := yaml.Unmarshal([]byte(cm.Data[keyNameConfig]), canaryConfig)
	if err != nil {
		log.Error(ctx, "fail to parse canary config", zap.Error(err))
		return empty, err
	}

	clsSpec := make(map[uint64]*repo.CanaryCluster)
	prjSpec := make(map[uint64]*entity.ProjectCanary)
	for _, cluster := range canaryConfig.Clusters {
		clsSpec[cluster.ClusterID] = cluster
	}
	for _, project := range canaryConfig.Projects {
		prjSpec[project.ProjectID] = project
	}

	for _, cluster := range clsSpec {
		if cluster.ProjectID == 0 {
			err = fmt.Errorf(
				"cluster canaries must be specified with a project id: %v",
				cluster,
			)
			log.Error(ctx, "invalid canary configmap", zap.Error(err))
			return empty, err
		}
		if project, ok := prjSpec[cluster.ProjectID]; ok {
			err = fmt.Errorf(
				"cluster canaries have conflict with project canaries: cluster: %v, project %v",
				cluster,
				project,
			)
			log.Error(ctx, "invalid canary configmap", zap.Error(err))
			return empty, err
		}
	}

	result := empty
	err = r.dbProvider.Transaction(ctx, func(ctx context.Context) error {
		result, err = r.reconcile(ctx, clsSpec, prjSpec)
		return err
	})
	if err != nil {
		log.Error(ctx, "fail to reconcile canary config", zap.Error(err))
	}

	return result, err
}

func (r *Reconciler) reconcile(
	ctx context.Context,
	clsSpec map[uint64]*repo.CanaryCluster,
	prjSpec map[uint64]*entity.ProjectCanary,
) (ctrl.Result, error) {
	currentClusters, err := r.canaryRepo.ListClusters(ctx)
	if err != nil {
		return empty, err
	}
	log.Debug(ctx, "current canary clusters", zap.Any("clusters", currentClusters))

	actualClusters := make(map[uint64]*repo.CanaryCluster, len(currentClusters))
	for _, cluster := range currentClusters {
		actualClusters[cluster.ClusterID] = cluster
	}

	currentProjects, err := r.canaryRepo.ListProjects(ctx)
	if err != nil {
		return empty, err
	}
	log.Debug(ctx, "current canary projects", zap.Any("projects", currentProjects))

	actualProjects := make(map[uint64]*entity.ProjectCanary, len(currentProjects))
	for _, project := range currentProjects {
		actualProjects[project.ProjectID] = project
	}

	addProjects := make(map[uint64]*entity.ProjectCanary)
	deleteProjects := make(map[uint64]*entity.ProjectCanary)
	addClusters := make(map[uint64]*repo.CanaryCluster)
	deleteClusters := make(map[uint64]*repo.CanaryCluster)

	// clusters to add
	for _, cluster := range clsSpec {
		if _, ok := actualClusters[cluster.ClusterID]; !ok {
			addClusters[cluster.ClusterID] = &repo.CanaryCluster{
				ClusterID: cluster.ClusterID,
				ProjectID: 0,
			}
		}
	}

	// clusters to delete
	for _, cluster := range actualClusters {
		if _, ok := clsSpec[cluster.ClusterID]; !ok {
			// If the cluster canary is migrating to a project canary, ignore it.
			if _, ok := prjSpec[cluster.ProjectID]; ok {
				continue
			}
			deleteClusters[cluster.ClusterID] = cluster
		}
	}

	// projects to add
	for _, project := range prjSpec {
		if _, ok := actualProjects[project.ProjectID]; !ok {
			addProjects[project.ProjectID] = project
			cls, err := r.canaryRepo.ListClustersInProject(ctx, project.ProjectID)
			if err != nil {
				return empty, err
			}
			for _, cluster := range cls {
				// if the cluster is already in the canary list, ignore it.
				if _, ok := actualClusters[cluster.ClusterID]; ok {
					continue
				}
				addClusters[cluster.ClusterID] = cluster
			}
		}
	}

	// projects to delete
	for _, project := range actualProjects {
		if _, ok := prjSpec[project.ProjectID]; !ok {
			deleteProjects[project.ProjectID] = project
			cls, err := r.canaryRepo.ListClustersInProject(ctx, project.ProjectID)
			if err != nil {
				return empty, err
			}
			for _, cluster := range cls {
				// if the cluster canary is move out of a project canary, ignore it.
				if _, ok := clsSpec[cluster.ClusterID]; ok {
					continue
				}
				deleteClusters[cluster.ClusterID] = cluster
			}
		}
	}

	log.Debug(ctx, "add projects", zap.Any("projects", addProjects))
	for _, prj := range addProjects {
		if err := r.canaryRepo.AddProject(ctx, prj.ProjectID); err != nil {
			return empty, err
		}
	}

	log.Debug(ctx, "delete projects", zap.Any("projects", deleteProjects))
	for _, prj := range deleteProjects {
		if err := r.canaryRepo.RemoveProject(ctx, prj.ProjectID); err != nil {
			return empty, err
		}
	}

	if len(addClusters) > 0 {
		adds := make([]*repo.CanaryCluster, 0, len(addClusters))
		for _, cluster := range addClusters {
			adds = append(adds, cluster)
		}
		log.Debug(ctx, "add clusters", zap.Any("clusters", addClusters))
		if err := r.canaryRepo.AddClusters(ctx, adds); err != nil {
			return empty, err
		}
	}

	if len(deleteClusters) > 0 {
		removes := make([]*repo.CanaryCluster, 0, len(deleteClusters))
		for _, cluster := range deleteClusters {
			removes = append(removes, cluster)
		}
		log.Debug(ctx, "delete clusters", zap.Any("clusters", deleteClusters))
		if err := r.canaryRepo.RemoveClusters(ctx, removes); err != nil {
			return empty, err
		}
	}

	return empty, nil
}
