package scripttask

import (
	"context"
	"fmt"
	"strconv"
	"time"

	"github.com/tidbcloud/pov-service/internal/common/constant"
	"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/repo"
	"github.com/tidbcloud/pov-service/internal/service"
)

func newPhaseProcessor(
	svc service.Workload,
	auditRepo repo.AuditLog,
	typ model.ScriptPhaseType,
) ScriptPhaseProcessor {
	switch typ {
	case model.ScriptPhaseTypeProceed:
		return proceedPhaseProcessor{svc, auditRepo}
	case model.ScriptPhaseTypeMutate:
		return mutatePhaseProcessor{svc, auditRepo}
	default:
		return nil
	}
}

type proceedPhaseProcessor struct {
	svc       service.Workload
	auditRepo repo.AuditLog
}

func (p proceedPhaseProcessor) Process(
	ctx context.Context,
	exec model.ScriptExec,
	phase model.ScriptPhaseExec,
) (model.ScriptPhaseExec, error) {
	proceed, ok := phase.(*model.ScriptProceedPhaseExec)
	if !ok {
		return nil, fmt.Errorf("invalid phase type: %T, expected: *model.ScriptProceedPhaseExec", phase.GetType())
	}

	err := p.auditRepo.Create(
		ctx, model.NewOperationLog(
			ctx,
			"tidbcloud-pov@pingcap.com",
			model.OperateTypeProceed,
			exec.TestID,
		).AppendMessage(proceed.GetMessage()),
	)
	if err != nil {
		log.Log.Errorf(err, "failed to create audit log for proceed phase of script exec %s", exec.ID)
		proceed.SetStatus(model.ExecStatusFailed)
		return proceed, nil
	}

	proceed.SetStatus(model.ExecStatusCompleted)
	return proceed, nil
}

type mutatePhaseProcessor struct {
	svc       service.Workload
	auditRepo repo.AuditLog
}

func (p mutatePhaseProcessor) Process(
	ctx context.Context,
	exec model.ScriptExec,
	phase model.ScriptPhaseExec,
) (model.ScriptPhaseExec, error) {
	mphase, ok := phase.(*model.ScriptMutationPhaseExec)
	if !ok {
		return nil, fmt.Errorf("invalid phase type: %T, expected: *model.ScriptMutationPhaseExec", phase.GetType())
	}
	if exec.WorkloadTest == nil {
		test, err := p.svc.GetWorkloadTest(ctx, exec.TestID)
		if err != nil {
			return nil, fmt.Errorf("failed to get workload test %s: %w", exec.TestID, err)
		}
		exec.WorkloadTest = &test
	}

	muts := mphase.Mutations
	operateTime := time.Now().UTC()
	fatCtx := icontext.NewOperateAtContext(ctx, operateTime)
	fatCtx = icontext.NewUserEmailContext(fatCtx, "tidbcloud-pov@pingcap.com")
	if mphase.Message != "" {
		fatCtx = icontext.NewOperateMessageContext(fatCtx, mphase.Message)
	}

	err := p.processDeclative(fatCtx, *exec.WorkloadTest, muts)
	if err != nil {
		phase.SetStatus(model.ExecStatusFailed)
		return phase, err
	}

	err = p.processInstruct(fatCtx, exec, muts)
	if err != nil {
		phase.SetStatus(model.ExecStatusFailed)
		return phase, err
	}

	phase.SetStatus(model.ExecStatusCompleted)
	return phase, nil
}

// process declative params
func (p mutatePhaseProcessor) processDeclative(
	ctx context.Context,
	test model.WorkloadTest,
	muts []model.ScriptMutation,
) error {
	// snapshot operate time to aggregate mutations
	declativeUpdateParams, err := p.aggregateUpdateParams(
		ctx,
		test,
		muts,
	)
	if err != nil {
		log.Log.Errorf(err, "failed to aggregate update params")
		return err
	}

	if err = p.svc.UpdateWorkloadTest(
		ctx, test.ID, declativeUpdateParams); err != nil {
		return err
	}
	return nil
}

// process instructive params
func (p mutatePhaseProcessor) processInstruct(
	ctx context.Context,
	exec model.ScriptExec,
	muts []model.ScriptMutation,
) error {
	for _, m := range muts {
		pro := newInstructMutateProcessor(p.svc, m.Param)
		if pro == nil {
			continue
		}
		if err := pro.Process(ctx, exec, m); err != nil {
			log.Log.Errorf(err, "failed to process instructive param %s", m.Param)
			return err
		}
	}

	return nil
}

// Get the declative params
func (p mutatePhaseProcessor) aggregateUpdateParams(
	_ context.Context,
	test model.WorkloadTest,
	muts []model.ScriptMutation,
) (params.UpdateWorkloadTest, error) {
	var (
		update params.UpdateWorkloadTest
		err    error
	)

	// setup the original params
	for _, param := range test.Params {
		switch param.Name {
		case string(constant.MaxConnectionNumber):
			update.MaxConn, err = strconv.Atoi(param.Value)
		// TODO: maybe use cluster ru instead.
		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
		}
	}

	for _, m := range muts {
		switch m.Param {
		case constant.MaxConnectionNumber:
			value, convErr := strconv.Atoi(m.ToValue)
			if convErr == nil {
				update.MaxConn = value
			}
		case constant.MaxRU:
			value, convErr := strconv.ParseInt(m.ToValue, 10, 64)
			if convErr == nil {
				update.MaxRU = value
			}
		case constant.MinRU:
			value, convErr := strconv.ParseInt(m.ToValue, 10, 64)
			if convErr == nil {
				update.MinRU = value
			}
		}
	}

	return update, nil
}
