// SPDX-FileCopyrightText: 2018 - 2022 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later

package housekeeping

import (
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/godbus/dbus"
	"github.com/linuxdeepin/dde-daemon/loader"
	configManager "github.com/linuxdeepin/go-dbus-factory/org.desktopspec.ConfigManager"
	notifications "github.com/linuxdeepin/go-dbus-factory/org.freedesktop.notifications"
	"github.com/linuxdeepin/go-lib/dbusutil"
	. "github.com/linuxdeepin/go-lib/gettext"
	"github.com/linuxdeepin/go-lib/log"
	"github.com/linuxdeepin/go-lib/utils"
)

const (
	// 500MB
	fsMinLeftSpace = 1024 * 1024 * 500
)

const (
	dsettingsAppID                = "org.deepin.dde.daemon"
	dsettingsAppName              = "org.deepin.dde.daemon.housekeeping"
	dsettingsKeyAvailSizeCritical = "AvailSizeCritical"
	dsettingsKeySpaceList         = "SpaceList"
	dsettingsKeyAlertMessage      = "AlertMessage"
)

const (
	policyByte uint8 = iota
	policyPercentage
)

func init() {
	loader.Register(NewDaemon(logger))
}

type sizePolicy struct {
	size   uint64
	policy byte
}

type Daemon struct {
	*loader.ModuleBase
	ticker          *time.Ticker
	stopChan        chan struct{}
	sysSigLoop      *dbusutil.SignalLoop
	availSizePolicy sizePolicy
	spaceList       []string
	alertMessage    string
}

func NewDaemon(logger *log.Logger) *Daemon {
	daemon := new(Daemon)
	daemon.ModuleBase = loader.NewModuleBase("housekeeping", daemon, logger)
	return daemon
}

func (*Daemon) GetDependencies() []string {
	return []string{}
}

var (
	logger = log.NewLogger("housekeeping")
)

func (d *Daemon) Start() error {
	if d.stopChan != nil {
		return nil
	}

	err := d.initDsgConfig()
	if err != nil {
		logger.Warning(err)
	}
	if d.sysSigLoop != nil {
		d.sysSigLoop.Start()
	}

	d.ticker = time.NewTicker(time.Minute * 1)
	d.stopChan = make(chan struct{})
	go func() {
		for {
			select {
			case _, ok := <-d.ticker.C:
				if !ok {
					logger.Error("Invalid ticker event")
					return
				}

				for _, v := range d.spaceList {
					if !d.checkSpace(v, !utils.IsDir(v)) {
						break
					}
				}

			case <-d.stopChan:
				logger.Debug("Stop housekeeping")
				if d.ticker != nil {
					d.ticker.Stop()
					d.ticker = nil
				}
				return
			}
		}
	}()
	return nil
}

func (d *Daemon) Stop() error {
	if d.stopChan != nil {
		close(d.stopChan)
		d.stopChan = nil
	}
	if d.sysSigLoop != nil {
		d.sysSigLoop.Stop()
	}
	return nil
}

func sendNotify(icon, summary, body string) error {
	sessionConn, err := dbus.SessionBus()
	if err != nil {
		return err
	}
	notifier := notifications.NewNotifications(sessionConn)
	_, err = notifier.Notify(0, "dde-control-center", 0,
		icon, summary, body,
		nil, nil, -1)
	return err
}

func sendNotify2(icon, summary, body, action, call string, timeout int32) error {
	sessionBus, err := dbus.SessionBus()
	if err != nil {
		return err
	}
	notifier := notifications.NewNotifications(sessionBus)
	_, err = notifier.Notify(0, "dde-control-center", 0,
		icon, summary, body,
		[]string{"_dbus", action},
		map[string]dbus.Variant{
			"x-deepin-action-_dbus":       dbus.MakeVariant(call),
			"x-deepin-ClickToDisappear":   dbus.MakeVariant(false),
			"x-deepin-DisappearAfterLock": dbus.MakeVariant(false),
		}, timeout)
	return err
}

func (d *Daemon) checkSpace(dir string, state bool) bool {
	if state {
		dir = os.Getenv(dir)
	}
	fs, err := utils.QueryFilesytemInfo(dir)
	if err != nil {
		logger.Error("Failed to get filesystem info for :", dir, err)
		return false
	}

	v := d.availSizePolicy
	switch v.policy {
	case policyPercentage:
		if fs.AvailSize > (fs.TotalSize * v.size / 100) {
			return true
		}
	case policyByte:
		if fs.AvailSize > v.size {
			return true
		}
	}

	logger.Info("checkSpace fs.AvailSize(M) : ", dir, fs.AvailSize/1024/1024)
	if d.alertMessage == "" {
		err = sendNotify2("dialog-warning", "",
			Tr("Insufficient disk space, please clean up in time!"),
			Tr("Go to clean up"),
			"dbus-send,--type=method_call,--print-reply,--dest=com.deepin.defender.hmiscreen,/com/deepin/defender/hmiscreen,com.deepin.defender.hmiscreen.ShowModule,string:diskcleaner",
			5000,
		)
	} else {
		err = sendNotify("dialog-warning", "", d.alertMessage)
	}
	if err != nil {
		logger.Warning("Failed to send notification for", dir, ":", err)
	}
	return false
}

func (d *Daemon) initDsgConfig() error {
	logger.Info("initDsgConfig.")
	systemBus, err := dbus.SystemBus()
	if err != nil {
		return err
	}
	d.sysSigLoop = dbusutil.NewSignalLoop(systemBus, 10)
	// dsg 配置
	ds := configManager.NewConfigManager(d.sysSigLoop.Conn())
	dsPath, err := ds.AcquireManager(0, dsettingsAppID, dsettingsAppName, "")
	if err != nil {
		return err
	}
	dsManager, err := configManager.NewManager(d.sysSigLoop.Conn(), dsPath)
	if err != nil {
		return err
	}

	getAvailSizeCritical := func() {
		data, err := dsManager.Value(0, dsettingsKeyAvailSizeCritical)
		if err != nil {
			logger.Warning(err)
			return
		}
		v := data.Value().(string)
		d.availSizePolicy, err = parseAvailSize(v)
		if err != nil {
			logger.Warning(err)
		}
		logger.Infof("available size policy: %v(%v)", d.availSizePolicy, v)
	}

	getSpaceList := func() {
		data, err := dsManager.Value(0, dsettingsKeySpaceList)
		if err != nil {
			logger.Warning(err)
			return
		}
		if v, ok := data.Value().([]dbus.Variant); ok {
			d.spaceList = nil
			for _, vv := range v {
				if space, ok := vv.Value().(string); ok {
					d.spaceList = append(d.spaceList, space)
				}
			}
			logger.Info("space list:", d.spaceList)
		}
	}

	getAlertMessage := func() {
		data, err := dsManager.Value(0, dsettingsKeyAlertMessage)
		if err != nil {
			logger.Warning(err)
			return
		}
		if msg, ok := data.Value().(string); ok {
			d.alertMessage = msg
			logger.Info("alert message:", d.alertMessage)
		}
	}

	getAvailSizeCritical()
	getSpaceList()
	getAlertMessage()

	dsManager.InitSignalExt(d.sysSigLoop, true)
	_, err = dsManager.ConnectValueChanged(func(key string) {
		switch key {
		case dsettingsKeyAvailSizeCritical:
			getAvailSizeCritical()
		case dsettingsKeySpaceList:
			getSpaceList()
		case dsettingsKeyAlertMessage:
			getAlertMessage()
		}
	})

	return err
}

func parseAvailSize(val string) (sizePolicy, error) {
	ret := sizePolicy{
		size:   fsMinLeftSpace,
		policy: policyByte,
	}
	val = strings.TrimSpace(val)
	if len(val) < 2 {
		return ret, fmt.Errorf("invalid size: %s", val)
	}

	last := len(val) - 1
	v, err := strconv.Atoi(strings.TrimSpace(val[0:last]))
	if err != nil {
		return ret, err
	}
	if v <= 0 {
		return ret, fmt.Errorf("invalid size: %s", val)
	}

	switch val[last] {
	case 'k', 'K':
		ret.size = uint64(v) * 1024
	case 'm', 'M':
		ret.size = uint64(v) * 1024 * 1024
	case 'g', 'G':
		ret.size = uint64(v) * 1024 * 1024 * 1024
	case '%':
		if v >= 100 {
			return ret, fmt.Errorf("invalid size: %s", val)
		}
		ret.size = uint64(v)
		ret.policy = policyPercentage
	default:
		return ret, fmt.Errorf("invalid size: %s", val)
	}

	return ret, nil
}
