package grouptasks

import (
	"encoding/json"
	"errors"
	"fmt"
	"jannex/admin-dashboard-backend/modules/cache"
	"jannex/admin-dashboard-backend/modules/config"
	"jannex/admin-dashboard-backend/modules/database"
	"jannex/admin-dashboard-backend/modules/logger"
	"jannex/admin-dashboard-backend/modules/structs"
	"jannex/admin-dashboard-backend/modules/systempermissions"
	"jannex/admin-dashboard-backend/modules/utils"
	"jannex/admin-dashboard-backend/socketclients"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"github.com/google/uuid"
	"github.com/rs/zerolog/log"
)

func InitLoadCategoryGroups() {
	entries, err := os.ReadDir(config.Cfg.FolderPaths.GroupTasksGroups)

	if err != nil {
		panic(err)
	}

	for _, entry := range entries {
		if !entry.IsDir() {
			continue
		}

		files, err := os.ReadDir(config.Cfg.FolderPaths.GroupTasksGroups + entry.Name())

		log.Debug().Msgf("entry %v", entry.Name())

		if err != nil {
			log.Error().Msg("Failed to read groups directory files, error: " + err.Error())
			return
		}

		for _, file := range files {
			if file.Name() == "index.json" {
				content, err := os.ReadFile(config.Cfg.FolderPaths.GroupTasksGroups + entry.Name() + "/index.json")

				if err != nil {
					log.Error().Msg("Failed to read file content, error: " + err.Error())
					return
				}

				var group structs.Group

				json.Unmarshal(content, &group)

				group.Id = entry.Name()

				cache.AddCategoryGroup(group)
			}
		}
	}
}

func ReloadCategoryGroups(category string) {
	entries, err := os.ReadDir(config.Cfg.FolderPaths.GroupTasksGroups)

	if err != nil {
		panic(err)
	}

	cache.RemoveAllCategoryGroupsByCategory(category)

	var categoryGroups []structs.Group

	// looping through groups directory
	for _, entry := range entries {
		if !entry.IsDir() {
			continue
		}

		files, err := os.ReadDir(config.Cfg.FolderPaths.GroupTasksGroups + entry.Name())

		if err != nil {
			log.Error().Msg("Failed to read groups directory files, error: " + err.Error())
			return
		}

		for _, file := range files {
			if file.Name() == "index.json" {
				content, err := os.ReadFile(config.Cfg.FolderPaths.GroupTasksGroups + entry.Name() + "/index.json")

				if err != nil {
					log.Error().Msg("Failed to read file content, error: " + err.Error())
					return
				}

				var group structs.Group

				json.Unmarshal(content, &group)

				group.Id = entry.Name()

				if group.Category == category {
					cache.AddCategoryGroup(group)

					categoryGroups = append(categoryGroups, group)
				}
			}
		}
	}

	if len(categoryGroups) > 0 {
		socketclients.BroadcastMessage(structs.SendSocketMessage{
			Cmd: utils.SentCmdGroupTasksReloaded,
			Body: struct {
				Category       string
				CategoryGroups []structs.Group
			}{
				Category:       category,
				CategoryGroups: categoryGroups,
			},
		})
	} else { // category was removed
		socketclients.BroadcastMessage(structs.SendSocketMessage{
			Cmd: utils.SentCmdGroupTasksReloaded,
			Body: struct {
				RemovedCategory    string
				RemovedPermissions []string
			}{
				RemovedCategory:    category,
				RemovedPermissions: systempermissions.RemoveDynamicGroupTasksPermissionsByCategory(category),
			},
		})
	}
}

func LookingForCategoryGroupChanges(userId string) {
	entries, err := os.ReadDir(config.Cfg.FolderPaths.GroupTasksGroups)

	if err != nil {
		panic(err)
	}

	var foundCategoryGroups []structs.Group

	// looping through groups directory
	for _, entry := range entries {
		if !entry.IsDir() {
			continue
		}

		files, err := os.ReadDir(config.Cfg.FolderPaths.GroupTasksGroups + entry.Name())

		if err != nil {
			log.Error().Msg("Failed to read groups directory files, error: " + err.Error())
			return
		}

		for _, file := range files {
			if file.Name() == "index.json" {
				content, err := os.ReadFile(config.Cfg.FolderPaths.GroupTasksGroups + entry.Name() + "/index.json")

				if err != nil {
					log.Error().Msg("Failed to read file content, error: " + err.Error())
					return
				}

				var group structs.Group

				json.Unmarshal(content, &group)

				group.Id = entry.Name()

				foundCategoryGroups = append(foundCategoryGroups, group)
			}
		}
	}

	cachedCategoryGroups := cache.GetCategoryGroups()
	var addedGroupTasksPermissions []string
	var newCategories []string

	var addedCategoryGroups []structs.CategoryGroup

	for _, foundCategoryGroup := range foundCategoryGroups {
		// check for new added category groups
		if !existsCategoryGroup(foundCategoryGroup.Category, cachedCategoryGroups) {
			cache.AddCategoryGroup(foundCategoryGroup)

			if !isInList(foundCategoryGroup.Category, newCategories) {
				newCategories = append(newCategories, foundCategoryGroup.Category)
			}
		}
	}

	var logResultAddedCategoryGroups []string
	var logResultRemovedCategoryGroups []string

	for _, newCategory := range newCategories {
		addedGroupTasksPermissions = append(addedGroupTasksPermissions, systempermissions.AddDynamicGroupTasksPermissionsByCategory(newCategory)...)

		cGroup := cache.GetCategoryGroupByCategory(newCategory)
		addedCategoryGroups = append(addedCategoryGroups, cGroup)

		logResultAddedCategoryGroups = append(logResultAddedCategoryGroups, newCategory)
	}

	var removedCategoryGroups []string
	var removedGroupTasksPermissions []string

	for _, cachedCategoryGroup := range cachedCategoryGroups {
		if !existsGroup(cachedCategoryGroup.Category, foundCategoryGroups) {
			removedCategoryGroups = append(removedCategoryGroups, cachedCategoryGroup.Category)
			cache.RemoveAllCategoryGroupsByCategory(cachedCategoryGroup.Category)

			removedGroupTasksPermissions = append(removedGroupTasksPermissions, systempermissions.RemoveDynamicGroupTasksPermissionsByCategory(cachedCategoryGroup.Category)...)

			logResultRemovedCategoryGroups = append(logResultRemovedCategoryGroups, cachedCategoryGroup.Category)
		}
	}

	result := make(map[string]interface{})

	if len(addedGroupTasksPermissions) > 0 {
		result["AddedPermissions"] = addedGroupTasksPermissions
		result["MasterRoleId"] = systempermissions.GetMasterRoleId()
	}

	if len(addedCategoryGroups) > 0 {
		result["AddedCategoryGroups"] = addedCategoryGroups
	}

	if len(removedCategoryGroups) > 0 {
		result["RemovedCategoryGroups"] = removedCategoryGroups
	}

	if len(removedGroupTasksPermissions) > 0 {
		result["RemovedPermissions"] = removedGroupTasksPermissions
	}

	socketclients.BroadcastMessage(structs.SendSocketMessage{
		Cmd:  utils.SentCmdGroupTasksCategoryGroupChanges,
		Body: result,
	})

	logger.AddGroupTasksLog(structs.LogMessage{
		Id:   7,
		Type: utils.LogTypeInfo,
		Messages: []structs.LogData{
			{Type: "userId", Value: userId},
			{Type: "result", Value: utils.MarshalJson(struct {
				AddedCategoryGroups   []string
				RemovedCategoryGroups []string
			}{
				AddedCategoryGroups:   logResultAddedCategoryGroups,
				RemovedCategoryGroups: logResultRemovedCategoryGroups,
			})},
		},
	})
}

func isInList(value string, list []string) bool {
	for _, x := range list {
		if x == value {
			return true
		}
	}

	return false
}

func existsCategoryGroup(category string, categoryGroupsList []structs.CategoryGroup) bool {
	for _, categoryGroup := range categoryGroupsList {
		if categoryGroup.Category == category {
			return true
		}
	}

	return false
}

func existsGroup(category string, groupsList []structs.Group) bool {
	for _, group := range groupsList {
		if group.Category == category {
			return true
		}
	}

	return false
}

const (
	RunGroupTaskStartTypeNormal   = 0
	RunGroupTaskStartTypeTryAgain = 1
	RunGroupTaskStartTypeUndo     = 2
)

type RunGroupTaskArgs struct {
	CreatorUserId string
	StartType     uint8
	GroupTaskId   string
	Category      string
	GroupId       string
	Step          uint8
	TaskStepId    string
	GlobalInputs  string
	TaskInputs    string
}

type InputParameters struct {
	ParameterName string `json:"parameterName"`
	Value         string `json:"value"`
}

type FoundFile struct {
	Path     string
	FileName string
}

const (
	UserActionTaskStepResume = 0
	UserActionTaskStepUndo   = 1
	UserActionTaskStepRepeat = 2
)

func HandleUserActionTaskStep(userId string, body map[string]interface{}) {
	action := uint8(body["action"].(float64))
	groupTaskId := body["groupTaskId"].(string)
	category := body["category"].(string)
	groupId := body["groupId"].(string)
	step := uint8(body["step"].(float64))
	taskStepId := body["taskStepId"].(string)

	var dbGroupTaskStep structs.GroupTaskSteps

	database.DB.Where("id = ?", taskStepId).Find(&dbGroupTaskStep)

	groupTaskStep := structs.GroupTaskSteps{
		Id:            taskStepId,
		CreatorUserId: userId,
		GroupTasksId:  groupTaskId,
		Step:          step,
	}

	nextStep := step

	if action == UserActionTaskStepResume {
		groupTaskStep.Status = utils.GroupTasksStatusFinished
		groupTaskStep.Inputs = dbGroupTaskStep.Inputs
		groupTaskStep.Log = dbGroupTaskStep.Log
		groupTaskStep.Files = dbGroupTaskStep.Files
		groupTaskStep.StartedAt = dbGroupTaskStep.StartedAt
		groupTaskStep.EndedAt = dbGroupTaskStep.EndedAt

		nextStep = step + 1

		logger.AddGroupTasksLog(structs.LogMessage{
			Id:   8,
			Type: utils.LogTypeInfo,
			Messages: []structs.LogData{
				{Type: "userId", Value: userId},
				{Type: "action", Value: "resume"},
				{Type: "taskStepId", Value: taskStepId},
				{Type: "groupTaskId", Value: groupTaskId},
			},
		})
	} else {
		groupTaskStep.Status = utils.GroupTasksStatusRunning
	}

	updateGroupTaskSteps(groupTaskStep)

	categoryGroup := GetCategoryGroupTaskByCategoryAndGroupId(category, groupId)

	// last step onFinish was set to pause so the resume is the confirmation to finish the group task
	if int(nextStep) > len(categoryGroup.Tasks) {
		groupTaskFinished(groupTaskId)
		return
	}

	updateGroupTask(groupTaskId, structs.GroupTasks{
		CurrentTasksStep: nextStep,
	})

	runGroupTaskArgs := RunGroupTaskArgs{
		CreatorUserId: userId,
		StartType:     RunGroupTaskStartTypeNormal,
		GroupTaskId:   groupTaskId,
		Category:      category,
		GroupId:       groupId,
		Step:          nextStep,
		TaskStepId:    taskStepId,
	}

	if action == UserActionTaskStepRepeat {
		runGroupTaskArgs.StartType = RunGroupTaskStartTypeTryAgain

		logger.AddGroupTasksLog(structs.LogMessage{
			Id:   8,
			Type: utils.LogTypeInfo,
			Messages: []structs.LogData{
				{Type: "userId", Value: userId},
				{Type: "action", Value: "repeat"},
				{Type: "taskStepId", Value: taskStepId},
				{Type: "groupTaskId", Value: groupTaskId},
			},
		})
	} else if action == UserActionTaskStepUndo {
		runGroupTaskArgs.StartType = RunGroupTaskStartTypeUndo
		runGroupTaskArgs.TaskInputs = dbGroupTaskStep.Inputs

		logger.AddGroupTasksLog(structs.LogMessage{
			Id:   8,
			Type: utils.LogTypeInfo,
			Messages: []structs.LogData{
				{Type: "userId", Value: userId},
				{Type: "action", Value: "undo"},
				{Type: "taskStepId", Value: taskStepId},
				{Type: "groupTaskId", Value: groupTaskId},
			},
		})
	}

	// run next task step
	go RunGroupTask(runGroupTaskArgs)
}

func RunGroupTask(args RunGroupTaskArgs) {
	categoryGroup := GetCategoryGroupTaskByCategoryAndGroupId(args.Category, args.GroupId)

	groupTaskStep := structs.GroupTaskSteps{
		CreatorUserId: args.CreatorUserId,
		GroupTasksId:  args.GroupTaskId,
		Step:          args.Step,
		Status:        utils.GroupTasksStatusRunning,
		Inputs:        args.TaskInputs,
		StartedAt:     time.Now(),
	}

	if args.StartType == RunGroupTaskStartTypeNormal {
		groupTaskStep.Id = uuid.New().String()

		database.DB.Create(&groupTaskStep)

		socketclients.BroadcastMessage(structs.SendSocketMessage{
			Cmd:  utils.SentCmdNewGroupTaskStep,
			Body: groupTaskStep,
		})
	} else {
		groupTaskStep.Id = args.TaskStepId

		updateGroupTaskSteps(groupTaskStep)
	}

	// set group task to running
	dbGroupTask := updateGroupTask(groupTaskStep.GroupTasksId, structs.GroupTasks{
		Status: utils.GroupTasksStatusRunning,
	})

	// global inputs
	var globalInputParameters []InputParameters

	if len(args.GlobalInputs) > 0 { // global inputs given in args because the group task was just created
		if err := json.Unmarshal([]byte(args.GlobalInputs), &globalInputParameters); err != nil {
			log.Error().Msgf("err unmarshalling global inputs %s", err.Error())
		}
	} else { // global inputs not given in args - fetch it from the database
		if err := json.Unmarshal([]byte(dbGroupTask.GlobalInputs), &globalInputParameters); err != nil {
			log.Error().Msgf("err unmarshalling global inputs %s", err.Error())
		}
	}

	// task parameters
	log.Debug().Msgf("script path %s", categoryGroup.Tasks[args.Step-1].ScriptPath)
	//commandArgs := []string{root + categoryGroup.Id + "/" + categoryGroup.Tasks[args.Step-1].ScriptPath}
	commandArgs := []string{categoryGroup.Tasks[args.Step-1].ScriptPath}

	if len(categoryGroup.Tasks[args.Step-1].Parameters) != 0 && len(args.TaskInputs) == 0 {
		updateGroupTask(groupTaskStep.GroupTasksId, structs.GroupTasks{
			Status: utils.GroupTasksStatusInputRequired,
		})

		groupTaskStep.Status = utils.GroupTasksStatusInputRequired

		updateGroupTaskSteps(groupTaskStep)

		logger.AddGroupTasksLog(structs.LogMessage{
			Id:   5,
			Type: utils.LogTypeInfo,
			Messages: []structs.LogData{
				{Type: "taskStepId", Value: groupTaskStep.Id},
				{Type: "groupTaskId", Value: groupTaskStep.GroupTasksId},
				{Type: "status", Value: "input required"},
			},
		})
		return
	} else if len(args.TaskInputs) > 0 {
		var taskParameterInputs []InputParameters

		if err := json.Unmarshal([]byte(args.TaskInputs), &taskParameterInputs); err != nil {
			log.Error().Msgf("err unmarshalling task parameter inputs %s", err.Error())
		}

		for _, taskParameterInput := range taskParameterInputs {
			commandArgs = append(commandArgs, taskParameterInput.Value)
		}

		// append undo as last arg to script
		if args.StartType == RunGroupTaskStartTypeUndo {
			commandArgs = append(commandArgs, "--undo")
		}
	}

	// create running task folder
	if _, err := os.Stat(config.Cfg.FolderPaths.GroupTasksRunningTasks + groupTaskStep.GroupTasksId); errors.Is(err, os.ErrNotExist) {
		if err := os.Mkdir(config.Cfg.FolderPaths.GroupTasksRunningTasks+groupTaskStep.GroupTasksId, os.ModePerm); err != nil {
			log.Error().Msgf("Error creating running tasks folder %s", err.Error())
		}

		// copy scripts to group tasks folder
		err := filepath.Walk(config.Cfg.FolderPaths.GroupTasksGroups+categoryGroup.Id, func(path string, info os.FileInfo, err error) error {
			if err != nil {
				log.Error().Msgf("Error walk %s", err.Error())
				return err
			}

			if !info.IsDir() && filepath.Ext(path) == ".py" {
				bytesRead, err := os.ReadFile(path)

				if err != nil {
					log.Error().Msgf("Error reading file %s", err.Error())
				}

				err = os.WriteFile(config.Cfg.FolderPaths.GroupTasksRunningTasks+groupTaskStep.GroupTasksId+"/"+info.Name(), bytesRead, 0644)

				if err != nil {
					log.Error().Msgf("Error writing file %s", err.Error())
				}
			}

			return nil
		})

		if err != nil {
			log.Error().Msgf("Failed to walk through task folder %s", err.Error())
		}
	}

	// execute command
	cmd := exec.Command("python3", commandArgs...)
	// path needs to be set here as the python scripts will use the path as base path to create files for example
	cmd.Dir = config.Cfg.FolderPaths.GroupTasksRunningTasks + groupTaskStep.GroupTasksId + "/"
	out, err := cmd.CombinedOutput()

	//cmdLog := string(cmd)
	cmdLog := string(out)

	if err != nil {
		if exitErr, ok := err.(*exec.ExitError); ok {
			exitCode := exitErr.ExitCode()
			log.Error().Msgf("exit code %d", exitCode)
			cmdLog += fmt.Sprintf("\nExit code: %v", exitCode)
		}

		log.Error().Msgf("error exec command %s", err.Error())
		groupTaskStep.Status = utils.GroupTasksStatusFailed

		logger.AddGroupTasksLog(structs.LogMessage{
			Id:   5,
			Type: utils.LogTypeInfo,
			Messages: []structs.LogData{
				{Type: "taskStepId", Value: groupTaskStep.Id},
				{Type: "groupTaskId", Value: groupTaskStep.GroupTasksId},
				{Type: "status", Value: "failed"},
			},
		})
	} else {
		groupTaskStep.Status = utils.GroupTasksStatusFinished

		logger.AddGroupTasksLog(structs.LogMessage{
			Id:   5,
			Type: utils.LogTypeInfo,
			Messages: []structs.LogData{
				{Type: "taskStepId", Value: groupTaskStep.Id},
				{Type: "groupTaskId", Value: groupTaskStep.GroupTasksId},
				{Type: "status", Value: "finished"},
			},
		})
	}

	fmt.Println(cmdLog)

	groupTaskStep.Log = cmdLog
	groupTaskStep.EndedAt = time.Now()

	if args.StartType != RunGroupTaskStartTypeUndo {
		// looking for files which are created by the scripts
		var foundFiles []FoundFile

		if err = filepath.Walk(config.Cfg.FolderPaths.GroupTasksRunningTasks+groupTaskStep.GroupTasksId+"/", func(path string, info os.FileInfo, err error) error {
			if err != nil {
				log.Error().Msgf("Error walk %s", err.Error())
				return err
			}

			if !info.IsDir() && filepath.Ext(path) != ".py" && !strings.Contains(path, "/oldFiles/") {
				foundFiles = append(foundFiles, FoundFile{Path: path, FileName: info.Name()})
			}

			return nil
		}); err != nil {
			log.Error().Msgf("Failed look for created files %s", err.Error())
		}

		if len(foundFiles) > 0 {
			// creating folder for the old files which can be used by the python scripts for the next steps
			if _, err := os.Stat(config.Cfg.FolderPaths.GroupTasksRunningTasks + groupTaskStep.GroupTasksId + "/oldFiles"); errors.Is(err, os.ErrNotExist) {
				if err := os.Mkdir(config.Cfg.FolderPaths.GroupTasksRunningTasks+groupTaskStep.GroupTasksId+"/oldFiles", os.ModePerm); err != nil {
					log.Error().Msgf("Error creating old files folder %s", err.Error())
				}
			}

			// copy files to public directory to access it via web ui
			if _, err := os.Stat(config.Cfg.FolderPaths.PublicStatic + "grouptasks/" + groupTaskStep.GroupTasksId); errors.Is(err, os.ErrNotExist) {
				if err := os.Mkdir(config.Cfg.FolderPaths.PublicStatic+"grouptasks/"+groupTaskStep.GroupTasksId, os.ModePerm); err != nil {
					log.Error().Msgf("Error creating task public folder %s", err.Error())
				}
			}

			var taskFiles []structs.GroupTaskStepFile

			for _, foundFile := range foundFiles {
				bytesRead, err := os.ReadFile(foundFile.Path)

				if err != nil {
					log.Error().Msgf("Error reading file %s", err.Error())
				}

				err = os.WriteFile(config.Cfg.FolderPaths.GroupTasksRunningTasks+groupTaskStep.GroupTasksId+"/oldFiles/"+foundFile.FileName, bytesRead, 0644)

				if err != nil {
					log.Error().Msgf("Error writing file %s", err.Error())
				}

				systemFileName := uuid.New().String() + filepath.Ext(foundFile.FileName)

				err = os.WriteFile(config.Cfg.FolderPaths.PublicStatic+"grouptasks/"+groupTaskStep.GroupTasksId+"/"+systemFileName, bytesRead, 0644)

				if err != nil {
					log.Error().Msgf("Error writing file %s", err.Error())
				}

				err = os.Remove(config.Cfg.FolderPaths.GroupTasksRunningTasks + groupTaskStep.GroupTasksId + "/" + foundFile.FileName)

				if err != nil {
					log.Error().Msgf("Failed to delete created file by task %s", err.Error())
				}

				taskFiles = append(taskFiles, structs.GroupTaskStepFile{
					OriginalFileName: foundFile.FileName,
					SystemFileName:   systemFileName,
				})
			}

			marshaledTaskFiles, err := json.Marshal(taskFiles)

			if err != nil {
				log.Error().Msgf("Failed to marshal task files %s", err.Error())
			} else {
				groupTaskStep.Files = string(marshaledTaskFiles)
			}
		}
	}

	// update group task step
	updateGroupTaskSteps(groupTaskStep)

	if groupTaskStep.Status == utils.GroupTasksStatusFailed {
		// set group task to failed
		updateGroupTask(groupTaskStep.GroupTasksId, structs.GroupTasks{
			Status: utils.GroupTasksStatusFailed,
		})

		logger.AddGroupTasksLog(structs.LogMessage{
			Id:   2,
			Type: utils.LogTypeInfo,
			Messages: []structs.LogData{
				{Type: "groupTaskId", Value: groupTaskStep.GroupTasksId},
				{Type: "status", Value: "failed"},
			},
		})
	} else {
		prevStartType := args.StartType
		args.StartType = RunGroupTaskStartTypeNormal

		if categoryGroup.Tasks[args.Step-1].OnFinish == "pause" {
			status := utils.GroupTasksStatusPaused

			if prevStartType == RunGroupTaskStartTypeUndo {
				status = utils.GroupTasksStatusUndoEnded
				groupTaskStep.Inputs = " "
				groupTaskStep.Files = " "

				logger.AddGroupTasksLog(structs.LogMessage{
					Id:   2,
					Type: utils.LogTypeInfo,
					Messages: []structs.LogData{
						{Type: "groupTaskId", Value: groupTaskStep.GroupTasksId},
						{Type: "status", Value: "Undo ended"},
					},
				})

				logger.AddGroupTasksLog(structs.LogMessage{
					Id:   5,
					Type: utils.LogTypeInfo,
					Messages: []structs.LogData{
						{Type: "taskStepId", Value: groupTaskStep.Id},
						{Type: "groupTaskId", Value: groupTaskStep.GroupTasksId},
						{Type: "status", Value: "Undo ended"},
					},
				})
			} else {
				logger.AddGroupTasksLog(structs.LogMessage{
					Id:   2,
					Type: utils.LogTypeInfo,
					Messages: []structs.LogData{
						{Type: "groupTaskId", Value: groupTaskStep.GroupTasksId},
						{Type: "status", Value: "paused"},
					},
				})

				logger.AddGroupTasksLog(structs.LogMessage{
					Id:   5,
					Type: utils.LogTypeInfo,
					Messages: []structs.LogData{
						{Type: "taskStepId", Value: groupTaskStep.Id},
						{Type: "groupTaskId", Value: groupTaskStep.GroupTasksId},
						{Type: "status", Value: "paused"},
					},
				})
			}

			groupTaskStep.Status = status

			updateGroupTaskSteps(groupTaskStep)
			updateGroupTask(groupTaskStep.GroupTasksId, structs.GroupTasks{
				Status: status,
			})
		} else {
			args.Step = args.Step + 1

			if int(args.Step-1) < len(categoryGroup.Tasks) {
				// clear task parameters, because otherwise the next task would have the parameters from the previous task
				args.TaskInputs = ""

				updateGroupTask(groupTaskStep.GroupTasksId, structs.GroupTasks{
					CurrentTasksStep: args.Step,
				})

				logger.AddGroupTasksLog(structs.LogMessage{
					Id:   9,
					Type: utils.LogTypeInfo,
					Messages: []structs.LogData{
						{Type: "groupTaskId", Value: groupTaskStep.GroupTasksId},
						{Type: "step", Value: utils.MarshalJson(args.Step)},
					},
				})

				RunGroupTask(args)
			} else {
				groupTaskFinished(groupTaskStep.GroupTasksId)
			}
		}

	}
}

func groupTaskFinished(groupTasksId string) {
	updateGroupTask(groupTasksId, structs.GroupTasks{
		Status:  utils.GroupTasksStatusFinished,
		EndedAt: time.Now(),
	})

	if err := os.RemoveAll(config.Cfg.FolderPaths.GroupTasksRunningTasks + groupTasksId + "/"); err != nil {
		log.Error().Msgf("Failed to delete running task folder %s", err.Error())
	}

	logger.AddGroupTasksLog(structs.LogMessage{
		Id:   2,
		Type: utils.LogTypeInfo,
		Messages: []structs.LogData{
			{Type: "groupTaskId", Value: groupTasksId},
			{Type: "status", Value: "finished"},
		},
	})
}

// Updates group task and send it to websocket users
func updateGroupTask(groupTasksId string, updates structs.GroupTasks) structs.GroupTasks {
	database.DB.Model(&structs.GroupTasks{}).Where("id = ?", groupTasksId).Updates(updates)

	var dbGroupTask structs.GroupTasks

	database.DB.First(&dbGroupTask, "id = ?", groupTasksId)

	socketclients.BroadcastMessage(structs.SendSocketMessage{
		Cmd:  utils.SentCmdUpdateGroupTask,
		Body: dbGroupTask,
	})

	return dbGroupTask
}

// Updates group task steps and send it to websocket users
func updateGroupTaskSteps(groupTaskStep structs.GroupTaskSteps) {
	database.DB.Model(&structs.GroupTaskSteps{}).Where("id = ?", groupTaskStep.Id).Updates(groupTaskStep)

	socketclients.BroadcastMessage(structs.SendSocketMessage{
		Cmd:  utils.SentCmdUpdateGroupTaskStep,
		Body: groupTaskStep,
	})
}

func GetAllGroupTasks() []structs.GroupTasks {
	var groupTasks []structs.GroupTasks

	database.DB.Find(&groupTasks)

	return groupTasks
}

func GetAllGroupTasksSteps() []structs.GroupTaskSteps {
	var groupTaskStepsLogs []structs.GroupTaskSteps

	database.DB.Find(&groupTaskStepsLogs)

	lockedGroupTaskSteps := cache.GetLockedGroupTaskSteps()
	groupTaskStepsInputs := cache.GetGroupTaskStepsInputs()

	for i, groupTaskStep := range groupTaskStepsLogs {
		if groupTaskStep.Status == utils.GroupTasksStatusInputRequired {
			groupTaskStepsLogs[i].Inputs = cache.GetGroupTaskStepsInputsValue(groupTaskStepsInputs, groupTaskStep.GroupTasksId, groupTaskStep.Step)

			for _, lockedGroupTaskStep := range lockedGroupTaskSteps {
				if groupTaskStep.GroupTasksId == lockedGroupTaskStep.GroupTaskId {
					groupTaskStepsLogs[i].LockedByUserId = lockedGroupTaskStep.LockedByUserId
				}
			}
		}
	}

	return groupTaskStepsLogs
}

func GetCategoryGroupTaskByCategoryAndGroupId(category string, groupId string) structs.Group {
	for _, categoryGroup := range cache.GetCategoryGroups() {
		if categoryGroup.Category == category {
			for _, group := range categoryGroup.Groups {
				if group.Id == groupId {
					return group
				}
			}
		}
	}

	return structs.Group{}
}

func StartUnlockLockedGroupTaskStepsTicker() {
	ticker := time.NewTicker(1 * time.Second)

	for range ticker.C {
		for index, taskStep := range cache.GetLockedGroupTaskSteps() {
			if time.Since(taskStep.LockedAt).Seconds() > utils.GroupTaskLockedTime {
				cache.RemoveLockedGroupTaskStep(index)

				socketclients.BroadcastMessage(
					structs.SendSocketMessage{
						Cmd: utils.SentCmdTaskUnlocked,
						Body: struct {
							GroupTaskId string
							Step        uint8
							RememberId  string
						}{
							GroupTaskId: taskStep.GroupTaskId,
							Step:        taskStep.Step,
							RememberId:  taskStep.RememberId,
						},
					})
			}
		}
	}
}

func StartGroupTask(userId string, groupTask structs.GroupTasks) {
	groupTask.Id = uuid.New().String()
	groupTask.CreatorUserId = userId
	groupTask.CurrentTasksStep = 1
	groupTask.Status = utils.GroupTasksStatusRunning
	groupTask.StartedAt = time.Now()

	database.DB.Create(&groupTask)

	socketclients.BroadcastMessage(structs.SendSocketMessage{
		Cmd:  utils.SentCmdNewGroupTaskStarted,
		Body: groupTask,
	})

	go RunGroupTask(RunGroupTaskArgs{
		CreatorUserId: userId,
		StartType:     RunGroupTaskStartTypeNormal,
		GroupTaskId:   groupTask.Id,
		Category:      groupTask.Category,
		GroupId:       groupTask.GroupId,
		Step:          1,
		TaskStepId:    "",
		GlobalInputs:  groupTask.GlobalInputs,
	})

	logger.AddGroupTasksLog(structs.LogMessage{
		Id:   0,
		Type: utils.LogTypeInfo,
		Messages: []structs.LogData{
			{Type: "userId", Value: userId},
			{Type: "groupTaskId", Value: groupTask.Id},
			{Type: "groupTaskName", Value: groupTask.GroupName},
		},
	})
}

func InstallGlobalPythonPackages(userId string) {
	logger.AddGroupTasksLog(structs.LogMessage{
		Id:   13,
		Type: utils.LogTypeInfo,
		Messages: []structs.LogData{
			{Type: "userId", Value: userId},
		},
	})

	socketclients.BroadcastMessageToUsersWithPermission(
		utils.PermissionGroupTasksInstallGlobalPythonPackages,
		structs.SendSocketMessage{
			Cmd: utils.SentCmdInstallingGlobalPythonPackages,
		})

	// check if requirements.txt exists
	if _, err := os.Stat(config.Cfg.FolderPaths.GroupTasksGroups + "requirements.txt"); errors.Is(err, os.ErrNotExist) {
		logger.AddGroupTasksLog(structs.LogMessage{
			Id:   14,
			Type: utils.LogTypeInfo,
			Messages: []structs.LogData{
				{Type: "error", Value: "requirements.txt not found"},
			},
		})

		socketclients.BroadcastMessageToUsersWithPermission(
			utils.PermissionGroupTasksInstallGlobalPythonPackages,
			structs.SendSocketMessage{
				Cmd: utils.SentCmdInstallingGlobalPythonPackagesFailed,
			})
		return
	}

	// install python dependencies
	cmd := exec.Command("pip3", "install", "-r", config.Cfg.FolderPaths.GroupTasksGroups+"requirements.txt")
	out, err := cmd.CombinedOutput()

	if err != nil {
		logger.AddGroupTasksLog(structs.LogMessage{
			Id:   14,
			Type: utils.LogTypeInfo,
			Messages: []structs.LogData{
				{Type: "error", Value: err.Error()},
			},
		})

		socketclients.BroadcastMessageToUsersWithPermission(
			utils.PermissionGroupTasksInstallGlobalPythonPackages,
			structs.SendSocketMessage{
				Cmd: utils.SentCmdInstallingGlobalPythonPackagesFailed,
			})
		return
	}

	logger.AddGroupTasksLog(structs.LogMessage{
		Id:   15,
		Type: utils.LogTypeInfo,
		Messages: []structs.LogData{
			{Type: "log", Value: string(out)},
		},
	})

	socketclients.BroadcastMessageToUsersWithPermission(
		utils.PermissionGroupTasksInstallGlobalPythonPackages,

		structs.SendSocketMessage{
			Cmd: utils.SentCmdInstallingGlobalPythonPackagesFinished,
		})
}

func InstallPythonPackages(userId string, category, groupId string) {
	logger.AddGroupTasksLog(structs.LogMessage{
		Id:   10,
		Type: utils.LogTypeInfo,
		Messages: []structs.LogData{
			{Type: "userId", Value: userId},
			{Type: "category", Value: category},
			{Type: "groupId", Value: groupId},
		},
	})

	messageBody := struct {
		Category string
		GroupId  string
	}{
		Category: category,
		GroupId:  groupId,
	}

	convertedXYPermission := systempermissions.ConvertXYPermission(utils.PermissionGroupTasksOverviewXYInstallPythonPackages, category)

	socketclients.BroadcastMessageToUsersWithPermission(
		convertedXYPermission,
		structs.SendSocketMessage{
			Cmd:  utils.SentCmdInstallingPythonPackages,
			Body: messageBody,
		})

	// check if requirements.txt exists
	if _, err := os.Stat(config.Cfg.FolderPaths.GroupTasksGroups + groupId + "/requirements.txt"); errors.Is(err, os.ErrNotExist) {
		logger.AddGroupTasksLog(structs.LogMessage{
			Id:   11,
			Type: utils.LogTypeInfo,
			Messages: []structs.LogData{
				{Type: "category", Value: category},
				{Type: "groupId", Value: groupId},
				{Type: "error", Value: "requirements.txt not found"},
			},
		})

		socketclients.BroadcastMessageToUsersWithPermission(
			convertedXYPermission,
			structs.SendSocketMessage{
				Cmd:  utils.SentCmdInstallingPythonPackagesFailed,
				Body: messageBody,
			})
		return
	}

	// install python dependencies
	cmd := exec.Command("pip3", "install", "-r", config.Cfg.FolderPaths.GroupTasksGroups+groupId+"/requirements.txt")
	out, err := cmd.CombinedOutput()

	if err != nil {
		logger.AddGroupTasksLog(structs.LogMessage{
			Id:   11,
			Type: utils.LogTypeInfo,
			Messages: []structs.LogData{
				{Type: "category", Value: category},
				{Type: "groupId", Value: groupId},
				{Type: "error", Value: err.Error()},
			},
		})

		socketclients.BroadcastMessageToUsersWithPermission(
			convertedXYPermission,
			structs.SendSocketMessage{
				Cmd:  utils.SentCmdInstallingPythonPackages,
				Body: messageBody,
			})
		return
	}

	logger.AddGroupTasksLog(structs.LogMessage{
		Id:   12,
		Type: utils.LogTypeInfo,
		Messages: []structs.LogData{
			{Type: "category", Value: category},
			{Type: "groupId", Value: groupId},
			{Type: "log", Value: string(out)},
		},
	})

	socketclients.BroadcastMessageToUsersWithPermission(
		convertedXYPermission,
		structs.SendSocketMessage{
			Cmd:  utils.SentCmdInstallingPythonPackagesFinished,
			Body: messageBody,
		})
}
