// 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 util

import (
	"bytes"
	"fmt"
	"io"
	"io/fs"
	"math/rand"
	"os"
	"path/filepath"
	"runtime"
	"runtime/debug"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/88250/gulu"
	"github.com/denisbrodbeck/machineid"
	"github.com/go-ole/go-ole"
	"github.com/go-ole/go-ole/oleutil"
	"github.com/jaypipes/ghw"
	"github.com/siyuan-note/httpclient"
	"github.com/siyuan-note/logging"
)

var DisabledFeatures []string

func DisableFeature(feature string) {
	DisabledFeatures = append(DisabledFeatures, feature)
	DisabledFeatures = gulu.Str.RemoveDuplicatedElem(DisabledFeatures)
}

var (
	UseSingleLineSave    = true // UseSingleLineSave 是否使用单行保存 .sy 和数据库 .json 文件。
	LargeFileWarningSize = 8    // LargeFileWarningSize 大文件警告大小，单位：MB
)

func ExceedLargeFileWarningSize(fileSize int) bool {
	return fileSize > LargeFileWarningSize*1024*1024
}

// IsUILoaded 是否已经加载了 UI。
var IsUILoaded = false

func WaitForUILoaded() {
	start := time.Now()
	for !IsUILoaded {
		time.Sleep(200 * time.Millisecond)
		if time.Since(start) > 30*time.Second {
			logging.LogErrorf("wait for ui loaded timeout: %s", logging.ShortStack())
			break
		}
	}
}

func HookUILoaded() {
	for !IsUILoaded {
		if 0 < len(SessionsByType("main")) {
			IsUILoaded = true
			return
		}
		time.Sleep(200 * time.Millisecond)
	}
}

// IsExiting 是否正在退出程序。
var IsExiting = atomic.Bool{}

// MobileOSVer 移动端操作系统版本。
var MobileOSVer string

// DatabaseVer 数据库版本。修改表结构的话需要修改这里。
const DatabaseVer = "20220501"

func logBootInfo() {
	plat := GetOSPlatform()
	logging.LogInfof("kernel is booting:\n"+
		"    * ver [%s]\n"+
		"    * arch [%s]\n"+
		"    * os [%s]\n"+
		"    * pid [%d]\n"+
		"    * runtime mode [%s]\n"+
		"    * working directory [%s]\n"+
		"    * read only [%v]\n"+
		"    * container [%s]\n"+
		"    * database [ver=%s]\n"+
		"    * workspace directory [%s]",
		Ver, runtime.GOARCH, plat, os.Getpid(), Mode, WorkingDir, ReadOnly, Container, DatabaseVer, WorkspaceDir)
	if 0 < len(DisabledFeatures) {
		logging.LogInfof("disabled features [%s]", strings.Join(DisabledFeatures, ", "))
	}

	go func() {
		driveType := getWorkspaceDriveType()
		if "" == driveType {
			return
		}

		if ghw.DriveTypeSSD.String() != driveType {
			logging.LogWarnf("workspace dir [%s] is not in SSD drive, performance may be affected", WorkspaceDir)
			WaitForUILoaded()
			time.Sleep(3 * time.Second)
			PushErrMsg(Langs[Lang][278], 15000)
		}
	}()
}

func getWorkspaceDriveType() string {
	if gulu.OS.IsDarwin() {
		return ghw.DriveTypeSSD.String()
	}

	if ContainerAndroid == Container || ContainerIOS == Container || ContainerHarmony == Container {
		return ghw.DriveTypeSSD.String()
	}

	if gulu.OS.IsWindows() {
		block, err := ghw.Block()
		if err != nil {
			logging.LogWarnf("get block storage info failed: %s", err)
			return ""
		}

		part := filepath.VolumeName(WorkspaceDir)
		for _, disk := range block.Disks {
			for _, partition := range disk.Partitions {
				if partition.MountPoint == part {
					return partition.Disk.DriveType.String()
				}
			}
		}
	} else if gulu.OS.IsLinux() {
		block, err := ghw.Block()
		if err != nil {
			logging.LogWarnf("get block storage info failed: %s", err)
			return ""
		}

		for _, disk := range block.Disks {
			for _, partition := range disk.Partitions {
				if strings.HasPrefix(WorkspaceDir, partition.MountPoint) {
					return partition.Disk.DriveType.String()
				}
			}
		}
	}
	return ""
}

func RandomSleep(minMills, maxMills int) {
	r := gulu.Rand.Int(minMills, maxMills)
	time.Sleep(time.Duration(r) * time.Millisecond)
}

func GetDeviceID() string {
	if ContainerStd == Container {
		machineID, err := machineid.ID()
		if err != nil {
			return gulu.Rand.String(12)
		}
		return machineID
	}
	return gulu.Rand.String(12)
}

func GetDeviceName() string {
	ret, err := os.Hostname()
	if err != nil {
		return "unknown"
	}
	return ret
}

func SetNetworkProxy(proxyURL string) {
	if err := os.Setenv("HTTPS_PROXY", proxyURL); err != nil {
		logging.LogErrorf("set env [HTTPS_PROXY] failed: %s", err)
	}
	if err := os.Setenv("HTTP_PROXY", proxyURL); err != nil {
		logging.LogErrorf("set env [HTTP_PROXY] failed: %s", err)
	}

	if "" != proxyURL {
		logging.LogInfof("use network proxy [%s]", proxyURL)
	} else {
		logging.LogInfof("use network proxy [system]")
	}

	httpclient.CloseIdleConnections()
}

const (
	// SQLFlushInterval 为数据库事务队列写入间隔。
	SQLFlushInterval = 3000 * time.Millisecond
)

var (
	Langs           = map[string]map[int]string{}
	TimeLangs       = map[string]map[string]interface{}{}
	TaskActionLangs = map[string]map[string]interface{}{}
	TrayMenuLangs   = map[string]map[string]interface{}{}
	AttrViewLangs   = map[string]map[string]interface{}{}
)

var (
	thirdPartySyncCheckTicker = time.NewTicker(time.Minute * 10)
)

func ReportFileSysFatalError(err error) {
	stack := debug.Stack()
	output := string(stack)
	if 5 < strings.Count(output, "\n") {
		lines := strings.Split(output, "\n")
		output = strings.Join(lines[5:], "\n")
	}
	logging.LogErrorf("check file system status failed: %s, %s", err, output)
	os.Exit(logging.ExitCodeFileSysErr)
}

var checkFileSysStatusLock = sync.Mutex{}

func CheckFileSysStatus() {
	if ContainerStd != Container {
		return
	}

	for {
		<-thirdPartySyncCheckTicker.C
		checkFileSysStatus()
	}
}

func checkFileSysStatus() {
	defer logging.Recover()

	if !checkFileSysStatusLock.TryLock() {
		logging.LogWarnf("check file system status is locked, skip")
		return
	}
	defer checkFileSysStatusLock.Unlock()

	const fileSysStatusCheckFile = ".siyuan/filesys_status_check"
	if IsCloudDrivePath(WorkspaceDir) {
		ReportFileSysFatalError(fmt.Errorf("workspace dir [%s] is in third party sync dir", WorkspaceDir))
		return
	}

	dir := filepath.Join(DataDir, fileSysStatusCheckFile)
	if err := os.RemoveAll(dir); err != nil {
		ReportFileSysFatalError(err)
		return
	}

	if err := os.MkdirAll(dir, 0755); err != nil {
		ReportFileSysFatalError(err)
		return
	}

	for i := 0; i < 7; i++ {
		tmp := filepath.Join(dir, "check_consistency")
		data := make([]byte, 1024*4)
		_, err := rand.Read(data)
		if err != nil {
			ReportFileSysFatalError(err)
			return
		}

		if err = os.WriteFile(tmp, data, 0644); err != nil {
			ReportFileSysFatalError(err)
			return
		}

		time.Sleep(5 * time.Second)

		for j := 0; j < 32; j++ {
			renamed := tmp + "_renamed"
			if err = os.Rename(tmp, renamed); err != nil {
				ReportFileSysFatalError(err)
				break
			}

			RandomSleep(500, 1000)

			f, err := os.Open(renamed)
			if err != nil {
				ReportFileSysFatalError(err)
				return
			}

			if err = f.Close(); err != nil {
				ReportFileSysFatalError(err)
				return
			}

			if err = os.Rename(renamed, tmp); err != nil {
				ReportFileSysFatalError(err)
				return
			}

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

			checkFilenames := bytes.Buffer{}
			for _, entry := range entries {
				if !entry.IsDir() && strings.Contains(entry.Name(), "check_") {
					checkFilenames.WriteString(entry.Name())
					checkFilenames.WriteString("\n")
				}
			}
			lines := strings.Split(strings.TrimSpace(checkFilenames.String()), "\n")
			if 1 < len(lines) {
				buf := bytes.Buffer{}
				for _, line := range lines {
					buf.WriteString("  ")
					buf.WriteString(line)
					buf.WriteString("\n")
				}
				output := buf.String()
				ReportFileSysFatalError(fmt.Errorf("dir [%s] has more than 1 file:\n%s", dir, output))
				return
			}
		}

		if err = os.RemoveAll(tmp); err != nil {
			ReportFileSysFatalError(err)
			return
		}
	}
}

func IsCloudDrivePath(workspaceAbsPath string) bool {
	if isICloudPath(workspaceAbsPath) {
		return true
	}

	if isKnownCloudDrivePath(workspaceAbsPath) {
		return true
	}

	if existAvailabilityStatus(workspaceAbsPath) {
		return true
	}

	return false
}

func isKnownCloudDrivePath(workspaceAbsPath string) bool {
	workspaceAbsPathLower := strings.ToLower(workspaceAbsPath)
	return strings.Contains(workspaceAbsPathLower, "onedrive") || strings.Contains(workspaceAbsPathLower, "dropbox") ||
		strings.Contains(workspaceAbsPathLower, "google drive") || strings.Contains(workspaceAbsPathLower, "pcloud") ||
		strings.Contains(workspaceAbsPathLower, "坚果云") ||
		strings.Contains(workspaceAbsPathLower, "天翼云")
}

func isICloudPath(workspaceAbsPath string) (ret bool) {
	if !gulu.OS.IsDarwin() {
		return false
	}

	workspaceAbsPathLower := strings.ToLower(workspaceAbsPath)

	// macOS 端对工作空间放置在 iCloud 路径下做检查 https://github.com/siyuan-note/siyuan/issues/7747
	iCloudRoot := filepath.Join(HomeDir, "Library", "Mobile Documents")
	WalkWithSymlinks(iCloudRoot, func(path string, d fs.DirEntry, err error) error {
		if !d.IsDir() {
			return nil
		}

		if strings.HasPrefix(workspaceAbsPathLower, strings.ToLower(path)) {
			ret = true
			logging.LogWarnf("workspace [%s] is in iCloud path [%s]", workspaceAbsPath, path)
			return io.EOF
		}
		return nil
	})
	return
}

func existAvailabilityStatus(workspaceAbsPath string) bool {
	if !gulu.OS.IsWindows() {
		return false
	}

	if !gulu.File.IsExist(workspaceAbsPath) {
		return false
	}

	// 改进 Windows 端第三方同步盘检测 https://github.com/siyuan-note/siyuan/issues/7777

	defer logging.Recover()

	checkAbsPath := filepath.Join(workspaceAbsPath, "data")
	if !gulu.File.IsExist(checkAbsPath) {
		checkAbsPath = workspaceAbsPath
	}
	if !gulu.File.IsExist(checkAbsPath) {
		logging.LogWarnf("check path [%s] not exist", checkAbsPath)
		return false
	}

	runtime.LockOSThread()
	defer runtime.LockOSThread()
	if err := ole.CoInitializeEx(0, ole.COINIT_MULTITHREADED); err != nil {
		logging.LogWarnf("initialize ole failed: %s", err)
		return false
	}
	defer ole.CoUninitialize()
	dir, file := filepath.Split(checkAbsPath)
	unknown, err := oleutil.CreateObject("Shell.Application")
	if err != nil {
		logging.LogWarnf("create shell application failed: %s", err)
		return false
	}
	shell, err := unknown.QueryInterface(ole.IID_IDispatch)
	if err != nil {
		logging.LogWarnf("query shell interface failed: %s", err)
		return false
	}
	defer shell.Release()

	result, err := oleutil.CallMethod(shell, "NameSpace", dir)
	if err != nil {
		logging.LogWarnf("call shell [NameSpace] failed: %s", err)
		return false
	}
	folderObj := result.ToIDispatch()

	result, err = oleutil.CallMethod(folderObj, "ParseName", file)
	if err != nil {
		logging.LogWarnf("call shell [ParseName] failed: %s", err)
		return false
	}
	fileObj := result.ToIDispatch()
	if nil == fileObj {
		logging.LogWarnf("call shell [ParseName] file is nil [%s]", checkAbsPath)
		return false
	}

	result, err = oleutil.CallMethod(folderObj, "GetDetailsOf", fileObj, 303)
	if err != nil {
		logging.LogWarnf("call shell [GetDetailsOf] failed: %s", err)
		return false
	}
	value := result
	if nil == value {
		return false
	}
	status := strings.ToLower(value.ToString())
	if "" == status || "availability status" == status || "可用性状态" == status {
		return false
	}

	if strings.Contains(status, "sync") || strings.Contains(status, "同步") ||
		strings.Contains(status, "available on this device") || strings.Contains(status, "在此设备上可用") ||
		strings.Contains(status, "available when online") || strings.Contains(status, "联机时可用") {
		logging.LogErrorf("[%s] third party sync status [%s]", checkAbsPath, status)
		return true
	}
	return false
}

const (
	EvtConfPandocInitialized = "conf.pandoc.initialized"

	EvtSQLHistoryRebuild      = "sql.history.rebuild"
	EvtSQLAssetContentRebuild = "sql.assetContent.rebuild"
)

var SearchCaseSensitive bool
