package service

import (
	"context"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/1Panel-dev/1Panel/agent/app/dto"
	"github.com/1Panel-dev/1Panel/agent/app/model"
	"github.com/1Panel-dev/1Panel/agent/app/repo"
	"github.com/1Panel-dev/1Panel/agent/app/task"
	"github.com/1Panel-dev/1Panel/agent/constant"
	"github.com/1Panel-dev/1Panel/agent/global"
	"github.com/1Panel-dev/1Panel/agent/i18n"
	"github.com/1Panel-dev/1Panel/agent/utils/common"
	"github.com/1Panel-dev/1Panel/agent/utils/docker"
	fileUtils "github.com/1Panel-dev/1Panel/agent/utils/files"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/build"
	"github.com/docker/docker/api/types/filters"
	"github.com/google/uuid"
)

const (
	rollbackPath         = "1panel/tmp"
	communityRestorePath = "1panel/tmp/community-restore"
	upgradePath          = "1panel/tmp/upgrade"
	uploadPath           = "1panel/uploads"
	downloadPath         = "1panel/download"
)

func (u *DeviceService) Scan() dto.CleanData {
	var (
		SystemClean dto.CleanData
		treeData    []dto.CleanTree
	)
	fileOp := fileUtils.NewFileOp()

	originalPath := path.Join(global.Dir.BaseDir, "1panel_original")
	originalSize, _ := fileOp.GetDirSize(originalPath)
	treeData = append(treeData, dto.CleanTree{
		ID:          uuid.NewString(),
		Label:       "1panel_original",
		Size:        uint64(originalSize),
		IsCheck:     originalSize > 0,
		IsRecommend: true,
		CanDelete:   false,
		Type:        "1panel_original",
		Children:    loadTreeWithDir(true, "1panel_original", originalPath, fileOp),
	})
	treeData = append(treeData, loadUpgradeTree(fileOp))
	treeData = append(treeData, loadAgentPackage(fileOp))

	SystemClean.BackupClean = loadBackupTree(fileOp)

	rollBackTree := loadRollBackTree()
	rollbackSize := uint64(0)
	for _, rollback := range rollBackTree {
		rollbackSize += rollback.Size
	}
	treeData = append(treeData, dto.CleanTree{
		ID:          uuid.NewString(),
		Label:       "rollback",
		Size:        rollbackSize,
		IsCheck:     rollbackSize > 0,
		IsRecommend: true,
		CanDelete:   false,
		Type:        "rollback",
		Children:    rollBackTree,
	})
	SystemClean.SystemClean = treeData

	uploadTreeData := loadUploadTree(fileOp)
	SystemClean.UploadClean = append(SystemClean.UploadClean, uploadTreeData...)

	downloadTreeData := loadDownloadTree(fileOp)
	SystemClean.DownloadClean = append(SystemClean.DownloadClean, downloadTreeData...)

	logTree := loadLogTree(fileOp)
	SystemClean.SystemLogClean = append(SystemClean.SystemLogClean, logTree...)

	containerTree := loadContainerTree()
	SystemClean.ContainerClean = append(SystemClean.ContainerClean, containerTree...)

	return SystemClean
}

func (u *DeviceService) Clean(req []dto.Clean) {
	size := uint64(0)
	for _, item := range req {
		size += item.Size
		switch item.TreeType {
		case "1panel_original":
			dropFileOrDir(path.Join(global.Dir.BaseDir, "1panel_original", item.Name))

		case "upgrade":
			dropFileOrDir(path.Join(global.Dir.BaseDir, upgradePath, item.Name))

		case "agent":
			dropFileOrDir(path.Join(global.Dir.BaseDir, "1panel/agent/package", item.Name))

		case "tmp_backup":
			dropFileOrDir(path.Join(global.Dir.LocalBackupDir, "tmp"))
		case "unknown_backup":
			dropFileOrDir(item.Name)

		case "rollback":
			dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app"))
			dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database"))
			dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website"))
			dropFileOrDir(path.Join(global.Dir.BaseDir, communityRestorePath))
		case "rollback_app":
			dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app", item.Name))
		case "rollback_database":
			dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "database", item.Name))
		case "rollback_website":
			dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "website", item.Name))
		case "rollback_community_restore":
			dropFileOrDir(path.Join(global.Dir.BaseDir, communityRestorePath, item.Name))

		case "upload":
			dropFileOrDir(path.Join(global.Dir.BaseDir, uploadPath, item.Name))
		case "upload_app":
			dropFileOrDir(path.Join(global.Dir.BaseDir, uploadPath, "app", item.Name))
		case "upload_database":
			dropFileOrDir(path.Join(global.Dir.BaseDir, uploadPath, "database", item.Name))
		case "upload_website":
			dropFileOrDir(path.Join(global.Dir.BaseDir, uploadPath, "website", item.Name))
		case "download":
			dropFileOrDir(path.Join(global.Dir.BaseDir, downloadPath, item.Name))
		case "download_app":
			dropFileOrDir(path.Join(global.Dir.BaseDir, downloadPath, "app", item.Name))
		case "download_database":
			dropFileOrDir(path.Join(global.Dir.BaseDir, downloadPath, "database", item.Name))
		case "download_website":
			dropFileOrDir(path.Join(global.Dir.BaseDir, downloadPath, "website", item.Name))

		case "system_log":
			if len(item.Name) == 0 {
				files, _ := os.ReadDir(global.Dir.LogDir)
				if len(files) == 0 {
					continue
				}
				for _, file := range files {
					if file.Name() == "1Panel-Core.log" || file.Name() == "1Panel.log" || file.IsDir() {
						continue
					}
					dropFileOrDir(path.Join(global.Dir.LogDir, file.Name()))
				}
			} else {
				dropFileOrDir(path.Join(global.Dir.LogDir, item.Name))
			}
		case "task_log":
			if len(item.Name) == 0 {
				files, _ := os.ReadDir(global.Dir.TaskDir)
				if len(files) == 0 {
					continue
				}
				for _, file := range files {
					if file.Name() == "ssl" || !file.IsDir() {
						continue
					}
					dropTaskLog(path.Join(global.Dir.TaskDir, file.Name()))
				}
			} else {
				dropTaskLog(path.Join(global.Dir.TaskDir, item.Name))
			}
		case "website_log":
			dropWebsiteLog(item.Name)
		case "script":
			dropFileOrDir(path.Join(global.Dir.TmpDir, "script", item.Name))
		case "images":
			_, _ = dropImages()
		case "containers":
			_, _ = dropContainers()
		case "volumes":
			_, _ = dropVolumes()
		case "build_cache":
			_, _ = dropBuildCache()
		case "app_tmp_download_version":
			dropFileOrDir(path.Join(global.Dir.RemoteAppResourceDir, item.Name))
		}
	}

	_ = cleanEmptyDirs(global.Dir.LocalBackupDir)
	_ = settingRepo.Update("LastCleanTime", time.Now().Format(constant.DateTimeLayout))
	_ = settingRepo.Update("LastCleanSize", fmt.Sprintf("%v", size))
	_ = settingRepo.Update("LastCleanData", fmt.Sprintf("%v", len(req)))
}

func doSystemClean(taskItem *task.Task) func(t *task.Task) error {
	return func(t *task.Task) error {
		size := int64(0)
		fileCount := 0
		dropWithTask(path.Join(global.Dir.BaseDir, "1panel_original"), taskItem, &size, &fileCount)

		upgradePath := path.Join(global.Dir.BaseDir, upgradePath)
		upgradeFiles, _ := os.ReadDir(upgradePath)
		if len(upgradeFiles) != 0 {
			sort.Slice(upgradeFiles, func(i, j int) bool {
				return upgradeFiles[i].Name() > upgradeFiles[j].Name()
			})
			for i := 0; i < len(upgradeFiles); i++ {
				if i != 0 {
					dropWithTask(path.Join(upgradePath, upgradeFiles[i].Name()), taskItem, &size, &fileCount)
				}
			}
		}

		dropWithTask(path.Join(global.Dir.LocalBackupDir, "tmp/system"), taskItem, &size, &fileCount)

		dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "app"), taskItem, &size, &fileCount)
		dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "website"), taskItem, &size, &fileCount)
		dropWithTask(path.Join(global.Dir.BaseDir, rollbackPath, "database"), taskItem, &size, &fileCount)
		dropWithTask(path.Join(global.Dir.BaseDir, communityRestorePath), taskItem, &size, &fileCount)

		upgrades := path.Join(global.Dir.BaseDir, upgradePath)
		oldUpgradeFiles, _ := os.ReadDir(upgrades)
		if len(oldUpgradeFiles) != 0 {
			for i := 0; i < len(oldUpgradeFiles); i++ {
				dropWithTask(path.Join(upgrades, oldUpgradeFiles[i].Name()), taskItem, &size, &fileCount)
			}
		}

		dropWithExclude(path.Join(global.Dir.BaseDir, uploadPath), []string{"theme", "skills-hub"}, taskItem, &size, &fileCount)
		dropWithTask(path.Join(global.Dir.BaseDir, downloadPath), taskItem, &size, &fileCount)

		logFiles, _ := os.ReadDir(global.Dir.LogDir)
		if len(logFiles) != 0 {
			for i := 0; i < len(logFiles); i++ {
				if logFiles[i].IsDir() {
					continue
				}
				if logFiles[i].Name() != "1Panel.log" && logFiles[i].Name() != "1Panel-Core.log" {
					dropWithTask(path.Join(global.Dir.LogDir, logFiles[i].Name()), taskItem, &size, &fileCount)
				}
			}
		}

		count1, size1 := dropVolumes()
		size += int64(size1)
		fileCount += count1
		count2, size2 := dropBuildCache()
		size += int64(size2)
		fileCount += count2

		timeNow := time.Now().Format(constant.DateTimeLayout)
		if fileCount != 0 {
			taskItem.Log(i18n.GetMsgWithMap("FileDropSum", map[string]interface{}{"size": common.LoadSizeUnit2F(float64(size)), "count": fileCount}))
		}

		_ = settingRepo.Update("LastCleanTime", timeNow)
		_ = settingRepo.Update("LastCleanSize", fmt.Sprintf("%v", size))
		_ = settingRepo.Update("LastCleanData", fmt.Sprintf("%v", fileCount))

		return nil
	}
}

func loadUpgradeTree(fileOp fileUtils.FileOp) dto.CleanTree {
	upgradePath := path.Join(global.Dir.BaseDir, upgradePath)
	upgradeSize, _ := fileOp.GetDirSize(upgradePath)
	upgradeTree := dto.CleanTree{
		ID:          uuid.NewString(),
		Label:       "upgrade",
		Size:        uint64(upgradeSize),
		IsCheck:     false,
		IsRecommend: true,
		CanDelete:   false,
		Type:        "upgrade",
		Children:    loadTreeWithDir(true, "upgrade", upgradePath, fileOp),
	}
	if len(upgradeTree.Children) != 0 {
		sort.Slice(upgradeTree.Children, func(i, j int) bool {
			return common.CompareVersion(upgradeTree.Children[i].Label, upgradeTree.Children[j].Label)
		})
		if global.IsMaster {
			var copiesSetting model.Setting
			_ = global.CoreDB.Where("key = ?", "UpgradeBackupCopies").First(&copiesSetting).Error
			copies, _ := strconv.Atoi(copiesSetting.Value)
			if copies == 0 || copies > len(upgradeTree.Children) {
				copies = len(upgradeTree.Children)
			}
			for i := 0; i < copies; i++ {
				upgradeTree.Children[i].IsCheck = false
				upgradeTree.Children[i].IsRecommend = false
			}
		} else {
			upgradeTree.Children[0].IsCheck = false
			upgradeTree.Children[0].IsRecommend = false
		}
	}
	return upgradeTree
}

func loadAgentPackage(fileOp fileUtils.FileOp) dto.CleanTree {
	pathItem := path.Join(global.Dir.BaseDir, "1panel/agent/package")
	itemTree := dto.CleanTree{
		ID:          uuid.NewString(),
		Label:       "agent_packages",
		IsCheck:     false,
		IsRecommend: true,
		CanDelete:   false,
		Type:        "agent",
	}
	files, _ := os.ReadDir(pathItem)
	for _, file := range files {
		if file.IsDir() {
			itemSize, _ := fileOp.GetDirSize(path.Join(pathItem, file.Name()))
			itemTree.Size += uint64(itemSize)
			itemTree.Children = append(itemTree.Children, dto.CleanTree{
				ID:          uuid.NewString(),
				Label:       file.Name(),
				Name:        file.Name(),
				Size:        uint64(itemSize),
				IsCheck:     true,
				IsRecommend: true,
				CanDelete:   true,
				Type:        "agent",
			})
		} else {
			itemSize, _ := file.Info()
			itemName := file.Name()
			isCurrentVersion := strings.HasPrefix(itemName, fmt.Sprintf("1panel-agent_%s_", global.CONF.Base.Version))
			if isCurrentVersion {
				continue
			}
			itemTree.Size += uint64(itemSize.Size())
			itemTree.Children = append(itemTree.Children, dto.CleanTree{
				ID:          uuid.NewString(),
				Label:       itemName,
				Name:        itemName,
				Size:        uint64(itemSize.Size()),
				IsCheck:     !isCurrentVersion,
				IsRecommend: true,
				CanDelete:   true,
				Type:        "agent",
			})
		}
	}
	if itemTree.Size == 0 {
		itemTree.IsCheck = false
	}
	return itemTree
}

func loadBackupTree(fileOp fileUtils.FileOp) []dto.CleanTree {
	var treeData []dto.CleanTree

	tmpSize, _ := fileOp.GetDirSize(path.Join(global.Dir.LocalBackupDir, "tmp"))
	treeData = append(treeData, dto.CleanTree{
		ID:          uuid.NewString(),
		Label:       "tmp_backup",
		Size:        uint64(tmpSize),
		IsCheck:     tmpSize != 0,
		IsRecommend: true,
		CanDelete:   true,
		Type:        "tmp_backup",
	})
	backupRecords, _ := backupRepo.ListRecord()
	var recordMap = make(map[string][]string)
	for _, record := range backupRecords {
		if val, ok := recordMap[record.FileDir]; ok {
			val = append(val, record.FileName)
			recordMap[record.FileDir] = val
		} else {
			recordMap[record.FileDir] = []string{record.FileName}
		}
	}

	treeData = append(treeData, loadUnknownApps(fileOp, recordMap))
	treeData = append(treeData, loadUnknownDbs(fileOp, recordMap))
	treeData = append(treeData, loadUnknownWebsites(fileOp, recordMap))
	treeData = append(treeData, loadUnknownSnapshot(fileOp))
	treeData = append(treeData, loadUnknownWebsiteLog(fileOp))
	return treeData
}

func loadUnknownApps(fileOp fileUtils.FileOp, recordMap map[string][]string) dto.CleanTree {
	apps, _ := appInstallRepo.ListBy(context.Background())
	var excludePaths []string
	for _, app := range apps {
		itemName := fmt.Sprintf("app/%s/%s", app.App.Key, app.Name)
		if val, ok := recordMap[itemName]; ok {
			for _, item := range val {
				excludePaths = append(excludePaths, path.Join(global.Dir.LocalBackupDir, itemName, item))
			}
		}
	}
	backupPath := path.Join(global.Dir.LocalBackupDir, "app")
	treeData := dto.CleanTree{
		ID:          uuid.NewString(),
		Label:       "unknown_app",
		IsCheck:     false,
		IsRecommend: false,
		CanDelete:   false,
		Name:        backupPath,
		Type:        "unknown_backup",
	}
	_ = loadFileOrDirWithExclude(fileOp, 0, backupPath, &treeData, excludePaths)
	return treeData
}
func loadUnknownDbs(fileOp fileUtils.FileOp, recordMap map[string][]string) dto.CleanTree {
	dbs, _ := databaseRepo.GetList()
	var excludePaths []string
	dbMap := make(map[string]struct{})
	for _, db := range dbs {
		dbMap[fmt.Sprintf("database/%s/%s", db.Type, db.Name)] = struct{}{}
	}
	for key, val := range recordMap {
		itemName := path.Dir(key)
		if _, ok := dbMap[itemName]; ok {
			for _, item := range val {
				excludePaths = append(excludePaths, path.Join(global.Dir.LocalBackupDir, key, item))
			}
		}
	}
	backupPath := path.Join(global.Dir.LocalBackupDir, "database")
	treeData := dto.CleanTree{
		ID:          uuid.NewString(),
		Label:       "unknown_database",
		Name:        backupPath,
		IsCheck:     false,
		IsRecommend: false,
		CanDelete:   false,
		Type:        "unknown_backup",
	}
	_ = loadFileOrDirWithExclude(fileOp, 0, backupPath, &treeData, excludePaths)
	return treeData
}
func loadUnknownWebsites(fileOp fileUtils.FileOp, recordMap map[string][]string) dto.CleanTree {
	websites, _ := websiteRepo.List()
	var excludePaths []string
	for _, website := range websites {
		itemName := fmt.Sprintf("website/%s", website.Alias)
		if val, ok := recordMap[itemName]; ok {
			for _, item := range val {
				excludePaths = append(excludePaths, path.Join(global.Dir.LocalBackupDir, itemName, item))
			}
		}
	}
	backupPath := path.Join(global.Dir.LocalBackupDir, "website")
	treeData := dto.CleanTree{
		ID:          uuid.NewString(),
		Label:       "unknown_website",
		Name:        backupPath,
		IsCheck:     false,
		IsRecommend: false,
		CanDelete:   false,
		Type:        "unknown_backup",
	}
	_ = loadFileOrDirWithExclude(fileOp, 0, backupPath, &treeData, excludePaths)
	return treeData
}
func loadUnknownSnapshot(fileOp fileUtils.FileOp) dto.CleanTree {
	snaps, _ := snapshotRepo.GetList()
	var excludePaths []string
	for _, item := range snaps {
		excludePaths = append(excludePaths, path.Join(global.Dir.LocalBackupDir, "system_snapshot", item.Name+".tar.gz"))
	}
	backupPath := path.Join(global.Dir.LocalBackupDir, "system_snapshot")
	treeData := dto.CleanTree{
		ID:          uuid.NewString(),
		Label:       "unknown_snapshot",
		Name:        backupPath,
		IsCheck:     false,
		IsRecommend: false,
		CanDelete:   false,
		Type:        "unknown_backup",
	}
	entries, _ := os.ReadDir(backupPath)
	for _, entry := range entries {
		childPath := filepath.Join(backupPath, entry.Name())
		if isExactPathMatch(childPath, excludePaths) {
			continue
		}
		childNode := dto.CleanTree{
			ID:          uuid.NewString(),
			Label:       entry.Name(),
			IsCheck:     false,
			IsRecommend: false,
			CanDelete:   true,
			Name:        childPath,
			Type:        "unknown_backup",
		}
		if entry.IsDir() {
			itemSize, _ := fileOp.GetDirSize(childPath)
			childNode.Size = uint64(itemSize)
			childNode.IsCheck = true
			childNode.IsRecommend = true
			treeData.Size += childNode.Size
		} else {
			info, _ := entry.Info()
			childNode.Size = uint64(info.Size())
			treeData.Size += childNode.Size
		}

		treeData.Children = append(treeData.Children, childNode)
	}
	return treeData
}

func loadUnknownWebsiteLog(fileOp fileUtils.FileOp) dto.CleanTree {
	treeData := dto.CleanTree{
		ID:          uuid.NewString(),
		Label:       "unknown_website_log",
		IsCheck:     false,
		IsRecommend: true,
		CanDelete:   false,
		Type:        "unknown_backup",
	}
	dir := path.Join(global.Dir.LocalBackupDir, "log/website")
	websites, _ := websiteRepo.List()
	websiteMap := make(map[string]struct{})
	for _, website := range websites {
		websiteMap[website.Alias] = struct{}{}
	}

	entries, _ := os.ReadDir(dir)
	for _, entry := range entries {
		if !entry.IsDir() {
			continue
		}
		dirName := entry.Name()
		if _, ok := websiteMap[dirName]; !ok {
			dirPath := path.Join(dir, dirName)
			itemSize, _ := fileOp.GetDirSize(dirPath)
			childData := dto.CleanTree{
				ID:          uuid.NewString(),
				Label:       dirName,
				IsCheck:     true,
				IsRecommend: true,
				CanDelete:   true,
				Name:        dirPath,
				Type:        "unknown_backup",
				Size:        uint64(itemSize),
			}
			treeData.Size += uint64(itemSize)
			treeData.Children = append(treeData.Children, childData)
		}
	}
	if treeData.Size > 0 {
		treeData.IsCheck = true
	}
	return treeData
}

func loadFileOrDirWithExclude(fileOp fileUtils.FileOp, index uint, dir string, rootTree *dto.CleanTree, excludes []string) error {
	index++
	entries, err := os.ReadDir(dir)
	if err != nil {
		return err
	}
	for _, entry := range entries {
		childPath := filepath.Join(dir, entry.Name())
		if isExactPathMatch(childPath, excludes) {
			continue
		}
		childNode := dto.CleanTree{
			ID:          uuid.NewString(),
			Label:       entry.Name(),
			IsCheck:     false,
			IsRecommend: false,
			CanDelete:   true,
			Name:        childPath,
			Type:        "unknown_backup",
		}
		if entry.IsDir() {
			if index < 4 {
				if err = loadFileOrDirWithExclude(fileOp, index, childPath, &childNode, excludes); err != nil {
					return err
				}
				childNode.Size = 0
				for _, child := range childNode.Children {
					childNode.Size += child.Size
				}
			} else {
				itemSize, _ := fileOp.GetDirSize(childPath)
				childNode.Size = uint64(itemSize)
			}
			if childNode.Size == 0 {
				continue
			}
		} else {
			info, _ := entry.Info()
			childNode.Size = uint64(info.Size())
		}

		rootTree.Size += childNode.Size
		rootTree.Children = append(rootTree.Children, childNode)
	}
	return nil
}

func isExactPathMatch(path string, excludePaths []string) bool {
	cleanPath := filepath.Clean(path)

	for _, excludePath := range excludePaths {
		cleanExclude := filepath.Clean(excludePath)
		if cleanPath == cleanExclude {
			return true
		}
	}

	return false
}

func loadRollBackTree() []dto.CleanTree {
	var treeData []dto.CleanTree
	treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "app"), "rollback_app")
	treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "website"), "rollback_website")
	treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, rollbackPath, "database"), "rollback_database")
	treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, communityRestorePath), "rollback_community_restore")

	return treeData
}

func loadUploadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
	var treeData []dto.CleanTree
	treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "app"), "upload_app")
	treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "website"), "upload_website")
	treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, uploadPath, "database"), "upload_database")

	path5 := path.Join(global.Dir.BaseDir, uploadPath)
	uploadTreeData := loadTreeWithAllFile(true, path5, "upload", path5, fileOp)
	treeData = append(treeData, uploadTreeData...)

	return treeData
}

func loadDownloadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
	var treeData []dto.CleanTree
	treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "app"), "download_app")
	treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "website"), "download_website")
	treeData = loadTreeWithCheck(treeData, path.Join(global.Dir.BaseDir, downloadPath, "database"), "download_database")

	path5 := path.Join(global.Dir.BaseDir, downloadPath)
	uploadTreeData := loadTreeWithAllFile(true, path5, "download", path5, fileOp)
	treeData = append(treeData, uploadTreeData...)

	appTmpDownloadTree := loadAppTmpDownloadTree(fileOp)
	if len(appTmpDownloadTree) > 0 {
		parentTree := dto.CleanTree{
			ID:          uuid.NewString(),
			Label:       "app_tmp_download",
			IsCheck:     true,
			IsRecommend: true,
			CanDelete:   false,
			Type:        "app_tmp_download",
			Name:        "apps",
		}
		for _, child := range appTmpDownloadTree {
			parentTree.Size += child.Size
		}
		parentTree.Children = appTmpDownloadTree
		treeData = append(treeData, parentTree)
	}
	return treeData
}

func loadLogTree(fileOp fileUtils.FileOp) []dto.CleanTree {
	var treeData []dto.CleanTree
	path1 := path.Join(global.Dir.LogDir)
	list1 := loadTreeWithAllFile(true, path1, "system_log", path1, fileOp)
	size := uint64(0)
	for _, file := range list1 {
		size += file.Size
	}
	treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "system_log", Size: size, Children: list1, Type: "system_log", IsRecommend: true, CanDelete: false})

	path2 := path.Join(global.Dir.TaskDir)
	list2 := loadTreeWithDir(false, "task_log", path2, fileOp)
	size2, _ := fileOp.GetDirSize(path2)
	treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "task_log", Size: uint64(size2), Children: list2, Type: "task_log", CanDelete: false})

	websiteLogList := loadWebsiteLogTree(fileOp)
	logTotalSize := uint64(0)
	for _, websiteLog := range websiteLogList {
		logTotalSize += websiteLog.Size
	}
	treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "website_log", Size: logTotalSize, Children: websiteLogList, Type: "website_log", IsRecommend: false, CanDelete: false})

	return treeData
}

func loadWebsiteLogTree(fileOp fileUtils.FileOp) []dto.CleanTree {
	websites, _ := websiteRepo.List()
	if len(websites) == 0 {
		return nil
	}
	var res []dto.CleanTree
	for _, website := range websites {
		size3, _ := fileOp.GetDirSize(path.Join(GetSiteDir(website.Alias), "log"))
		res = append(res, dto.CleanTree{
			ID:        uuid.NewString(),
			Label:     website.PrimaryDomain,
			Size:      uint64(size3),
			Type:      "website_log",
			Name:      website.Alias,
			CanDelete: true,
		})
	}
	return res
}

func loadAppTmpDownloadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
	appDirs, err := os.ReadDir(global.Dir.RemoteAppResourceDir)
	if err != nil {
		return nil
	}
	var res []dto.CleanTree
	for _, appDir := range appDirs {
		if !appDir.IsDir() {
			continue
		}
		appKey := appDir.Name()
		app, _ := appRepo.GetFirst(appRepo.WithKey(appKey))
		if app.ID == 0 {
			continue
		}
		appPath := filepath.Join(global.Dir.RemoteAppResourceDir, appKey)
		versionDirs, err := os.ReadDir(appPath)
		if err != nil {
			continue
		}
		appDetails, _ := appDetailRepo.GetBy(appDetailRepo.WithAppId(app.ID))
		existingVersions := make(map[string]bool)
		for _, appDetail := range appDetails {
			existingVersions[appDetail.Version] = true
		}
		var missingVersions []string
		for _, versionDir := range versionDirs {
			if !versionDir.IsDir() {
				continue
			}

			version := versionDir.Name()
			if !existingVersions[version] {
				missingVersions = append(missingVersions, version)
			}
		}
		if len(missingVersions) > 0 {
			var appTree dto.CleanTree
			appTree.ID = uuid.NewString()
			appTree.Label = app.Name
			appTree.Type = "app_tmp_download"
			appTree.Name = appKey
			appTree.IsRecommend = true
			appTree.IsCheck = true
			appTree.CanDelete = false
			for _, version := range missingVersions {
				versionPath := filepath.Join(appPath, version)
				size, _ := fileOp.GetDirSize(versionPath)
				appTree.Size += uint64(size)
				appTree.Children = append(appTree.Children, dto.CleanTree{
					ID:          uuid.NewString(),
					Label:       version,
					Size:        uint64(size),
					IsCheck:     true,
					IsRecommend: true,
					CanDelete:   true,
					Type:        "app_tmp_download_version",
					Name:        path.Join(appKey, version),
				})
			}
			res = append(res, appTree)
		}
	}
	return res
}

func loadContainerTree() []dto.CleanTree {
	var treeData []dto.CleanTree
	client, err := docker.NewDockerClient()
	if err != nil {
		return treeData
	}
	defer client.Close()
	diskUsage, err := client.DiskUsage(context.Background(), types.DiskUsageOptions{})
	if err != nil {
		return treeData
	}
	imageSize := uint64(0)
	for _, file := range diskUsage.Images {
		if file.Containers == 0 {
			imageSize += uint64(file.Size)
		}
	}
	treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_images", Size: imageSize, Children: nil, Type: "images", IsRecommend: true, CanDelete: true})

	containerSize := uint64(0)
	for _, file := range diskUsage.Containers {
		if file.State != "running" {
			containerSize += uint64(file.SizeRw)
		}
	}
	treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_containers", Size: containerSize, Children: nil, Type: "containers", IsRecommend: true, CanDelete: true})

	volumeSize := uint64(0)
	for _, file := range diskUsage.Volumes {
		if file.UsageData.RefCount <= 0 {
			volumeSize += uint64(file.UsageData.Size)
		}
	}
	treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_volumes", Size: volumeSize, IsCheck: volumeSize > 0, Children: nil, Type: "volumes", IsRecommend: true, CanDelete: true})

	var buildCacheTotalSize int64
	for _, cache := range diskUsage.BuildCache {
		if cache.Type == "source.local" {
			buildCacheTotalSize += cache.Size
		}
	}
	treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "build_cache", Size: uint64(buildCacheTotalSize), IsCheck: buildCacheTotalSize > 0, Type: "build_cache", IsRecommend: true, CanDelete: true})
	return treeData
}

func loadTreeWithCheck(treeData []dto.CleanTree, pathItem, treeType string) []dto.CleanTree {
	list, size := loadTreeWithFileSize(true, pathItem, treeType, pathItem)
	if len(list) == 0 || size == 0 {
		return treeData
	}
	treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: treeType, Size: size, IsCheck: size > 0, Children: list, Type: treeType, IsRecommend: true, CanDelete: false})
	return treeData
}

func loadTreeWithFileSize(isCheck bool, originalPath, treeType, pathItem string) ([]dto.CleanTree, uint64) {
	var (
		lists []dto.CleanTree
		total uint64
	)

	entries, err := os.ReadDir(pathItem)
	if err != nil {
		return lists, total
	}
	for _, entry := range entries {
		item := dto.CleanTree{
			ID:          uuid.NewString(),
			Label:       entry.Name(),
			Type:        treeType,
			Name:        strings.TrimPrefix(path.Join(pathItem, entry.Name()), originalPath+"/"),
			IsCheck:     isCheck,
			IsRecommend: isCheck,
			CanDelete:   true,
		}
		entryPath := path.Join(pathItem, entry.Name())
		if entry.IsDir() {
			children, size := loadTreeWithFileSize(isCheck, originalPath, treeType, entryPath)
			if len(children) == 0 {
				continue
			}
			item.Children = children
			item.Size = size
		} else {
			info, err := entry.Info()
			if err != nil {
				continue
			}
			item.Size = uint64(info.Size())
		}
		if item.Size == 0 {
			continue
		}
		total += item.Size
		lists = append(lists, item)
	}
	return lists, total
}

func loadTreeWithDir(isCheck bool, treeType, pathItem string, fileOp fileUtils.FileOp) []dto.CleanTree {
	var lists []dto.CleanTree
	files, err := os.ReadDir(pathItem)
	if err != nil {
		return lists
	}
	for _, file := range files {
		if file.Name() == "ssl" {
			continue
		}
		if file.IsDir() {
			size, err := fileOp.GetDirSize(path.Join(pathItem, file.Name()))
			if err != nil {
				continue
			}
			item := dto.CleanTree{
				ID:          uuid.NewString(),
				Label:       file.Name(),
				Type:        treeType,
				Size:        uint64(size),
				Name:        strings.TrimPrefix(file.Name(), "/"),
				IsCheck:     isCheck,
				IsRecommend: isCheck,
				CanDelete:   true,
			}
			lists = append(lists, item)
		}
	}
	return lists
}

func loadTreeWithAllFile(isCheck bool, originalPath, treeType, pathItem string, fileOp fileUtils.FileOp) []dto.CleanTree {
	var lists []dto.CleanTree

	files, err := os.ReadDir(pathItem)
	if err != nil {
		return lists
	}
	for _, file := range files {
		if treeType == "upload" && ((file.Name() == "theme" || file.Name() == "skills-hub") && file.IsDir()) {
			continue
		}
		if treeType == "system_log" && (file.Name() == "1Panel-Core.log" || file.Name() == "1Panel.log" || file.IsDir()) {
			continue
		}
		if (treeType == "upload" || treeType == "download") && file.IsDir() && (file.Name() == "app" || file.Name() == "database" || file.Name() == "website" || file.Name() == "directory") {
			continue
		}
		size := uint64(0)
		name := strings.TrimPrefix(path.Join(pathItem, file.Name()), originalPath+"/")
		if file.IsDir() {
			sizeItem, err := fileOp.GetDirSize(path.Join(pathItem, file.Name()))
			if err != nil {
				continue
			}
			size = uint64(sizeItem)
		} else {
			fileInfo, err := file.Info()
			if err != nil {
				continue
			}
			size = uint64(fileInfo.Size())
		}
		item := dto.CleanTree{
			ID:          uuid.NewString(),
			Label:       file.Name(),
			Type:        treeType,
			Size:        size,
			Name:        name,
			IsCheck:     isCheck,
			IsRecommend: isCheck,
			CanDelete:   true,
		}
		if file.IsDir() {
			item.Children = loadTreeWithAllFile(isCheck, originalPath, treeType, path.Join(pathItem, file.Name()), fileOp)
		}
		lists = append(lists, item)
	}
	return lists
}

func dropFileOrDir(itemPath string) {
	if err := os.RemoveAll(itemPath); err != nil {
		global.LOG.Errorf("drop file %s failed, err %v", itemPath, err)
	}
}
func dropFile(itemPath string) {
	info, err := os.Stat(itemPath)
	if err != nil {
		return
	}
	if info.IsDir() {
		return
	}
	if err := os.Remove(itemPath); err != nil {
		global.LOG.Errorf("drop file %s failed, err %v", itemPath, err)
	}
}

func dropBuildCache() (int, int) {
	client, err := docker.NewDockerClient()
	if err != nil {
		global.LOG.Errorf("do not get docker client")
		return 0, 0
	}
	defer client.Close()
	opts := build.CachePruneOptions{}
	opts.All = true
	res, err := client.BuildCachePrune(context.Background(), opts)
	if err != nil {
		global.LOG.Errorf("drop build cache failed, err %v", err)
		return 0, 0
	}
	return len(res.CachesDeleted), int(res.SpaceReclaimed)
}

func dropImages() (int, int) {
	client, err := docker.NewDockerClient()
	if err != nil {
		global.LOG.Errorf("do not get docker client")
		return 0, 0
	}
	defer client.Close()
	pruneFilters := filters.NewArgs()
	pruneFilters.Add("dangling", "false")
	res, err := client.ImagesPrune(context.Background(), pruneFilters)
	if err != nil {
		global.LOG.Errorf("drop images failed, err %v", err)
		return 0, 0
	}
	return len(res.ImagesDeleted), int(res.SpaceReclaimed)
}

func dropContainers() (int, int) {
	client, err := docker.NewDockerClient()
	if err != nil {
		global.LOG.Errorf("do not get docker client")
		return 0, 0
	}
	defer client.Close()
	pruneFilters := filters.NewArgs()
	res, err := client.ContainersPrune(context.Background(), pruneFilters)
	if err != nil {
		global.LOG.Errorf("drop containers failed, err %v", err)
		return 0, 0
	}
	return len(res.ContainersDeleted), int(res.SpaceReclaimed)
}

func dropVolumes() (int, int) {
	client, err := docker.NewDockerClient()
	if err != nil {
		global.LOG.Errorf("do not get docker client")
		return 0, 0
	}
	defer client.Close()
	pruneFilters := filters.NewArgs()
	versions, err := client.ServerVersion(context.Background())
	if err != nil {
		global.LOG.Errorf("do not get docker api versions")
		return 0, 0
	}
	if common.ComparePanelVersion(versions.APIVersion, "1.42") {
		pruneFilters.Add("all", "true")
	}
	res, err := client.VolumesPrune(context.Background(), pruneFilters)
	if err != nil {
		global.LOG.Errorf("drop volumes failed, err %v", err)
		return 0, 0
	}
	return len(res.VolumesDeleted), int(res.SpaceReclaimed)
}

func dropWebsiteLog(alias string) {
	accessLogPath := path.Join(GetSiteDir(alias), "log", "access.log")
	errorLogPath := path.Join(GetSiteDir(alias), "log", "error.log")
	if err := os.Truncate(accessLogPath, 0); err != nil {
		global.LOG.Errorf("truncate access log %s failed, err %v", accessLogPath, err)
	}

	if err := os.Truncate(errorLogPath, 0); err != nil {
		global.LOG.Errorf("truncate error log %s failed, err %v", errorLogPath, err)
	}
}

func dropTaskLog(logDir string) {
	files, err := os.ReadDir(logDir)
	if err != nil {
		return
	}
	taskType := path.Base(logDir)
	var usedTasks []string
	switch taskType {
	case "Cronjob":
		_ = global.DB.Model(&model.JobRecords{}).Where("task_id != ?", "").Select("task_id").Find(&usedTasks).Error
	case "Snapshot":
		var (
			snapIDs     []string
			recoverIDs  []string
			rollbackIDs []string
		)
		_ = global.DB.Model(&model.Snapshot{}).Where("task_id != ?", "").Select("task_id").Find(&snapIDs).Error
		_ = global.DB.Model(&model.Snapshot{}).Where("task_recover_id != ", "").Select("task_id").Find(&recoverIDs).Error
		_ = global.DB.Model(&model.Snapshot{}).Where("task_rollback_id != ?", "").Select("task_id").Find(&rollbackIDs).Error
		usedTasks = append(usedTasks, snapIDs...)
		usedTasks = append(usedTasks, recoverIDs...)
		usedTasks = append(usedTasks, rollbackIDs...)
	case "Backup":
		_ = global.DB.Model(&model.BackupRecord{}).Where("task_id != ?", "").Select("task_id").Find(&usedTasks).Error
	case "Clam":
		_ = global.DB.Model(&model.ClamRecord{}).Where("task_id != ?", "").Select("task_id").Find(&usedTasks).Error
	case "Tamper":
		xpackDB, err := common.LoadDBConnByPathWithErr(path.Join(global.CONF.Base.InstallDir, "1panel/db/xpack.db"), "xpack.db")
		if err == nil {
			_ = xpackDB.Table("tampers").Where("task_id != ?", "").Select("task_id").Find(&usedTasks).Error
		}
		defer common.CloseDB(xpackDB)
	case "System":
		xpackDB, err := common.LoadDBConnByPathWithErr(path.Join(global.CONF.Base.InstallDir, "1panel/db/xpack.db"), "xpack.db")
		if err == nil {
			_ = xpackDB.Model("nodes").Where("task_id != ?", "").Select("task_id").Find(&usedTasks).Error
		}
		defer common.CloseDB(xpackDB)
	default:
		dropFileOrDir(logDir)
		_ = taskRepo.Delete(repo.WithByType(taskType))
		return
	}
	usedMap := make(map[string]struct{})
	for _, item := range usedTasks {
		if _, ok := usedMap[item]; !ok {
			usedMap[item] = struct{}{}
		}
	}
	for _, item := range files {
		if _, ok := usedMap[strings.TrimSuffix(item.Name(), ".log")]; ok {
			continue
		}
		_ = os.Remove(logDir + "/" + item.Name())
	}
	_ = taskRepo.Delete(repo.WithByType(taskType), taskRepo.WithByIDNotIn(usedTasks))
}

func dropWithExclude(pathToDelete string, excludeSubDirs []string, taskItem *task.Task, size *int64, count *int) {
	entries, err := os.ReadDir(pathToDelete)
	if err != nil {
		return
	}

	for _, entry := range entries {
		name := entry.Name()
		fullPath := filepath.Join(pathToDelete, name)
		excluded := false
		for _, ex := range excludeSubDirs {
			if name == ex {
				excluded = true
				break
			}
		}
		if excluded {
			continue
		}
		dropWithTask(fullPath, taskItem, size, count)
	}
}

func dropWithTask(itemPath string, taskItem *task.Task, size *int64, count *int) {
	itemSize := int64(0)
	itemCount := 0
	scanFile(itemPath, &itemSize, &itemCount)
	*size += itemSize
	*count += itemCount
	if err := os.RemoveAll(itemPath); err != nil {
		taskItem.Log(i18n.GetWithNameAndErr("FileDropFailed", itemPath, err))
		return
	}
	if itemCount != 0 {
		taskItem.Log(i18n.GetMsgWithMap("FileDropSuccess", map[string]interface{}{"name": itemPath, "count": itemCount, "size": common.LoadSizeUnit2F(float64(itemSize))}))
	}
}

func scanFile(pathItem string, size *int64, count *int) {
	files, _ := os.ReadDir(pathItem)
	for _, f := range files {
		if f.IsDir() {
			scanFile(path.Join(pathItem, f.Name()), size, count)
		} else {
			fileInfo, err := f.Info()
			if err != nil {
				continue
			}
			*count++
			*size += fileInfo.Size()
		}
	}
}

func cleanEmptyDirs(root string) error {
	dirsToCheck := make([]string, 0)
	err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
		if err != nil {
			return nil
		}
		if d.IsDir() {
			dirsToCheck = append(dirsToCheck, path)
		}
		return nil
	})
	if err != nil {
		return err
	}

	for i := len(dirsToCheck) - 1; i >= 0; i-- {
		dir := dirsToCheck[i]
		if dir == root {
			continue
		}

		entries, err := os.ReadDir(dir)
		if err != nil {
			continue
		}

		if len(entries) == 0 {
			_ = os.Remove(dir)
		}
	}
	return nil
}
