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

package main

import (
	"bytes"
	"errors"
	"fmt"
	"math/rand"
	"os"
	"os/exec"
	"os/user"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/linuxdeepin/lastore-daemon/src/internal/config"
	"github.com/linuxdeepin/lastore-daemon/src/internal/system"
	"github.com/linuxdeepin/lastore-daemon/src/internal/utils"

	"github.com/godbus/dbus/v5"
	"github.com/linuxdeepin/go-lib/dbusutil"
	"github.com/linuxdeepin/go-lib/keyfile"
)

const (
	run              = "systemd-run"
	deepinDaemonUser = "deepin-daemon"
)

var lastoreUnitCache = "/run/lastore/lastoreUnitCache"

// isFirstBoot startOfflineTask执行前执行有效
func isFirstBoot() bool {
	return !system.NormalFileExists(lastoreUnitCache)
}

type systemdEventType string

const (
	AutoCheck              systemdEventType = "AutoCheck"
	AutoClean              systemdEventType = "AutoClean"
	OsVersionChanged       systemdEventType = "OsVersionChanged"
	InitIdleDownload       systemdEventType = "InitIdleDownload"
	AutoDownload           systemdEventType = "AutoDownload"
	AbortAutoDownload      systemdEventType = "AbortAutoDownload"
	UpdateTimer            systemdEventType = "UpdateTimer"
	RetryPostUpgradeResult systemdEventType = "RetryPostUpgradeResult"
	AutoCheckRegularly     systemdEventType = "AutoCheckRegularly"
)

type UnitName string

// lastoreAutoCheck: automatic update check scheduled task unit
//
// Complete workflow:
//  1. System startup phase
//     startOfflineTask() -> getLastoreSystemUnitMap()
//     -> Create lastoreAutoCheck.timer
//     -> Delay time = getNextAutoCheckDelay()
//
//	↓
//
//  2. Timer trigger phase
//     systemd-run executes dbus-send
//     -> Call Manager.HandleSystemEvent("AutoCheck")
//
//	↓
//
//  3. Event processing phase
//     delHandleSystemEvent() -> handleAutoCheckEvent()
//     -> Execute actual update check operations (UpdateSource/platform version request)
//     -> Update LastCheckTime
//     -> Call updateAutoCheckSystemUnit() to reset timer

const (
	lastoreAutoClean        UnitName = "lastoreAutoClean"
	lastoreAutoCheck        UnitName = "lastoreAutoCheck"
	lastoreAutoUpdateToken  UnitName = "lastoreAutoUpdateToken"
	watchOsVersion          UnitName = "watchOsVersion"
	lastoreInitIdleDownload UnitName = "lastoreInitIdleDownload"
	lastoreRegularlyUpdate  UnitName = "lastoreRegularlyUpdate"
	lastorePostUpgrade      UnitName = "lastorePostUpgrade"
	lastoreRetryPostMsg     UnitName = "lastoreRetryPostMsg"
)

type lastoreUnitMap map[UnitName][]string

// 定时任务和文件监听map
func (m *Manager) getLastoreSystemUnitMap() lastoreUnitMap {
	handleEventCmdArgs := []string{
		"/usr/bin/dbus-send", "--system", "--print-reply", "--dest=org.deepin.dde.Lastore1", "/org/deepin/dde/Lastore1", "org.deepin.dde.Lastore1.Manager.HandleSystemEvent",
	}
	genHandleEventCmdArgs := func(prop []string, eventKey systemdEventType) []string {
		return append(append(prop, handleEventCmdArgs...), fmt.Sprintf("string:%s", eventKey))
	}
	unitMap := make(lastoreUnitMap)
	if (m.config.GetLastoreDaemonStatus()&config.DisableUpdate) == 0 && !m.ImmutableAutoRecovery { // 更新禁用未开启且无忧还原未开启时
		unitMap[lastoreAutoCheck] = genHandleEventCmdArgs([]string{
			fmt.Sprintf("--on-active=%d", m.getNextAutoCheckDelay())}, AutoCheck)
	}
	unitMap[lastoreAutoClean] = genHandleEventCmdArgs([]string{
		"--on-active=600"}, AutoClean) // 10分钟后自动检查是否需要清理
	unitMap[lastoreAutoUpdateToken] = genHandleEventCmdArgs([]string{
		"--on-active=60"}, OsVersionChanged) // 60s后更新token文件
	unitMap[watchOsVersion] = genHandleEventCmdArgs([]string{
		"--path-property=PathModified=/etc/os-version"}, OsVersionChanged) // 监听os-version文件，更新token

	// Start processing idle downloads 11 minutes after the system boots up
	// Delayed to 11min to avoid affecting boot performance. AutoClean task runs at 10min,
	// so this task executes one minute after that.
	if !m.ImmutableAutoRecovery && m.updater.getIdleDownloadEnabled() {
		unitMap[lastoreInitIdleDownload] = genHandleEventCmdArgs([]string{
			"--on-active=660"}, InitIdleDownload)
	}

	// PostUpgradeCron 可以配置为*:0/30,每小时的0分和30分触发一次
	if len(strings.TrimSpace(m.config.PostUpgradeCron)) > 0 {
		unitMap[lastoreRetryPostMsg] = genHandleEventCmdArgs([]string{
			fmt.Sprintf("--unit=%s", lastoreRetryPostMsg),
			fmt.Sprintf(`--on-calendar=%v`, m.config.PostUpgradeCron)}, RetryPostUpgradeResult) // 定时上报更新结果
	}
	updateTime, err := time.Parse(time.RFC3339, m.config.UpdateTime)
	if err == nil {
		nowTime := time.Now()
		updateTime = time.Date(nowTime.Year(), nowTime.Month(), nowTime.Day(), updateTime.Hour(), updateTime.Minute(), 0, 0, nowTime.Location())
		if updateTime.Before(nowTime) {
			updateTime = updateTime.Add(time.Duration(24) * time.Hour)
		}
		// 提前60s触发
		if m.config.IntranetUpdate {
			unitMap[lastoreRegularlyUpdate] = genHandleEventCmdArgs([]string{
				fmt.Sprintf("--on-active=%d", int(updateTime.Sub(nowTime)/time.Second-60))}, AutoCheckRegularly)
		} else {
			unitMap[lastoreRegularlyUpdate] = genHandleEventCmdArgs([]string{
				fmt.Sprintf("--on-active=%d", int(updateTime.Sub(nowTime)/time.Second-60))}, AutoCheck)
		}
	}
	if m.config.IntranetUpdate {
		unitMap[lastoreGatherInfo] = []string{
			fmt.Sprintf("--on-active=%d", rand.New(rand.NewSource(time.Now().UnixNano())).Intn(600)+60),
			"--uid=root",
			"/usr/bin/lastore-tools",
			"posthardware",
		}
	}

	return unitMap
}

// 开启定时任务和文件监听(通过systemd-run实现)
func (m *Manager) startOfflineTask() {
	m.lastoreUnitCacheMu.Lock()
	defer m.lastoreUnitCacheMu.Unlock()

	if !isFirstBoot() {
		return
	}
	kf := keyfile.NewKeyFile()
	for name, cmdArgs := range m.getLastoreSystemUnitMap() {
		var args []string
		args = append(args, fmt.Sprintf("--unit=%s", name))
		args = append(args, cmdArgs...)
		cmd := exec.Command(run, args...)
		logger.Info(cmd.String())
		var errBuffer bytes.Buffer
		cmd.Stderr = &errBuffer
		err := cmd.Run()
		if err != nil {
			logger.Warning(err)
			logger.Warning(errBuffer.String())
			continue
		}
		kf.SetString("UnitName", string(name), fmt.Sprintf("%s.unit", name))
	}

	err := utils.SaveKeyFileSecurely(lastoreUnitCache, kf, 0644)
	if err != nil {
		logger.Warning(err)
	}
}

// 保存检查过更新的状态
func (m *Manager) saveUpdateSourceOnce() {
	m.lastoreUnitCacheMu.Lock()
	defer m.lastoreUnitCacheMu.Unlock()

	kf := keyfile.NewKeyFile()
	err := kf.LoadFromFile(lastoreUnitCache)
	if err != nil {
		logger.Warning(err)
		return
	}
	kf.SetBool("RecordData", "UpdateSourceOnce", true)
	err = utils.SaveKeyFileSecurely(lastoreUnitCache, kf, 0644)
	if err != nil {
		logger.Warning(err)
	}
}

// 读取检查过更新的状态
func (m *Manager) loadUpdateSourceOnce() {
	m.lastoreUnitCacheMu.Lock()
	defer m.lastoreUnitCacheMu.Unlock()

	kf := keyfile.NewKeyFile()
	err := kf.LoadFromFile(lastoreUnitCache)
	if err != nil {
		logger.Warning(err)
		return
	}
	updateSourceOnce, err := kf.GetBool("RecordData", "UpdateSourceOnce")
	if err == nil {
		m.PropsMu.Lock()
		m.updateSourceOnce = updateSourceOnce
		m.PropsMu.Unlock()
		if m.statusManager != nil {
			m.statusManager.updateSourceOnce = updateSourceOnce
		}
	} else {
		logger.Warning(err)
	}
}

// systemd计时服务需要根据上一次更新时间而变化
func (m *Manager) updateAutoCheckSystemUnit() error {
	if m.ImmutableAutoRecovery {
		logger.Info("immutable auto recovery is enabled, stopping and don't allow to update auto check timer")
		// 在无忧还原模式下，主动停止现有的自动检查定时器
		_ = m.stopTimerUnit(lastoreAutoCheck)
		return errors.New("immutable auto recovery is enabled, don't allow to update auto check timer")
	}
	return m.updateTimerUnit(lastoreAutoCheck)
}

func (m *Manager) stopTimerUnit(unitName UnitName) error {
	timerName := fmt.Sprintf("%s.%s", unitName, "timer")
	_, err := m.systemd.GetUnit(0, timerName)
	if err == nil {
		_, err = m.systemd.StopUnit(0, timerName, "replace")
		if err != nil {
			logger.Warning(err)
			return err
		}
	} else {
		return err
	}
	return nil
}

// 重新启动systemd unit,先GetUnit，如果能获取到，就调用StopUnit(replace).如果获取不到,证明已经处理完成,直接重新创建对应unit执行
func (m *Manager) updateTimerUnit(unitName UnitName) error {
	timerName := fmt.Sprintf("%s.%s", unitName, "timer")
	_, err := m.systemd.GetUnit(0, timerName)
	if err == nil {
		_, err = m.systemd.StopUnit(0, timerName, "replace")
		if err != nil {
			logger.Warning(err)
		}
	}
	var args []string
	args = append(args, fmt.Sprintf("--unit=%s", unitName))
	autoCheckArgs, ok := m.getLastoreSystemUnitMap()[unitName]
	if ok {
		args = append(args, autoCheckArgs...)
		cmd := exec.Command(run, args...)
		var errBuffer bytes.Buffer
		cmd.Stderr = &errBuffer
		err = cmd.Run()
		if err != nil {
			logger.Warning(err)
			logger.Warning(errBuffer.String())
			return errors.New(errBuffer.String())
		}
		logger.Debug(cmd.String())
	}
	return nil
}

func (m *Manager) handleAutoDownload() {
	if m.ImmutableAutoRecovery {
		logger.Debug("Immutable auto recovery is enabled, don't allow to auto download")
		return
	}

	logger.Debug("Start auto download")
	_, err := m.prepareDistUpgrade(dbus.Sender(m.service.Conn().Names()[0]), m.CheckUpdateMode, initiatorAuto)
	if err != nil {
		logger.Warning(err)
	}
}

func (m *Manager) handleAbortAutoDownload() {
	if m.ImmutableAutoRecovery {
		logger.Debug("Immutable auto recovery is enabled, don't allow to abort auto download")
		return
	}

	job := m.jobManager.findJobById(system.PrepareDistUpgradeJobType)
	if job == nil {
		// job not found
		return
	}

	if job.initiator == initiatorUser {
		logger.Debug("User manually triggered, don't allow to abort auto download")
		return
	}

	logger.Debug("Abort auto download")
	err := m.CleanJob(system.PrepareDistUpgradeJobType)
	if err != nil {
		logger.Warning(err)
	}
}

func (m *Manager) getNextUpdateDelay() time.Duration {
	elapsed := time.Since(m.config.LastCheckTime)
	remained := m.config.CheckInterval - elapsed
	if remained < _minDelayTime {
		// ensure delay at least have 10 seconds
		return _minDelayTime
	}

	return remained
}

func (m *Manager) getNextAutoCheckDelay() int {
	if m.config.IntranetUpdate {
		if m.isAutoCheckTimerFirstRun {
			randomDelay := rand.New(rand.NewSource(time.Now().UnixNano())).Intn(m.config.StartCheckRange[1]-m.config.StartCheckRange[0]) + m.config.StartCheckRange[0]
			return randomDelay
		} else {
			checkInterval := m.config.CheckInterval
			if checkInterval < 0 {
				checkInterval = 0
			}
			return int((checkInterval) / time.Second)
		}
	} else {
		randomDelay := rand.New(rand.NewSource(time.Now().UnixNano())).Intn(m.config.StartCheckRange[1]-m.config.StartCheckRange[0]) + m.config.StartCheckRange[0]
		return int(m.getNextUpdateDelay()/time.Second) + randomDelay
	}
}

// isAllowedToTriggerSystemEvent checks if the uid is allowed to trigger system events
func isAllowedToTriggerSystemEvent(uid uint32, eventType systemdEventType) bool {
	// Allow regular users to trigger OsVersionChanged event
	// TODO: This should be fixed in the future to only allow restricted users to trigger this event.
	if eventType == OsVersionChanged {
		return true
	}

	// Allow root user for all operations
	if uid == 0 {
		return true
	}

	// Allow deepin-daemon user for all operations
	if u, err := user.Lookup(deepinDaemonUser); err == nil {
		if daemonUID, err := strconv.ParseUint(u.Uid, 10, 32); err == nil && uid == uint32(daemonUID) {
			return true
		}
	}

	return false
}

func (m *Manager) delHandleSystemEvent(sender dbus.Sender, eventType string) error {
	uid, err := m.service.GetConnUID(string(sender))
	if err != nil {
		logger.Warning(err)
		return dbusutil.ToError(err)
	}

	evType := systemdEventType(eventType)
	if !isAllowedToTriggerSystemEvent(uid, evType) {
		err = fmt.Errorf("uid %d is not allowed to trigger system event %v", uid, evType)
		logger.Warning(err)
		return dbusutil.ToError(err)
	}

	m.service.DelayAutoQuit()
	switch evType {
	case AutoCheckRegularly:
		go func() {
			err := m.handleAutoCheckRegularlyEvent()
			if err != nil {
				logger.Warning(err)
			}
		}()
	case AutoCheck:
		go func() {
			err := m.handleAutoCheckEvent()
			if err != nil {
				logger.Warning(err)
			}
		}()
	case AutoClean:
		go func() {
			err := m.handleAutoCleanEvent()
			if err != nil {
				logger.Warning(err)
			}
		}()
	case OsVersionChanged:
		logger.Info("enter update token from OsVersionChanged")
		m.syncHardwareRelatedData()
	case InitIdleDownload:
		m.updater.initIdleDownloadConfig()
	case AutoDownload:
		if m.updater.getIdleDownloadEnabled() { // 如果自动下载关闭,则空闲下载同样会关闭
			m.handleAutoDownload()
		}
	case AbortAutoDownload:
		if m.updater.getIdleDownloadEnabled() {
			m.handleAbortAutoDownload()
		}
	case UpdateTimer:
		go func() {
			// 触发检查更新，检查更新时会处理定时更新
			_, err := m.updateSource(dbus.Sender(m.service.Conn().Names()[0]))
			if err != nil {
				logger.Warning(err)
			}
		}()
	case RetryPostUpgradeResult:
		go func() {
			m.updatePlatform.RetryPostHistory()
		}()
	default:
		return dbusutil.ToError(fmt.Errorf("can not handle %s event", eventType))
	}

	return nil
}

func isTimerUnitFileExists(unitName UnitName) bool {
	timerUnitFile := filepath.Join("/run/systemd/transient", string(unitName)+".timer")

	if _, err := os.Stat(timerUnitFile); err != nil {
		return false
	}

	return true
}
