//go:build windows

package main

import (
	"bufio"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/netip"
	"os"
	"os/signal"
	"path/filepath"
	"sync"
	"syscall"
	"time"

	"github.com/dblohm7/wingoes/com"
	"github.com/tailscale/win"
	"golang.org/x/sys/windows"
	"golang.org/x/sys/windows/svc"
	"golang.org/x/sys/windows/svc/mgr"
	"golang.zx2c4.com/wintun"
	"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
	"tailscale.com/client/local"
	"tailscale.com/cmd/mirage/internal/localapiready"
	"tailscale.com/cmd/mirage/internal/servicebootstrap"
	"tailscale.com/cmd/mirage/internal/subprocesslaunch"
	"tailscale.com/cmd/mirage/internal/upgradeprep"
	"tailscale.com/types/logger"
	"tailscale.com/util/backoff"
	"tailscale.com/util/miragebrand"
	"tailscale.com/util/winutil"
	"tailscale.com/wf"
)

var isWindowsServiceProc = sync.OnceValue(func() bool {
	v, err := svc.IsWindowsService()
	if err != nil {
		return false
	}
	return v
})

func init() {
	comType := com.Service
	if !isWindowsServiceProc() {
		comType = com.ConsoleApp
	}
	if err := com.StartRuntime(comType); err != nil {
		log.Printf("wingoes.com.StartRuntime(%d) failed: %v", comType, err)
	}
}

const (
	mirageServiceName        = "Mirage"
	mirageServiceDisplayName = "Mirage"
	obsoleteServiceName      = "miraged"
)

var mirageServiceDependencies = []string{
	"Dnscache",
	"iphlpsvc",
	"netprofm",
	"WinHttpAutoProxySvc",
}

func installMirageService() error {
	if err := ensureMirageServiceRunning(); err != nil {
		return err
	}
	ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
	defer cancel()
	if err := waitForMirageBackendReady(ctx); err != nil {
		return fmt.Errorf("mirage backend did not become ready after service install: %w", err)
	}
	return nil
}

func prepareMirageBackendForGUI(ctx context.Context) error {
	return bootstrapMirageBackendForGUI(ctx, guiBackendBootstrapDeps{
		ensureRunning:     ensureMirageServiceRunning,
		waitReady:         waitForMirageBackendReady,
		elevateInstall:    elevateMirageInstall,
		requiresElevation: requiresMirageServiceElevation,
	})
}

func ensureMirageServiceRunning() error {
	m, err := mgr.Connect()
	if err != nil {
		return fmt.Errorf("failed to connect to Windows service manager: %w", err)
	}
	defer m.Disconnect()

	if err := cleanupObsoleteMirageService(m); err != nil {
		return fmt.Errorf("cleanup obsolete service: %w", err)
	}

	exe, err := os.Executable()
	if err != nil {
		return err
	}

	spec := servicebootstrap.Spec{
		Name:         mirageServiceName,
		Executable:   exe,
		DisplayName:  mirageServiceDisplayName,
		Description:  "Connects this computer to others on the Mirage network.",
		Dependencies: append([]string(nil), mirageServiceDependencies...),
		RecoveryActions: []servicebootstrap.RecoveryAction{
			{Type: servicebootstrap.RecoveryActionRestart, Delay: 1 * time.Second},
			{Type: servicebootstrap.RecoveryActionRestart, Delay: 2 * time.Second},
			{Type: servicebootstrap.RecoveryActionRestart, Delay: 4 * time.Second},
			{Type: servicebootstrap.RecoveryActionRestart, Delay: 9 * time.Second},
		},
		RecoveryResetPeriod: 60,
	}
	if err := servicebootstrap.EnsureRunning(windowsServiceManager{mgr: m}, spec, servicebootstrap.WaitConfig{}); err != nil {
		return fmt.Errorf("failed to ensure %q service is running: %w", mirageServiceName, err)
	}
	return nil
}

func cleanupObsoleteMirageService(m *mgr.Mgr) error {
	obsolete, err := m.OpenService(obsoleteServiceName)
	if err != nil {
		if errors.Is(err, windows.ERROR_SERVICE_DOES_NOT_EXIST) {
			return nil
		}
		return err
	}
	log.Printf("found obsolete service %q; stopping and deleting in favor of %q", obsoleteServiceName, mirageServiceName)
	if err := stopServiceAndWait(obsolete, obsoleteServiceName, 20*time.Second); err != nil {
		obsolete.Close()
		return err
	}
	if err := obsolete.Delete(); err != nil && !errors.Is(err, windows.ERROR_SERVICE_MARKED_FOR_DELETE) {
		obsolete.Close()
		return fmt.Errorf("delete obsolete service %q: %w", obsoleteServiceName, err)
	}
	obsolete.Close()
	if err := waitForServiceDeletion(m, obsoleteServiceName, 5*time.Second); err != nil {
		log.Printf("obsolete service %q cleanup incomplete: %v", obsoleteServiceName, err)
	}
	return nil
}

func stopServiceAndWait(service *mgr.Service, name string, timeout time.Duration) error {
	status, err := service.Query()
	if err != nil {
		return fmt.Errorf("query service %q: %w", name, err)
	}
	if status.State != svc.Stopped && status.State != svc.StopPending {
		if _, err := service.Control(svc.Stop); err != nil && !errors.Is(err, windows.ERROR_SERVICE_NOT_ACTIVE) {
			return fmt.Errorf("stop service %q: %w", name, err)
		}
	}

	deadline := time.Now().Add(timeout)
	for time.Now().Before(deadline) {
		status, err = service.Query()
		if err != nil {
			return fmt.Errorf("query service %q: %w", name, err)
		}
		if status.State == svc.Stopped {
			return nil
		}
		time.Sleep(500 * time.Millisecond)
	}
	return fmt.Errorf("service %q did not stop within %s", name, timeout)
}

func waitForServiceDeletion(m *mgr.Mgr, name string, timeout time.Duration) error {
	deadline := time.Now().Add(timeout)
	for time.Now().Before(deadline) {
		service, err := m.OpenService(name)
		if err != nil {
			if errors.Is(err, windows.ERROR_SERVICE_DOES_NOT_EXIST) {
				return nil
			}
			return err
		}
		service.Close()
		time.Sleep(500 * time.Millisecond)
	}
	return fmt.Errorf("service %q did not delete within %s", name, timeout)
}

func elevateMirageInstall() error {
	exePath, err := os.Executable()
	if err != nil {
		return fmt.Errorf("os.Executable: %w", err)
	}
	cwd, err := os.Getwd()
	if err != nil {
		return fmt.Errorf("os.Getwd: %w", err)
	}

	verbPtr, err := syscall.UTF16PtrFromString("runas")
	if err != nil {
		return fmt.Errorf("UTF16PtrFromString(runas): %w", err)
	}
	exePtr, err := syscall.UTF16PtrFromString(exePath)
	if err != nil {
		return fmt.Errorf("UTF16PtrFromString(%q): %w", exePath, err)
	}
	argPtr, err := syscall.UTF16PtrFromString("-install")
	if err != nil {
		return fmt.Errorf("UTF16PtrFromString(-install): %w", err)
	}
	cwdPtr, err := syscall.UTF16PtrFromString(cwd)
	if err != nil {
		return fmt.Errorf("UTF16PtrFromString(%q): %w", cwd, err)
	}

	if err := windows.ShellExecute(0, verbPtr, exePtr, argPtr, cwdPtr, win.SW_HIDE); err != nil {
		return fmt.Errorf("ShellExecute(runas -install): %w", err)
	}
	return nil
}

func requiresMirageServiceElevation(err error) bool {
	return errors.Is(err, windows.ERROR_ACCESS_DENIED) ||
		errors.Is(err, windows.ERROR_ELEVATION_REQUIRED) ||
		errors.Is(err, windows.ERROR_PRIVILEGE_NOT_HELD)
}

func uninstallMirageService() error {
	m, err := mgr.Connect()
	if err != nil {
		return fmt.Errorf("failed to connect to Windows service manager: %w", err)
	}
	defer m.Disconnect()

	for _, name := range []string{mirageServiceName, obsoleteServiceName} {
		service, err := m.OpenService(name)
		if err != nil {
			if errors.Is(err, windows.ERROR_SERVICE_DOES_NOT_EXIST) {
				continue
			}
			return fmt.Errorf("failed to open %q service: %w", name, err)
		}

		if err := stopServiceAndWait(service, name, 20*time.Second); err != nil {
			service.Close()
			return err
		}
		if err := service.Delete(); err != nil {
			service.Close()
			return fmt.Errorf("failed to delete service %q: %w", name, err)
		}
		service.Close()

		bo := backoff.NewBackoff("uninstall-mirage", logger.Discard, 30*time.Second)
		end := time.Now().Add(15 * time.Second)
		for time.Until(end) > 0 {
			if service, err := m.OpenService(name); err == nil {
				service.Close()
				bo.BackOff(context.Background(), errors.New("service not deleted"))
				continue
			}
			break
		}
	}
	return nil
}

func runMiragePrepareUpgrade() error {
	m, err := mgr.Connect()
	if err != nil {
		return fmt.Errorf("failed to connect to Windows service manager: %w", err)
	}
	defer m.Disconnect()

	exe, err := os.Executable()
	if err != nil {
		return err
	}
	dir := filepath.Dir(exe)
	files := uniqueStrings(
		exe,
		filepath.Join(dir, miragebrand.WindowsGUIExecutableName()),
		filepath.Join(dir, miragebrand.WindowsCLIExecutableName()),
		filepath.Join(dir, "Mirage.exe"),
		filepath.Join(dir, "wintun.dll"),
	)
	return upgradeprep.Prepare(upgradeWindowsServiceManager{mgr: m}, restartManagerLocker{logf: log.Printf}, upgradeprep.Config{
		ServiceNames: []string{mirageServiceName, obsoleteServiceName},
		Files:        files,
		Attempts:     30,
		Delay:        time.Second,
	})
}

func waitForMirageBackendReady(ctx context.Context) error {
	var lc local.Client
	return localapiready.Wait(ctx, func(ctx context.Context) error {
		_, err := lc.Status(ctx)
		return err
	}, localapiready.Config{
		Timeout:  45 * time.Second,
		Interval: 500 * time.Millisecond,
	})
}

func runMirageWindowsService() error {
	log.Printf("Mirage service entering svc.Run")
	defer log.Printf("Mirage service exiting svc.Run")
	return svc.Run(mirageServiceName, &mirageService{})
}

func uniqueStrings(values ...string) []string {
	seen := map[string]bool{}
	out := make([]string, 0, len(values))
	for _, value := range values {
		if value == "" || seen[value] {
			continue
		}
		seen[value] = true
		out = append(out, value)
	}
	return out
}

type windowsServiceManager struct {
	mgr *mgr.Mgr
}

func (m windowsServiceManager) OpenService(name string) (servicebootstrap.Service, error) {
	service, err := m.mgr.OpenService(name)
	if err != nil {
		if errors.Is(err, windows.ERROR_SERVICE_DOES_NOT_EXIST) {
			return nil, servicebootstrap.ErrNotInstalled
		}
		return nil, err
	}
	return &windowsService{service: service}, nil
}

func (m windowsServiceManager) CreateService(spec servicebootstrap.Spec) (servicebootstrap.Service, error) {
	cfg := mgr.Config{
		ServiceType:  windows.SERVICE_WIN32_OWN_PROCESS,
		StartType:    mgr.StartAutomatic,
		ErrorControl: mgr.ErrorNormal,
		Dependencies: append([]string(nil), spec.Dependencies...),
		DisplayName:  spec.DisplayName,
		Description:  spec.Description,
	}
	service, err := m.mgr.CreateService(spec.Name, spec.Executable, cfg)
	if err != nil {
		return nil, err
	}
	return &windowsService{service: service}, nil
}

type windowsService struct {
	service *mgr.Service
}

func (s *windowsService) Query() (servicebootstrap.State, error) {
	status, err := s.service.Query()
	if err != nil {
		return servicebootstrap.StateUnknown, err
	}
	switch status.State {
	case svc.Stopped:
		return servicebootstrap.StateStopped, nil
	case svc.StartPending:
		return servicebootstrap.StateStartPending, nil
	case svc.StopPending:
		return servicebootstrap.StateStopPending, nil
	case svc.Running:
		return servicebootstrap.StateRunning, nil
	default:
		return servicebootstrap.StateUnknown, nil
	}
}

func (s *windowsService) Start() error {
	return s.service.Start()
}

func (s *windowsService) SetRecoveryActions(actions []servicebootstrap.RecoveryAction, resetPeriod uint32) error {
	recoveryActions := make([]mgr.RecoveryAction, 0, len(actions))
	for _, action := range actions {
		switch action.Type {
		case servicebootstrap.RecoveryActionRestart:
			recoveryActions = append(recoveryActions, mgr.RecoveryAction{Type: mgr.ServiceRestart, Delay: action.Delay})
		}
	}
	return s.service.SetRecoveryActions(recoveryActions, resetPeriod)
}

func (s *windowsService) Close() error {
	s.service.Close()
	return nil
}

type upgradeWindowsServiceManager struct {
	mgr *mgr.Mgr
}

func (m upgradeWindowsServiceManager) OpenService(name string) (upgradeprep.Service, error) {
	service, err := m.mgr.OpenService(name)
	if err != nil {
		if errors.Is(err, windows.ERROR_SERVICE_DOES_NOT_EXIST) {
			return nil, upgradeprep.ErrNotInstalled
		}
		return nil, err
	}
	return &upgradeWindowsService{service: service}, nil
}

type upgradeWindowsService struct {
	service *mgr.Service
}

func (s *upgradeWindowsService) Query() (upgradeprep.State, error) {
	status, err := s.service.Query()
	if err != nil {
		return upgradeprep.StateUnknown, err
	}
	switch status.State {
	case svc.Stopped:
		return upgradeprep.StateStopped, nil
	case svc.StartPending:
		return upgradeprep.StateStartPending, nil
	case svc.StopPending:
		return upgradeprep.StateStopPending, nil
	case svc.Running:
		return upgradeprep.StateRunning, nil
	default:
		return upgradeprep.StateUnknown, nil
	}
}

func (s *upgradeWindowsService) Stop() error {
	status, err := s.service.Query()
	if err != nil {
		return err
	}
	if status.State == svc.Stopped || status.State == svc.StopPending {
		return nil
	}
	_, err = s.service.Control(svc.Stop)
	if errors.Is(err, windows.ERROR_SERVICE_NOT_ACTIVE) {
		return nil
	}
	return err
}

func (s *upgradeWindowsService) Close() error {
	s.service.Close()
	return nil
}

type restartManagerLocker struct {
	logf logger.Logf
}

func (l restartManagerLocker) Terminate(paths []string) error {
	if len(paths) == 0 {
		return nil
	}
	rms, err := winutil.NewRestartManagerSession(l.logf)
	if err != nil {
		return err
	}
	defer rms.Close()

	if err := rms.AddPaths(paths); err != nil {
		return err
	}
	ups, err := rms.AffectedProcesses()
	if err != nil {
		return err
	}
	rps := winutil.NewRestartableProcesses()
	defer rps.Close()

	selfPID := uint32(os.Getpid())
	for _, up := range ups {
		if up.PID == selfPID {
			continue
		}
		rp, err := up.AsRestartableProcess()
		if err != nil {
			if errors.Is(err, winutil.ErrDefunctProcess) {
				continue
			}
			return err
		}
		rps.Add(rp)
	}
	return rps.Terminate(l.logf, 0xEE01, 20*time.Second)
}

type mirageService struct{}

func (service *mirageService) Execute(args []string, req <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
	log.Printf("mirage service host entering Execute")
	changes <- svc.Status{State: svc.StartPending}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	done := make(chan struct{})
	go func() {
		defer close(done)
		log.Printf("mirage service host starting subprocess supervisor")
		babysitMirageProc(ctx, []string{"/subproc"}, log.Printf)
		log.Printf("mirage service host subprocess supervisor exited")
	}()

	changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop}
	log.Printf("mirage service host reported Running to SCM")
	for {
		select {
		case <-done:
			log.Printf("mirage service host observed subprocess supervisor completion")
			return false, windows.NO_ERROR
		case cmd := <-req:
			log.Printf("mirage service host got SCM command %v", cmd.Cmd)
			switch cmd.Cmd {
			case svc.Stop:
				changes <- svc.Status{State: svc.StopPending}
				cancel()
			case svc.Interrogate:
				changes <- cmd.CurrentStatus
			}
		}
	}
}

func runMirageFirewallHelper() error {
	if len(os.Args) < 3 {
		return errors.New("missing firewall tunnel GUID")
	}
	guid, err := windows.GUIDFromString(os.Args[2])
	if err != nil {
		return fmt.Errorf("invalid GUID %q: %w", os.Args[2], err)
	}
	luid, err := winipcfg.LUIDFromGUID(&guid)
	if err != nil {
		return fmt.Errorf("no interface with GUID %q: %w", guid, err)
	}

	fw, err := wf.New(uint64(luid))
	if err != nil {
		return fmt.Errorf("failed to enable firewall: %w", err)
	}

	decoder := json.NewDecoder(os.Stdin)
	for {
		var routes []netip.Prefix
		if err := decoder.Decode(&routes); err != nil {
			return err
		}
		if err := fw.UpdatePermittedRoutes(routes); err != nil {
			return err
		}
	}
}

func babysitMirageProc(ctx context.Context, args []string, logf logger.Logf) {
	executable, err := os.Executable()
	if err != nil {
		panic("cannot determine executable: " + err.Error())
	}

	var proc struct {
		mu     sync.Mutex
		p      *os.Process
		wStdin *os.File
	}

	done := make(chan struct{})
	go func() {
		interrupt := make(chan os.Signal, 1)
		signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
		select {
		case sig := <-interrupt:
			logf("babysitMirageProc: got signal: %v", sig)
			close(done)
			proc.mu.Lock()
			if proc.p != nil {
				proc.p.Signal(sig)
			}
			proc.mu.Unlock()
		case <-ctx.Done():
			logf("babysitMirageProc: context done")
			close(done)
			proc.mu.Lock()
			if proc.wStdin != nil {
				proc.wStdin.Close()
			}
			proc.mu.Unlock()
		}
	}()

	bo := backoff.NewBackoff("babysitMirageProc", logf, 30*time.Second)
	for {
		startTime := time.Now()
		logf("starting subprocess %q with args %v", executable, args)
		cmd := subprocesslaunch.New(executable, args)
		cmd.SysProcAttr = &syscall.SysProcAttr{
			CreationFlags: windows.DETACHED_PROCESS,
		}

		logf("creating subprocess stdin pipe")
		rStdin, wStdin, err := os.Pipe()
		if err != nil {
			logf("os.Pipe(stdin): %v", err)
			return
		}
		logf("creating subprocess stdout/stderr pipe")
		rStdout, wStdout, err := os.Pipe()
		if err != nil {
			logf("os.Pipe(stdout): %v", err)
			return
		}

		go func(r *os.File) {
			defer r.Close()
			reader := bufio.NewReader(r)
			for {
				line, err := reader.ReadString('\n')
				if line != "" {
					logf("%s", line)
				}
				if err != nil {
					return
				}
			}
		}(rStdout)

		cmd.Stdin = rStdin
		cmd.Stdout = wStdout
		cmd.Stderr = wStdout
		logf("starting subprocess")
		err = cmd.Start()
		rStdin.Close()
		wStdout.Close()

		if err != nil {
			logf("starting subprocess failed: %v", err)
		} else {
			logf("subprocess started with pid %d", cmd.Process.Pid)
			proc.mu.Lock()
			proc.p = cmd.Process
			proc.wStdin = wStdin
			proc.mu.Unlock()
			err = cmd.Wait()
			logf("subprocess exited: %v", err)
		}
		wStdin.Close()

		if time.Since(startTime) < 60*time.Second {
			bo.BackOff(ctx, fmt.Errorf("subproc early exit: %v", err))
		} else {
			bo.BackOff(ctx, nil)
		}
		select {
		case <-done:
			return
		default:
		}
	}
}

func uninstallWinTun(logf logger.Logf) {
	dll := windows.NewLazyDLL(fullyQualifiedWintunPath(logf))
	if err := dll.Load(); err != nil {
		logf("cannot load wintun.dll for uninstall: %v", err)
		return
	}
	logf("removing wintun driver...")
	logf("uninstall: %v", wintun.Uninstall())
}

func fullyQualifiedWintunPath(logf logger.Logf) string {
	dir := filepath.Dir(os.Args[0])
	if imgName, err := winutil.ProcessImageName(windows.CurrentProcess()); err == nil {
		dir = filepath.Dir(imgName)
	} else {
		logf("ProcessImageName failed: %v", err)
	}
	return filepath.Join(dir, "wintun.dll")
}
