package server

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"

	"github.com/go-logr/logr"
	"github.com/pingcap/log"
	"github.com/pkg/errors"
	"github.com/tidbcloud/config-service/api/v1alpha1"
	"github.com/tidbcloud/config-service/pkg/configeval"
	"github.com/tidbcloud/config-service/pkg/utils"
	"github.com/tidbcloud/config-service/sdk/api"
	"go.uber.org/zap"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type Server struct {
	// client not be a fully "direct" client -- it read from a cache
	infraApiCli             client.Client
	client                  client.Client
	log                     logr.Logger
	skipEvalNotFoundCluster bool
}

func NewServer(infraApiCli client.Client, cli client.Client, log logr.Logger, skipEval bool) *Server {
	return &Server{
		infraApiCli:             infraApiCli,
		client:                  cli,
		log:                     log,
		skipEvalNotFoundCluster: skipEval,
	}
}

func (s *Server) DescribeClusterConfiguration(w http.ResponseWriter, r *http.Request) {
	var input api.DescribeClusterConfigInput

	err := json.NewDecoder(r.Body).Decode(&input)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	err = validateDescribeClusterConfigInput(input)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	cc, err := s.findAndValidateClusterConfig(input.TenantID, input.ClusterID)
	if err != nil {
		s.log.Error(err, "find cluster config")
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	output, err := generateConfigOutput(input, cc)
	if err != nil {
		s.log.Error(err, "generate cluster config output")
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	err = json.NewEncoder(w).Encode(output)
	if err != nil {
		s.log.Error(err, "encoder cluster config")
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
}

func validateDescribeClusterConfigInput(input api.DescribeClusterConfigInput) error {
	if input.TenantID == nil {
		return fmt.Errorf("must specipy tenant ID")
	}
	for _, comp := range input.Components {
		if len(string(comp.Component)) < 1 {
			return fmt.Errorf("name must be specified for each component")
		}
	}
	return nil
}

func (s *Server) findAndValidateClusterConfig(tenantID, clusterID *uint64) (*v1alpha1.ClusterConfig, error) {
	cc, err := s.findClusterConfig(tenantID, clusterID)
	if err != nil {
		return nil, err
	}

	if tenantID != nil && clusterID != nil {
		c, err := configeval.FindCluster(s.infraApiCli, strconv.FormatUint(*tenantID, 10), strconv.FormatUint(*clusterID, 10))
		if err != nil {
			if s.skipEvalNotFoundCluster {
				err = configeval.RemoveExpClusterConifg(cc, c)
				if err != nil {
					return nil, errors.Wrapf(err, "remove clusterconfig, tenant id %v, cluster id %v", *tenantID, *clusterID)
				}
			} else {
				return nil, errors.Wrapf(err, "find cluster tenant id %v, cluster id %v", *tenantID, *clusterID)
			}
		} else {
			err = configeval.EvalClusterConfig(s.infraApiCli, cc, c)
			if err != nil {
				return nil, err
			}
		}
	}

	err = validateClusterConfig(cc)
	if err != nil {
		return nil, err
	}
	return cc, nil
}

// findClusterConfig find cluster config cr by tenantID and clusterID.
func (s *Server) findClusterConfig(tenantID, clusterID *uint64) (*v1alpha1.ClusterConfig, error) {
	tenant := strconv.FormatUint(*tenantID, 10)
	cluster := ""
	if clusterID != nil {
		cluster = strconv.FormatUint(*clusterID, 10)
	}
	return utils.FindAndValidateClusterConfig(s.client, tenant, cluster)
}

func validateClusterConfig(cc *v1alpha1.ClusterConfig) error {
	if cc.Spec.ConfigMeta == nil {
		// should not nil
		log.Warn("malformed cluster config, config meta is nil", zap.String("name", cc.Name))
		return errors.New("malformed cluster config, config meta is nil")
	}
	return nil
}

func generateConfigOutput(input api.DescribeClusterConfigInput, cc *v1alpha1.ClusterConfig) (*api.DescribeClusterConfigOutput, error) {
	componentConfig, err := generateComponentOutput(input, cc)
	if err != nil {
		return nil, err
	}
	output := api.DescribeClusterConfigOutput{
		Version: cc.Spec.Version,
		ConfigMeta: &api.ConfigMeta{
			ID:            cc.ObjectMeta.Name,
			GitCommitID:   cc.Spec.ConfigMeta.GitCommitID,
			ConfigVersion: cc.Spec.ConfigMeta.ConfigVersion,
		},
		Components: componentConfig,
	}

	return &output, nil
}

func getComponentSpec(cc *v1alpha1.ClusterConfig, comp api.ComponentName, instanceType string) *v1alpha1.ComponentSpec {
	switch comp {
	case api.ComponentTiKV:
		return cc.GetTiKVSpec(instanceType)
	case api.ComponentTiProxy:
		return cc.GetTiProxySepc(instanceType)
	case api.ComponentPD:
		return cc.GetPDSpec(instanceType)
	case api.ComponentTiDB:
		return cc.GetTiDBSpec(instanceType)
	case api.ComponentTiFlash:
		return cc.GetTiFlashSpec(instanceType)
	case api.ComponentTiFlashLearner:
		return cc.GetTiFlashLearnerSpec(instanceType)
	default:
		return nil
	}
}

func generateComponentOutput(input api.DescribeClusterConfigInput, cc *v1alpha1.ClusterConfig) ([]api.ComponentOutput, error) {
	var componentOutputs []api.ComponentOutput
	for _, component := range input.Components {
		componentSpec := getComponentSpec(cc, component.Component, component.InstanceType)
		if componentSpec == nil {
			continue
		}

		co := api.ComponentOutput{
			Image: api.Image{
				BaseImage: componentSpec.BaseImage,
				Tag:       componentSpec.Tag,
			},
			Component:    component.Component,
			InstanceType: component.InstanceType,
			Config:       api.NewTomlConfig(componentSpec.Config.MP),
		}

		componentOutputs = append(componentOutputs, co)
	}
	return componentOutputs, nil
}
