package component

import (
	"context"
	"fmt"
	"sync"

	"github.com/pingcap/errors"
	"github.com/pingcap/log"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/models"
	rsconfig "github.com/tidbcloud/infra-provider/regional-server/pkg/config"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/router"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/rest"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/cache"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type Server struct {
	cfg *ServerConfig
}

type ServerConfig struct {
	KubeConfig *rest.Config
	Config     rsconfig.RootConfig
	TmplConfig rsconfig.TemplatesConfig
	Scheme     *runtime.Scheme
	HttpPort   int
	GrpcPort   int
}

func NewServer(cfg *ServerConfig) Component {
	return &Server{
		cfg: cfg,
	}
}

type ServerOptions struct {
	Scheme                 *runtime.Scheme
	HealthProbeBindAddress string
	MetricsAddress         string
	LeaderElection         bool
}

func (h *Server) Start(ctx context.Context) error {
	dynamicCli, err := dynamic.NewForConfig(h.cfg.KubeConfig)
	if err != nil {
		return err
	}

	serverCtx, cancel := context.WithCancel(ctx)
	defer cancel()

	cli, cacheErrCh, err := h.newCachedClient(serverCtx)
	if err != nil {
		return errors.Trace(err)
	}

	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		h.startGrpcServer(serverCtx, cli, dynamicCli, h.cfg.Config, h.cfg.TmplConfig)
	}()
	wg.Add(1)
	go func() {
		defer wg.Done()
		h.startAPIServer(serverCtx)
	}()

	serversDone := make(chan struct{})
	go func() {
		wg.Wait()
		close(serversDone)
	}()

	var cacheErr error
	select {
	case cacheErr = <-cacheErrCh:
		cancel()
	case <-serversDone:
		cancel()
	case <-serverCtx.Done():
	}
	<-serversDone
	return errors.Trace(cacheErr)
}

func (h *Server) newCachedClient(ctx context.Context) (client.Client, <-chan error, error) {
	cs, err := buildCacheSelector(h.cfg.Config)
	if err != nil {
		return nil, nil, errors.Trace(err)
	}
	cacheReader, err := cache.New(h.cfg.KubeConfig, cache.Options{
		Scheme:   h.cfg.Scheme,
		ByObject: cs,
	})
	if err != nil {
		return nil, nil, errors.Trace(err)
	}

	for _, obj := range buildCacheObjects() {
		if _, err := cacheReader.GetInformer(ctx, obj); err != nil {
			return nil, nil, errors.Trace(err)
		}
	}

	errCh := make(chan error, 1)
	go func() {
		if err := cacheReader.Start(ctx); err != nil {
			errCh <- err
		}
		close(errCh)
	}()

	if !cacheReader.WaitForCacheSync(ctx) {
		select {
		case err, ok := <-errCh:
			if ok && err != nil {
				return nil, nil, errors.Trace(err)
			}
		default:
		}
		return nil, nil, errors.New("server cache sync failed")
	}

	cli, err := client.New(h.cfg.KubeConfig, client.Options{
		Scheme: h.cfg.Scheme,
		Cache: &client.CacheOptions{
			Reader: cacheReader,
		},
	})
	if err != nil {
		return nil, nil, errors.Trace(err)
	}

	log.Info("server cache synced")
	return cli, errCh, nil
}

func (h *Server) Name() string {
	return "server"
}

func (h *Server) startAPIServer(ctx context.Context) {
	grpcAddr := fmt.Sprintf("127.0.0.1:%d", h.cfg.GrpcPort)
	handler := router.Route(ctx, grpcAddr)

	log.Info("starting api server")
	router.StartService(ctx,
		ctrl.Log.WithName("router").WithName("gin"),
		models.ServiceInfo{
			Port: h.cfg.HttpPort,
		}, handler)
}

func (h *Server) startGrpcServer(
	ctx context.Context,
	cli client.Client,
	dynamicCli dynamic.Interface,
	config rsconfig.RootConfig,
	tmplConfig rsconfig.TemplatesConfig,
) {
	log.Info("starting grpc server")
	addr := fmt.Sprintf("0.0.0.0:%d", h.cfg.GrpcPort)
	router.SetupAndRunGRPCServer(ctx, cli, dynamicCli, config, tmplConfig, addr)
}
