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"

	"git.ex.umbach.dev/Alex/roese-utils/rslogger"
	"git.ex.umbach.dev/Alex/roese-utils/rspagination"
	"git.ex.umbach.dev/Alex/roese-utils/rsutils"
	"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())

		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.BroadcastMessageToTopic(
			utils.SubscribedDynamicTopicGroupTasks+category,
			structs.SendSocketMessage{
				Cmd: utils.SentCmdGroupTasksReloaded,
				Body: struct {
					Category       string
					CategoryGroups []structs.Group
				}{
					Category:       category,
					CategoryGroups: categoryGroups,
				},
			})
	} else { // category was removed
		socketclients.BroadcastMessageToTopic(
			utils.SubscribedDynamicTopicGroupTasks+category,
			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(rslogger.LogTypeInfo, "User %s has executed action %s on step %s of groupTaskId %s",
			userId, "resume", taskStepId, groupTaskId)
	} else {
		groupTaskStep.Status = utils.GroupTasksStatusRunning
	}

	updateGroupTaskSteps(groupTaskStep, category)

	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, category)
		return
	}

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

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

	if action == UserActionTaskStepRepeat {
		runGroupTaskArgs.StartType = RunGroupTaskStartTypeTryAgain

		logger.AddGroupTasksLog(rslogger.LogTypeInfo, "User %s has executed action %s on step %s of groupTaskId %s",
			userId, "repeat", taskStepId, groupTaskId)
	} else if action == UserActionTaskStepUndo {
		runGroupTaskArgs.StartType = RunGroupTaskStartTypeUndo
		runGroupTaskArgs.TaskInputs = dbGroupTaskStep.Inputs

		logger.AddGroupTasksLog(rslogger.LogTypeInfo, "User %s has executed action %s on step %s of groupTaskId %s",
			userId, "undo", taskStepId, 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.BroadcastMessageToTopic(
			utils.SubscribedDynamicTopicGroupTasks+args.Category,
			structs.SendSocketMessage{
				Cmd:  utils.SentCmdNewGroupTaskStep,
				Body: groupTaskStep,
			})
	} else {
		groupTaskStep.Id = args.TaskStepId

		updateGroupTaskSteps(groupTaskStep, args.Category)
	}

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

	// 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,
		}, args.Category)

		groupTaskStep.Status = utils.GroupTasksStatusInputRequired

		updateGroupTaskSteps(groupTaskStep, args.Category)

		logger.AddGroupTasksLog(rslogger.LogTypeInfo, "Step %s of groupTaskId %s status set to %s",
			groupTaskStep.Id, groupTaskStep.GroupTasksId, "input required")
		return
	} else if len(args.TaskInputs) > 0 {
		commandArgs = append(commandArgs, args.TaskInputs)

		// 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" */ info.Name() != "requirements.txt" && info.Name() != "index.json" {
				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(rslogger.LogTypeWarning, "Step %s of groupTaskId %s status set to %s",
			groupTaskStep.Id, groupTaskStep.GroupTasksId, "failed")
	} else {
		groupTaskStep.Status = utils.GroupTasksStatusFinished

		logger.AddGroupTasksLog(rslogger.LogTypeInfo, "Step %s of groupTaskId %s status set to %s",
			groupTaskStep.Id, groupTaskStep.GroupTasksId, "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, args.Category)

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

		logger.AddGroupTasksLog(rslogger.LogTypeWarning, "GroupTaskId %s status set to %s",
			groupTaskStep.GroupTasksId, "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(rslogger.LogTypeInfo, "GroupTaskId %s status set to %s",
					groupTaskStep.GroupTasksId, "Undo ended")

				logger.AddGroupTasksLog(rslogger.LogTypeInfo, "Step %s of groupTaskId %s status set to %s",
					groupTaskStep.Id, groupTaskStep.GroupTasksId, "Undo ended")
			} else {
				logger.AddGroupTasksLog(rslogger.LogTypeInfo, "GroupTaskId %s status set to %s",
					groupTaskStep.GroupTasksId, "paused")

				logger.AddGroupTasksLog(rslogger.LogTypeInfo, "Step %s of groupTaskId %s status set to %s",
					groupTaskStep.Id, groupTaskStep.GroupTasksId, "paused")
			}

			groupTaskStep.Status = status

			updateGroupTaskSteps(groupTaskStep, args.Category)
			updateGroupTask(groupTaskStep.GroupTasksId, structs.GroupTasks{
				Status: status,
			}, args.Category)
		} 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,
				}, args.Category)

				logger.AddGroupTasksLog(rslogger.LogTypeInfo, "System has started step %s of groupTaskId %s",
					rsutils.MarshalJson(args.Step), groupTaskStep.GroupTasksId)

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

	}
}

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

	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(rslogger.LogTypeInfo, "GroupTaskId %s status set to %s", groupTasksId, "finished")
}

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

	var dbGroupTask structs.GroupTasks

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

	socketclients.BroadcastMessageToTopic(
		utils.SubscribedDynamicTopicGroupTasks+category,
		structs.SendSocketMessage{
			Cmd:  utils.SentCmdUpdateGroupTask,
			Body: dbGroupTask,
		})

	return dbGroupTask
}

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

	socketclients.BroadcastMessageToTopic(
		utils.SubscribedDynamicTopicGroupTasks+category,
		structs.SendSocketMessage{
			Cmd:  utils.SentCmdUpdateGroupTaskStep,
			Body: groupTaskStep,
		})
}

func GetAllGroupTasks(category string, query rspagination.PageQuery) []structs.GroupTasks {
	var groupTasks []structs.GroupTasks

	rspagination.DbPageQuery(database.DB, query,
		utils.GroupTasksPaginationLimit,
		&groupTasks,
		"started_at DESC",
		"category = ?",
		category)

	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 GetGroupTaskSteps(groupTaskId string) structs.GetGroupTaskStepsResponse {
	var groupTask structs.GroupTasks

	database.DB.Where("id = ?", groupTaskId).Find(&groupTask)

	var groupTaskSteps []structs.GroupTaskSteps

	database.DB.Where("group_tasks_id = ?", groupTaskId).Find(&groupTaskSteps)

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

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

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

	return structs.GetGroupTaskStepsResponse{
		GroupTask:      groupTask,
		GroupTaskSteps: groupTaskSteps,
	}
}

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.BroadcastMessageToTopic(
					utils.SubscribedDynamicTopicGroupTasks+taskStep.Category,
					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.BroadcastMessageToTopic(
		utils.SubscribedDynamicTopicGroupTasks+groupTask.Category,
		structs.SendSocketMessage{
			Cmd: utils.SentCmdNewGroupTaskStarted,
			Body: struct {
				GroupTask  structs.GroupTasks
				TotalPages int
			}{
				GroupTask: groupTask,
				TotalPages: rspagination.GetTotalPages(database.DB, utils.GroupTasksPaginationLimit,
					[]structs.GroupTasks{},
					"category = ?",
					groupTask.Category),
			},
		})

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

	logger.AddGroupTasksLog(rslogger.LogTypeInfo, "User %s has started group task groupTaskId %s groupTaskName %s",
		userId, groupTask.Id, groupTask.GroupName)
}

func InstallGlobalPythonPackages(userId string) {
	logger.AddGroupTasksLog(rslogger.LogTypeInfo, "User %s has requested installing the global python packages",
		userId)

	socketclients.BroadcastMessageToUsersWithPermissionAndTopic(
		utils.PermissionGroupTasksInstallGlobalPythonPackages,
		utils.SubscribedDynamicTopicGroupTasks,
		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(rslogger.LogTypeWarning, "Installing the global python packages failed. Error %s", "requirements.txt not found")

		socketclients.BroadcastMessageToUsersWithPermissionAndTopic(
			utils.PermissionGroupTasksInstallGlobalPythonPackages,
			utils.SubscribedDynamicTopicGroupTasks,
			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(rslogger.LogTypeWarning, "Installing the global python packages failed. Error %s", err.Error())

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

	logger.AddGroupTasksLog(rslogger.LogTypeInfo, "Installation of the global python packages finished. Log %s", string(out))

	socketclients.BroadcastMessageToUsersWithPermissionAndTopic(
		utils.PermissionGroupTasksInstallGlobalPythonPackages,
		utils.SubscribedDynamicTopicGroupTasks,
		structs.SendSocketMessage{
			Cmd: utils.SentCmdInstallingGlobalPythonPackagesFinished,
		})
}

func InstallPythonPackages(userId string, category string, groupId string) {
	logger.AddGroupTasksLog(rslogger.LogTypeInfo, "User %s has requested installing python packages for groupId %s of category %s", userId, groupId, category)

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

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

	socketclients.BroadcastMessageToUsersWithPermissionAndTopic(
		convertedXYPermission,
		utils.SubscribedDynamicTopicGroupTasks+category,
		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(rslogger.LogTypeWarning, "Installing python packages for groupId %s of category %s failed. Error: %s",
			category, groupId, "requirements.txt not found")

		socketclients.BroadcastMessageToUsersWithPermissionAndTopic(
			convertedXYPermission,
			utils.SubscribedDynamicTopicGroupTasks+category,
			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()

	log.Info().Msgf("pa %s", config.Cfg.FolderPaths.GroupTasksGroups+groupId+"/requirements.txt")

	if err != nil {
		logger.AddGroupTasksLog(rslogger.LogTypeWarning, "Installing python packages for groupId %s of category %s failed. Error: %s",
			category, groupId, err.Error())

		socketclients.BroadcastMessageToUsersWithPermissionAndTopic(
			convertedXYPermission,
			utils.SubscribedDynamicTopicGroupTasks+category,
			structs.SendSocketMessage{
				Cmd:  utils.SentCmdInstallingPythonPackagesFailed,
				Body: messageBody,
			})
		return
	}

	logger.AddGroupTasksLog(rslogger.LogTypeInfo, "Installing python packages for groupId %s of category %s finished. Log: %s",
		category, groupId, string(out))

	socketclients.BroadcastMessageToUsersWithPermissionAndTopic(
		convertedXYPermission,
		utils.SubscribedDynamicTopicGroupTasks+category,
		structs.SendSocketMessage{
			Cmd:  utils.SentCmdInstallingPythonPackagesFinished,
			Body: messageBody,
		})
}

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

	if err != nil {
		log.Error().Msgf("Error reading group tasks groups folder %s", err.Error())
	}

	// loop throught cached categories and check if for the category exists an folder
	// if not delete the category from cache
	for _, category := range cache.GetCategoryGroups() {

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

		if err != nil {
			log.Error().Msgf("Error reading group tasks groups folder %s", err.Error())
		}

		var exists bool

		for _, entry := range entries {
			if entry.Name() == category.Category {
				exists = true
				break
			}
		}

		if !exists {
			cache.RemoveCategory(category.Category)
		}
	}

	// loop throught all folders in group tasks groups folder and add them to cache if not already in cache
	for _, entry := range entries {
		if !entry.IsDir() {
			continue
		}

		// add category to cache if not already in cache
		if !cache.ExistsCategory(entry.Name()) {
			files, err := os.ReadDir(config.Cfg.FolderPaths.GroupTasksGroups + entry.Name())

			if err != nil {
				log.Error().Msgf("Error reading group tasks groups folder %s", err.Error())
				return err
			}

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

					var group structs.Group

					json.Unmarshal(content, &group)

					group.Id = entry.Name()

					cache.AddCategoryGroup(group)
				}
			}
		}
	}

	systempermissions.HandleMasterRolePermissions()

	return nil
}
