package task

import (
	"context"
	"fmt"
	"time"

	"github.com/tidbcloud/pov-service/internal/client/email"
	"github.com/tidbcloud/pov-service/internal/gateway"
	"github.com/tidbcloud/pov-service/internal/model"
	"github.com/tidbcloud/pov-service/internal/pkg/log"
	"github.com/tidbcloud/pov-service/internal/pkg/utils"
	"github.com/tidbcloud/pov-service/internal/repo"
	"github.com/tidbcloud/pov-service/internal/service"
)

// TaskProcessor defines the interface for processing workload tasks
type TaskProcessor interface {
	// CanProcess returns true if this processor can handle the given test and task phase
	CanProcess(test model.WorkloadTest, taskPhase model.WorkloadTaskPhase) bool

	// Process processes the workload test, returning the updated test
	Process(ctx context.Context, test model.WorkloadTest) (model.WorkloadTest, error)

	// GetName returns the processor name for logging
	GetName() string
}

// ProcessorManager manages all task processors
type ProcessorManager struct {
	processors     []TaskProcessor
	processorHooks []ProcessorHook
	db             repo.WorkloadTest
	gateway        gateway.Workload
	svc            service.Workload
	emailCli       email.Client
	// TODO: record audit log from eventbus
	auditRepo repo.AuditLog
}

// NewProcessorManager creates a new processor manager
func NewProcessorManager(
	params ProcessorManagerParams,
) *ProcessorManager {
	pm := &ProcessorManager{
		db:        params.Db,
		auditRepo: params.AuditRepo,
		gateway:   params.Gateway,
		svc:       params.Svc,
		emailCli:  params.EmailCli,
	}

	// Register all processors
	// TODO:  Add validate mechanism to make sure there is no leaked status/phase
	pm.processors = []TaskProcessor{
		NewTestStartProcessor(pm),
		NewCreateClusterTriggerProcessor(pm),
		NewCreateClusterRunningProcessor(pm),
		NewPrepareDataTriggerProcessor(pm),
		NewPrepareDataRunningProcessor(pm),
		NewRunTrafficTriggerProcessor(pm),
		NewRunTrafficRunningProcessor(pm),
		NewPhaseTransitionProcessor(pm, params.TransHooks...),
		NewStopTestProcessor(pm),
		NewStopTestRunningProcessor(pm),
	}
	pm.processorHooks = params.ProcessHooks
	log.Log.Infof("Registered %d task processors with %d hooks", len(pm.processors), len(pm.processorHooks))

	return pm
}

// ProcessAll processes all workload tests using the declarative pattern
func (pm *ProcessorManager) ProcessAll(ctx context.Context) error {
	// Get all active tests once to avoid multiple database calls
	activeTests, err := pm.db.ListActive(ctx)
	if err != nil {
		return fmt.Errorf("failed to list active tests: %w", err)
	}

	// Process each processor with the same active tests
	for _, processor := range pm.processors {
		if err := pm.processWithProcessor(ctx, processor, activeTests); err != nil {
			log.Log.Errorf(err, "processor %s failed", processor.GetName())
		}
	}
	return nil
}

// processWithProcessor processes workload tests using a specific processor
func (pm *ProcessorManager) processWithProcessor(ctx context.Context, processor TaskProcessor, activeTests []model.WorkloadTest) error {
	for _, test := range activeTests {
		// Check if processor can handle this test
		if processor.CanProcess(test, pm.getCurrentTaskPhase(test)) {
			start := time.Now()
			// Try to process with locking
			if err := pm.processWorkloadTestWithLock(ctx, test.ID, processor); err != nil {
				return err
			} else {
				newTest, err := pm.db.Get(ctx, test.ID)
				if err != nil {
					log.Log.Errorf(err, "failed to get workload test %s", test.ID)
					return err
				}

				// Trigger hooks
				go func() {
					for _, hook := range pm.processorHooks {
						if hook.Match(processor, newTest) {
							log.Log.Infof("processor %s trigger hook for workload test %s", processor.GetName(), test.ID)
							if err := hook.Do(ctx, newTest); err != nil {
								log.Log.Errorf(err, "processor %s trigger hook for workload test %s failed", processor.GetName(), test.ID)
							}
						}
					}
				}()
			}
			log.Log.Infof("processor %s processed workload test %s in %.2f s", processor.GetName(), test.ID, time.Since(start).Seconds())
		}
	}

	return nil
}

// processWorkloadTestWithLock processes a test with locking to prevent concurrent processing
// The lock is held for the entire operation to prevent race conditions
//
// IMPORTANT: Trigger processors (CreateClusterTriggerProcessor, PrepareDataTriggerProcessor,
// RunTrafficTriggerProcessor...) must handle external service errors gracefully by logging the error
// and returning nil (not the error) to avoid retrying non-idempotent operations.
// This ensures external services are called exactly once, preventing duplicate resource creation.
func (pm *ProcessorManager) processWorkloadTestWithLock(ctx context.Context, testID string, processor TaskProcessor) error {
	// Start transaction and hold it for the entire operation
	tx, err := pm.db.BeginTx(ctx)
	if err != nil {
		return fmt.Errorf("failed to begin transaction: %w", err)
	}
	defer tx.Rollback()

	// Get test with lock - lock is held until transaction commits
	test, err := pm.db.GetAndLockForUpdateInTx(ctx, tx, testID)
	if err != nil {
		log.Log.Infof("failed to get and lock workload test %s", testID)
		return nil
	}

	// Re-check if processor can still handle this test (conditions might have changed)
	if !processor.CanProcess(test, pm.getCurrentTaskPhase(test)) {
		log.Log.Debugf("workload test %s no longer matches processor %s after lock", testID, processor.GetName())
		return nil
	}

	// Process the test while holding the lock
	updatedTest, err := processor.Process(ctx, test)
	if err != nil {
		// an error means retry again, so each processor should decide if it should retry or not
		log.Log.Errorf(err, "processor %s failed to process workload test %s", processor.GetName(), testID)
		return fmt.Errorf("processor %s failed to process workload test %s: %w", processor.GetName(), testID, err)
	}

	// Update the test in database within the same transaction
	if err := pm.db.UpdateInTx(ctx, tx, updatedTest); err != nil {
		log.Log.Errorf(err, "failed to update workload test %s after processing", testID)
		return fmt.Errorf("failed to update workload test %s after processing: %w", testID, err)
	}

	// Commit the transaction - this releases the lock
	if err := tx.Commit(); err != nil {
		log.Log.Errorf(err, "failed to commit transaction")
		return fmt.Errorf("failed to commit transaction: %w", err)
	}
	return nil
}

// getCurrentTaskPhase returns the current task phase for the test
func (pm *ProcessorManager) getCurrentTaskPhase(test model.WorkloadTest) model.WorkloadTaskPhase {
	currentTask := test.CurrentPhaseTask()
	if currentTask == nil {
		return model.WorkloadTaskPhasePending
	}
	return currentTask.Phase
}

// Base processor struct that implements common functionality
type BaseProcessor struct {
	manager *ProcessorManager
}

// Helper methods for common task field updates

// setTaskTriggering sets a task to triggering phase
func (bp *BaseProcessor) setTaskTriggering(task *model.WorkloadTask[any]) {
	task.Phase = model.WorkloadTaskPhaseTriggering
	task.Status = model.ExecStatusRunning
	task.StartedAt = utils.TimePointer(time.Now().UTC())
}

// setTaskFailed sets a task to failed phase with error message
func (bp *BaseProcessor) setTaskFailed(task *model.WorkloadTask[any], err error) {
	task.Phase = model.WorkloadTaskPhaseFailed
	task.Status = model.ExecStatusFailed
	task.Message = err.Error()
	task.FinishedAt = utils.TimePointer(time.Now().UTC())
}

// setTaskRunning sets a task to running phase with external service info
func (bp *BaseProcessor) setTaskRunning(task *model.WorkloadTask[any], taskBase model.WorkloadTaskBase) {
	task.RelatedServiceID = taskBase.RelatedServiceID
	task.Phase = model.WorkloadTaskPhaseRunning
}

// updateTaskFromRunning updates a task with running results
func (bp *BaseProcessor) updateTaskFromRunning(task *model.WorkloadTask[any], taskBase model.WorkloadTaskBase) {
	if task.StartedAt != nil {
		taskBase.StartedAt = task.StartedAt
	}
	originalID := task.ID // Preserve original ID
	task.WorkloadTaskBase = taskBase
	task.ID = originalID
	task.Phase = model.WorkloadTaskPhaseRunning
}

// setTaskCompleted sets a task to completed phase
func (bp *BaseProcessor) setTaskCompleted(task *model.WorkloadTask[any]) {
	task.Phase = model.WorkloadTaskPhaseCompleted
	task.SetProgress(1.0)
}

// setTaskFailedFromRunning sets a task to failed phase from running results
func (bp *BaseProcessor) setTaskFailedFromRunning(task *model.WorkloadTask[any]) {
	task.Phase = model.WorkloadTaskPhaseFailed
}

// setTaskStopping sets a task to stopping phase
func (bp *BaseProcessor) setTaskStopping(task *model.WorkloadTask[any]) {
	task.Phase = model.WorkloadTaskPhaseStopping
}

// setTaskStopped sets a task to stopping phase
func (bp *BaseProcessor) setTaskStopped(task *model.WorkloadTask[any]) {
	task.Phase = model.WorkloadTaskPhaseStopped
	task.Status = model.ExecStatusStopped
	task.FinishedAt = utils.TimePointer(time.Now().UTC())
}
