// SiYuan - Refactor your thinking
// Copyright (c) 2020-present, b3log.org
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

package api

import (
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"
	"unicode/utf8"

	"github.com/88250/gulu"
	"github.com/gin-gonic/gin"
	"github.com/siyuan-note/logging"
	"github.com/siyuan-note/siyuan/kernel/model"
	"github.com/siyuan-note/siyuan/kernel/util"
)

func checkWorkspaceDir(c *gin.Context) {
	ret := gulu.Ret.NewResult()
	defer c.JSON(http.StatusOK, ret)

	arg, ok := util.JsonArg(c, ret)
	if !ok {
		return
	}

	path := arg["path"].(string)
	if isInvalidWorkspacePath(path) {
		ret.Code = -1
		ret.Msg = "This workspace name is not allowed, please use another name"
		return
	}

	if !gulu.File.IsExist(path) {
		ret.Code = -1
		ret.Msg = "This workspace does not exist"
		return
	}

	entries, err := os.ReadDir(path)
	if err != nil {
		ret.Code = -1
		ret.Msg = fmt.Sprintf("read workspace dir [%s] failed: %s", path, err)
	}

	var existsConf, existsData bool
	for _, entry := range entries {
		if !existsConf {
			existsConf = "conf" == entry.Name() && entry.IsDir()
		}
		if !existsData {
			existsData = "data" == entry.Name() && entry.IsDir()
		}

		if existsConf && existsData {
			break
		}
	}

	if existsConf {
		existsConf = gulu.File.IsExist(filepath.Join(path, "conf", "conf.json"))
	}

	ret.Data = map[string]interface{}{
		"isWorkspace": existsConf && existsData,
	}
}

func createWorkspaceDir(c *gin.Context) {
	ret := gulu.Ret.NewResult()
	defer c.JSON(http.StatusOK, ret)

	arg, ok := util.JsonArg(c, ret)
	if !ok {
		return
	}

	absPath := arg["path"].(string)
	absPath = util.RemoveInvalid(absPath)
	absPath = strings.TrimSpace(absPath)
	if isInvalidWorkspacePath(absPath) {
		ret.Code = -1
		ret.Msg = "This workspace name is not allowed, please use another name"
		return
	}

	if !gulu.File.IsExist(absPath) {
		if err := os.MkdirAll(absPath, 0755); err != nil {
			ret.Code = -1
			ret.Msg = fmt.Sprintf("create workspace dir [%s] failed: %s", absPath, err)
			return
		}
	}

	workspacePaths, err := util.ReadWorkspacePaths()
	if err != nil {
		ret.Code = -1
		ret.Msg = err.Error()
		return
	}

	workspacePaths = append(workspacePaths, absPath)

	if err = util.WriteWorkspacePaths(workspacePaths); err != nil {
		ret.Code = -1
		ret.Msg = err.Error()
		return
	}
}

func removeWorkspaceDir(c *gin.Context) {
	ret := gulu.Ret.NewResult()
	defer c.JSON(http.StatusOK, ret)

	arg, ok := util.JsonArg(c, ret)
	if !ok {
		return
	}

	path := arg["path"].(string)

	if util.IsWorkspaceLocked(path) || util.WorkspaceDir == path {
		msg := "Cannot remove current workspace"
		ret.Code = -1
		ret.Msg = msg
		ret.Data = map[string]interface{}{"closeTimeout": 3000}
		return
	}

	workspacePaths, err := util.ReadWorkspacePaths()
	if err != nil {
		ret.Code = -1
		ret.Msg = err.Error()
		return
	}

	workspacePaths = gulu.Str.RemoveElem(workspacePaths, path)

	if err = util.WriteWorkspacePaths(workspacePaths); err != nil {
		ret.Code = -1
		ret.Msg = err.Error()
		return
	}
}

func removeWorkspaceDirPhysically(c *gin.Context) {
	ret := gulu.Ret.NewResult()
	defer c.JSON(http.StatusOK, ret)

	arg, ok := util.JsonArg(c, ret)
	if !ok {
		return
	}

	path := arg["path"].(string)
	if gulu.File.IsDir(path) {
		err := os.RemoveAll(path)
		if err != nil {
			ret.Code = -1
			ret.Msg = err.Error()
			return
		}
	}

	logging.LogInfof("removed workspace [%s] physically", path)
	if util.WorkspaceDir == path {
		os.Exit(logging.ExitCodeOk)
	}
}

type Workspace struct {
	Path   string `json:"path"`
	Closed bool   `json:"closed"`
}

func getMobileWorkspaces(c *gin.Context) {
	ret := gulu.Ret.NewResult()
	defer c.JSON(http.StatusOK, ret)

	if util.ContainerIOS != util.Container && util.ContainerAndroid != util.Container && util.ContainerHarmony != util.Container {
		return
	}

	root := filepath.Dir(util.WorkspaceDir)
	dirs, err := os.ReadDir(root)
	if err != nil {
		logging.LogErrorf("read dir [%s] failed: %s", root, err)
		ret.Code = -1
		ret.Msg = err.Error()
		return
	}

	ret.Data = []string{}
	var paths []string
	for _, dir := range dirs {
		if dir.IsDir() {
			absPath := filepath.Join(root, dir.Name())
			if isInvalidWorkspacePath(absPath) {
				continue
			}

			paths = append(paths, absPath)
		}
	}
	ret.Data = paths
}

func getWorkspaces(c *gin.Context) {
	ret := gulu.Ret.NewResult()
	defer c.JSON(http.StatusOK, ret)

	workspacePaths, err := util.ReadWorkspacePaths()
	if err != nil {
		ret.Code = -1
		ret.Msg = err.Error()
		return
	}

	if role := model.GetGinContextRole(c); !model.IsValidRole(role, []model.Role{
		model.RoleAdministrator,
	}) {
		ret.Data = []*Workspace{}
		return
	}

	var workspaces, openedWorkspaces, closedWorkspaces []*Workspace
	for _, p := range workspacePaths {
		closed := !util.IsWorkspaceLocked(p)
		if closed {
			closedWorkspaces = append(closedWorkspaces, &Workspace{Path: p, Closed: closed})
		} else {
			openedWorkspaces = append(openedWorkspaces, &Workspace{Path: p, Closed: closed})
		}
	}
	sort.Slice(openedWorkspaces, func(i, j int) bool {
		return util.NaturalCompare(filepath.Base(openedWorkspaces[i].Path), filepath.Base(openedWorkspaces[j].Path))
	})
	sort.Slice(closedWorkspaces, func(i, j int) bool {
		return util.NaturalCompare(filepath.Base(closedWorkspaces[i].Path), filepath.Base(closedWorkspaces[j].Path))
	})
	workspaces = append(workspaces, openedWorkspaces...)
	workspaces = append(workspaces, closedWorkspaces...)
	ret.Data = workspaces
}

func setWorkspaceDir(c *gin.Context) {
	ret := gulu.Ret.NewResult()
	defer c.JSON(http.StatusOK, ret)

	arg, ok := util.JsonArg(c, ret)
	if !ok {
		return
	}

	path := arg["path"].(string)
	if util.WorkspaceDir == path {
		ret.Code = -1
		ret.Msg = model.Conf.Language(78)
		ret.Data = map[string]interface{}{"closeTimeout": 3000}
		return
	}

	if util.IsCloudDrivePath(path) {
		ret.Code = -1
		ret.Msg = model.Conf.Language(196)
		ret.Data = map[string]interface{}{"closeTimeout": 7000}
		return
	}

	if gulu.OS.IsWindows() {
		// 改进判断工作空间路径实现 https://github.com/siyuan-note/siyuan/issues/7569
		installDirLower := strings.ToLower(filepath.Dir(util.WorkingDir))
		pathLower := strings.ToLower(path)
		if strings.HasPrefix(pathLower, installDirLower) && util.IsSubPath(installDirLower, pathLower) {
			ret.Code = -1
			ret.Msg = model.Conf.Language(98)
			ret.Data = map[string]interface{}{"closeTimeout": 5000}
			return
		}
	}

	workspacePaths, err := util.ReadWorkspacePaths()
	if err != nil {
		ret.Code = -1
		ret.Msg = err.Error()
		return
	}

	workspacePaths = append(workspacePaths, path)
	workspacePaths = gulu.Str.RemoveDuplicatedElem(workspacePaths)
	workspacePaths = gulu.Str.RemoveElem(workspacePaths, path)
	workspacePaths = append(workspacePaths, path) // 切换的工作空间固定放在最后一个

	if err = util.WriteWorkspacePaths(workspacePaths); err != nil {
		ret.Code = -1
		ret.Msg = err.Error()
		return
	}

	if util.ContainerAndroid == util.Container || util.ContainerIOS == util.Container || util.ContainerHarmony == util.Container {
		util.PushMsg(model.Conf.Language(42), 1000*15)
		time.Sleep(time.Second * 1)
		model.Close(false, false, 1)
		time.Sleep(time.Second * 1)
	}
}

func isInvalidWorkspacePath(absPath string) bool {
	if "" == absPath {
		return true
	}
	name := filepath.Base(absPath)
	if "" == name {
		return true
	}
	if strings.HasPrefix(name, ".") {
		return true
	}
	if !gulu.File.IsValidFilename(name) {
		return true
	}
	if 32 < utf8.RuneCountInString(name) {
		// Adjust workspace name length limit to 32 runes https://github.com/siyuan-note/siyuan/issues/9440
		return true
	}
	toLower := strings.ToLower(name)
	return gulu.Str.Contains(toLower, []string{"conf", "home", "data", "temp"})
}
