//go:build windows

package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net"
	"os"
	"os/exec"
	"runtime"
	"sort"
	"strings"
	"time"

	"github.com/FuturFusion/vsock"
	"github.com/shirou/gopsutil/v4/disk"
	"golang.org/x/sys/windows"
	"golang.org/x/sys/windows/registry"
	"golang.org/x/sys/windows/svc"
	"golang.org/x/sys/windows/svc/debug"
	"golang.org/x/sys/windows/svc/eventlog"

	"github.com/lxc/incus/v6/internal/ports"
	"github.com/lxc/incus/v6/internal/server/metrics"
	"github.com/lxc/incus/v6/internal/version"
	"github.com/lxc/incus/v6/shared/api"
	"github.com/lxc/incus/v6/shared/logger"
)

var (
	// https://dev.to/cosmic_predator/writing-a-windows-service-in-go-1d1m
	osBaseWorkingDirectory = "C:\\"
	osAgentConfigPath      = "C:\\Program Files\\Incus-Agent\\incus-agent.yml"
	osVioSerialPath        = `\\.\org.linuxcontainers.incus`
)

func osGetListener(port int64) (net.Listener, error) {
	const CIDAny uint32 = 4294967295 // Equivalent to VMADDR_CID_ANY.

	// Setup the listener on wildcard CID for inbound connections from Incus.
	// We use the VMADDR_CID_ANY CID so that if the VM's CID changes in the future the listener still works.
	// A CID change can occur when restoring a stateful VM that was previously using one CID but is
	// subsequently restored using a different one.
	l, err := vsock.ListenContextID(CIDAny, ports.HTTPSDefaultPort, nil)
	if err != nil {
		return nil, fmt.Errorf("WINDOWS: Failed to listen on vsock: %w", err)
	}

	logger.Info("Started vsock listener")

	return l, nil
}

// Start of Windows service code block
// Inspired of https://github.com/golang/sys/blob/master/windows/svc/example/service.go
var elog debug.Log

type incusAgentService struct {
	agentCmd *cmdAgent
}

func (m *incusAgentService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (ssec bool, errno uint32) {
	const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown

	tick := time.Tick(2 * time.Second)

	changes <- svc.Status{State: svc.StartPending}

	d := newDaemon(m.agentCmd.global.flagLogDebug, m.agentCmd.global.flagLogVerbose, m.agentCmd.global.flagSecretsLocation)

	// Start the server.
	err := startHTTPServer(d, m.agentCmd.global.flagLogDebug)
	if err != nil {
		changes <- svc.Status{State: svc.StopPending}
		elog.Error(1, fmt.Sprintf("Failed to start HTTP server: %s", err))
		return
	}

	changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}

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

	// Start status notifier in background.
	cancelStatusNotifier := m.agentCmd.startStatusNotifier(ctx, d.chConnected)
	defer cancelStatusNotifier()

loop:
	for {
		select {
		case <-tick:
		case c := <-r:
			switch c.Cmd {
			case svc.Interrogate:
				changes <- c.CurrentStatus
			case svc.Stop, svc.Shutdown:
				break loop
			default:
				elog.Error(1, fmt.Sprintf("Unexpected control request #%d", c))
			}
		}
	}

	changes <- svc.Status{State: svc.StopPending}

	return
}

func runService(name string, agentCmd *cmdAgent) error {
	var err error

	if agentCmd.global.flagLogDebug {
		elog = debug.New(name)
	} else {
		elog, err = eventlog.Open(name)
		if err != nil {
			return err
		}
	}

	defer elog.Close()

	elog.Info(1, fmt.Sprintf("Starting %s service", name))
	run := svc.Run
	if agentCmd.global.flagLogDebug {
		run = debug.Run
	}

	err = run(name, &incusAgentService{agentCmd: agentCmd})
	if err != nil {
		elog.Error(1, fmt.Sprintf("%s service failed: %v", name, err))
		return err
	}

	elog.Info(1, fmt.Sprintf("%s service stopped", name))
	return nil
}

// End of Windows service code block

func osGetEnvironment() (*api.ServerEnvironment, error) {
	serverName, err := os.Hostname()
	if err != nil {
		return nil, err
	}

	env := &api.ServerEnvironment{
		Kernel:             "Windows",
		KernelArchitecture: runtime.GOARCH,
		Server:             "incus-agent",
		ServerPid:          os.Getpid(),
		ServerVersion:      version.Version,
		ServerName:         serverName,
	}

	return env, nil
}

func osMountShared(src string, dst string, fstype string, opts []string) error {
	return errors.New("Dynamic mounts aren't supported on Windows")
}

func osUmount(src string, dst string, fstype string) error {
	return errors.New("Dynamic mounts aren't supported on Windows")
}

func osGetFilesystemMetrics(d *Daemon) ([]metrics.FilesystemMetrics, error) {
	partitions, err := disk.Partitions(true)
	if err != nil {
		return nil, err
	}

	sort.Slice(partitions, func(i, j int) bool {
		return partitions[i].Mountpoint < partitions[j].Mountpoint
	})

	fsMetrics := make([]metrics.FilesystemMetrics, 0, len(partitions))
	for _, partition := range partitions {
		usage, err := disk.Usage(partition.Mountpoint)
		if err != nil {
			continue
		}

		fsMetrics = append(fsMetrics, metrics.FilesystemMetrics{
			Device:         partition.Device,
			Mountpoint:     partition.Mountpoint,
			FSType:         partition.Fstype,
			AvailableBytes: usage.Free,
			FreeBytes:      usage.Free,
			SizeBytes:      usage.Total,
		})
	}

	return fsMetrics, nil
}

func osGetOSState() *api.InstanceStateOSInfo {
	// Get Windows registry.
	k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
	if err != nil {
		return nil
	}

	defer k.Close()

	// Get local hostname.
	hostname, err := os.Hostname()
	if err != nil {
		return nil
	}

	// Get build info.
	v := *windows.RtlGetVersion()

	osVersion, _, err := k.GetStringValue("CurrentVersion")
	if err != nil {
		return nil
	}

	osName, _, err := k.GetStringValue("ProductName")
	if err != nil {
		return nil
	}

	osBuild, _, err := k.GetStringValue("CurrentBuild")
	if err != nil {
		return nil
	}

	// Windows 11 always self-reports as Windows 10.
	// The documented diferentiator is the build ID.
	if v.BuildNumber > 22000 {
		osName = strings.Replace(osName, "Windows 10", "Windows 11", 1)
	}

	// Prepare OS struct.
	osInfo := &api.InstanceStateOSInfo{
		OS:            osName,
		OSVersion:     osBuild,
		KernelVersion: osVersion,
		Hostname:      hostname,
		FQDN:          hostname,
	}

	return osInfo
}

func osGetInteractiveConsole(s *execWs) (io.ReadWriteCloser, io.ReadWriteCloser, error) {
	return nil, nil, errors.New("Only non-interactive exec sessions are currently supported on Windows")
}

func osPrepareExecCommand(s *execWs, cmd *exec.Cmd) {
	if s.cwd == "" {
		cmd.Dir = osBaseWorkingDirectory
	}

	return
}

func osHandleExecControl(control api.InstanceExecControl, s *execWs, pty io.ReadWriteCloser, cmd *exec.Cmd, l logger.Logger) {
	// Ignore control messages.
	return
}

func osExitStatus(err error) (int, error) {
	return 0, err
}

func osSetEnv(post *api.InstanceExecPost, env map[string]string) {
	// SystemRoot is already set by default
	env["SystemDrive"] = "C:"

	// Program Files directories
	env["ProgramFiles"] = fmt.Sprintf("%s\\Program Files", env["SystemDrive"])
	env["ProgramFiles(x86)"] = fmt.Sprintf("%s (x86)", env["ProgramFiles"])
	env["ProgramW6432"] = fmt.Sprintf("%s", env["ProgramFiles"])
	env["CommonProgramFiles"] = fmt.Sprintf("%s\\Common Files", env["ProgramFiles"])
	env["CommonProgramFiles(x86)"] = fmt.Sprintf("%s (x86)\\Common Files", env["ProgramFiles"])
	env["CommonProgramW6432"] = fmt.Sprintf("%s\\Common Files", env["ProgramFiles"])

	// Windows directories
	env["WINDIR"] = fmt.Sprintf("%s\\WINDOWS", env["SystemDrive"])
	env["TMP"] = fmt.Sprintf("%s\\Temp", env["WINDIR"])
	env["TEMP"] = env["TMP"]

	// System32 directories
	system32 := fmt.Sprintf("%s\\System32", env["WINDIR"])
	env["ComSpec"] = fmt.Sprintf("%s\\cmd.exe", system32)
	env["DriverData"] = fmt.Sprintf("%s\\Drivers\\DriverData", system32)

	// User profile directories
	env["USERPROFILE"] = fmt.Sprintf("%s\\config\\systemprofile", system32)
	env["LOCALAPPDATA"] = fmt.Sprintf("%s\\AppData\\Local", env["USERPROFILE"])
	env["APPDATA"] = fmt.Sprintf("%s\\AppData\\Roaming", env["USERPROFILE"])

	// Miscellaneous
	env["COMPUTERNAME"] = ""
	env["PATH"] = fmt.Sprintf("%s;%s;%s\\WindowsPowerShell\\v1.0", system32, env["WINDIR"], system32)
	env["PATHEXT"] = "COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.CPL"

	env["ProgramData"] = fmt.Sprintf("%s\\ProgramData", env["SystemDrive"])
	env["ALLUSERSPROFILE"] = env["ProgramData"]
	env["PUBLIC"] = fmt.Sprintf("%s\\Users\\Public", env["SystemDrive"])

	// Set the default working directory.
	if post.Cwd == "" {
		post.Cwd = system32
	}
}
