package experiments

import (
	"crypto/md5"
	"errors"
	"fmt"
	"hash/fnv"
	"strconv"

	"gitlab.alibaba-inc.com/pai_biz_arch/pairec-experiment/client_golang/api"
	"gitlab.alibaba-inc.com/pai_biz_arch/pairec-experiment/client_golang/common"
	"gitlab.alibaba-inc.com/pai_biz_arch/pairec-experiment/client_golang/model"
)

type ClientOption func(c *ExperimentClient)

func WithLogger(l Logger) ClientOption {
	return func(e *ExperimentClient) {
		e.Logger = l
	}
}

func WithErrorLogger(l Logger) ClientOption {
	return func(e *ExperimentClient) {
		e.ErrorLogger = l
	}
}
func WithToken(token string) ClientOption {
	return func(e *ExperimentClient) {
		e.Token = token
	}
}

type ExperimentClient struct {
	// Host A/B Test server host
	Host string
	// Token the request header Authorization
	Token string

	// Environment control the sdk shoud get which environment data .
	// Valid value is daily, prepub,product
	Environment string

	// APIClient invoke api to connect to the A/B Test Server
	APIClient *api.APIClient

	//
	sceneMap map[string]*model.Scene

	// sceneParamData map of parameters of scene name
	sceneParamData map[string]model.SceneParams

	// Logger specifies a logger used to report internal changes within the writer
	Logger Logger

	// ErrorLogger is the logger to report errors
	ErrorLogger Logger
}

func NewExperimentClient(host, environment string, opts ...ClientOption) (*ExperimentClient, error) {
	client := ExperimentClient{
		Host:        host,
		Environment: environment,
		sceneMap:    make(map[string]*model.Scene, 0),
	}

	for _, opt := range opts {
		opt(&client)
	}

	if err := client.Validate(); err != nil {
		return nil, err
	}

	cfg := api.NewConfiguration(client.Host, client.Token)
	client.APIClient = api.NewAPIClient(cfg)

	client.LoadExperimentData()
	client.LoadSceneParamsData()

	go client.loopLoadExperimentData()
	go client.loopLoadSceneParamsData()

	return &client, nil
}

// Validate check the  ExperimentClient value
func (e *ExperimentClient) Validate() error {
	if e.Host == "" {
		return errors.New("host is empty")
	}

	if e.Environment == "" {
		return errors.New("environment is empty")
	}
	if err := common.CheckEnvironmentValue(e.Environment); err != nil {
		return err
	}
	return nil
}

// MatchExperiment specifies to find match experiment by the ExperimentContext
// If not find the scene return error or return ExperimentResult
func (e *ExperimentClient) MatchExperiment(sceneName string, experimentContext *model.ExperimentContext) *model.ExperimentResult {
	sceneData := e.sceneMap

	scene, exist := sceneData[sceneName]

	if !exist {
		e.logError(fmt.Errorf("scene:%s, not found the scene info", sceneName))
		return model.NewExperimentResult(sceneName, experimentContext)
	}

	experimentResult := model.NewExperimentResult(sceneName, experimentContext)
	var defaultExperimentRoom *model.ExperimentRoom
	var matchExperimentRoom *model.ExperimentRoom
	for _, experimentRoom := range scene.ExperimentRooms {
		if experimentRoom.Type == common.ExpRoom_Type_Base {
			defaultExperimentRoom = experimentRoom
		} else if experimentRoom.Match(experimentContext) {
			matchExperimentRoom = experimentRoom
			break
		}

	}

	if matchExperimentRoom == nil {
		matchExperimentRoom = defaultExperimentRoom
	}

	if matchExperimentRoom != nil {
		experimentResult.ExperimentRoom = matchExperimentRoom
		experimentResult.Layers = matchExperimentRoom.Layers

		for _, layer := range matchExperimentRoom.Layers {
			for _, experimentGroup := range layer.ExperimentGroups {
				if experimentGroup.Match(experimentContext) {
					experimentResult.AddMatchExperimentGroup(layer.LayerName, experimentGroup)
					var defaultExperiment *model.Experiment
					var matchExperiment *model.Experiment
					for _, experiment := range experimentGroup.Experiments {
						if experiment.Type == common.Experiment_Type_Default {
							defaultExperiment = experiment
						}

					}
					// find match experiment
					if matchExperiment == nil {
						// first match debug users
						for _, experiment := range experimentGroup.Experiments {
							if experiment.Type != common.Experiment_Type_Default && experiment.MatchDebugUsers(experimentContext) {
								matchExperiment = experiment
								e.logInfo("match experiment debug users uid:%s", experimentContext.Uid)
								break
							}
						}
						if matchExperiment == nil {
							hashKey := fmt.Sprintf("%s_EXPROOM%d_LAYER%d_EXPGROUP%d", experimentContext.Uid, experimentGroup.ExpRoomId,
								experimentGroup.LayerId, experimentGroup.ExpGroupId)
							hashValue := e.hashValue(hashKey)
							e.logInfo("match experiment hash key:%s, value:%d", hashKey, hashValue)
							hashValueStr := strconv.FormatUint(hashValue, 10)
							experimentContext.SetExperimentHashString(hashValueStr)
							for _, experiment := range experimentGroup.Experiments {
								if experiment.Type != common.Experiment_Type_Default && experiment.Match(experimentContext) {
									matchExperiment = experiment
									break
								}
							}
						}

					}

					if matchExperiment == nil {
						// if defaultExperiment not found ,set baseExperiment is  defaultExperiment
						if defaultExperiment == nil {
							for _, experiment := range experimentGroup.Experiments {
								if experiment.Type == common.Experiment_Type_Base {
									defaultExperiment = experiment.Clone()
									defaultExperiment.Type = common.Experiment_Type_Default
								}
							}
						}

						matchExperiment = defaultExperiment
					}

					if matchExperiment != nil {
						experimentResult.AddMatchExperiment(layer.LayerName, matchExperiment)
					}
				}
			}
		}

	}

	experimentResult.Init()
	return experimentResult
}

func (e *ExperimentClient) hashValue(hashKey string) uint64 {
	md5 := md5.Sum([]byte(hashKey))
	hash := fnv.New64()
	hash.Write(md5[:])

	return hash.Sum64()
}
func (e *ExperimentClient) logInfo(msg string, args ...interface{}) {
	if e.Logger != nil {
		e.Logger.Printf(msg, args...)
	}
}
