package workload

import (
	"context"
	"encoding/json"
	"errors"
	"strconv"
	"time"

	"github.com/tidbcloud/pov-service/internal/common/constant"
	appErr "github.com/tidbcloud/pov-service/internal/common/errors"
	"github.com/tidbcloud/pov-service/internal/common/options"
	"github.com/tidbcloud/pov-service/internal/common/params"
	"github.com/tidbcloud/pov-service/internal/model"
	"github.com/tidbcloud/pov-service/internal/pkg/icontext"
	"github.com/tidbcloud/pov-service/internal/pkg/log"
	"github.com/tidbcloud/pov-service/internal/pkg/utils"
)

var errScriptRepoNotConfigured = errors.New("script repository is not configured")

func (s *scriptImpl) ListTemplates(ctx context.Context) ([]model.ScriptTemplate, error) {
	if s.scriptRepo == nil {
		return nil, errScriptRepoNotConfigured
	}
	return s.scriptRepo.ListTemplates(ctx)
}

func (s *scriptImpl) List(
	ctx context.Context,
	params params.ListScripts,
) ([]model.Script, error) {
	if s.scriptRepo == nil {
		return nil, errScriptRepoNotConfigured
	}

	opts := options.NewListScriptOption()
	if params.TestID != "" {
		test, err := s.workload.GetWorkloadTest(ctx, params.TestID)
		if err != nil {
			return nil, err
		}
		opts = opts.AddFilter(options.WorkloadID(test.WorkloadID))
		if test.Cluster != nil {
			opts = opts.AddFilter(options.ClusterTypeOrEmpty(test.Cluster.Type))
		}
	}

	return s.scriptRepo.ListScripts(ctx, opts)
}

func (s *scriptImpl) Activate(
	ctx context.Context,
	testID string,
	scriptID int64) (model.ScriptExec, error) {
	log.Log.Infof("activate script %d for test %s", scriptID, testID)

	script, err := s.scriptRepo.GetScript(ctx, scriptID)
	if err != nil {
		return model.ScriptExec{}, err
	}

	test, err := s.workload.GetWorkloadTest(ctx, testID)
	if err != nil {
		return model.ScriptExec{}, err
	}
	if test.Phase != model.WorkloadTestPhaseRunWorkload ||
		test.Status != model.ExecStatusRunning {
		return model.ScriptExec{}, appErr.NewInvalidArgument("workload test is not running")
	}
	scriptExec := model.NewScriptExec(
		testID,
		script,
	)

	ret, err := s.scriptExecRepo.Create(
		ctx, scriptExec,
	)
	if err != nil {
		log.Log.Errorf(err, "failed to create script exec for test %s", testID)
		return model.ScriptExec{}, err
	}
	log.Log.Infof("activate script %s for test %s", ret.ID, testID)
	return ret, err
}

func (s *scriptImpl) CreateTemplate(
	ctx context.Context,
	tmpl model.ScriptTemplate) (model.ScriptTemplate, error) {
	if s.scriptRepo == nil {
		return model.ScriptTemplate{}, errScriptRepoNotConfigured
	}
	return s.scriptRepo.CreateTemplate(ctx, tmpl)
}

func (s *scriptImpl) UpdateTemplate(ctx context.Context, tmpl model.ScriptTemplate) (model.ScriptTemplate, error) {
	if s.scriptRepo == nil {
		return model.ScriptTemplate{}, errScriptRepoNotConfigured
	}
	return s.scriptRepo.UpdateTemplate(ctx, tmpl)
}

func (s *scriptImpl) DeleteTemplate(ctx context.Context, id int64) error {
	if s.scriptRepo == nil {
		return errScriptRepoNotConfigured
	}
	return s.scriptRepo.DeleteTemplate(ctx, id)
}

func (s *scriptImpl) Create(ctx context.Context, script model.Script) (model.Script, error) {
	if s.scriptRepo == nil {
		return model.Script{}, errScriptRepoNotConfigured
	}
	return s.scriptRepo.CreateScript(ctx, script)
}

func (s *scriptImpl) Update(ctx context.Context, script model.Script) (model.Script, error) {
	if s.scriptRepo == nil {
		return model.Script{}, errScriptRepoNotConfigured
	}
	return s.scriptRepo.UpdateScript(ctx, script)
}

func (s *scriptImpl) Delete(ctx context.Context, id int64) error {
	if s.scriptRepo == nil {
		return errScriptRepoNotConfigured
	}
	return s.scriptRepo.DeleteScript(ctx, id)
}

func (s *scriptImpl) runScript(ctx context.Context, testID string, script model.Script) {
	log.Log.Infof("activate script %d for test %s", script.ID, testID)

	test, err := s.workload.GetWorkloadTest(ctx, testID)
	if err != nil {
		log.Log.Errorf(err, "failed to get workload test %s when activating script %d", testID, script.ID)
		return
	}
	if test.Status != model.ExecStatusRunning {
		log.Log.Errorf(err, "workload test %s is not running while activating script %d", testID, script.ID)
		return
	}

	var current model.WorkloadTest
	if err := utils.Await(
		ctx, 5*time.Minute, 5*time.Second,
		func() (bool, error) {
			var waitErr error
			current, waitErr = s.workload.GetWorkloadTest(ctx, testID)
			if waitErr != nil {
				return false, waitErr
			}
			task := current.CurrentPhaseTask()
			return task.Phase == model.WorkloadTaskPhaseRunning, nil
		},
	); err != nil {
		log.Log.Errorf(err, "failed to wait for workload test %s ready state for script %d", testID, script.ID)
		return
	}

	startTime := time.Now()
	mutates := script.GetMutatePhases()
	for i := 0; i < len(mutates); {
		mutate := mutates[i]

		current, err = s.workload.GetWorkloadTest(ctx, testID)
		if err != nil {
			log.Log.Errorf(err, "failed to get workload test %s when executing script %d", testID, script.ID)
			continue
		}
		if current.Status != model.ExecStatusRunning {
			log.Log.Errorf(err, "workload test %s stopped while executing script %d", testID, script.ID)
			break
		}

		toUpdate, err := getUpdateWorkloadTestParams(current)
		if err != nil {
			log.Log.Errorf(err, "failed to build workload test params for script %d", script.ID)
			return
		}
		toInstruct := params.InstructWorkloadTest{}

		ctxWithMsg := ctx
		if mutate.Message != "" {
			ctxWithMsg = icontext.NewOperateMessageContext(ctx, mutate.Message)
		}

		if time.Since(startTime) >= mutate.StartDuration {
			for _, m := range mutate.Mutations {
				switch m.Param {
				case constant.MaxConnectionNumber:
					value, convErr := strconv.Atoi(m.ToValue)
					if convErr == nil {
						toUpdate.MaxConn = value
					}
				case constant.MaxRU:
					value, convErr := strconv.ParseInt(m.ToValue, 10, 64)
					if convErr == nil {
						toUpdate.MaxRU = value
					}
				case constant.MinRU:
					value, convErr := strconv.ParseInt(m.ToValue, 10, 64)
					if convErr == nil {
						toUpdate.MinRU = value
					}
				case constant.SQLStatement:
					toInstruct.SQLStatement = m.ToValue
				case constant.InjectFailure:
					// Parse JSON from ToValue to extract duration
					var failureParams struct {
						Duration string `json:"duration"`
					}
					if err := json.Unmarshal([]byte(m.ToValue), &failureParams); err == nil {
						if err = s.workload.InjectFailure(ctxWithMsg, testID, failureParams.Duration); err != nil {
							log.Log.Errorf(err, "failed to inject failure for workload test %s using script %d", testID, script.ID)
							return
						}
					} else {
						log.Log.Errorf(err, "failed to parse inject failure parameters for script %d", script.ID)
					}
				case constant.RollbackFailure:
					// Call RollbackFailure method
					if err = s.workload.RollbackFailure(ctxWithMsg, testID); err != nil {
						log.Log.Errorf(err, "failed to rollback failure for workload test %s using script %d", testID, script.ID)
						return
					}
				}
			}

			log.Log.Infof("updating workload test %s using script %d", testID, script.ID)
			if err = s.workload.UpdateWorkloadTest(ctxWithMsg, testID, toUpdate); err != nil {
				log.Log.Errorf(err, "failed to update workload test %s for script %d", testID, script.ID)
				return
			}
			if toInstruct.SQLStatement != "" {
				if err = s.workload.InstructWorkloadTest(ctxWithMsg, testID, toInstruct); err != nil {
					log.Log.Errorf(err, "failed to run instruction on workload test %s for script %d", testID, script.ID)
					return
				}
			}

			i++
		}

		if i < len(mutates) {
			time.Sleep(10 * time.Second)
		}
	}
}

func getUpdateWorkloadTestParams(
	test model.WorkloadTest,
) (params.UpdateWorkloadTest, error) {
	var (
		update params.UpdateWorkloadTest
		err    error
	)

	for _, param := range test.Params {
		switch param.Name {
		case string(constant.MaxConnectionNumber):
			update.MaxConn, err = strconv.Atoi(param.Value)
		case string(constant.MaxRU):
			update.MaxRU, err = strconv.ParseInt(param.Value, 10, 64)
		case string(constant.MinRU):
			update.MinRU, err = strconv.ParseInt(param.Value, 10, 64)
		}
		if err != nil {
			return params.UpdateWorkloadTest{}, err
		}
	}

	return update, nil
}
