package bridge

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/netip"
	"os"
	"os/exec"
	"sort"
	"strings"
	"time"

	"tailscale.com/client/local"
	"tailscale.com/cmd/mirage-qt/internal/startup"
	"tailscale.com/ipn"
	"tailscale.com/ipn/ipnstate"
	"tailscale.com/net/tsaddr"
	"tailscale.com/paths"
	"tailscale.com/tailcfg"
	"tailscale.com/types/netmap"
	"tailscale.com/types/views"
	"tailscale.com/util/syspolicy/policyclient"
)

const (
	apiTimeout         = 8 * time.Second
	netmapWatchTimeout = 2 * time.Second
)

type App struct {
	local      local.Client
	configPath string
}

type ActionRequest struct {
	Name            string
	JSON            bool
	ControllerInput string
	StableID        string
	Enabled         bool
}

type snapshot struct {
	status             *ipnstate.Status
	prefs              *ipn.Prefs
	netmap             *netmap.NetworkMap
	controllerInput    string
	hasControllerStore bool
	controlURL         string
	autoStartEnabled   bool
}

type guiSnapshot struct {
	BackendState       string          `json:"backendState"`
	StateLabel         string          `json:"stateLabel"`
	ControllerInput    string          `json:"controllerInput"`
	HasControllerStore bool            `json:"hasControllerStore"`
	ControlURL         string          `json:"controlURL"`
	AuthURL            string          `json:"authURL,omitempty"`
	AccountLabel       string          `json:"accountLabel"`
	SocketPath         string          `json:"socketPath"`
	Version            string          `json:"version"`
	AdminURL           string          `json:"adminURL,omitempty"`
	Self               guiSelf         `json:"self"`
	Peers              []guiPeer       `json:"peers"`
	ExitNodes          []guiExitNode   `json:"exitNodes"`
	Prefs              guiPrefsSummary `json:"prefs"`
}

type guiNetmapSummary struct {
	AccountLabel string        `json:"accountLabel"`
	AdminURL     string        `json:"adminURL,omitempty"`
	Self         guiSelf       `json:"self"`
	Peers        []guiPeer     `json:"peers"`
	ExitNodes    []guiExitNode `json:"exitNodes"`
}

type guiSelf struct {
	Name     string   `json:"name"`
	HostName string   `json:"hostName,omitempty"`
	DNSName  string   `json:"dnsName,omitempty"`
	IPs      []string `json:"ips,omitempty"`
}

type guiPeer struct {
	StableID   string   `json:"stableID,omitempty"`
	Name       string   `json:"name"`
	HostName   string   `json:"hostName,omitempty"`
	DNSName    string   `json:"dnsName,omitempty"`
	IP         string   `json:"ip,omitempty"`
	IPs        []string `json:"ips,omitempty"`
	OwnerLabel string   `json:"ownerLabel,omitempty"`
	UserLabel  string   `json:"userLabel,omitempty"`
	Online     bool     `json:"online"`
	Shared     bool     `json:"shared"`
	Tagged     bool     `json:"tagged"`
	Tags       []string `json:"tags,omitempty"`
	CanUseExit bool     `json:"canUseExit"`
}

type guiExitNode struct {
	StableID string `json:"stableID,omitempty"`
	Name     string `json:"name"`
	IP       string `json:"ip,omitempty"`
	Selected bool   `json:"selected"`
}

type guiPrefsSummary struct {
	WantRunning            bool     `json:"wantRunning"`
	LoggedOut              bool     `json:"loggedOut"`
	CorpDNS                bool     `json:"corpDNS"`
	RouteAll               bool     `json:"routeAll"`
	ForceDaemon            bool     `json:"forceDaemon"`
	ShieldsUp              bool     `json:"shieldsUp"`
	AllowsIncoming         bool     `json:"allowsIncoming"`
	AutoStartUp            bool     `json:"autoStartUp"`
	ExitNodeID             string   `json:"exitNodeID,omitempty"`
	ExitNodeAllowLANAccess bool     `json:"exitNodeAllowLANAccess"`
	AdvertisesExitNode     bool     `json:"advertisesExitNode"`
	AdvertiseRoutes        []string `json:"advertiseRoutes,omitempty"`
}

func NewApp() (*App, error) {
	configPath, err := defaultConfigPath()
	if err != nil {
		return nil, err
	}
	return &App{
		local: local.Client{
			Socket:        paths.DefaultTailscaledSocket(),
			UseSocketOnly: true,
		},
		configPath: configPath,
	}, nil
}

func (a *App) SocketPath() string { return a.local.Socket }

func launcherExecutablePath(executable func() (string, error)) (string, error) {
	if exe := strings.TrimSpace(os.Getenv("MIRAGE_BRIDGE_PROGRAM")); exe != "" {
		return exe, nil
	}
	return executable()
}

func (a *App) startupController() startup.Controller {
	return startup.Controller{
		ExecPath: func() (string, error) {
			return launcherExecutablePath(os.Executable)
		},
	}
}

func (a *App) currentControllerInput() (string, bool, error) {
	state, err := loadConfig(a.configPath)
	if err != nil {
		return "", false, err
	}
	if state.ControllerInput == "" {
		return DefaultControllerInput, false, nil
	}
	return normalizeControllerInput(state.ControllerInput), true, nil
}

func (a *App) saveControllerInput(controllerInput string) error {
	state, err := loadConfig(a.configPath)
	if err != nil {
		return err
	}
	state.ControllerInput = normalizeControllerInput(controllerInput)
	return saveConfig(a.configPath, state)
}

func (a *App) resetControllerInput() error {
	state, err := loadConfig(a.configPath)
	if err != nil {
		return err
	}
	state.ControllerInput = ""
	return saveConfig(a.configPath, state)
}

func (a *App) authKey() (string, error) {
	state, err := loadConfig(a.configPath)
	if err != nil {
		return "", err
	}
	return state.AuthKey, nil
}

func (a *App) refreshSnapshot() (*snapshot, error) {
	controllerInput, hasControllerStore, err := a.currentControllerInput()
	if err != nil {
		return nil, err
	}
	ctx, cancel := context.WithTimeout(context.Background(), apiTimeout)
	defer cancel()

	status, err := a.local.Status(ctx)
	if err != nil {
		return nil, err
	}
	prefs, err := a.local.GetPrefs(ctx)
	if err != nil {
		return nil, err
	}
	autoStartEnabled, err := a.startupController().Enabled()
	if err != nil {
		autoStartEnabled = false
	}

	return &snapshot{
		status:             status,
		prefs:              prefs,
		netmap:             a.fetchInitialNetmap(),
		controllerInput:    controllerInput,
		hasControllerStore: hasControllerStore,
		controlURL:         controlURLForControllerInput(controllerInput),
		autoStartEnabled:   autoStartEnabled,
	}, nil
}

func (a *App) fetchInitialNetmap() *netmap.NetworkMap {
	ctx, cancel := context.WithTimeout(context.Background(), netmapWatchTimeout)
	defer cancel()
	watcher, err := a.local.WatchIPNBus(ctx, ipn.NotifyInitialNetMap)
	if err != nil {
		return nil
	}
	defer watcher.Close()

	for {
		n, err := watcher.Next()
		if err != nil {
			return nil
		}
		if n.NetMap != nil {
			return n.NetMap
		}
	}
}

func (a *App) RunAction(w io.Writer, req ActionRequest) error {
	if req.Name == "service-status" {
		return writeActionValue(w, req.JSON, a.serviceStatus())
	}
	if req.Name == "" || req.Name == "snapshot" {
		snap, err := a.refreshSnapshot()
		if err != nil {
			return err
		}
		return writeActionValue(w, req.JSON, buildGUISnapshot(snap))
	}

	switch req.Name {
	case "login":
		return a.startLoginFlow(req.ControllerInput)
	case "connect":
		return a.setWantRunningValue(true)
	case "disconnect":
		return a.setWantRunningValue(false)
	case "logout":
		return a.logoutCurrentDevice()
	case "switch-controller":
		if strings.TrimSpace(req.ControllerInput) == "" {
			return a.resetControllerInput()
		}
		return a.saveControllerInput(req.ControllerInput)
	case "select-exit-node":
		return a.setExitNodeValue(tailcfg.StableNodeID(req.StableID))
	case "set-exit-node-lan":
		return a.setExitNodeAllowLANAccessValue(req.Enabled)
	case "set-advertise-exit":
		return a.setAdvertiseExitNodeValue(req.Enabled)
	case "set-allows-incoming":
		return a.setAllowsIncomingValue(req.Enabled)
	case "set-corp-dns":
		return a.setCorpDNSValue(req.Enabled)
	case "set-route-all":
		return a.setRouteAllValue(req.Enabled)
	case "set-force-daemon":
		return a.setForceDaemonValue(req.Enabled)
	case "restore-defaults":
		return a.restoreDefaultPrefsValue()
	case "start-daemon":
		if err := startMirageService(); err != nil {
			return err
		}
		return waitForLocalAPIReady(context.Background(), 30*time.Second, 500*time.Millisecond, func(ctx context.Context) error {
			_, err := a.local.Status(ctx)
			return err
		})
	case "grant-operator":
		return a.grantOperatorAccess()
	case "set-autostart":
		return a.startupController().SetEnabled(req.Enabled)
	default:
		return fmt.Errorf("unknown bridge action %q", req.Name)
	}
}

func writeActionValue(w io.Writer, jsonOutput bool, v any) error {
	if !jsonOutput {
		return json.NewEncoder(w).Encode(v)
	}
	return json.NewEncoder(w).Encode(v)
}

func (a *App) startLoginFlow(controllerInput string) error {
	controllerInput = normalizeControllerInput(controllerInput)
	if err := a.saveControllerInput(controllerInput); err != nil {
		return err
	}
	snap, err := a.refreshSnapshot()
	if err != nil {
		return err
	}

	controlURL := controlURLForControllerInput(controllerInput)
	if snap.status.BackendState != ipn.Running.String() {
		prefs := snap.prefs.Clone()
		if prefs == nil {
			prefs = ipn.NewPrefs()
		}
		prefs.ControlURL = controlURL
		prefs.WantRunning = true

		ctx, cancel := context.WithTimeout(context.Background(), apiTimeout)
		defer cancel()
		if err := a.local.CheckPrefs(ctx, prefs); err != nil {
			if !shouldIgnoreLoginCheckPrefsError(err) {
				return fmt.Errorf("check prefs failed: %w", err)
			}
		}
		authKey, err := a.authKey()
		if err != nil {
			return err
		}
		if err := a.local.Start(ctx, ipn.Options{AuthKey: authKey, UpdatePrefs: prefs}); err != nil {
			return fmt.Errorf("start login flow failed: %w", err)
		}
	}

	ctx, cancel := context.WithTimeout(context.Background(), apiTimeout)
	defer cancel()
	return a.local.StartLoginInteractive(ctx)
}

func (a *App) logoutCurrentDevice() error {
	ctx, cancel := context.WithTimeout(context.Background(), apiTimeout)
	defer cancel()
	return a.local.Logout(ctx)
}

func (a *App) setWantRunningValue(want bool) error {
	ctx, cancel := context.WithTimeout(context.Background(), apiTimeout)
	defer cancel()
	_, err := a.local.EditPrefs(ctx, &ipn.MaskedPrefs{
		Prefs:          ipn.Prefs{WantRunning: want},
		WantRunningSet: true,
	})
	return err
}

func (a *App) applyMaskedPrefs(desc string, maskedPrefs *ipn.MaskedPrefs) error {
	ctx, cancel := context.WithTimeout(context.Background(), apiTimeout)
	defer cancel()

	curPrefs, err := a.local.GetPrefs(ctx)
	if err != nil {
		return fmt.Errorf("%s failed: %w", desc, err)
	}
	checkPrefs := curPrefs.Clone()
	checkPrefs.ApplyEdits(maskedPrefs)
	if err := a.local.CheckPrefs(ctx, checkPrefs); err != nil {
		return fmt.Errorf("%s check failed: %w", desc, err)
	}
	if _, err := a.local.EditPrefs(ctx, maskedPrefs); err != nil {
		return fmt.Errorf("%s failed: %w", desc, err)
	}
	return nil
}

func (a *App) setExitNodeValue(id tailcfg.StableNodeID) error {
	return a.applyMaskedPrefs("set exit node", &ipn.MaskedPrefs{
		Prefs:         ipn.Prefs{ExitNodeID: id},
		ExitNodeIDSet: true,
	})
}

func (a *App) setExitNodeAllowLANAccessValue(enabled bool) error {
	return a.applyMaskedPrefs("set exit node lan access", &ipn.MaskedPrefs{
		Prefs:                     ipn.Prefs{ExitNodeAllowLANAccess: enabled},
		ExitNodeAllowLANAccessSet: true,
	})
}

func (a *App) setAdvertiseExitNodeValue(enabled bool) error {
	snap, err := a.refreshSnapshot()
	if err != nil {
		return err
	}
	if enabled && !snap.prefs.ExitNodeID.IsZero() {
		return errors.New("cannot advertise this device as an exit node while using another exit node")
	}

	var routes []netip.Prefix
	if enabled {
		routes = []netip.Prefix{tsaddr.AllIPv4(), tsaddr.AllIPv6()}
	}
	return a.applyMaskedPrefs("set advertise exit node", &ipn.MaskedPrefs{
		Prefs:              ipn.Prefs{AdvertiseRoutes: routes},
		AdvertiseRoutesSet: true,
	})
}

func (a *App) setAllowsIncomingValue(enabled bool) error {
	return a.applyMaskedPrefs("set incoming traffic", &ipn.MaskedPrefs{
		Prefs:        ipn.Prefs{ShieldsUp: !enabled},
		ShieldsUpSet: true,
	})
}

func (a *App) setCorpDNSValue(enabled bool) error {
	return a.applyMaskedPrefs("set corp dns", &ipn.MaskedPrefs{
		Prefs:      ipn.Prefs{CorpDNS: enabled},
		CorpDNSSet: true,
	})
}

func (a *App) setRouteAllValue(enabled bool) error {
	return a.applyMaskedPrefs("set route all", &ipn.MaskedPrefs{
		Prefs:       ipn.Prefs{RouteAll: enabled},
		RouteAllSet: true,
	})
}

func (a *App) setForceDaemonValue(enabled bool) error {
	return a.applyMaskedPrefs("set force daemon", &ipn.MaskedPrefs{
		Prefs:          ipn.Prefs{ForceDaemon: enabled},
		ForceDaemonSet: true,
	})
}

func (a *App) restoreDefaultPrefsValue() error {
	return a.applyMaskedPrefs("restore defaults", &ipn.MaskedPrefs{
		Prefs: ipn.Prefs{
			ShieldsUp:   false,
			CorpDNS:     true,
			RouteAll:    true,
			ForceDaemon: false,
		},
		ShieldsUpSet:   true,
		CorpDNSSet:     true,
		RouteAllSet:    true,
		ForceDaemonSet: true,
	})
}

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

func accountLabel(snap *snapshot) string {
	if snap == nil {
		return "-"
	}
	if snap.netmap != nil {
		profile, ok := snap.netmap.UserProfiles[snap.netmap.SelfNode.User()]
		if ok {
			if label := userProfileLabel(profile, snap.netmap.SelfNode.User().String()); label != "" {
				return label
			}
		}
	}
	if snap.status != nil && snap.status.Self != nil {
		if profile, ok := snap.status.User[snap.status.Self.UserID]; ok {
			if label := userProfileLabel(profile.View(), fmt.Sprint(snap.status.Self.UserID)); label != "" {
				return label
			}
		}
	}
	return "-"
}

func userProfileLabel(profile tailcfg.UserProfileView, fallback string) string {
	switch {
	case profile.DisplayName() != "":
		return profile.DisplayName()
	case profile.LoginName() != "":
		return profile.LoginName()
	default:
		return fallback
	}
}

func adminConsoleURL(snap *snapshot) string {
	if snap == nil || snap.prefs == nil {
		return ""
	}
	return snap.prefs.AdminPageURL(policyclient.Get())
}

func buildGUISnapshot(snap *snapshot) *guiSnapshot {
	return &guiSnapshot{
		BackendState:       snap.status.BackendState,
		StateLabel:         stateSummary(snap.status.BackendState),
		ControllerInput:    snap.controllerInput,
		HasControllerStore: snap.hasControllerStore,
		ControlURL:         snap.controlURL,
		AuthURL:            snap.status.AuthURL,
		AccountLabel:       accountLabel(snap),
		SocketPath:         paths.DefaultTailscaledSocket(),
		Version:            snap.status.Version,
		AdminURL:           adminConsoleURL(snap),
		Self:               buildGUISelf(snap),
		Peers:              buildGUIPeers(snap),
		ExitNodes:          buildGUIExitNodes(snap),
		Prefs:              buildGUIPrefsSummary(snap),
	}
}

func buildGUINetmapSummary(snap *snapshot) *guiNetmapSummary {
	return &guiNetmapSummary{
		AccountLabel: accountLabel(snap),
		AdminURL:     adminConsoleURL(snap),
		Self:         buildGUISelf(snap),
		Peers:        buildGUIPeers(snap),
		ExitNodes:    buildGUIExitNodes(snap),
	}
}

func buildGUIPrefsSummary(snap *snapshot) guiPrefsSummary {
	return guiPrefsSummary{
		WantRunning:            snap.prefs.WantRunning,
		LoggedOut:              snap.prefs.LoggedOut,
		CorpDNS:                snap.prefs.CorpDNS,
		RouteAll:               snap.prefs.RouteAll,
		ForceDaemon:            snap.prefs.ForceDaemon,
		ShieldsUp:              snap.prefs.ShieldsUp,
		AllowsIncoming:         !snap.prefs.ShieldsUp,
		AutoStartUp:            snap.autoStartEnabled,
		ExitNodeID:             string(snap.prefs.ExitNodeID),
		ExitNodeAllowLANAccess: snap.prefs.ExitNodeAllowLANAccess,
		AdvertisesExitNode:     snap.prefs.AdvertisesExitNode(),
		AdvertiseRoutes:        prefixesToStringsSlice(snap.prefs.AdvertiseRoutes),
	}
}

func buildGUISelf(snap *snapshot) guiSelf {
	if snap.netmap != nil && snap.netmap.SelfNode.Valid() {
		self := guiSelf{
			Name:     snap.netmap.SelfNode.DisplayName(true),
			HostName: snap.netmap.SelfNode.Hostinfo().Hostname(),
			IPs:      prefixesToStringsView(snap.netmap.GetAddresses()),
		}
		if self.HostName == "" {
			self.HostName = strings.TrimSuffix(snap.netmap.SelfNode.Name(), ".")
		}
		if dnsName := strings.TrimSuffix(snap.netmap.SelfNode.Name(), "."); dnsName != "" {
			self.DNSName = dnsName
		}
		return self
	}
	if snap.status != nil && snap.status.Self != nil {
		self := guiSelf{
			Name:     snap.status.Self.HostName,
			HostName: snap.status.Self.HostName,
			DNSName:  strings.TrimSuffix(snap.status.Self.DNSName, "."),
			IPs:      addrsToStrings(snap.status.Self.TailscaleIPs),
		}
		if self.Name == "" && self.DNSName != "" {
			self.Name = self.DNSName
		}
		return self
	}
	return guiSelf{Name: "-"}
}

func buildGUIPeers(snap *snapshot) []guiPeer {
	if snap.netmap == nil {
		return nil
	}
	peers := make([]guiPeer, 0, len(snap.netmap.Peers))
	for _, node := range snap.netmap.Peers {
		userLabel := userProfileLabel(snap.netmap.UserProfiles[node.User()], node.User().String())
		ownerID := node.User()
		shared := !node.Sharer().IsZero() && node.Sharer() != node.User()
		if shared {
			ownerID = node.Sharer()
		}
		ownerLabel := userProfileLabel(snap.netmap.UserProfiles[ownerID], ownerID.String())
		tags := stringsFromView(node.Tags())
		peers = append(peers, guiPeer{
			StableID:   string(node.StableID()),
			Name:       node.DisplayName(true),
			HostName:   node.Hostinfo().Hostname(),
			DNSName:    strings.TrimSuffix(node.Name(), "."),
			IP:         preferredPrefixString(node.Addresses()),
			IPs:        prefixesToStringsView(node.Addresses()),
			OwnerLabel: ownerLabel,
			UserLabel:  userLabel,
			Online:     node.Online().Get(),
			Shared:     shared,
			Tagged:     node.Tags().Len() > 0,
			Tags:       tags,
			CanUseExit: tsaddr.ContainsExitRoutes(node.AllowedIPs()),
		})
	}
	sort.Slice(peers, func(i, j int) bool {
		if peers[i].OwnerLabel != peers[j].OwnerLabel {
			return peers[i].OwnerLabel < peers[j].OwnerLabel
		}
		return peers[i].Name < peers[j].Name
	})
	return peers
}

func buildGUIExitNodes(snap *snapshot) []guiExitNode {
	exitNodes := []guiExitNode{{
		Name:     "不使用",
		Selected: snap.prefs.ExitNodeID.IsZero(),
	}}
	if snap.netmap == nil {
		return exitNodes
	}
	for _, node := range snap.netmap.Peers {
		if !tsaddr.ContainsExitRoutes(node.AllowedIPs()) {
			continue
		}
		exitNodes = append(exitNodes, guiExitNode{
			StableID: string(node.StableID()),
			Name:     node.DisplayName(true),
			IP:       preferredPrefixString(node.Addresses()),
			Selected: !snap.prefs.ExitNodeID.IsZero() && snap.prefs.ExitNodeID == node.StableID(),
		})
	}
	sort.Slice(exitNodes[1:], func(i, j int) bool { return exitNodes[i+1].Name < exitNodes[j+1].Name })
	return exitNodes
}

func preferredPrefixString(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 prefixesToStringsSlice(prefixes []netip.Prefix) []string {
	out := make([]string, 0, len(prefixes))
	for _, prefix := range prefixes {
		out = append(out, prefix.String())
	}
	return out
}

func prefixesToStringsView(prefixes views.Slice[netip.Prefix]) []string {
	out := make([]string, 0, prefixes.Len())
	for i := 0; i < prefixes.Len(); i++ {
		out = append(out, prefixes.At(i).String())
	}
	return out
}

func addrsToStrings(addrs []netip.Addr) []string {
	out := make([]string, 0, len(addrs))
	for _, addr := range addrs {
		out = append(out, addr.String())
	}
	return out
}

func stringsFromView(values views.Slice[string]) []string {
	out := make([]string, 0, values.Len())
	for i := 0; i < values.Len(); i++ {
		out = append(out, values.At(i))
	}
	return out
}

func startMirageService() error {
	unit := detectServiceUnit()
	candidates := []string{unit}
	if unit != "tailscaled" {
		candidates = append(candidates, "tailscaled")
	}
	if unit != "miraged" {
		candidates = append(candidates, "miraged")
	}
	var lastErr error
	for _, candidate := range candidates {
		out, err := exec.Command("pkexec", "systemctl", "start", candidate).CombinedOutput()
		if err == nil {
			return nil
		}
		lastErr = fmt.Errorf("start %s: %v: %s", candidate, err, strings.TrimSpace(string(out)))
	}
	return lastErr
}

func detectServiceUnit() string {
	if _, err := exec.LookPath("miraged"); err == nil {
		return "miraged"
	}
	return "tailscaled"
}
