package main

import (
	"context"
	"fmt"
	"net"
	"os"
	"sync"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/fvbock/endless"
	"github.com/gin-gonic/gin"
	grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery"
	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"github.com/juju/errors"
	"github.com/mailgun/mailgun-go"
	"github.com/patrickmn/go-cache"
	"github.com/pingcap/dbaas/central/config"
	infraapi "github.com/pingcap/dbaas/central/infra-api"
	"github.com/pingcap/dbaas/central/runtime/deployer"
	deployerprovider "github.com/pingcap/dbaas/central/runtime/deployer/provider"
	runtimemanager "github.com/pingcap/dbaas/central/runtime/manager"
	"github.com/pingcap/dbaas/pkg/mail"
	"github.com/pingcap/dbaas/pkg/salesforce"
	"github.com/pingcap/dbaas/pkg/secret/encrypt"
	"github.com/pingcap/dbaas/pkg/zendesk"
	"github.com/pingcap/log"
	clusterModel "github.com/tidbcloud/central-cluster-service/pkg/model/cluster"
	"github.com/tidbcloud/central-cluster-service/pkg/service/cluster"
	service "github.com/tidbcloud/central-sdk/pkg/service"
	"google.golang.org/grpc"

	pb "github.com/tidbcloud/central-cluster-service/pkg/service/proto"
)

const (
	gRPCPort = 9191
)

func main() {
	err := Run()
	panic(err)
}

func Run() (err error) {
	exitCh := make(chan error)
	var once sync.Once
	wg := &WaitGroupWrapper{}

	exitFunc := func(err error) {
		once.Do(func() {
			if err != nil {
			}
			exitCh <- err
		})
	}

	wg.Wrap(func() {
		exitFunc(runGRPC())
	})

	wg.Wrap(func() {
		exitFunc(runWeb())
	})

	return <-exitCh
}

type WaitGroupWrapper struct {
	sync.WaitGroup
}

func (w *WaitGroupWrapper) Wrap(cb func()) {
	w.Add(1)
	go func() {
		cb()
		w.Done()
	}()
}

func runGRPC() error {
	port := gRPCPort
	lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
	if err != nil {
		return err
	}

	s := grpc.NewServer(
		grpc.StreamInterceptor(
			grpc_recovery.StreamServerInterceptor(),
		),
		grpc.UnaryInterceptor(
			grpc_recovery.UnaryServerInterceptor(),
		),
	)
	// add implementation here.
	centralService := service.New()
	clusterModel := getClusterModel()
	pb.RegisterClusterServiceServer(s, cluster.NewService(centralService, clusterModel))
	fmt.Println("[grpc] run:", port)

	if err = s.Serve(lis); err != nil {
		return err
	}

	return nil
}

func runGateway() (*runtime.ServeMux, error) {
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	mux := runtime.NewServeMux()
	opts := []grpc.DialOption{grpc.WithInsecure()}
	grpcServerEndpoint := fmt.Sprintf("localhost:%d", gRPCPort)
	if err := pb.RegisterClusterServiceHandlerFromEndpoint(ctx, mux, grpcServerEndpoint, opts); err != nil {
		return nil, err
	}

	return mux, nil
}

func getClusterModel() clusterModel.Model {
	rm, infraAPI, enc, getDeployer, mailClient, salesforceClient, tenantModel, aesCrypt := getDependencies()
	return clusterModel.NewModel(log.L(), rm, infraAPI, enc, getDeployer, mailClient, salesforceClient, tenantModel, aesCrypt)
}

func getDependencies() (rm runtimemanager.Manager, infraAPI *infraapi.API, enc encrypt.Encryptor, getDeployer deployer.ClusterDeployer, mailClient, salesforceClient *salesforce.Client, tenantModel tenantmodel.Model, aesCrypt *encrypt.AESCrypt) {
	var err error
	// rm
	rm = runtimemanager.NewGardenRuntimeManager(log.L(), gardenerConfig.GardenerProjectName, makeVault, shootCache)

	// Infra API
	if config.Global().InfraAPIConfig.NeedInit() {
		// using the gardenerProjectName as the identifier of different central deployment
		infraAPI, err = infraapi.NewAPI(gardenerConfig.GardenerProjectName, baseKubeCli, config.Global().InfraAPIConfig)
		if err != nil {
			return errors.Annotate(err, "initialize the infra API failed")
		}
	} else {
		infraAPI = &infraapi.API{
			InfraAPIConfig: config.Global().InfraAPIConfig,
		}
	}

	// getDeployer
	getDeployer, err = func() (deployer.ClusterDeployer, error) {
		awsConfig := aws.NewConfig().
			WithMaxRetries(maxRetries).
			WithRegion(os.Getenv("REGION"))
		opt := awsSession.Options{Config: *awsConfig}
		ses, err := awsSession.NewSessionWithOptions(opt)
		if err != nil {
			return nil, err
		}

		op, operr := deployerprovider.NewClusterDeployer(deployerprovider.DeployerConf{
			Log:         log.L(),
			Config:      gardenConfig,
			ShootCache:  shootCache,
			Session:     ses,
			ProjectName: gardenerConfig.GardenerProjectName,
			BaseDomain:  gardenerConfig.GardenBaseDomain,
		}, infraAPI)
		return op, operr
	}()
	if err != nil {
		return err
	}

	serverConfig := config.Global().ServerConfig
	enc, err = secretprovider.NewSecretEncryptor(config.Aws)
	if err != nil {
		return errors.Trace(err)
	}

	aesCrypt, err = encrypt.NewAESCrypt([]byte(serverConfig.SaltSecret))
	if err != nil {
		return errors.Trace(err)
	}

	// MailGun
	session := tokenSession.NewSession(aesCrypt)
	mg := mailgun.NewMailgun(mail.Domain, serverConfig.MailGunApiKey)
	mailClient = mail.NewMail(mg)
	zendeskConfig := config.Global().ZendeskConfig
	var zendeskClient *zendesk.Client
	if zendeskConfig.EnableZendesk {
		zendeskClient, err = zendesk.NewClient(zendeskConfig.ZendeskDomain, zendeskConfig.ZendeskEmail, zendeskConfig.ZendeskAPIToken)
		if err != nil {
			return errors.Annotate(err, "initialize the zendesk client failed")
		}
	}

	// SalesforceClient
	salesforceConfig := config.Global().SalesforceConfig
	if salesforceConfig.EnableSalesforce {
		accessTokenCache := cache.New(20*time.Minute, 10*time.Minute)
		salesforceClient, err = salesforce.NewClient(accessTokenCache)
		if err != nil {
			return err
		}
	}

	tenantModel = tenantmodel.NewModel(log.L(), session, mailClient, m, billingClient, zendeskClient, salesforceClient)

	return
}

func runWeb() error {
	var err error
	port := 8080
	mux, err := runGateway()
	if err != nil {
		return err
	}

	r := gin.Default()

	r.Any("/api/v1/*any", gin.WrapF(mux.ServeHTTP))
	// r.StaticFile("/swagger/swagger.json", "/.swagger.json")

	fmt.Println("[http] run:", port)
	err = endless.ListenAndServe(fmt.Sprintf(":%d", port), r)
	if err != nil {
		return err
	}
	return nil
}
