//go:build windows

package main

import (
	"context"
	"errors"
	"fmt"
	"log"
	"net/netip"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/tailscale/walk"
	"tailscale.com/cmd/mirage/internal/config"
	"tailscale.com/cmd/mirage/internal/localapiready"
	"tailscale.com/cmd/mirage/internal/shellclient"
	"tailscale.com/cmd/mirage/internal/startup"
	"tailscale.com/ipn"
	"tailscale.com/net/tsaddr"
	"tailscale.com/tailcfg"
	"tailscale.com/types/netmap"
	"tailscale.com/types/views"
	"tailscale.com/util/syspolicy/policyclient"
	"tailscale.com/version"
)

type connectField struct {
	statusAction     *walk.Action
	controllerAction *walk.Action
	loginAction      *walk.Action
	reauthAction     *walk.Action
	connectAction    *walk.Action
	disconnectAction *walk.Action
	refreshAction    *walk.Action
	switchCtrlAction *walk.Action
}

type userField struct {
	userMenu      *walk.Action
	consoleAction *walk.Action
	logoutAction  *walk.Action
}

type nodeField struct {
	nodeAction *walk.Action
	nodesMenu  *walk.Action
}

type exitField struct {
	exitNodeMenu         *walk.Action
	exitNodeListTitle    *walk.Action
	exitNodeList         *walk.ActionList
	exitNodeIDMap        map[tailcfg.StableNodeID]int
	exitPrefTitle        *walk.Action
	exitAllowLocalAction *walk.Action
	exitRunExitAction    *walk.Action
}

type prefField struct {
	prefMenu              *walk.Action
	prefAllowIncomeAction *walk.Action
	prefUsingDNSAction    *walk.Action
	prefUsingSubnetAction *walk.Action
	prefUnattendAction    *walk.Action
	prefToDefaultAction   *walk.Action
	autoStartUpAction     *walk.Action
	aboutAction           *walk.Action
}

type shellData struct {
	Version       string
	State         ipn.State
	Prefs         *ipn.Prefs
	NetMap        *netmap.NetworkMap
	ClientVersion *tailcfg.ClientVersion
}

type mirageShell struct {
	ctx    context.Context
	cancel context.CancelFunc

	mw      *walk.MainWindow
	tray    *walk.NotifyIcon
	uiReady bool

	client     *shellclient.Client
	configPath string

	controlURL  string
	backendErr  string
	spinnerStop chan struct{}

	data shellData

	connectField *connectField
	userField    *userField
	nodeField    *nodeField
	exitField    *exitField
	prefField    *prefField

	aboutAction *walk.Action
	exitAction  *walk.Action
}

func runGUI() error {
	shell, err := newMirageShell()
	if err != nil {
		return err
	}
	return shell.Run()
}

func newMirageShell() (*mirageShell, error) {
	ctx, cancel := context.WithCancel(context.Background())
	cfgPath := config.DefaultPath(os.Getenv("ProgramData"))

	mw, err := walk.NewMainWindow()
	if err != nil {
		cancel()
		return nil, err
	}
	tray, err := walk.NewNotifyIcon(mw)
	if err != nil {
		cancel()
		mw.Dispose()
		return nil, err
	}
	if err := tray.SetVisible(true); err != nil {
		cancel()
		tray.Dispose()
		mw.Dispose()
		return nil, err
	}

	s := &mirageShell{
		ctx:        ctx,
		cancel:     cancel,
		mw:         mw,
		tray:       tray,
		configPath: cfgPath,
		client: &shellclient.Client{
			ConfigPath: cfgPath,
		},
		data: shellData{
			State:         ipn.NoState,
			Prefs:         ipn.NewPrefs(),
			NetMap:        &netmap.NetworkMap{},
			ClientVersion: &tailcfg.ClientVersion{},
		},
	}
	if startupController, err := startup.NewWindowsController(); err != nil {
		log.Printf("startup controller unavailable: %v", err)
	} else {
		if err := startupController.RepairEnabledPath(); err != nil {
			log.Printf("startup controller path repair failed: %v", err)
		}
		s.client.Startup = startupController
	}
	if err := s.buildMenu(); err != nil {
		s.Close()
		return nil, err
	}
	s.mw.Closing().Attach(func(canceled *bool, reason walk.CloseReason) {
		s.cancel()
	})
	s.tray.MouseUp().Attach(func(x, y int, button walk.MouseButton) {
		if button == walk.LeftButton && s.data.State == ipn.NoState && s.backendErr != "" {
			go s.sendNotify("Mirage 后台未就绪", s.backendErr, notifyWarn)
		}
	})
	return s, nil
}

func (s *mirageShell) Run() error {
	defer s.Close()

	if err := s.reloadController(); err != nil {
		log.Printf("load controller config failed: %v", err)
	}
	if err := prepareMirageBackendForGUI(s.ctx); err != nil {
		log.Printf("prepare mirage backend for GUI failed: %v", err)
	}
	if err := s.refreshStatus(); err != nil {
		log.Printf("initial refresh failed: %v", err)
	}

	s.uiReady = true
	go func() {
		time.Sleep(200 * time.Millisecond)
		s.watchLoop()
	}()
	s.mw.Run()
	s.uiReady = false
	return nil
}

func (s *mirageShell) waitForBackendReady() error {
	return localapiready.Wait(s.ctx, func(ctx context.Context) error {
		_, err := s.client.Local.Status(ctx)
		return err
	}, localapiready.Config{
		Timeout:  20 * time.Second,
		Interval: 500 * time.Millisecond,
	})
}

func (s *mirageShell) Close() {
	s.cancel()
	if s.tray != nil {
		s.tray.Dispose()
		s.tray = nil
	}
	if s.mw != nil {
		s.mw.Dispose()
		s.mw = nil
	}
}

func (s *mirageShell) runOnUI(fn func()) {
	if s.uiReady && s.mw != nil {
		s.mw.Synchronize(fn)
		return
	}
	fn()
}

func preferredAddrString(prefixes views.Slice[netip.Prefix]) string {
	for i := 0; i < prefixes.Len(); i++ {
		addr := prefixes.At(i).Addr()
		if addr.Is4() {
			return addr.String()
		}
	}
	for i := 0; i < prefixes.Len(); i++ {
		addr := prefixes.At(i).Addr()
		if addr.IsValid() {
			return addr.String()
		}
	}
	return ""
}

func userLabel(profile tailcfg.UserProfileView, fallback tailcfg.UserID) string {
	switch {
	case profile.DisplayName() != "":
		return profile.DisplayName()
	case profile.LoginName() != "":
		return profile.LoginName()
	case !fallback.IsZero():
		return fallback.String()
	default:
		return "未知账号"
	}
}

func userLoginLabel(profile tailcfg.UserProfileView, fallback tailcfg.UserID) string {
	switch {
	case profile.LoginName() != "":
		return profile.LoginName()
	case profile.DisplayName() != "":
		return profile.DisplayName()
	case !fallback.IsZero():
		return fallback.String()
	default:
		return "未知账号"
	}
}

func stateSummaryText(state ipn.State) string {
	switch state {
	case ipn.Running:
		return "已连接"
	case ipn.Starting:
		return "连接中"
	case ipn.Stopped:
		return "已登录，未连接"
	case ipn.NeedsMachineAuth:
		return "等待设备授权"
	case ipn.InUseOtherUser:
		return "当前设备有其他登录用户"
	case ipn.NeedsLogin:
		return "未登录"
	default:
		return "未初始化"
	}
}

func controllerSummaryText(controlURL string) string {
	if controlURL == "" {
		return config.DefaultControlURL
	}
	return controlURL
}

func parseBackendState(state string) ipn.State {
	switch state {
	case ipn.InUseOtherUser.String():
		return ipn.InUseOtherUser
	case ipn.NeedsLogin.String():
		return ipn.NeedsLogin
	case ipn.NeedsMachineAuth.String():
		return ipn.NeedsMachineAuth
	case ipn.Stopped.String():
		return ipn.Stopped
	case ipn.Starting.String():
		return ipn.Starting
	case ipn.Running.String():
		return ipn.Running
	default:
		return ipn.NoState
	}
}

func (s *mirageShell) buildMenu() error {
	s.setTip("Mirage - 初始化中")
	s.setIcon(iconLogo)

	var err error
	if s.connectField, err = s.newConnectField(); err != nil {
		return err
	}
	if s.userField, err = s.newUserField(); err != nil {
		return err
	}
	if s.nodeField, err = s.newNodeField(); err != nil {
		return err
	}
	if s.exitField, err = s.newExitField(); err != nil {
		return err
	}
	if s.prefField, err = s.newPrefField(); err != nil {
		return err
	}

	s.exitAction = walk.NewAction()
	s.exitAction.SetText("退出")
	s.exitAction.Triggered().Attach(func() {
		s.cancel()
		if s.mw != nil {
			s.mw.Close()
		}
	})
	if err := s.tray.ContextMenu().Actions().Add(s.exitAction); err != nil {
		return err
	}

	s.connectField.loginAction.Triggered().Attach(s.DoLogin)
	s.connectField.reauthAction.Triggered().Attach(s.DoLogin)
	s.connectField.connectAction.Triggered().Attach(s.DoConn)
	s.connectField.disconnectAction.Triggered().Attach(s.DoDisconn)
	s.connectField.refreshAction.Triggered().Attach(s.RefreshStatus)
	s.connectField.switchCtrlAction.Triggered().Attach(s.SwitchController)

	s.userField.logoutAction.Triggered().Attach(s.DoLogout)
	s.userField.consoleAction.Triggered().Attach(func() {
		adminURL := s.data.Prefs.AdminPageURL(policyclient.Get())
		if adminURL == "" {
			s.sendNotify("Mirage", "当前控制器没有提供控制台地址", notifyWarn)
			return
		}
		openURLInBrowser(adminURL)
	})

	s.exitField.exitAllowLocalAction.Triggered().Attach(s.SetAllowLocalNet)
	s.exitField.exitRunExitAction.Triggered().Attach(s.SetAsExitNode)

	s.prefField.prefAllowIncomeAction.Triggered().Attach(s.SetAllowIncome)
	s.prefField.prefUsingDNSAction.Triggered().Attach(s.SetDNSOpt)
	s.prefField.prefUsingSubnetAction.Triggered().Attach(s.SetSubnetOpt)
	s.prefField.prefUnattendAction.Triggered().Attach(s.SetUnattendOpt)
	s.prefField.prefToDefaultAction.Triggered().Attach(s.SetPrefsDefault)
	s.prefField.autoStartUpAction.Triggered().Attach(s.ToggleAutoStartUp)
	s.prefField.aboutAction.Triggered().Attach(s.ShowAbout)

	s.nodeField.nodeAction.Triggered().Attach(func() {
		if selfIP := preferredAddrString(s.data.NetMap.GetAddresses()); selfIP != "" {
			walk.Clipboard().SetText(selfIP)
			s.sendNotify("我的地址", "已复制 IP 地址 ("+selfIP+") 到剪贴板", notifyInfo)
			return
		}
		s.sendNotify("我的地址", "当前设备没有可复制的 Mirage 地址", notifyWarn)
	})

	s.applyState(ipn.NoState)
	s.applyPrefs(s.data.Prefs)
	s.refreshOverview()
	return nil
}

func (s *mirageShell) setIcon(state trayIconState) {
	s.runOnUI(func() {
		if s.tray != nil {
			if err := s.tray.SetIcon(trayIcons[state]); err != nil {
				log.Printf("set icon failed: %v", err)
			}
		}
	})
}

func (s *mirageShell) setTip(tip string) {
	s.runOnUI(func() {
		if s.tray != nil {
			if err := s.tray.SetToolTip(tip); err != nil {
				log.Printf("set tooltip failed: %v", err)
			}
		}
	})
}

func (s *mirageShell) newConnectField() (*connectField, error) {
	cf := &connectField{
		statusAction:     walk.NewAction(),
		controllerAction: walk.NewAction(),
		loginAction:      walk.NewAction(),
		reauthAction:     walk.NewAction(),
		connectAction:    walk.NewAction(),
		disconnectAction: walk.NewAction(),
		refreshAction:    walk.NewAction(),
		switchCtrlAction: walk.NewAction(),
	}
	cf.statusAction.SetText("状态: 初始化中")
	cf.statusAction.SetEnabled(false)
	cf.controllerAction.SetText("控制器: 初始化中")
	cf.controllerAction.SetEnabled(false)
	cf.loginAction.SetText("登录…")
	cf.reauthAction.SetText("重新认证…")
	cf.reauthAction.SetVisible(false)
	cf.connectAction.SetVisible(false)
	cf.disconnectAction.SetText("断开")
	cf.disconnectAction.SetVisible(false)
	cf.refreshAction.SetText("刷新状态")
	cf.switchCtrlAction.SetText("切换控制器…")

	actions := s.tray.ContextMenu().Actions()
	for _, act := range []*walk.Action{
		cf.statusAction,
		cf.controllerAction,
		walk.NewSeparatorAction(),
		cf.loginAction,
		cf.reauthAction,
		cf.connectAction,
		cf.disconnectAction,
		cf.refreshAction,
		cf.switchCtrlAction,
		walk.NewSeparatorAction(),
	} {
		if err := actions.Add(act); err != nil {
			return nil, err
		}
	}
	return cf, nil
}

func (s *mirageShell) newUserField() (*userField, error) {
	menu, err := walk.NewMenu()
	if err != nil {
		return nil, err
	}
	uf := &userField{
		userMenu:      walk.NewMenuAction(menu),
		consoleAction: walk.NewAction(),
		logoutAction:  walk.NewAction(),
	}
	uf.userMenu.SetText("账号")
	uf.userMenu.SetVisible(false)
	uf.consoleAction.SetText("控制台")
	uf.logoutAction.SetText("登出")
	uf.userMenu.Menu().Actions().Add(uf.consoleAction)
	uf.userMenu.Menu().Actions().Add(walk.NewSeparatorAction())
	uf.userMenu.Menu().Actions().Add(uf.logoutAction)

	if err := s.tray.ContextMenu().Actions().Add(uf.userMenu); err != nil {
		return nil, err
	}
	if err := s.tray.ContextMenu().Actions().Add(walk.NewSeparatorAction()); err != nil {
		return nil, err
	}
	return uf, nil
}

func (s *mirageShell) newNodeField() (*nodeField, error) {
	menu, err := walk.NewMenu()
	if err != nil {
		return nil, err
	}
	nf := &nodeField{
		nodeAction: walk.NewAction(),
		nodesMenu:  walk.NewMenuAction(menu),
	}
	nf.nodeAction.SetText("本设备")
	nf.nodeAction.SetVisible(false)
	nf.nodesMenu.SetText("网内设备")
	nf.nodesMenu.SetVisible(false)

	if err := s.tray.ContextMenu().Actions().Add(nf.nodeAction); err != nil {
		return nil, err
	}
	if err := s.tray.ContextMenu().Actions().Add(nf.nodesMenu); err != nil {
		return nil, err
	}
	if err := s.tray.ContextMenu().Actions().Add(walk.NewSeparatorAction()); err != nil {
		return nil, err
	}
	return nf, nil
}

func (s *mirageShell) newExitField() (*exitField, error) {
	menu, err := walk.NewMenu()
	if err != nil {
		return nil, err
	}
	exitNodeListMenu, err := walk.NewMenu()
	if err != nil {
		return nil, err
	}
	ef := &exitField{
		exitNodeMenu:         walk.NewMenuAction(menu),
		exitNodeListTitle:    walk.NewAction(),
		exitNodeList:         walk.NewMenuAction(exitNodeListMenu).Menu().Actions(),
		exitNodeIDMap:        make(map[tailcfg.StableNodeID]int),
		exitPrefTitle:        walk.NewAction(),
		exitAllowLocalAction: walk.NewAction(),
		exitRunExitAction:    walk.NewAction(),
	}
	ef.exitNodeMenu.SetText("出口节点")
	ef.exitNodeMenu.SetVisible(false)
	ef.exitNodeListTitle.SetText("无可用出口节点")
	ef.exitNodeListTitle.SetEnabled(false)
	ef.exitPrefTitle.SetText("配置项")
	ef.exitPrefTitle.SetEnabled(false)
	ef.exitAllowLocalAction.SetText("本地网络不走出口")
	ef.exitAllowLocalAction.SetCheckable(true)
	ef.exitRunExitAction.SetText("用作出口节点…")
	ef.exitRunExitAction.SetCheckable(true)

	ef.exitNodeMenu.Menu().Actions().Add(ef.exitNodeListTitle)
	ef.exitNodeMenu.Menu().Actions().Add(walk.NewSeparatorAction())
	ef.exitNodeMenu.Menu().Actions().Add(ef.exitPrefTitle)
	ef.exitNodeMenu.Menu().Actions().Add(ef.exitAllowLocalAction)
	ef.exitNodeMenu.Menu().Actions().Add(ef.exitRunExitAction)

	if err := s.tray.ContextMenu().Actions().Add(ef.exitNodeMenu); err != nil {
		return nil, err
	}
	if err := s.tray.ContextMenu().Actions().Add(walk.NewSeparatorAction()); err != nil {
		return nil, err
	}
	return ef, nil
}

func (s *mirageShell) newPrefField() (*prefField, error) {
	menu, err := walk.NewMenu()
	if err != nil {
		return nil, err
	}
	pf := &prefField{
		prefMenu:              walk.NewMenuAction(menu),
		prefAllowIncomeAction: walk.NewAction(),
		prefUsingDNSAction:    walk.NewAction(),
		prefUsingSubnetAction: walk.NewAction(),
		prefUnattendAction:    walk.NewAction(),
		prefToDefaultAction:   walk.NewAction(),
		autoStartUpAction:     walk.NewAction(),
		aboutAction:           walk.NewAction(),
	}
	pf.prefMenu.SetText("配置项")
	pf.prefAllowIncomeAction.SetText("允许入流量连接")
	pf.prefAllowIncomeAction.SetCheckable(true)
	pf.prefAllowIncomeAction.SetChecked(true)
	pf.prefUsingDNSAction.SetText("使用 DNS 设置")
	pf.prefUsingDNSAction.SetCheckable(true)
	pf.prefUsingDNSAction.SetChecked(true)
	pf.prefUsingSubnetAction.SetText("使用子网转发")
	pf.prefUsingSubnetAction.SetCheckable(true)
	pf.prefUsingSubnetAction.SetChecked(true)
	pf.prefUnattendAction.SetText("无人值守运行")
	pf.prefUnattendAction.SetCheckable(true)
	pf.prefToDefaultAction.SetText("恢复默认设置")
	pf.autoStartUpAction.SetText("开机自启动")
	pf.autoStartUpAction.SetCheckable(true)
	pf.autoStartUpAction.SetVisible(s.client.Startup != nil)
	pf.aboutAction.SetText("关于…")

	pf.prefMenu.Menu().Actions().Add(pf.prefAllowIncomeAction)
	pf.prefMenu.Menu().Actions().Add(pf.prefUsingDNSAction)
	pf.prefMenu.Menu().Actions().Add(pf.prefUsingSubnetAction)
	pf.prefMenu.Menu().Actions().Add(pf.prefUnattendAction)
	pf.prefMenu.Menu().Actions().Add(walk.NewSeparatorAction())
	pf.prefMenu.Menu().Actions().Add(pf.prefToDefaultAction)
	if pf.autoStartUpAction.Visible() {
		pf.prefMenu.Menu().Actions().Add(pf.autoStartUpAction)
	}

	if err := s.tray.ContextMenu().Actions().Add(pf.prefMenu); err != nil {
		return nil, err
	}
	if err := s.tray.ContextMenu().Actions().Add(pf.aboutAction); err != nil {
		return nil, err
	}
	if err := s.tray.ContextMenu().Actions().Add(walk.NewSeparatorAction()); err != nil {
		return nil, err
	}
	return pf, nil
}

func (s *mirageShell) refreshOverview() {
	stateText := stateSummaryText(s.data.State)
	if s.backendErr != "" {
		stateText = "后台未就绪"
	} else if selfIP := preferredAddrString(s.data.NetMap.GetAddresses()); selfIP != "" && s.data.State == ipn.Running {
		stateText += " (" + selfIP + ")"
	}
	s.connectField.statusAction.SetText("状态: " + stateText)
	s.connectField.controllerAction.SetText("控制器: " + controllerSummaryText(s.controlURL))
	s.setTip("Mirage - " + stateText)
}

func (s *mirageShell) stopSpinner() {
	if s.spinnerStop != nil {
		close(s.spinnerStop)
		s.spinnerStop = nil
	}
}

func (s *mirageShell) changeIconDueRunState() {
	s.stopSpinner()
	switch s.data.State {
	case ipn.NeedsLogin:
		s.setIcon(iconLogo)
	case ipn.NoState:
		if s.backendErr != "" {
			s.setIcon(iconIssue)
		} else {
			s.setIcon(iconLogo)
		}
	case ipn.Stopped:
		s.setIcon(iconDisconnected)
	case ipn.Running:
		switch {
		case s.data.Prefs != nil && s.data.Prefs.AdvertisesExitNode():
			s.setIcon(iconAsExitNode)
		case s.data.Prefs != nil && (!s.data.Prefs.ExitNodeID.IsZero() || s.data.Prefs.ExitNodeIP.IsValid()):
			s.setIcon(iconExitNode)
		default:
			s.setIcon(iconConnected)
		}
	case ipn.Starting:
		stop := make(chan struct{})
		s.spinnerStop = stop
		go func(stop <-chan struct{}) {
			iconA := true
			ticker := time.NewTicker(300 * time.Millisecond)
			defer ticker.Stop()
			for {
				select {
				case <-ticker.C:
					if iconA {
						s.setIcon(iconConnecting1)
					} else {
						s.setIcon(iconConnecting2)
					}
					iconA = !iconA
				case <-stop:
					return
				case <-s.ctx.Done():
					return
				}
			}
		}(stop)
	default:
		s.setIcon(iconLogo)
	}
}

func (s *mirageShell) applyState(state ipn.State) {
	s.data.State = state
	s.changeIconDueRunState()
	switch state {
	case ipn.Stopped:
		s.connectField.connectAction.SetText("连接")
		s.connectField.connectAction.SetEnabled(true)
		s.connectField.connectAction.SetVisible(true)
		s.connectField.disconnectAction.SetVisible(false)
		s.connectField.loginAction.SetVisible(false)
		s.connectField.reauthAction.SetVisible(true)
		s.userField.userMenu.SetVisible(true)
		s.nodeField.nodeAction.SetText("本设备")
		s.nodeField.nodeAction.SetEnabled(false)
		s.nodeField.nodeAction.SetVisible(true)
		s.nodeField.nodesMenu.SetEnabled(false)
		s.nodeField.nodesMenu.SetVisible(true)
		s.exitField.exitNodeMenu.SetEnabled(false)
		s.exitField.exitNodeMenu.SetVisible(true)
	case ipn.Starting:
		s.connectField.connectAction.SetEnabled(false)
		s.connectField.connectAction.SetText("正在连接…")
		s.connectField.connectAction.SetVisible(true)
		s.connectField.disconnectAction.SetEnabled(true)
		s.connectField.disconnectAction.SetVisible(true)
		s.connectField.loginAction.SetVisible(false)
		s.connectField.reauthAction.SetVisible(true)
		s.userField.userMenu.SetVisible(true)
		s.nodeField.nodeAction.SetText("本设备")
		s.nodeField.nodeAction.SetEnabled(false)
		s.nodeField.nodeAction.SetVisible(true)
		s.nodeField.nodesMenu.SetEnabled(false)
		s.nodeField.nodesMenu.SetVisible(true)
		s.exitField.exitNodeMenu.SetEnabled(false)
		s.exitField.exitNodeMenu.SetVisible(true)
	case ipn.Running:
		s.connectField.connectAction.SetText("已连接")
		s.connectField.connectAction.SetEnabled(false)
		s.connectField.connectAction.SetVisible(true)
		s.connectField.disconnectAction.SetEnabled(true)
		s.connectField.disconnectAction.SetVisible(true)
		s.connectField.loginAction.SetVisible(false)
		s.connectField.reauthAction.SetVisible(true)
		s.userField.userMenu.SetVisible(true)
		s.nodeField.nodeAction.SetEnabled(true)
		s.nodeField.nodeAction.SetVisible(true)
		s.nodeField.nodesMenu.SetEnabled(true)
		s.nodeField.nodesMenu.SetVisible(true)
		s.exitField.exitNodeMenu.SetEnabled(true)
		s.exitField.exitNodeMenu.SetVisible(true)
	default:
		s.connectField.connectAction.SetVisible(false)
		s.connectField.disconnectAction.SetVisible(false)
		s.connectField.loginAction.SetVisible(true)
		s.connectField.reauthAction.SetVisible(false)
		s.userField.userMenu.SetVisible(false)
		s.nodeField.nodeAction.SetVisible(false)
		s.nodeField.nodesMenu.SetVisible(false)
		s.exitField.exitNodeMenu.SetVisible(false)
	}
	s.refreshOverview()
}

func (s *mirageShell) updateCurrentExitNode(stableID tailcfg.StableNodeID) {
	for i := 0; i < s.exitField.exitNodeList.Len(); i++ {
		s.exitField.exitNodeList.At(i).SetChecked(false)
	}
	if index, ok := s.exitField.exitNodeIDMap[stableID]; ok && index < s.exitField.exitNodeList.Len() {
		s.exitField.exitNodeList.At(index).SetChecked(true)
	}
	if s.data.NetMap == nil {
		s.exitField.exitNodeMenu.SetText("出口节点")
		return
	}
	if node, ok := s.data.NetMap.PeerWithStableID(s.data.Prefs.ExitNodeID); ok {
		s.exitField.exitNodeMenu.SetText("出口节点(" + node.DisplayName(true) + ")")
		return
	}
	s.exitField.exitNodeMenu.SetText("出口节点")
}

func (s *mirageShell) applyPrefs(prefs *ipn.Prefs) {
	if prefs == nil {
		return
	}
	s.data.Prefs = prefs
	s.changeIconDueRunState()
	s.userField.consoleAction.SetVisible(prefs.AdminPageURL(policyclient.Get()) != "")
	s.updateCurrentExitNode(prefs.ExitNodeID)
	s.exitField.exitAllowLocalAction.SetChecked(prefs.ExitNodeAllowLANAccess)
	if prefs.AdvertisesExitNode() {
		s.exitField.exitRunExitAction.SetText("正用作出口节点")
		s.exitField.exitRunExitAction.SetChecked(true)
	} else {
		s.exitField.exitRunExitAction.SetText("用作出口节点…")
		s.exitField.exitRunExitAction.SetChecked(false)
	}
	s.prefField.prefAllowIncomeAction.SetChecked(!prefs.ShieldsUp)
	s.prefField.prefUsingDNSAction.SetChecked(prefs.CorpDNS)
	s.prefField.prefUsingSubnetAction.SetChecked(prefs.RouteAll)
	s.prefField.prefUnattendAction.SetChecked(prefs.ForceDaemon)
	s.refreshOverview()
}

func (s *mirageShell) applyNetMap(nm *netmap.NetworkMap) {
	if nm == nil || !nm.SelfNode.Valid() {
		s.refreshOverview()
		return
	}
	s.data.NetMap = nm

	selfProfile := nm.UserProfiles[nm.SelfNode.User()]
	s.userField.userMenu.SetText("账号: " + userLabel(selfProfile, nm.SelfNode.User()))

	selfIP := preferredAddrString(nm.GetAddresses())
	selfName := nm.SelfNode.DisplayName(true)
	if selfIP != "" {
		s.nodeField.nodeAction.SetText("本设备: " + selfName + " (" + selfIP + ")")
	} else {
		s.nodeField.nodeAction.SetText("本设备: " + selfName)
	}

	s.nodeField.nodesMenu.Menu().Actions().Clear()
	myNodeContain, err := walk.NewMenu()
	if err != nil {
		log.Printf("init my-node menu failed: %v", err)
	}
	myNodeMenu := walk.NewMenuAction(myNodeContain)
	myNodeMenu.SetText("我的设备")
	tagNodeContain, err := walk.NewMenu()
	if err != nil {
		log.Printf("init tag-node menu failed: %v", err)
	}
	tagNodeMenu := walk.NewMenuAction(tagNodeContain)
	tagNodeMenu.SetText("标签节点")
	peerNodeContain, err := walk.NewMenu()
	if err != nil {
		log.Printf("init peer-node menu failed: %v", err)
	}
	peerMenuList := peerNodeContain.Actions()

	for i := 0; i < s.exitField.exitNodeList.Len(); i++ {
		s.exitField.exitNodeMenu.Menu().Actions().Remove(s.exitField.exitNodeList.At(i))
	}
	s.exitField.exitNodeList.Clear()
	for stableID := range s.exitField.exitNodeIDMap {
		delete(s.exitField.exitNodeIDMap, stableID)
	}

	for _, node := range nm.Peers {
		nodeName := node.DisplayName(true)
		nodeIP := preferredAddrString(node.Addresses())

		tmpNodeAction := walk.NewAction()
		if nodeIP != "" {
			tmpNodeAction.SetText(nodeName + " (" + nodeIP + ")")
		} else {
			tmpNodeAction.SetText(nodeName)
		}

		capturedName := nodeName
		capturedIP := nodeIP
		tmpNodeAction.Triggered().Attach(func() {
			if capturedIP == "" {
				s.sendNotify(capturedName, "该节点没有可复制的 Mirage 地址", notifyWarn)
				return
			}
			walk.Clipboard().SetText(capturedIP)
			s.sendNotify(capturedName, "已复制节点 IP ("+capturedIP+") 到剪贴板", notifyInfo)
		})

		if tsaddr.ContainsExitRoutes(node.AllowedIPs()) {
			exitNodeName := nodeName
			exitNodeID := node.StableID()
			tmpExitNodeAction := walk.NewAction()
			tmpExitNodeAction.SetText(exitNodeName)
			tmpExitNodeAction.SetCheckable(true)
			tmpExitNodeAction.SetChecked(exitNodeID != "" && !s.data.Prefs.ExitNodeID.IsZero() && s.data.Prefs.ExitNodeID == exitNodeID)
			tmpExitNodeAction.Triggered().Attach(func() {
				for i := 0; i < s.exitField.exitNodeList.Len(); i++ {
					s.exitField.exitNodeList.At(i).SetChecked(false)
				}
				s.setUseExitNode(exitNodeID)
			})
			s.exitField.exitNodeList.Add(tmpExitNodeAction)
			s.exitField.exitNodeIDMap[exitNodeID] = s.exitField.exitNodeList.Len()
			s.exitField.exitNodeMenu.Menu().Actions().Insert(s.exitField.exitNodeList.Len(), tmpExitNodeAction)
		}

		switch {
		case node.Tags().Len() > 0:
			tagNodeMenu.Menu().Actions().Add(tmpNodeAction)
		case node.User() == nm.SelfNode.User() && node.ID() != nm.SelfNode.ID():
			myNodeMenu.Menu().Actions().Add(tmpNodeAction)
		case node.User() != nm.SelfNode.User():
			peerMenu := &walk.Action{}
			peerMenuExist := false
			nodeOwner := strconv.FormatInt(int64(node.User()), 10)
			if !node.Sharer().IsZero() && node.Sharer() != node.User() {
				nodeOwner = strconv.FormatInt(int64(node.Sharer()), 10)
			}
			for i := 0; i < peerMenuList.Len(); i++ {
				if peerMenu = peerMenuList.At(i); peerMenu.Text() == nodeOwner {
					peerMenuExist = true
					break
				}
			}
			if !peerMenuExist {
				peerMenuContain, err := walk.NewMenu()
				if err != nil {
					log.Printf("init peer owner menu failed: %v", err)
				}
				peerMenu = walk.NewMenuAction(peerMenuContain)
				peerMenu.SetText(nodeOwner)
				peerMenuList.Add(peerMenu)
			}
			peerMenu.Menu().Actions().Add(tmpNodeAction)
		}
	}

	if myNodeMenu.Menu().Actions().Len() > 0 {
		header := walk.NewAction()
		header.SetText(userLoginLabel(selfProfile, nm.SelfNode.User()))
		header.SetEnabled(false)
		myNodeMenu.Menu().Actions().Insert(0, header)
		myNodeMenu.Menu().Actions().Insert(1, walk.NewSeparatorAction())
		s.nodeField.nodesMenu.Menu().Actions().Add(myNodeMenu)
		s.nodeField.nodesMenu.Menu().Actions().Add(walk.NewSeparatorAction())
	}

	for i := 0; i < peerMenuList.Len(); i++ {
		peerID, err := strconv.ParseInt(peerMenuList.At(i).Text(), 10, 64)
		if err != nil {
			log.Printf("parse peer user id failed: %v", err)
			continue
		}
		profile := nm.UserProfiles[tailcfg.UserID(peerID)]
		peerMenuList.At(i).SetText(userLabel(profile, tailcfg.UserID(peerID)))
		header := walk.NewAction()
		header.SetText(userLoginLabel(profile, tailcfg.UserID(peerID)))
		header.SetEnabled(false)
		peerMenuList.At(i).Menu().Actions().Insert(0, header)
		peerMenuList.At(i).Menu().Actions().Insert(1, walk.NewSeparatorAction())
		s.nodeField.nodesMenu.Menu().Actions().Add(peerMenuList.At(i))
	}

	if tagNodeMenu.Menu().Actions().Len() > 0 {
		header := walk.NewAction()
		header.SetText("标签节点")
		header.SetEnabled(false)
		tagNodeMenu.Menu().Actions().Insert(0, header)
		tagNodeMenu.Menu().Actions().Insert(1, walk.NewSeparatorAction())
		s.nodeField.nodesMenu.Menu().Actions().Add(tagNodeMenu)
	}
	s.nodeField.nodesMenu.SetVisible(s.nodeField.nodesMenu.Menu().Actions().Len() > 0)

	if s.exitField.exitNodeList.Len() > 0 {
		noneExitAction := walk.NewAction()
		noneExitAction.SetText("不使用")
		noneExitAction.SetCheckable(true)
		noneExitAction.SetChecked(s.data.Prefs.ExitNodeID.IsZero())
		noneExitAction.Triggered().Attach(func() {
			for i := 0; i < s.exitField.exitNodeList.Len(); i++ {
				s.exitField.exitNodeList.At(i).SetChecked(false)
			}
			s.setUseExitNode("")
		})
		s.exitField.exitNodeIDMap[""] = 0
		s.exitField.exitNodeList.Insert(0, noneExitAction)
		s.exitField.exitNodeMenu.Menu().Actions().Insert(1, noneExitAction)
		s.exitField.exitNodeListTitle.SetText("出口节点")
	} else {
		s.exitField.exitNodeListTitle.SetText("无可用出口节点")
	}

	if !nm.SelfNode.KeyExpiry().IsZero() && !nm.SelfNode.KeyExpiry().After(time.Now().AddDate(0, 0, 7)) {
		lastDays := strings.TrimSuffix((nm.SelfNode.KeyExpiry().Sub(time.Now()) / (24 * time.Hour)).String(), "ns")
		go func(days string) {
			if popConfirmDialog("临期设备延期提醒", "该设备密钥还有"+days+"天过期，是否现在进行登录延期（将轮换新设备密钥）", 320, 150) {
				if err := s.client.Local.StartLoginInteractive(s.ctx); err != nil {
					s.sendNotify("Mirage", "开始延期认证失败: "+err.Error(), notifyError)
				}
			}
		}(lastDays)
	}

	s.refreshOverview()
}

func (s *mirageShell) setBackendUnavailable(err error) {
	s.backendErr = err.Error()
	s.applyState(ipn.NoState)
	s.refreshOverview()
}

func (s *mirageShell) handleNotify(n ipn.Notify) {
	s.runOnUI(func() {
		s.backendErr = ""
		if n.Version != "" {
			s.data.Version = n.Version
		}
		if n.ClientVersion != nil {
			s.data.ClientVersion = n.ClientVersion
		}
		if n.Prefs != nil && n.Prefs.Valid() {
			s.applyPrefs(n.Prefs.AsStruct().Clone())
		}
		if n.State != nil {
			s.applyState(*n.State)
		}
		if n.NetMap != nil {
			s.applyNetMap(n.NetMap)
		}
		if n.ErrMessage != nil && *n.ErrMessage != "" {
			go s.sendNotify("Mirage 后台错误", *n.ErrMessage, notifyError)
		}
	})
	if n.BrowseToURL != nil && *n.BrowseToURL != "" {
		openURLInBrowser(*n.BrowseToURL)
	}
}

func (s *mirageShell) watchLoop() {
	const watchMask = ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap

	for {
		if err := s.ctx.Err(); err != nil {
			return
		}

		watcher, err := s.client.Local.WatchIPNBus(s.ctx, watchMask)
		if err != nil {
			s.runOnUI(func() {
				s.setBackendUnavailable(err)
			})
			select {
			case <-time.After(5 * time.Second):
				continue
			case <-s.ctx.Done():
				return
			}
		}

		for {
			n, err := watcher.Next()
			if err != nil {
				watcher.Close()
				if s.ctx.Err() != nil {
					return
				}
				s.runOnUI(func() {
					s.setBackendUnavailable(err)
				})
				break
			}
			s.handleNotify(n)
		}
	}
}

func (s *mirageShell) reloadController() error {
	controlURL, err := s.client.ControlURL()
	if err != nil {
		return err
	}
	s.controlURL = controlURL
	s.refreshOverview()
	return nil
}

func (s *mirageShell) refreshStatus() error {
	if err := s.reloadController(); err != nil {
		return err
	}
	prefs, err := s.client.Local.GetPrefs(s.ctx)
	if err != nil {
		s.setBackendUnavailable(err)
		return err
	}
	st, err := s.client.Local.Status(s.ctx)
	if err != nil {
		s.setBackendUnavailable(err)
		return err
	}
	s.backendErr = ""
	s.data.Version = st.Version
	s.applyPrefs(prefs)
	s.applyState(parseBackendState(st.BackendState))
	return nil
}

func (s *mirageShell) createPref() *ipn.Prefs {
	prefs := ipn.NewPrefs()
	prefs.ControlURL = controllerSummaryText(s.controlURL)
	prefs.WantRunning = true
	prefs.RouteAll = s.prefField.prefUsingSubnetAction.Checked()
	prefs.ExitNodeAllowLANAccess = s.exitField.exitAllowLocalAction.Checked()
	prefs.CorpDNS = s.prefField.prefUsingDNSAction.Checked()
	prefs.ShieldsUp = !s.prefField.prefAllowIncomeAction.Checked()
	prefs.RunSSH = false
	prefs.AdvertiseRoutes = nil
	prefs.AdvertiseTags = nil
	prefs.Hostname = ""
	prefs.ForceDaemon = s.prefField.prefUnattendAction.Checked()
	prefs.LoggedOut = false
	return prefs
}

func (s *mirageShell) kickLogin() error {
	prefs := s.createPref()
	if err := s.client.Local.CheckPrefs(s.ctx, prefs); err != nil {
		return err
	}
	authKey, err := s.client.AuthKey()
	if err != nil {
		return err
	}
	return s.client.Local.Start(s.ctx, ipn.Options{
		AuthKey:     authKey,
		UpdatePrefs: prefs,
	})
}

func (s *mirageShell) updatePref(desc string, newPrefs *ipn.MaskedPrefs) {
	curPrefs, err := s.client.Local.GetPrefs(s.ctx)
	if err != nil {
		go s.sendNotify(desc, "读取配置失败: "+err.Error(), notifyError)
		return
	}

	checkPrefs := curPrefs.Clone()
	checkPrefs.ApplyEdits(newPrefs)
	if err := s.client.Local.CheckPrefs(s.ctx, checkPrefs); err != nil {
		go s.sendNotify(desc, "配置检查失败: "+err.Error(), notifyError)
		return
	}

	if _, err := s.client.Local.EditPrefs(s.ctx, newPrefs); err != nil {
		go s.sendNotify(desc, "更新配置失败: "+err.Error(), notifyError)
		return
	}
}

func (s *mirageShell) setUseExitNode(stableID tailcfg.StableNodeID) {
	if s.exitField.exitRunExitAction.Checked() {
		go s.sendNotify("设置出口节点", "当前节点已用作出口节点，无法同时使用其他出口节点", notifyWarn)
		return
	}
	maskedPrefs := &ipn.MaskedPrefs{
		Prefs: ipn.Prefs{
			ExitNodeID: stableID,
		},
		ExitNodeIDSet: true,
	}
	curPrefs, err := s.client.Local.GetPrefs(s.ctx)
	if err != nil {
		go s.sendNotify("设置出口节点", "读取当前配置失败: "+err.Error(), notifyError)
		return
	}

	checkPrefs := curPrefs.Clone()
	checkPrefs.ApplyEdits(maskedPrefs)
	if err := s.client.Local.CheckPrefs(s.ctx, checkPrefs); err != nil {
		go s.sendNotify("设置出口节点", "配置检查失败: "+err.Error(), notifyError)
		return
	}

	if _, err := s.client.Local.EditPrefs(s.ctx, maskedPrefs); err != nil {
		go s.sendNotify("设置出口节点", "更新配置失败: "+err.Error(), notifyError)
		return
	}
}

func (s *mirageShell) DoConn() {
	if _, err := s.client.Local.EditPrefs(s.ctx, &ipn.MaskedPrefs{
		Prefs: ipn.Prefs{
			WantRunning: true,
		},
		WantRunningSet: true,
	}); err != nil {
		go s.sendNotify("Mirage", "连接失败: "+err.Error(), notifyError)
	}
}

func (s *mirageShell) DoDisconn() {
	st, err := s.client.Local.Status(s.ctx)
	if err != nil {
		go s.sendNotify("Mirage", "读取状态失败: "+err.Error(), notifyError)
		return
	}
	if st.BackendState == ipn.Running.String() || st.BackendState == ipn.Starting.String() {
		if _, err := s.client.Local.EditPrefs(s.ctx, &ipn.MaskedPrefs{
			Prefs: ipn.Prefs{
				WantRunning: false,
			},
			WantRunningSet: true,
		}); err != nil {
			go s.sendNotify("Mirage", "断开失败: "+err.Error(), notifyError)
		}
	}
}

func (s *mirageShell) DoLogout() {
	if err := s.client.Local.Logout(s.ctx); err != nil {
		go s.sendNotify("登出失败", err.Error(), notifyError)
	}
}

func (s *mirageShell) DoLogin() {
	if err := s.reloadController(); err != nil {
		go s.sendNotify("读取控制器配置失败", err.Error(), notifyError)
		return
	}
	st, err := s.client.Local.Status(s.ctx)
	if err != nil && !errors.Is(err, context.Canceled) {
		go s.sendNotify("Mirage 后台未就绪", err.Error(), notifyError)
		return
	}
	if st == nil || st.BackendState != ipn.Running.String() {
		if err := s.kickLogin(); err != nil {
			go s.sendNotify("开始登录失败", err.Error(), notifyError)
			return
		}
	}
	if err := s.client.Local.StartLoginInteractive(s.ctx); err != nil {
		go s.sendNotify("开始登录失败", err.Error(), notifyError)
	}
}

func (s *mirageShell) RefreshStatus() {
	if err := s.refreshStatus(); err != nil {
		go s.sendNotify("刷新状态失败", err.Error(), notifyError)
		return
	}
	go s.sendNotify("状态已刷新", "", notifyInfo)
}

func (s *mirageShell) SwitchController() {
	currentInput, _, err := s.client.CurrentControllerInput()
	if err != nil {
		go s.sendNotify("读取控制器配置失败", err.Error(), notifyError)
		return
	}
	s.tray.SetVisible(false)
	confirm, newInput := popTextInputDialog("切换控制器", "请输入控制器代码或完整 URL（留空恢复默认）:", currentInput)
	s.tray.SetVisible(true)
	if !confirm {
		return
	}
	if strings.TrimSpace(newInput) == "" {
		err = s.client.ResetControllerInput()
	} else {
		err = s.client.SaveControllerInput(newInput)
	}
	if err != nil {
		go s.sendNotify("保存控制器失败", err.Error(), notifyError)
		return
	}
	if err := s.reloadController(); err != nil {
		go s.sendNotify("更新控制器状态失败", err.Error(), notifyError)
		return
	}
	if s.data.State != ipn.NoState && s.data.State != ipn.NeedsLogin {
		if popConfirmDialog("切换控制器", "新控制器已保存。是否现在登出并重新登录？", 320, 140) {
			s.DoLogout()
		}
	}
	go s.sendNotify("控制器已更新", s.controlURL, notifyInfo)
}

func (s *mirageShell) ShowAbout() {
	currentVersion := s.data.Version
	if currentVersion == "" {
		currentVersion = version.Long()
	}
	msg := "Mirage 客户端版本：" + currentVersion
	if cv := s.data.ClientVersion; cv != nil {
		switch {
		case cv.LatestVersion == "":
			msg += "\n最新版本未知"
		case cv.LatestVersion == currentVersion:
			msg += "\n已是最新版本"
		default:
			msg += "\n官方最新版本：" + cv.LatestVersion + "\n是否下载？"
			if walk.MsgBox(s.mw, "关于 Mirage", msg, walk.MsgBoxIconInformation|walk.MsgBoxOKCancel) == walk.DlgCmdOK {
				openURLInBrowser(cv.NotifyURL)
			}
			return
		}
	}
	walk.MsgBox(s.mw, "关于 Mirage", msg, walk.MsgBoxIconInformation)
}

func (s *mirageShell) SetAllowIncome() {
	newValue := s.prefField.prefAllowIncomeAction.Checked()
	s.prefField.prefAllowIncomeAction.SetChecked(!newValue)
	s.updatePref("设置允许入流量", &ipn.MaskedPrefs{
		Prefs: ipn.Prefs{
			ShieldsUp: !newValue,
		},
		ShieldsUpSet: true,
	})
}

func (s *mirageShell) SetDNSOpt() {
	newValue := s.prefField.prefUsingDNSAction.Checked()
	s.prefField.prefUsingDNSAction.SetChecked(!newValue)
	s.updatePref("设置 DNS 选项", &ipn.MaskedPrefs{
		Prefs: ipn.Prefs{
			CorpDNS: newValue,
		},
		CorpDNSSet: true,
	})
}

func (s *mirageShell) SetSubnetOpt() {
	newValue := s.prefField.prefUsingSubnetAction.Checked()
	s.prefField.prefUsingSubnetAction.SetChecked(!newValue)
	s.updatePref("设置子网选项", &ipn.MaskedPrefs{
		Prefs: ipn.Prefs{
			RouteAll: newValue,
		},
		RouteAllSet: true,
	})
}

func (s *mirageShell) SetUnattendOpt() {
	newValue := s.prefField.prefUnattendAction.Checked()
	s.prefField.prefUnattendAction.SetChecked(!newValue)
	s.updatePref("设置无人值守", &ipn.MaskedPrefs{
		Prefs: ipn.Prefs{
			ForceDaemon: newValue,
		},
		ForceDaemonSet: true,
	})
}

func (s *mirageShell) SetPrefsDefault() {
	s.updatePref("恢复默认配置", &ipn.MaskedPrefs{
		Prefs: ipn.Prefs{
			ShieldsUp:   false,
			CorpDNS:     true,
			RouteAll:    true,
			ForceDaemon: false,
		},
		ShieldsUpSet:   true,
		CorpDNSSet:     true,
		RouteAllSet:    true,
		ForceDaemonSet: true,
	})
}

func (s *mirageShell) SetAllowLocalNet() {
	newValue := s.exitField.exitAllowLocalAction.Checked()
	s.updatePref("设置本地网络不走出口", &ipn.MaskedPrefs{
		Prefs: ipn.Prefs{
			ExitNodeAllowLANAccess: newValue,
		},
		ExitNodeAllowLANAccessSet: true,
	})
}

func (s *mirageShell) SetAsExitNode() {
	newValue := s.exitField.exitRunExitAction.Checked()
	var routes []netip.Prefix

	if newValue {
		if !popConfirmDialog("用作出口节点？", "你确定要将此设备用作出口节点吗？\n\nWindows 设备上的出口节点稳定性可能不如 Linux。", 320, 150) {
			s.exitField.exitRunExitAction.SetChecked(false)
			return
		}
		st, err := s.client.Local.Status(s.ctx)
		if err != nil {
			go s.sendNotify("设置出口节点", "读取状态失败: "+err.Error(), notifyError)
			return
		}
		if st.ExitNodeStatus != nil {
			go s.sendNotify("设置出口节点", "当前正在使用其他出口节点，不能同时作为出口节点", notifyWarn)
			return
		}
		routes = append(routes, tsaddr.AllIPv4(), tsaddr.AllIPv6())
	}

	s.updatePref("设置为出口节点", &ipn.MaskedPrefs{
		Prefs: ipn.Prefs{
			AdvertiseRoutes: routes,
		},
		AdvertiseRoutesSet: true,
	})
}

func (s *mirageShell) ToggleAutoStartUp() {
	if s.client.Startup == nil {
		return
	}
	if err := s.client.SetAutoStartUp(s.ctx); err != nil {
		go s.sendNotify("设置开机自启动失败", err.Error(), notifyError)
		return
	}
	enabled, err := s.client.GetAutoStartUp(s.ctx)
	if err != nil {
		go s.sendNotify("读取开机自启动状态失败", err.Error(), notifyError)
		return
	}
	s.prefField.autoStartUpAction.SetChecked(enabled)
	go s.sendNotify("开机自启动", fmt.Sprintf("当前状态：%t", enabled), notifyInfo)
}
