package task

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

	"github.com/tidbcloud/pov-service/internal/common/constant"
	"github.com/tidbcloud/pov-service/internal/model"
	"github.com/tidbcloud/pov-service/internal/pkg/log"
)

const (
	RunTrafficTriggerProcessorName = "run-traffic-trigger"
	RunTrafficRunningProcessorName = "run-traffic-running"
)

// RunTrafficTriggerProcessor handles triggering run traffic tasks
type RunTrafficTriggerProcessor struct {
	BaseProcessor
}

func NewRunTrafficTriggerProcessor(manager *ProcessorManager) *RunTrafficTriggerProcessor {
	return &RunTrafficTriggerProcessor{BaseProcessor{manager}}
}

func (p *RunTrafficTriggerProcessor) GetName() string {
	return RunTrafficTriggerProcessorName
}

func (p *RunTrafficTriggerProcessor) CanProcess(test model.WorkloadTest, taskPhase model.WorkloadTaskPhase) bool {
	return test.Phase == model.WorkloadTestPhaseRunWorkload &&
		test.Status == model.ExecStatusRunning &&
		taskPhase == model.WorkloadTaskPhasePending
}

func (p *RunTrafficTriggerProcessor) Process(ctx context.Context, test model.WorkloadTest) (model.WorkloadTest, error) {
	log.Log.Infof("Triggering run traffic for test %s", test.ID)

	currentTask := test.CurrentPhaseTask()
	if currentTask == nil {
		return test, fmt.Errorf("no current task found")
	}

	// Update task phase to triggering
	p.setTaskTriggering(currentTask)

	// Get prepare data result
	prepareResult, err := test.GetPrepareDataResult()
	if err != nil {
		log.Log.Errorf(err, "failed to get prepare data result for test %s", test.ID)
		p.setTaskFailed(currentTask, err)
		return test, nil // Return nil to commit the transaction and avoid retry
	}

	go func() {
		log.Log.Infof("overprovision workload for test %s", test.ID)
		if err := p.manager.gateway.OverProvision(ctx, *test.Cluster); err != nil {
			log.Log.Errorf(err, "failed to overprovision workload for test %s", test.ID)
		}
	}()

	// Trigger run traffic
	taskBase, err := p.manager.gateway.TriggerRunTraffic(ctx, *test.Cluster, prepareResult, test.Params)
	if err != nil {
		// Don't return error for trigger processors to avoid retrying non-idempotent operations
		// Instead, mark the task as failed and let it be handled by phase transition processor
		log.Log.Errorf(err, "failed to trigger run traffic for test %s", test.ID)
		p.setTaskFailed(currentTask, err)
		return test, nil // Return nil to commit the transaction and avoid retry
	}
	currentTask.Start()

	// Update task with external service info
	p.setTaskRunning(currentTask, taskBase)

	return test, nil
}

// RunTrafficRunningProcessor handles running run traffic tasks
type RunTrafficRunningProcessor struct {
	BaseProcessor
}

func NewRunTrafficRunningProcessor(manager *ProcessorManager) *RunTrafficRunningProcessor {
	return &RunTrafficRunningProcessor{BaseProcessor{manager}}
}

func (p *RunTrafficRunningProcessor) GetName() string {
	return RunTrafficRunningProcessorName
}

func (p *RunTrafficRunningProcessor) CanProcess(test model.WorkloadTest, taskPhase model.WorkloadTaskPhase) bool {
	return test.Phase == model.WorkloadTestPhaseRunWorkload &&
		test.IsOnTheFly() &&
		(taskPhase == model.WorkloadTaskPhaseRunning ||
			taskPhase == model.WorkloadTaskPhaseUpdating)
}

func (p *RunTrafficRunningProcessor) Process(
	ctx context.Context,
	test model.WorkloadTest,
) (model.WorkloadTest, error) {
	log.Log.Infof("Polling run traffic for test %s", test.ID)

	currentTask := test.CurrentPhaseTask()
	if currentTask == nil {
		return test, fmt.Errorf("no current task found")
	}

	var taskBase model.WorkloadTaskBase
	// Poll the task
	switch currentTask.Phase {
	case model.WorkloadTaskPhaseRunning:
		tb, err := p.manager.gateway.PollRunTrafficTask(ctx, currentTask.WorkloadTaskBase)
		if err != nil {
			log.Log.Errorf(err, "failed to poll run traffic task for test %s", test.ID)
			return test, nil
		}
		taskBase = tb
	case model.WorkloadTaskPhaseUpdating: // Updating traffic task will recreate a new workload execution in tiworkload
		tb, err := p.updateTraffic(ctx, currentTask, test)
		if err != nil {
			log.Log.Errorf(err, "failed to update traffic task for test %s", test.ID)
			p.setTaskFailed(currentTask, fmt.Errorf("failed to update traffic"))
			return test, nil
		}
		taskBase = tb
	}

	// Update task with latest info
	p.updateTaskFromRunning(currentTask, taskBase)
	if durationMin, ok := test.GetDurationMinConf(); ok {
		passedDuration := currentTask.ExecDuration().Minutes()
		currentTask.SetProgress(float64(passedDuration) / float64(durationMin))
	}

	// Check if completed
	switch taskBase.Status {
	case model.ExecStatusCompleted:
		p.setTaskCompleted(currentTask)
		log.Log.Infof("Run traffic completed for test %s", test.ID)
	case model.ExecStatusFailed, model.ExecStatusStopped:
		p.setTaskFailedFromRunning(currentTask)
		log.Log.Errorf(fmt.Errorf("task failed"), "Run traffic failed for test %s: %s", test.ID, taskBase.Message)
	}

	return test, nil
}

func (p *RunTrafficRunningProcessor) updateTraffic(
	ctx context.Context,
	task *model.WorkloadTask[any],
	test model.WorkloadTest,
) (model.WorkloadTaskBase, error) {
	params := make(model.WorkloadParams, len(test.Params))
	copy(params, test.Params)

	int64ID, err := strconv.ParseInt(task.RelatedServiceID, 10, 64)
	if err != nil {
		log.Log.Errorf(err, "failed to parse related service id for task %s", task.ID)
		return model.WorkloadTaskBase{}, nil
	}

	duration, ok := test.GetDurationMinConf()
	if !ok {
		return model.WorkloadTaskBase{}, nil
	}

	// Calculate remaining duration
	passedMin := time.Since(*task.StartedAt).Minutes()
	leftMin := max(duration-int(passedMin), 1)
	params.Set(string(constant.Duration), fmt.Sprintf("%d", leftMin))

	tb, err := p.manager.gateway.TriggerRecreateTraffic(ctx, int64ID, params)
	if err != nil {
		// Don't return error for trigger processors to avoid retrying non-idempotent operations
		// Instead, mark the task as failed and let it be handled by phase transition processor
		log.Log.Errorf(err, "failed to trigger run traffic for task %s", task.ID)
		return model.WorkloadTaskBase{}, nil
	}

	return tb, nil
}
