package service

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

	"github.com/1Panel-dev/1Panel/agent/app/repo"
	"github.com/1Panel-dev/1Panel/agent/app/task"
	"github.com/1Panel-dev/1Panel/agent/i18n"

	"github.com/1Panel-dev/1Panel/agent/app/dto"
	"github.com/1Panel-dev/1Panel/agent/app/model"
	"github.com/1Panel-dev/1Panel/agent/constant"
	"github.com/1Panel-dev/1Panel/agent/global"
	"github.com/1Panel-dev/1Panel/agent/utils/common"
	"github.com/1Panel-dev/1Panel/agent/utils/files"
	"github.com/pkg/errors"
)

func (u *CronjobService) handleApp(cronjob model.Cronjob, startTime time.Time, taskItem *task.Task) error {
	apps := loadAppsForJob(cronjob)
	if len(apps) == 0 {
		return errors.New("no such app in database!")
	}
	accountMap, err := NewBackupClientMap(strings.Split(cronjob.SourceAccountIDs, ","))
	if err != nil {
		return err
	}
	for _, app := range apps {
		retry := 0
		taskItem.AddSubTaskWithOps(task.GetTaskName(app.Name, task.TaskBackup, task.TaskScopeCronjob), func(task *task.Task) error {
			var record model.BackupRecord
			record.From = "cronjob"
			record.Type = "app"
			record.CronjobID = cronjob.ID
			record.Name = app.App.Key
			record.DetailName = app.Name
			record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
			backupDir := path.Join(global.Dir.LocalBackupDir, fmt.Sprintf("tmp/app/%s/%s", app.App.Key, app.Name))
			record.FileName = fmt.Sprintf("app_%s_%s.tar.gz", app.Name, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
			if err := doAppBackup(&app, task, backupDir, record.FileName, cronjob.ExclusionRules, cronjob.Secret); err != nil {
				if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
					retry++
					return err
				} else {
					task.Log(i18n.GetMsgWithDetail("IgnoreBackupErr", err.Error()))
					return nil
				}
			}
			downloadPath, err := u.uploadCronjobBackFile(cronjob, task, accountMap, path.Join(backupDir, record.FileName))
			if err != nil {
				if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
					retry++
					return err
				}
				task.Log(i18n.GetMsgWithDetail("IgnoreUploadErr", err.Error()))
				return nil
			}
			record.FileDir = path.Dir(downloadPath)
			if err := backupRepo.CreateRecord(&record); err != nil {
				global.LOG.Errorf("save backup record failed, err: %v", err)
				return err
			}
			u.removeExpiredBackup(cronjob, accountMap, record)
			return nil
		}, nil, int(cronjob.RetryTimes), time.Duration(cronjob.Timeout)*time.Second)
	}
	return nil
}

func (u *CronjobService) handleWebsite(cronjob model.Cronjob, startTime time.Time, taskItem *task.Task) error {
	webs := loadWebsForJob(cronjob)
	if len(webs) == 0 {
		return errors.New("no such website in database!")
	}
	accountMap, err := NewBackupClientMap(strings.Split(cronjob.SourceAccountIDs, ","))
	if err != nil {
		return err
	}
	for _, web := range webs {
		retry := 0
		taskItem.AddSubTaskWithOps(task.GetTaskName(web.Alias, task.TaskBackup, task.TaskScopeCronjob), func(task *task.Task) error {
			var record model.BackupRecord
			record.From = "cronjob"
			record.Type = "website"
			record.CronjobID = cronjob.ID
			record.Name = web.Alias
			record.DetailName = web.Alias
			record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
			backupDir := path.Join(global.Dir.LocalBackupDir, fmt.Sprintf("tmp/website/%s", web.Alias))
			record.FileName = fmt.Sprintf("website_%s_%s.tar.gz", web.Alias, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))

			if err := doWebsiteBackup(&web, taskItem, backupDir, record.FileName, cronjob.ExclusionRules, cronjob.Secret); err != nil {
				if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
					retry++
					return err
				} else {
					task.Log(i18n.GetMsgWithDetail("IgnoreBackupErr", err.Error()))
					return nil
				}
			}

			downloadPath, err := u.uploadCronjobBackFile(cronjob, task, accountMap, path.Join(backupDir, record.FileName))
			if err != nil {
				if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
					retry++
					return err
				}
				task.Log(i18n.GetMsgWithDetail("IgnoreUploadErr", err.Error()))
				return nil
			}
			record.FileDir = path.Dir(downloadPath)
			if err := backupRepo.CreateRecord(&record); err != nil {
				global.LOG.Errorf("save backup record failed, err: %v", err)
				return err
			}
			u.removeExpiredBackup(cronjob, accountMap, record)
			return nil
		}, nil, int(cronjob.RetryTimes), time.Duration(cronjob.Timeout)*time.Second)
	}
	return nil
}

func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Time, taskItem *task.Task) error {
	dbs := loadDbsForJob(cronjob)
	if len(dbs) == 0 {
		return errors.New("no such db in database!")
	}
	accountMap, err := NewBackupClientMap(strings.Split(cronjob.SourceAccountIDs, ","))
	if err != nil {
		return err
	}
	for _, dbInfo := range dbs {
		retry := 0
		itemName := fmt.Sprintf("%s[%s] - %s", dbInfo.Database, dbInfo.DBType, dbInfo.Name)
		taskItem.AddSubTaskWithOps(task.GetTaskName(itemName, task.TaskBackup, task.TaskScopeCronjob), func(task *task.Task) error {
			var record model.BackupRecord
			record.From = "cronjob"
			record.Type = dbInfo.DBType
			record.CronjobID = cronjob.ID
			record.Name = dbInfo.Database
			record.DetailName = dbInfo.Name
			record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs

			backupDir := path.Join(global.Dir.LocalBackupDir, fmt.Sprintf("tmp/database/%s/%s/%s", dbInfo.DBType, record.Name, dbInfo.Name))
			record.FileName = fmt.Sprintf("db_%s_%s.sql.gz", dbInfo.Name, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
			if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" || cronjob.DBType == "mysql-cluster" {
				if err := doMysqlBackup(dbInfo, backupDir, record.FileName); err != nil {
					if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
						retry++
						return err
					} else {
						task.Log(i18n.GetMsgWithDetail("IgnoreBackupErr", err.Error()))
						return nil
					}
				}
			} else {
				if err := doPostgresqlgBackup(dbInfo, backupDir, record.FileName); err != nil {
					if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
						retry++
						return err
					} else {
						task.Log(i18n.GetMsgWithDetail("IgnoreBackupErr", err.Error()))
						return nil
					}
				}
			}

			downloadPath, err := u.uploadCronjobBackFile(cronjob, task, accountMap, path.Join(backupDir, record.FileName))
			if err != nil {
				if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
					retry++
					return err
				}
				task.Log(i18n.GetMsgWithDetail("IgnoreUploadErr", err.Error()))
				return nil
			}
			record.FileDir = path.Dir(downloadPath)
			if err := backupRepo.CreateRecord(&record); err != nil {
				global.LOG.Errorf("save backup record failed, err: %v", err)
				return err
			}
			u.removeExpiredBackup(cronjob, accountMap, record)
			return nil
		}, nil, int(cronjob.RetryTimes), time.Duration(cronjob.Timeout)*time.Second)
	}
	return nil
}

func (u *CronjobService) handleDirectory(cronjob model.Cronjob, startTime time.Time, taskItem *task.Task) error {
	taskItem.AddSubTaskWithOps(task.GetTaskName(cronjob.SourceDir, task.TaskBackup, task.TaskScopeCronjob), func(task *task.Task) error {
		accountMap, err := NewBackupClientMap(strings.Split(cronjob.SourceAccountIDs, ","))
		if err != nil {
			return err
		}
		fileName := fmt.Sprintf("%s.tar.gz", startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(2))
		if cronjob.IsDir || len(strings.Split(cronjob.SourceDir, ",")) == 1 {
			fileName = loadFileName(cronjob.SourceDir)
		}
		backupDir := path.Join(global.Dir.LocalBackupDir, fmt.Sprintf("tmp/%s/%s", cronjob.Type, cronjob.Name))

		fileOp := files.NewFileOp()
		if cronjob.IsDir {
			taskItem.Logf("Dir: %s, Excludes: %s", cronjob.SourceDir, cronjob.ExclusionRules)
			if err := fileOp.TarGzCompressPro(true, cronjob.SourceDir, path.Join(backupDir, fileName), cronjob.Secret, cronjob.ExclusionRules); err != nil {
				return err
			}
		} else {
			taskItem.Logf("Files: %s", cronjob.SourceDir)
			fileLists := strings.Split(cronjob.SourceDir, ",")
			if err := fileOp.TarGzFilesWithCompressPro(fileLists, path.Join(backupDir, fileName), cronjob.Secret); err != nil {
				return err
			}
		}
		var record model.BackupRecord
		record.From = "cronjob"
		record.Type = "directory"
		record.CronjobID = cronjob.ID
		record.Name = cronjob.Name
		record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
		downloadPath, err := u.uploadCronjobBackFile(cronjob, task, accountMap, path.Join(backupDir, fileName))
		if err != nil {
			taskItem.LogFailedWithErr("Upload backup file", err)
			return err
		}
		record.FileDir = path.Dir(downloadPath)
		record.FileName = fileName
		if err := backupRepo.CreateRecord(&record); err != nil {
			taskItem.LogFailedWithErr("Save record", err)
			return err
		}
		u.removeExpiredBackup(cronjob, accountMap, record)
		return nil
	}, nil, int(cronjob.RetryTimes), time.Duration(cronjob.Timeout)*time.Second)
	return nil
}

func (u *CronjobService) handleSystemLog(cronjob model.Cronjob, startTime time.Time, taskItem *task.Task) error {
	taskItem.AddSubTaskWithOps(task.GetTaskName(i18n.GetMsgByKey("BackupSystemLog"), task.TaskBackup, task.TaskScopeCronjob), func(task *task.Task) error {
		accountMap, err := NewBackupClientMap(strings.Split(cronjob.SourceAccountIDs, ","))
		if err != nil {
			return err
		}
		nameItem := startTime.Format(constant.DateTimeSlimLayout) + common.RandStrAndNum(5)
		fileName := fmt.Sprintf("system_log_%s.tar.gz", nameItem)
		backupDir := path.Join(global.Dir.LocalBackupDir, "tmp/log", nameItem)
		if err := handleBackupLogs(taskItem, backupDir, fileName, cronjob.Secret); err != nil {
			return err
		}
		var record model.BackupRecord
		record.From = "cronjob"
		record.Type = "log"
		record.CronjobID = cronjob.ID
		record.Name = cronjob.Name
		record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
		downloadPath, err := u.uploadCronjobBackFile(cronjob, task, accountMap, path.Join(path.Dir(backupDir), fileName))
		if err != nil {
			taskItem.LogFailedWithErr("Upload backup file", err)
			return err
		}
		record.FileDir = path.Dir(downloadPath)
		record.FileName = fileName
		if err := backupRepo.CreateRecord(&record); err != nil {
			taskItem.LogFailedWithErr("Save record", err)
			return err
		}
		u.removeExpiredBackup(cronjob, accountMap, record)
		return nil
	}, nil, int(cronjob.RetryTimes), time.Duration(cronjob.Timeout)*time.Second)
	return nil
}

func (u *CronjobService) handleSnapshot(cronjob model.Cronjob, jobRecord model.JobRecords, taskItem *task.Task) error {
	accountMap, err := NewBackupClientMap(strings.Split(cronjob.SourceAccountIDs, ","))
	if err != nil {
		return err
	}

	var record model.BackupRecord
	record.From = "cronjob"
	record.Type = "snapshot"
	record.CronjobID = cronjob.ID
	record.Name = cronjob.Name
	record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
	record.FileDir = "system_snapshot"

	versionItem, _ := settingRepo.Get(settingRepo.WithByKey("SystemVersion"))
	scope := "core"
	if !global.IsMaster {
		scope = "agent"
	}

	itemData, err := loadSnapWithRule(cronjob)
	if err != nil {
		return err
	}
	req := dto.SnapshotCreate{
		Name:   fmt.Sprintf("snapshot-1panel-%s-%s-linux-%s-%s", scope, versionItem.Value, loadOs(), jobRecord.StartTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5)),
		Secret: cronjob.Secret,
		TaskID: jobRecord.TaskID,

		SourceAccountIDs:  record.SourceAccountIDs,
		DownloadAccountID: cronjob.DownloadAccountID,
		AppData:           itemData.AppData,
		PanelData:         itemData.PanelData,
		BackupData:        itemData.BackupData,
		WithDockerConf:    true,
		WithMonitorData:   true,
		WithLoginLog:      true,
		WithOperationLog:  true,
		WithSystemLog:     true,
		WithTaskLog:       true,
		IgnoreFiles:       strings.Split(cronjob.ExclusionRules, ","),
	}

	if err := NewISnapshotService().SnapshotCreate(taskItem, req, jobRecord.ID, cronjob.RetryTimes, cronjob.Timeout); err != nil {
		return err
	}
	record.FileName = req.Name + ".tar.gz"

	if err := backupRepo.CreateRecord(&record); err != nil {
		global.LOG.Errorf("save backup record failed, err: %v", err)
		return err
	}
	u.removeExpiredBackup(cronjob, accountMap, record)
	return nil
}

func loadAppsForJob(cronjob model.Cronjob) []model.AppInstall {
	var apps []model.AppInstall
	if cronjob.AppID == "all" {
		apps, _ = appInstallRepo.ListBy(context.Background())
	} else {
		appIds := strings.Split(cronjob.AppID, ",")
		var idItems []uint
		for i := 0; i < len(appIds); i++ {
			itemID, _ := strconv.Atoi(appIds[i])
			idItems = append(idItems, uint(itemID))
		}
		appItems, _ := appInstallRepo.ListBy(context.Background(), repo.WithByIDs(idItems))
		apps = appItems
	}
	return apps
}

type DatabaseHelper struct {
	ID       uint
	DBType   string
	Database string
	Name     string
}

func loadDbsForJob(cronjob model.Cronjob) []DatabaseHelper {
	var dbs []DatabaseHelper
	if cronjob.DBName == "all" {
		if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" || cronjob.DBType == "mysql-cluster" {
			databaseService := NewIDatabaseService()
			mysqlItems, _ := databaseService.LoadItems(cronjob.DBType)
			for _, mysql := range mysqlItems {
				dbs = append(dbs, DatabaseHelper{
					ID:       mysql.ID,
					DBType:   cronjob.DBType,
					Database: mysql.Database,
					Name:     mysql.Name,
				})
			}
		} else {
			pgItems, _ := postgresqlRepo.List()
			for _, pg := range pgItems {
				dbs = append(dbs, DatabaseHelper{
					ID:       pg.ID,
					DBType:   cronjob.DBType,
					Database: pg.PostgresqlName,
					Name:     pg.Name,
				})
			}
		}
		return dbs
	}
	dbNames := strings.Split(cronjob.DBName, ",")
	for _, name := range dbNames {
		itemID, _ := strconv.Atoi(name)
		if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" || cronjob.DBType == "mysql-cluster" {
			mysqlItem, _ := mysqlRepo.Get(repo.WithByID(uint(itemID)))
			dbs = append(dbs, DatabaseHelper{
				ID:       mysqlItem.ID,
				DBType:   cronjob.DBType,
				Database: mysqlItem.MysqlName,
				Name:     mysqlItem.Name,
			})
		} else {
			pgItem, _ := postgresqlRepo.Get(repo.WithByID(uint(itemID)))
			dbs = append(dbs, DatabaseHelper{
				ID:       pgItem.ID,
				DBType:   cronjob.DBType,
				Database: pgItem.PostgresqlName,
				Name:     pgItem.Name,
			})
		}
	}
	return dbs
}

func loadWebsForJob(cronjob model.Cronjob) []model.Website {
	var weblist []model.Website
	if cronjob.Website == "all" {
		weblist, _ = websiteRepo.List()
		return weblist
	}
	websites := strings.Split(cronjob.Website, ",")
	var idItems []uint
	for i := 0; i < len(websites); i++ {
		itemID, _ := strconv.Atoi(websites[i])
		idItems = append(idItems, uint(itemID))
	}
	weblist, _ = websiteRepo.GetBy(repo.WithByIDs(idItems))
	return weblist
}

func handleBackupLogs(taskItem *task.Task, targetDir, fileName string, secret string) error {
	fileOp := files.NewFileOp()
	websites, err := websiteRepo.List()
	if err != nil {
		return err
	}
	if len(websites) != 0 {
		webItem := GetOpenrestyDir(SitesRootDir)
		for _, website := range websites {
			taskItem.Logf("%s Website logs %s...", i18n.GetMsgByKey("TaskBackup"), website.Alias)
			dirItem := path.Join(targetDir, "website", website.Alias)
			if _, err := os.Stat(dirItem); err != nil && os.IsNotExist(err) {
				if err = os.MkdirAll(dirItem, os.ModePerm); err != nil {
					return err
				}
			}
			itemDir := path.Join(webItem, website.Alias, "log")
			logFiles, _ := os.ReadDir(itemDir)
			if len(logFiles) != 0 {
				for i := 0; i < len(logFiles); i++ {
					if !logFiles[i].IsDir() {
						_ = fileOp.CopyFile(path.Join(itemDir, logFiles[i].Name()), dirItem)
					}
				}
			}
			itemDir2 := path.Join(global.Dir.LocalBackupDir, "tmp/log/website", website.Alias)
			logFiles2, _ := os.ReadDir(itemDir2)
			if len(logFiles2) != 0 {
				for i := 0; i < len(logFiles2); i++ {
					if !logFiles2[i].IsDir() {
						_ = fileOp.CopyFile(path.Join(itemDir2, logFiles2[i].Name()), dirItem)
					}
				}
			}
		}
	}

	systemDir := path.Join(targetDir, "system")
	if _, err := os.Stat(systemDir); err != nil && os.IsNotExist(err) {
		if err = os.MkdirAll(systemDir, os.ModePerm); err != nil {
			return err
		}
	}

	taskItem.Logf("%s System logs...", i18n.GetMsgByKey("TaskBackup"))
	systemLogFiles, _ := os.ReadDir(global.Dir.LogDir)
	if len(systemLogFiles) != 0 {
		for i := 0; i < len(systemLogFiles); i++ {
			if !systemLogFiles[i].IsDir() {
				_ = fileOp.CopyFile(path.Join(global.Dir.LogDir, systemLogFiles[i].Name()), systemDir)
			}
		}
	}

	taskItem.Logf("%s SSH logs...", i18n.GetMsgByKey("TaskBackup"))
	loginLogFiles, _ := os.ReadDir("/var/log")
	loginDir := path.Join(targetDir, "login")
	if _, err := os.Stat(loginDir); err != nil && os.IsNotExist(err) {
		if err = os.MkdirAll(loginDir, os.ModePerm); err != nil {
			return err
		}
	}
	if len(loginLogFiles) != 0 {
		for i := 0; i < len(loginLogFiles); i++ {
			if !loginLogFiles[i].IsDir() && (strings.HasPrefix(loginLogFiles[i].Name(), "secure") || strings.HasPrefix(loginLogFiles[i].Name(), "auth.log")) {
				_ = fileOp.CopyFile(path.Join("/var/log", loginLogFiles[i].Name()), loginDir)
			}
		}
	}
	taskItem.Log("backup ssh log successful!")

	if err := fileOp.TarGzCompressPro(true, targetDir, path.Join(path.Dir(targetDir), fileName), secret, ""); err != nil {
		return err
	}
	defer func() {
		_ = os.RemoveAll(targetDir)
	}()
	return nil
}

func loadSnapWithRule(cronjob model.Cronjob) (dto.SnapshotData, error) {
	itemData, err := NewISnapshotService().LoadSnapshotData()
	if err != nil {
		return itemData, err
	}

	if len(cronjob.SnapshotRule) == 0 {
		return itemData, nil
	}
	var snapRule dto.SnapshotRule
	if err := json.Unmarshal([]byte(cronjob.SnapshotRule), &snapRule); err != nil {
		return itemData, err
	}
	if len(snapRule.IgnoreAppIDs) == 0 && !snapRule.WithImage {
		return itemData, nil
	}

	var ignoreApps []model.AppInstall
	if len(snapRule.IgnoreAppIDs) != 0 {
		ignoreApps, _ = appInstallRepo.ListBy(context.Background(), repo.WithByIDs(snapRule.IgnoreAppIDs))
	}
	if len(ignoreApps) == 0 && !snapRule.WithImage {
		return itemData, nil
	}
	for i := 0; i < len(itemData.AppData); i++ {
		isIgnore := false
		for _, ignore := range ignoreApps {
			if ignore.App.Key == itemData.AppData[i].Key && ignore.Name == itemData.AppData[i].Name {
				isIgnore = true
				itemData.AppData[i].IsCheck = false
				for j := 0; j < len(itemData.AppData[i].Children); j++ {
					if itemData.AppData[i].Children[j].Label == "appData" {
						itemData.AppData[i].Children[j].IsCheck = false
					}
				}
				break
			}
		}
		if snapRule.WithImage && !isIgnore {
			for j := 0; j < len(itemData.AppData[i].Children); j++ {
				if itemData.AppData[i].Children[j].Label == "appImage" {
					itemData.AppData[i].Children[j].IsCheck = true
				}
			}
		}
	}
	return itemData, nil
}

func loadFileName(src string) string {
	dirs := strings.Split(filepath.ToSlash(src), "/")
	var keyPart string
	if len(dirs) >= 3 {
		keyPart = filepath.Join(dirs[len(dirs)-3], dirs[len(dirs)-2], dirs[len(dirs)-1])
	}
	cleanName := strings.ReplaceAll(keyPart, string(filepath.Separator), "_")
	timestamp := time.Now().Format(constant.DateTimeSlimLayout)
	return fmt.Sprintf("%s_%s_%s.tar.gz", cleanName, timestamp, common.RandStrAndNum(2))
}
