package service

import (
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"path"
	"reflect"
	"strconv"
	"strings"

	"github.com/1Panel-dev/1Panel/backend/app/repo"

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

type AppInstallService struct {
}

func (a AppInstallService) Page(req dto.AppInstalledRequest) (int64, []dto.AppInstalled, error) {
	var opts []repo.DBOption

	if req.Name != "" {
		opts = append(opts, commonRepo.WithLikeName(req.Name))
	}

	total, installs, err := appInstallRepo.Page(req.Page, req.PageSize, opts...)
	if err != nil {
		return 0, nil, err
	}

	installDTOs, err := handleInstalled(installs)
	if err != nil {
		return 0, nil, err
	}

	return total, installDTOs, nil
}

func (a AppInstallService) CheckExist(key string) (*dto.CheckInstalled, error) {

	res := &dto.CheckInstalled{
		IsExist: false,
	}
	app, err := appRepo.GetFirst(appRepo.WithKey(key))
	if err != nil {
		return res, nil
	}
	res.App = app.Name
	appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithAppId(app.ID))
	if reflect.DeepEqual(appInstall, model.AppInstall{}) {
		return res, nil
	}
	if err := syncById(appInstall.ID); err != nil {
		return nil, err
	}

	res.ContainerName = appInstall.ContainerName
	res.Name = appInstall.Name
	res.Version = appInstall.Version
	res.CreatedAt = appInstall.CreatedAt
	res.Status = appInstall.Status
	res.AppInstallID = appInstall.ID
	res.IsExist = true
	if len(appInstall.Backups) > 0 {
		res.LastBackupAt = appInstall.Backups[0].CreatedAt.Format("2006-01-02 15:04:05")
	}

	return res, nil
}

func (a AppInstallService) LoadPort(key string) (int64, error) {
	app, err := appInstallRepo.LoadBaseInfoByKey(key)
	if err != nil {
		return int64(0), nil
	}
	return app.Port, nil
}

func (a AppInstallService) Search(req dto.AppInstalledRequest) ([]dto.AppInstalled, error) {
	var installs []model.AppInstall
	var err error
	if req.Type != "" {
		apps, err := appRepo.GetBy(appRepo.WithType(req.Type))
		if err != nil {
			return nil, err
		}
		var ids []uint
		for _, app := range apps {
			ids = append(ids, app.ID)
		}
		installs, err = appInstallRepo.GetBy(appInstallRepo.WithAppIdsIn(ids))
		if err != nil {
			return nil, err
		}
	} else {
		installs, err = appInstallRepo.GetBy()
		if err != nil {
			return nil, err
		}
	}

	return handleInstalled(installs)
}

func (a AppInstallService) Operate(req dto.AppInstallOperate) error {
	install, err := appInstallRepo.GetFirst(commonRepo.WithByID(req.InstallId))
	if err != nil {
		return err
	}

	dockerComposePath := install.GetComposePath()

	switch req.Operate {
	case dto.Up:
		out, err := compose.Up(dockerComposePath)
		if err != nil {
			return handleErr(install, err, out)
		}
		install.Status = constant.Running
	case dto.Down:
		out, err := compose.Stop(dockerComposePath)
		if err != nil {
			return handleErr(install, err, out)
		}
		install.Status = constant.Stopped
	case dto.Restart:
		out, err := compose.Restart(dockerComposePath)
		if err != nil {
			return handleErr(install, err, out)
		}
		install.Status = constant.Running
	case dto.Delete:
		tx, ctx := getTxAndContext()
		if err := deleteAppInstall(ctx, install); err != nil {
			tx.Rollback()
			return err
		}
		tx.Commit()
		return nil
	case dto.Sync:
		return syncById(install.ID)
	case dto.Backup:
		tx, ctx := getTxAndContext()
		if err := backupInstall(ctx, install); err != nil {
			tx.Rollback()
			return err
		}
		tx.Commit()
		return nil
	case dto.Restore:
		return restoreInstall(install, req.BackupId)
	case dto.Update:
		return updateInstall(install.ID, req.DetailId)
	default:
		return errors.New("operate not support")
	}

	return appInstallRepo.Save(&install)
}

func (a AppInstallService) SyncAll() error {
	allList, err := appInstallRepo.GetBy()
	if err != nil {
		return err
	}
	go func() {
		for _, i := range allList {
			if err := syncById(i.ID); err != nil {
				global.LOG.Errorf("sync install app[%s] error,mgs: %s", i.Name, err.Error())
			}
		}
	}()
	return nil
}

func (a AppInstallService) PageInstallBackups(req dto.AppBackupRequest) (int64, []model.AppInstallBackup, error) {
	return appInstallBackupRepo.Page(req.Page, req.PageSize, appInstallBackupRepo.WithAppInstallID(req.AppInstallID))
}

func (a AppInstallService) DeleteBackup(req dto.AppBackupDeleteRequest) error {

	backups, err := appInstallBackupRepo.GetBy(commonRepo.WithIdsIn(req.Ids))
	if err != nil {
		return err
	}
	fileOp := files.NewFileOp()

	var errStr strings.Builder
	for _, backup := range backups {
		dst := path.Join(backup.Path, backup.Name)
		if err := fileOp.DeleteFile(dst); err != nil {
			errStr.WriteString(err.Error())
			continue
		}
		if err := appInstallBackupRepo.Delete(context.TODO(), commonRepo.WithIdsIn(req.Ids)); err != nil {
			errStr.WriteString(err.Error())
		}
	}
	if errStr.String() != "" {
		return errors.New(errStr.String())
	}
	return nil
}

func (a AppInstallService) GetServices(key string) ([]dto.AppService, error) {
	app, err := appRepo.GetFirst(appRepo.WithKey(key))
	if err != nil {
		return nil, err
	}
	installs, err := appInstallRepo.GetBy(appInstallRepo.WithAppId(app.ID), appInstallRepo.WithStatus(constant.Running))
	if err != nil {
		return nil, err
	}
	var res []dto.AppService
	for _, install := range installs {
		paramMap := make(map[string]string)
		if install.Param != "" {
			_ = json.Unmarshal([]byte(install.Param), &paramMap)
		}
		res = append(res, dto.AppService{
			Label:  install.Name,
			Value:  install.ServiceName,
			Config: paramMap,
		})
	}
	return res, nil
}

func (a AppInstallService) GetUpdateVersions(installId uint) ([]dto.AppVersion, error) {
	install, err := appInstallRepo.GetFirst(commonRepo.WithByID(installId))
	var versions []dto.AppVersion
	if err != nil {
		return versions, err
	}
	app, err := appRepo.GetFirst(commonRepo.WithByID(install.AppId))
	if err != nil {
		return versions, err
	}
	details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(app.ID))
	if err != nil {
		return versions, err
	}
	for _, detail := range details {
		if common.CompareVersion(detail.Version, install.Version) {
			versions = append(versions, dto.AppVersion{
				Version:  detail.Version,
				DetailId: detail.ID,
			})
		}
	}
	return versions, nil
}

func (a AppInstallService) ChangeAppPort(req dto.PortUpdate) error {
	var (
		files    []string
		newFiles []string
	)
	app, err := appInstallRepo.LoadBaseInfoByKey(req.Key)
	if err != nil {
		return err
	}

	ComposeDir := fmt.Sprintf("%s/%s/%s", constant.AppInstallDir, req.Key, req.Name)
	ComposeFile := fmt.Sprintf("%s/%s/%s/docker-compose.yml", constant.AppInstallDir, req.Key, req.Name)
	path := fmt.Sprintf("%s/.env", ComposeDir)
	lineBytes, err := ioutil.ReadFile(path)
	if err != nil {
		return err
	} else {
		files = strings.Split(string(lineBytes), "\n")
	}
	for _, line := range files {
		if strings.HasPrefix(line, "PANEL_APP_PORT_HTTP=") {
			newFiles = append(newFiles, fmt.Sprintf("PANEL_APP_PORT_HTTP=%v", req.Port))
		} else {
			newFiles = append(newFiles, line)
		}
	}
	file, err := os.OpenFile(path, os.O_WRONLY, 0666)
	if err != nil {
		return err
	}
	defer file.Close()
	_, err = file.WriteString(strings.Join(newFiles, "\n"))
	if err != nil {
		return err
	}

	if err := mysqlRepo.UpdateDatabaseInfo(app.ID, map[string]interface{}{
		"env": strings.ReplaceAll(app.Env, strconv.FormatInt(app.Port, 10), strconv.FormatInt(req.Port, 10)),
	}); err != nil {
		return err
	}
	stdout, err := compose.Down(ComposeFile)
	if err != nil {
		return errors.New(stdout)
	}
	stdout, err = compose.Up(ComposeFile)
	if err != nil {
		return errors.New(stdout)
	}
	return nil
}

func (a AppInstallService) DeleteCheck(installId uint) ([]dto.AppResource, error) {
	var res []dto.AppResource
	appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(installId))
	if err != nil {
		return nil, err
	}
	app, err := appRepo.GetFirst(commonRepo.WithByID(appInstall.AppId))
	if err != nil {
		return nil, err
	}
	if app.Type == "website" {
		websites, _ := websiteRepo.GetBy(websiteRepo.WithAppInstallId(appInstall.ID))
		for _, website := range websites {
			res = append(res, dto.AppResource{
				Type: "website",
				Name: website.PrimaryDomain,
			})
		}
	}
	if app.Key == "nginx" {
		websites, _ := websiteRepo.GetBy()
		for _, website := range websites {
			res = append(res, dto.AppResource{
				Type: "website",
				Name: website.PrimaryDomain,
			})
		}
	}
	if app.Type == "runtime" {
		resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(appInstall.ID))
		for _, resource := range resources {
			linkInstall, _ := appInstallRepo.GetFirst(commonRepo.WithByID(resource.AppInstallId))
			res = append(res, dto.AppResource{
				Type: "app",
				Name: linkInstall.Name,
			})
		}
	}

	if app.Key == "mysql" {
		databases, _ := mysqlRepo.List()
		for _, database := range databases {
			res = append(res, dto.AppResource{
				Type: "database",
				Name: database.Name,
			})
		}
	}

	return res, nil
}

func syncById(installId uint) error {
	appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(installId))
	if err != nil {
		return err
	}

	containerNames, err := getContainerNames(appInstall)
	if err != nil {
		return err
	}

	cli, err := docker.NewClient()
	if err != nil {
		return err
	}
	containers, err := cli.ListContainersByName(containerNames)
	if err != nil {
		return err
	}
	var (
		errorContainers    []string
		notFoundContainers []string
		runningContainers  []string
		exitedContainers   []string
	)

	for _, n := range containers {
		switch n.State {
		case "exited":
			exitedContainers = append(exitedContainers, n.Names[0])
		case "running":
			runningContainers = append(runningContainers, n.Names[0])
		default:
			errorContainers = append(errorContainers, n.Names[0])
		}
	}
	for _, old := range containerNames {
		exist := false
		for _, new := range containers {
			if common.ExistWithStrArray(old, new.Names) {
				exist = true
				break
			}
		}
		if !exist {
			notFoundContainers = append(notFoundContainers, old)
		}
	}

	containerCount := len(containers)
	errCount := len(errorContainers)
	notFoundCount := len(notFoundContainers)
	existedCount := len(exitedContainers)
	normalCount := len(containerNames)
	runningCount := len(runningContainers)

	if containerCount == 0 {
		appInstall.Status = constant.Error
		appInstall.Message = "container is not found"
		return appInstallRepo.Save(&appInstall)
	}
	if errCount == 0 && existedCount == 0 {
		appInstall.Status = constant.Running
		return appInstallRepo.Save(&appInstall)
	}
	if existedCount == normalCount {
		appInstall.Status = constant.Stopped
		return appInstallRepo.Save(&appInstall)
	}
	if errCount == normalCount {
		appInstall.Status = constant.Error
	}
	if runningCount < normalCount {
		appInstall.Status = constant.UnHealthy
	}

	var errMsg strings.Builder
	if errCount > 0 {
		errMsg.Write([]byte(string(rune(errCount)) + " error containers:"))
		for _, e := range errorContainers {
			errMsg.Write([]byte(e))
		}
		errMsg.Write([]byte("\n"))
	}
	if notFoundCount > 0 {
		errMsg.Write([]byte(string(rune(notFoundCount)) + " not found containers:"))
		for _, e := range notFoundContainers {
			errMsg.Write([]byte(e))
		}
		errMsg.Write([]byte("\n"))
	}
	appInstall.Message = errMsg.String()
	return appInstallRepo.Save(&appInstall)
}