package secondaryconfirm

import (
	"context"
	"fmt"
	"strings"

	"github.com/gin-gonic/gin"
	"github.com/go-logr/logr"
	"github.com/google/uuid"
	"github.com/tidbcloud/config-manager/pkg/controllers/clusterconfig"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"

	"github.com/tidbcloud/infra-provider/pkg/gitops-sdk/revision"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
)

// @title		Config Manager Secondary Confirm API
// @version		1.0
// @description	Whatever.
// @BasePath		/

const (
	DisableConfigApply = "cluster.tidbcloud.com/disable-config-apply"
)

type API struct {
	client     client.Client
	revisioner revision.Interface
	nsPrefix   string
	logger     *logr.Logger
}

func NewAPI(cli client.Client, nsPrefix string) (*API, error) {
	logger := ctrl.Log.WithName("api")
	return &API{
		client:     cli,
		revisioner: revision.NewRevisioner(cli),
		nsPrefix:   nsPrefix,
		logger:     &logger,
	}, nil
}

func (api *API) GetConfigRevision(ctx context.Context, c *gin.Context, param *ConfigRevisionGetParam) (*ConfigRevisionInfo, error) {
	logger := log.FromContext(ctx)
	out := &ConfigRevisionInfo{
		ConfigRevisionID: param.ConfigRevisionID,
		Status:           ConfigRevisionStatusOutdated,
	}
	ns := api.nsPrefix + param.ProjectID

	api.logger.Info("get config revision", "cluster", param.ClusterID, "target", param.ConfigRevisionID)
	cc := &cloudv1.ConfigChange{}
	ccName := cloudv1.GenConfigChangeName(param.ConfigRevisionID)
	err := api.client.Get(c, types.NamespacedName{Namespace: ns, Name: ccName}, cc)
	if err != nil && !apierrors.IsNotFound(err) {
		api.logger.Error(err, "get ConfigChange failed", "ns", ns, "name", ccName)
		return nil, err
	}
	// ConfigChange not found
	if apierrors.IsNotFound(err) {
		logger.Info("ConfigChange not found", "ns", ns, "name", ccName)
		out.Status, err = api.getConfigRevisionStatusWhenConfigChangeNotExists(ctx, c, param)
		if err != nil {
			return nil, err
		}
	} else {
		logger.Info("ConfigChange found", "phase", cc.Status.Phase, "name", ccName)
		if !cc.Done() {
			out.Status = ConfigRevisionStatusApplying
		} else if cc.Status.Phase == cloudv1.ConfigChangePhaseCompleted {
			out.Status = ConfigRevisionStatusApplied
		} else {
			out.Status = ConfigRevisionStatusOutdated
		}
	}

	return out, nil
}

// getStatus4OldConfigRevision gets status for old ConfigRevision
func (api *API) getConfigRevisionStatusWhenConfigChangeNotExists(ctx context.Context, c *gin.Context, param *ConfigRevisionGetParam) (string, error) {
	logger := log.FromContext(ctx)
	ns := api.nsPrefix + param.ProjectID

	cluster := &cloudv1.Cluster{}
	err := api.client.Get(c, types.NamespacedName{Namespace: ns, Name: param.ClusterID}, cluster)
	if err != nil {
		// if the cluster is NotFound, treat ConfigRevision as Outdated
		if apierrors.IsNotFound(err) {
			logger.Info("cluster found", "ns", ns, "name", param.ClusterID)
			return ConfigRevisionStatusOutdated, nil
		}

		api.logger.Error(err, "get Cluster failed", "name", param.ClusterID)
		return "", err
	}

	if param.ConfigRevisionID == cluster.Spec.ConfigRevisionRef.Name {
		logger.Info("ConfigRevision is using", "ns", ns, "name", param.ConfigRevisionID, "cluster", param.ClusterID)
		return ConfigRevisionStatusApplied, nil
	}

	lastCR, err := clusterconfig.GetLastCreatedConfigRevision(api.revisioner, param.ClusterID)
	if err != nil {
		return "", fmt.Errorf("get last created CR: %w", err)
	}
	logger.Info("last ConfigRevision", "ns", ns, "name", lastCR.Name)
	if param.ConfigRevisionID != lastCR.Name {
		return ConfigRevisionStatusOutdated, nil
	} else {
		return ConfigRevisionStatusGenerated, nil
	}
}

func (api *API) Diff(ctx context.Context, param *DiffParam) (*DiffResult, error) {
	cluster := &cloudv1.Cluster{}
	ns := api.nsPrefix + param.ProjectID
	if err := api.client.Get(ctx, types.NamespacedName{
		Namespace: ns,
		Name:      param.ClusterID,
	}, cluster); err != nil {
		api.logger.Error(err, "get cluster failed", "ns", ns, "cluster_id", param.ClusterID)
		// do not ignore not found error
		if apierrors.IsNotFound(err) {
			return nil, ErrNotFound(err.Error())
		}
		return nil, err
	}
	api.logger.Info("config revision diff", "cluster", param.ClusterID, "base", cluster.Spec.ConfigRevisionRef.Name, "target", param.ConfigRevisionID)
	nextRevision, err := api.revisioner.GetConfigRevisions(types.NamespacedName{Name: param.ConfigRevisionID})
	if err != nil {
		api.logger.Error(err, "get next configrevision failed", "configrevision_id", param.ConfigRevisionID)
		return nil, err
	}
	if cluster.Spec.ConfigRevisionRef.Name == param.ConfigRevisionID {
		return &DiffResult{
			VersionPair:              &VersionPair{},
			PDConfigPair:             &emptyConfigPair,
			TiProxyConfigPair:        &emptyConfigPair,
			TiDBConfigPair:           &emptyConfigPair,
			TiKVConfigPair:           &emptyConfigPair,
			TiFlashConfigPair:        &emptyConfigPair,
			TiFlashLearnerConfigPair: &emptyConfigPair,
			BackupPair:               &emptyConfigPair,
			RestorePair:              &emptyConfigPair,
			LightningPair:            &emptyConfigPair,
			TiCDCConfigPair:          &emptyConfigPair,
			SkipApply:                true,
			CreationTimestampUnix:    nextRevision.CreationTimestamp.Unix(),
			CurrConfigRevisionID:     cluster.Spec.ConfigRevisionRef.Name,
		}, nil
	}
	currentRevision, err := api.revisioner.GetConfigRevisions(types.NamespacedName{Name: cluster.Spec.ConfigRevisionRef.Name})
	if err != nil {
		api.logger.Error(err, "get current configrevision failed", "configrevisionref", cluster.Spec.ConfigRevisionRef.Name)
		return nil, err
	}
	// TODO: check the diff output
	pdConfigPair, err := genConfigPair(currentRevision.Spec.PD, nextRevision.Spec.PD)
	if err != nil {
		api.logger.Error(err, "generate config pair for pd failed", "currRev", currentRevision.Name, "nextRev", nextRevision.Name)
		return nil, err
	}
	tiproxyConfigPair, err := genConfigPair(currentRevision.Spec.TiProxy, nextRevision.Spec.TiProxy)
	if err != nil {
		api.logger.Error(err, "generate config pair for tiproxy failed", "currRev", currentRevision.Name, "nextRev", nextRevision.Name)
		return nil, err
	}
	tidbConfigPair, err := genConfigPair(currentRevision.Spec.TiDB, nextRevision.Spec.TiDB)
	if err != nil {
		api.logger.Error(err, "generate config pair for tidb failed", "currRev", currentRevision.Name, "nextRev", nextRevision.Name)
		return nil, err
	}
	tikvConfigPair, err := genConfigPair(currentRevision.Spec.TiKV, nextRevision.Spec.TiKV)
	if err != nil {
		api.logger.Error(err, "generate config pair for tikv failed", "currRev", currentRevision.Name, "nextRev", nextRevision.Name)
		return nil, err
	}
	tiflashConfigPair, err := genConfigPair(currentRevision.Spec.TiFlash, nextRevision.Spec.TiFlash)
	if err != nil {
		api.logger.Error(err, "generate config pair for tiflashj failed", "currRev", currentRevision.Name, "nextRev", nextRevision.Name)
		return nil, err
	}
	tiflashLearnerConfigPair, err := genConfigPair(currentRevision.Spec.TiFlashLearner, nextRevision.Spec.TiFlashLearner)
	if err != nil {
		api.logger.Error(err, "generate config pair for tiflash learner failed", "currRev", currentRevision.Name, "nextRev", nextRevision.Name)
		return nil, err
	}
	backupConfigPair, err := genConfigPair(currentRevision.Spec.Backup, nextRevision.Spec.Backup)
	if err != nil {
		api.logger.Error(err, "generate config pair for backup failed", "currRev", currentRevision.Name, "nextRev", nextRevision.Name)
		return nil, err
	}
	restoreConfigPair, err := genConfigPair(currentRevision.Spec.Restore, nextRevision.Spec.Restore)
	if err != nil {
		api.logger.Error(err, "generate config pair for restore failed", "currRev", currentRevision.Name, "nextRev", nextRevision.Name)
		return nil, err
	}
	lightningConfigPair, err := genConfigPair(currentRevision.Spec.Lightning, nextRevision.Spec.Lightning)
	if err != nil {
		api.logger.Error(err, "generate config pair for lightning failed", "currRev", currentRevision.Name, "nextRev", nextRevision.Name)
		return nil, err
	}
	ticdcConfigPair, err := genConfigPair(currentRevision.Spec.TiCDC, nextRevision.Spec.TiCDC)
	if err != nil {
		api.logger.Error(err, "generate config pair for ticdc failed", "currRev", currentRevision.Name, "nextRev", nextRevision.Name)
		return nil, err
	}

	return &DiffResult{
		VersionPair: &VersionPair{
			Current: currentRevision.Spec.Version,
			Next:    nextRevision.Spec.Version,
		},
		PDConfigPair:             pdConfigPair,
		TiProxyConfigPair:        tiproxyConfigPair,
		TiDBConfigPair:           tidbConfigPair,
		TiKVConfigPair:           tikvConfigPair,
		TiFlashConfigPair:        tiflashConfigPair,
		TiFlashLearnerConfigPair: tiflashLearnerConfigPair,
		BackupPair:               backupConfigPair,
		RestorePair:              restoreConfigPair,
		LightningPair:            lightningConfigPair,
		TiCDCConfigPair:          ticdcConfigPair,
		SkipApply:                false,
		CreationTimestampUnix:    nextRevision.CreationTimestamp.Unix(),
		CurrConfigRevisionID:     cluster.Spec.ConfigRevisionRef.Name,
	}, nil
}

func (api *API) Apply(ctx context.Context, param *ApplyParam) (*ApplyResult, error) {
	if param.Strategy == "" {
		param.Strategy = cloudv1.ConfigChangeStrategyRollingRestart
	}
	api.logger.Info("apply config revision", "cluster", param.ClusterID, "base", param.CurrConfigRevisionID, "target", param.ConfigRevisionID)
	cluster := &cloudv1.Cluster{}
	if err := api.client.Get(ctx, types.NamespacedName{
		Namespace: api.nsPrefix + param.ProjectID,
		Name:      param.ClusterID,
	}, cluster); err != nil {
		// do not ignore not found error
		if apierrors.IsNotFound(err) {
			return nil, ErrNotFound(err.Error())
		}
		return nil, err
	}
	api.logger.Info("apply config revision", "using", cluster.Spec.ConfigRevisionRef.Name)
	if reason := cluster.Annotations[DisableConfigApply]; reason != "" {
		api.logger.Info("config apply is disabled", "cluster", param.ClusterID, "reason", reason)
		return nil, ErrClusterConfigApplyDisabled(reason)
	}
	if cluster.Spec.ConfigRevisionRef.Name == param.ConfigRevisionID {
		return nil, nil
	}
	if param.CurrConfigRevisionID != "" && // for backward compatibility
		param.CurrConfigRevisionID != cluster.Spec.ConfigRevisionRef.Name {
		// if the given CurrConfigRevisionID is NOT same as cluster using, return ErrStatusConflict to guide user to
		// refresh page and retry
		api.logger.Info("the given CurrConfigRevisionID is NOT same as cluster using", "currConfigRevisionID", param.CurrConfigRevisionID, "cluster", cluster.Spec.ConfigRevisionRef.Name)
		return nil, ErrStatusConflict
	}
	nextRevision, err := api.revisioner.GetConfigRevisions(types.NamespacedName{Name: param.ConfigRevisionID})
	if err != nil {
		api.logger.Error(err, "get next configRevision failed", "configrevision_id", param.ConfigRevisionID)
		return nil, err
	}
	currRevision, err := api.revisioner.GetConfigRevisions(types.NamespacedName{Name: cluster.Spec.ConfigRevisionRef.Name})
	if err != nil {
		api.logger.Error(err, "get current configRevision failed", "configrevision_id", cluster.Spec.ConfigRevisionRef.Name)
		return nil, err
	}

	crs, err := api.revisioner.ListConfigRevisions(cloudv1.SelectAllConfigRevisions4Cluster(cluster.Name))
	if err != nil {
		return nil, fmt.Errorf("list configRevisions: %w", err)
	}
	latestCR := cloudv1.LatestCreatedConfigRevision(crs)
	if nextRevision.Name != latestCR.Name {
		api.logger.Info("configRevision is not latest", "latestCR", latestCR.Name, "nextRevision", nextRevision.Name)
		return nil, fmt.Errorf("configRevision %s is not latest", nextRevision.Name)
	}

	if err := api.applyCon2Cluster(ctx, currRevision, nextRevision, cluster, param.Strategy); err != nil {
		if strings.Contains(err.Error(), "under config changing") {
			return nil, ErrClusterUnderConfigChanging
		}
		api.logger.Error(err, "apply configRevision")
		return nil, err
	}
	err = SendApplyEvent(ctx, cluster, currRevision, nextRevision)
	if err != nil {
		api.logger.Error(err, "send update cluster config event failed")
	}
	return &ApplyResult{}, nil
}

func (api *API) applyCon2Cluster(ctx context.Context, currRevision, nextRevision *cloudv1.ConfigRevision, cluster *cloudv1.Cluster, strategy cloudv1.ConfigChangeStrategy) error {
	cc := &cloudv1.ConfigChange{
		ObjectMeta: metav1.ObjectMeta{
			Name:      cloudv1.GenConfigChangeName(nextRevision.Name),
			Namespace: cluster.Namespace,
		},
		Spec: cloudv1.ConfigChangeSpec{
			ClusterName:        cluster.Name,
			ConfigRevisionName: nextRevision.Name,
			ChangeStrategy:     strategy,
			Paused:             nil,
		},
	}
	if err := api.client.Create(ctx, cc); err != nil {
		return fmt.Errorf("create config change failed: %w", err)
	}
	return nil
}

// @Summary		    Diff config revision content
// @Tags		    config-manager
// @Description	    diff config revision content
// @Accept			json
// @Produce			json
// @Param			project_id				query		string				true	"project id"
// @Param			cluster_id				query		string				true	"cluster id"
// @Param 			config_revision_id		query		string				true	"config revision id"
// @Success			200	                    {object}	DiffResult
// @Failure      	400  		            {object}  	ErrResp
// @Failure      	500  		            {object}  	ErrResp
// @Router			/api/v1/secondaryconfirm/diff [get]
func (api *API) DiffHandler(ctx *gin.Context) {
	param := &DiffParam{}
	param.ProjectID = ctx.Query("project_id")
	param.ClusterID = ctx.Query("cluster_id")
	param.ConfigRevisionID = ctx.Query("config_revision_id")
	if err := param.Validate(); err != nil {
		EncodeError(ctx, ErrInvalidParameter(err.Error()))
		return
	}
	data, err := api.Diff(ctx, param)
	if err != nil {
		EncodeError(ctx, err)
		return
	}
	EncodeResp(ctx, data)
}

// @Summary		    Get config revision info
// @Tags		    config-manager
// @Description	    get config revision by id
// @Accept			json
// @Produce			json
// @Param 			config_revision_id		path		string				true	"name of ConfigRevision"
// @Param 			cluster_id      		query		string				true	"name of Cluster"
// @Param 			project_id      		query		string				true	"project id"
// @Success			200	                    {object}	ConfigRevisionInfo
// @Failure      	400  		            {object}  	ErrResp
// @Failure      	500  		            {object}  	ErrResp
// @Router			/api/v1/secondaryconfirm/config_revision/{config_revision_id} [get]
func (api *API) GetConfigRevisionHandler(c *gin.Context) {
	param := &ConfigRevisionGetParam{}
	if err := c.BindQuery(param); err != nil {
		EncodeError(c, ErrInvalidParameter(fmt.Sprintf("bind query param: %s", err)))
		return
	}
	if err := c.BindUri(param); err != nil {
		EncodeError(c, ErrInvalidParameter(fmt.Sprintf("bind uri param: %s", err)))
		return
	}
	logger := log.FromContext(c).WithValues("uuid", uuid.New().String())
	ctx := log.IntoContext(c.Request.Context(), logger)
	logger.Info("params", "param", param)
	if err := param.Validate(); err != nil {
		EncodeError(c, ErrInvalidParameter(err.Error()))
		return
	}
	data, err := api.GetConfigRevision(ctx, c, param)
	if err != nil {
		EncodeError(c, err)
		return
	}
	EncodeResp(c, data)
}

// @Summary		    Get config revision info
// @Tags		    config-manager
// @Description	    get config revision by id
// @Accept			json
// @Produce			json
// @Param 			batch_get_params        body		ConfigRevisionBatchGetParam					true	"parameter of batch get ConfigRevision"
// @Success			200	                    {object}	[]ConfigRevisionInfo
// @Failure      	400  		            {object}  	ErrResp
// @Failure      	500  		            {object}  	ErrResp
// @Router			/api/v1/secondaryconfirm/config_revision:batch_get [post]
func (api *API) BatchGetConfigRevisionHandler(c *gin.Context) {
	param := &ConfigRevisionBatchGetParam{}
	if err := c.BindJSON(param); err != nil {
		EncodeError(c, ErrInvalidParameter(fmt.Sprintf("bind param: %s", err)))
		return
	}
	if err := param.Validate(); err != nil {
		EncodeError(c, ErrInvalidParameter(err.Error()))
		return
	}
	logger := log.FromContext(c).WithValues("uuid", uuid.New().String())
	ctx := log.IntoContext(c.Request.Context(), logger)
	logger.Info("params", "param", param)

	var out []*ConfigRevisionInfo
	for _, each := range param.List {
		data, err := api.GetConfigRevision(ctx, c, &each)
		if err != nil {
			EncodeError(c, err)
			return
		}

		out = append(out, data)
	}

	EncodeResp(c, out)
}

// @Summary		Apply config revision on cluster
// @Tags		config-manager
// @Description	apply config revision on cluster
// @Accept			json
// @Produce			json
// @Param			param			body		ApplyParam				true	"param"
// @Success			200				{object}	ApplyResult
// @Failure      	400  			{object}  	ErrResp
// @Failure      	500  			{object}  	ErrResp
// @Router			/api/v1/secondaryconfirm/apply [post]
func (api *API) ApplyHandler(ctx *gin.Context) {
	param := &ApplyParam{}
	if err := ctx.BindJSON(param); err != nil {
		EncodeError(ctx, ErrInvalidParameter(err.Error()))
		return
	}
	if err := param.Validate(); err != nil {
		EncodeError(ctx, ErrInvalidParameter(err.Error()))
		return
	}
	data, err := api.Apply(ctx, param)
	if err != nil {
		EncodeError(ctx, err)
		return
	}
	EncodeResp(ctx, data)
}

func genConfigPair(currentCfg *cloudv1.Configuration, nextCfg *cloudv1.Configuration) (*ConfigPair, error) {
	var currentToml, nextToml string
	if currentCfg != nil && currentCfg.Config != nil {
		currentTomlBytes, err := currentCfg.Config.MarshalTOML()
		if err != nil {
			return nil, err
		}
		currentToml = string(currentTomlBytes)
	}
	if nextCfg != nil && nextCfg.Config != nil {
		nextTomlBytes, err := nextCfg.Config.MarshalTOML()
		if err != nil {
			return nil, err
		}
		nextToml = string(nextTomlBytes)
	}
	currentImage := currentCfg.Image()
	nextImage := nextCfg.Image()

	return &ConfigPair{
		CurrentImage: currentImage,
		NextImage:    nextImage,
		CurrentToml:  currentToml,
		NextToml:     nextToml,
	}, nil
}
