package scripttask

import (
	"context"
	"fmt"
	"time"

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

type execProcessor struct {
	db        repo.ScriptExec
	svc       service.Workload
	auditRepo repo.AuditLog
}

func (e execProcessor) Process(ctx context.Context) error {
	actives, err := e.db.List(ctx,
		options.NewListScriptExecOption().AddFilter(
			options.Statuses(model.ActiveStatuses...),
		))
	if err != nil {
		log.Log.Errorf(err, "failed to list active script execs")
		return err
	}

	for _, scriptExec := range actives {
		err = e.processOneWithLock(ctx, scriptExec)
		if err != nil {
			log.Log.Errorf(err, "failed to process script exec %s", scriptExec.ID)
		}
	}

	return nil
}

func (e execProcessor) processOneWithLock(
	ctx context.Context,
	scriptExec model.ScriptExec,
) error {
	// Start transaction and hold it for the entire operation
	tx, err := e.db.BeginTx(ctx)
	if err != nil {
		return fmt.Errorf("failed to begin transaction: %w", err)
	}
	defer tx.Rollback()

	log.Log.Infof("processing script exec %s", scriptExec.ID)
	scriptExec, err = e.db.GetAndLockForUpdateInTx(ctx, tx, scriptExec.ID)
	if err != nil {
		log.Log.Infof("failed to get and lock script exec %s", scriptExec.ID)
		return err
	}

	now := time.Now().UTC()

	test, err := e.svc.GetWorkloadTest(ctx, scriptExec.TestID)
	if err != nil {
		log.Log.Errorf(err, "failed to get workload test %s", scriptExec.TestID)
		return err
	}
	scriptExec.WorkloadTest = &test

	if test.IsFinished() {
		log.Log.Infof("stop script exec %s for workload test %s is finished", scriptExec.ID, test.ID)
		scriptExec, err = e.stop(ctx, scriptExec)
	} else if now.Sub(*scriptExec.StartedAt) >= scriptExec.TotalDuration {
		log.Log.Infof("complete script exec %s for workload test %s", scriptExec.ID, test.ID)
		scriptExec, err = e.complete(ctx, scriptExec)
	} else {
		scriptExec, err = e.trigger(ctx, scriptExec)
		if err != nil {
			log.Log.Errorf(err, "failed to process script exec %s", scriptExec.ID)
		}
	}
	if err != nil {
		log.Log.Errorf(err, "failed to process script exec %s", scriptExec.ID)
	}

	if err := e.db.UpdateInTx(ctx, tx, scriptExec); err != nil {
		log.Log.Errorf(err, "failed to update script exec %s", scriptExec.ID)
	}

	// 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
}

func (e execProcessor) trigger(
	ctx context.Context,
	scriptExec model.ScriptExec,
) (exec model.ScriptExec, err error) {
	exec = scriptExec

	for i, phase := range scriptExec.Phases {
		if phase.GetStatus() != model.ExecStatusPending {
			continue
		}
		startDuration := phase.GetStartDuration()
		if time.Now().UTC().Sub(*scriptExec.StartedAt) < startDuration {
			continue
		}

		log.Log.Infof("start phase %d of script exec %s", i, scriptExec.ID)
		phase.SetStatus(model.ExecStatusRunning)

		phaseProcessor := newPhaseProcessor(
			e.svc, e.auditRepo, phase.GetType())
		if phaseProcessor == nil {
			log.Log.Errorf(nil, "failed to get phase processor for phase %d of script exec %s", i, scriptExec.ID)
			phase.SetStatus(model.ExecStatusFailed)
		} else {
			phase, err = phaseProcessor.Process(ctx, scriptExec, phase)
			if err != nil {
				log.Log.Errorf(err, "failed to process phase %d of script exec %s", i, scriptExec.ID)
			}
		}

		exec.Phases[i] = phase
	}
	if err != nil {
		exec.OnFailure()
	}
	return
}

func (e execProcessor) stop(
	ctx context.Context,
	scriptExec model.ScriptExec,
) (model.ScriptExec, error) {
	scriptExec.OnStop()
	return scriptExec, nil
}

func (e execProcessor) complete(
	ctx context.Context,
	scriptExec model.ScriptExec,
) (model.ScriptExec, error) {
	scriptExec.OnComplete()
	return scriptExec, nil
}
