//go:build windows
// +build windows

package dev

import (
	"bytes"
	"errors"
	"fmt"
	"net"
	"sort"
	"sync"
	"sync/atomic"
	"time"

	"github.com/Dreamacro/clash/listener/tun/dev/wintun"
	"github.com/Dreamacro/clash/log"

	"golang.org/x/sys/windows"
	"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
)

const messageTransportHeaderSize = 0 // size of data preceding content in transport message

type tunWindows struct {
	wt        *wintun.Adapter
	handle    windows.Handle
	close     int32
	running   sync.WaitGroup
	forcedMTU int
	rate      rateJuggler
	session   wintun.Session
	readWait  windows.Handle
	closeOnce sync.Once

	url        string
	name       string
	tunAddress string
	autoRoute  bool
}

// OpenTunDevice return a TunDevice according a URL
func OpenTunDevice(tunAddress string, autoRoute bool) (TunDevice, error) {

	requestedGUID, err := windows.GUIDFromString("{330EAEF8-7578-5DF2-D97B-8DADC0EA85CB}")
	if err == nil {
		WintunStaticRequestedGUID = &requestedGUID
		log.Debugln("Generate GUID: %s", WintunStaticRequestedGUID.String())
	} else {
		log.Warnln("Error parese GUID from string: %v", err)
	}

	interfaceName := "Meta Tunnel"
	mtu := 9000

	tun, err := CreateTUN(interfaceName, mtu, tunAddress, autoRoute)
	if err != nil {
		return nil, err
	}

	return tun, nil
}

//
// CreateTUN creates a Wintun interface with the given name. Should a Wintun
// interface with the same name exist, it is reused.
//
func CreateTUN(ifname string, mtu int, tunAddress string, autoRoute bool) (TunDevice, error) {
	return CreateTUNWithRequestedGUID(ifname, WintunStaticRequestedGUID, mtu, tunAddress, autoRoute)
}

//
// CreateTUNWithRequestedGUID creates a Wintun interface with the given name and
// a requested GUID. Should a Wintun interface with the same name exist, it is reused.
//
func CreateTUNWithRequestedGUID(ifname string, requestedGUID *windows.GUID, mtu int, tunAddress string, autoRoute bool) (TunDevice, error) {
	wt, err := wintun.CreateAdapter(ifname, WintunTunnelType, requestedGUID)
	if err != nil {
		return nil, fmt.Errorf("Error creating interface: %w", err)
	}

	forcedMTU := 1420
	if mtu > 0 {
		forcedMTU = mtu
	}

	tun := &tunWindows{
		name:       ifname,
		wt:         wt,
		handle:     windows.InvalidHandle,
		forcedMTU:  forcedMTU,
		tunAddress: tunAddress,
		autoRoute:  autoRoute,
	}

	// config tun ip
	err = tun.configureInterface()
	if err != nil {
		tun.wt.Close()
		return nil, fmt.Errorf("error configure interface: %w", err)
	}

	tun.session, err = wt.StartSession(0x800000) // Ring capacity, 8 MiB
	if err != nil {
		tun.wt.Close()
		return nil, fmt.Errorf("error starting session: %w", err)
	}
	tun.readWait = tun.session.ReadWaitEvent()
	return tun, nil
}

func (tun *tunWindows) Name() string {
	return tun.name
}

func (tun *tunWindows) IsClose() bool {
	return atomic.LoadInt32(&tun.close) == 1
}

func (tun *tunWindows) Read(buff []byte) (int, error) {
	return tun.Read0(buff, messageTransportHeaderSize)
}

func (tun *tunWindows) Write(buff []byte) (int, error) {
	return tun.Write0(buff, messageTransportHeaderSize)
}

func (tun *tunWindows) URL() string {
	return fmt.Sprintf("dev://%s", tun.Name())
}

func (tun *tunWindows) configureInterface() error {
	retryOnFailure := wintun.StartedAtBoot()
	tryTimes := 0
startOver:
	var err error
	if tryTimes > 0 {
		log.Infoln("Retrying interface configuration after failure because system just booted (T+%v): %v", windows.DurationSinceBoot(), err)
		time.Sleep(time.Second)
		retryOnFailure = retryOnFailure && tryTimes < 15
	}
	tryTimes++

	luid := winipcfg.LUID(tun.LUID())
	log.Infoln("[wintun]: tun adapter LUID: %d", luid)
	mtu, err := tun.MTU()

	if err != nil {
		return errors.New("unable to get device mtu")
	}

	family := winipcfg.AddressFamily(windows.AF_INET)
	familyV6 := winipcfg.AddressFamily(windows.AF_INET6)

	tunAddress := wintun.ParseIPCidr(tun.tunAddress + "/16")

	addresses := []net.IPNet{tunAddress.IPNet()}

	err = luid.FlushIPAddresses(familyV6)
	if err == windows.ERROR_NOT_FOUND && retryOnFailure {
		goto startOver
	} else if err != nil {
		return err
	}
	err = luid.FlushDNS(family)
	if err == windows.ERROR_NOT_FOUND && retryOnFailure {
		goto startOver
	} else if err != nil {
		return err
	}
	err = luid.FlushDNS(familyV6)
	if err == windows.ERROR_NOT_FOUND && retryOnFailure {
		goto startOver
	} else if err != nil {
		return err
	}
	err = luid.FlushRoutes(familyV6)
	if err == windows.ERROR_NOT_FOUND && retryOnFailure {
		goto startOver
	} else if err != nil {
		return err
	}

	foundDefault4 := false
	foundDefault6 := false

	if tun.autoRoute {
		allowedIPs := []*wintun.IPCidr{
			//wintun.ParseIPCidr("0.0.0.0/0"),
			wintun.ParseIPCidr("1.0.0.0/8"),
			wintun.ParseIPCidr("2.0.0.0/7"),
			wintun.ParseIPCidr("4.0.0.0/6"),
			wintun.ParseIPCidr("8.0.0.0/5"),
			wintun.ParseIPCidr("16.0.0.0/4"),
			wintun.ParseIPCidr("32.0.0.0/3"),
			wintun.ParseIPCidr("64.0.0.0/2"),
			wintun.ParseIPCidr("128.0.0.0/1"),
			wintun.ParseIPCidr("224.0.0.0/4"),
			wintun.ParseIPCidr("255.255.255.255/32"),
		}

		estimatedRouteCount := len(allowedIPs)
		routes := make([]winipcfg.RouteData, 0, estimatedRouteCount)
		var haveV4Address, haveV6Address bool = true, false

		for _, allowedip := range allowedIPs {
			allowedip.MaskSelf()
			if (allowedip.Bits() == 32 && !haveV4Address) || (allowedip.Bits() == 128 && !haveV6Address) {
				continue
			}
			route := winipcfg.RouteData{
				Destination: allowedip.IPNet(),
				Metric:      0,
			}
			if allowedip.Bits() == 32 {
				if allowedip.Cidr == 0 {
					foundDefault4 = true
				}
				route.NextHop = net.IPv4zero
			} else if allowedip.Bits() == 128 {
				if allowedip.Cidr == 0 {
					foundDefault6 = true
				}
				route.NextHop = net.IPv6zero
			}
			routes = append(routes, route)
		}

		deduplicatedRoutes := make([]*winipcfg.RouteData, 0, len(routes))
		sort.Slice(routes, func(i, j int) bool {
			if routes[i].Metric != routes[j].Metric {
				return routes[i].Metric < routes[j].Metric
			}
			if c := bytes.Compare(routes[i].NextHop, routes[j].NextHop); c != 0 {
				return c < 0
			}
			if c := bytes.Compare(routes[i].Destination.IP, routes[j].Destination.IP); c != 0 {
				return c < 0
			}
			if c := bytes.Compare(routes[i].Destination.Mask, routes[j].Destination.Mask); c != 0 {
				return c < 0
			}
			return false
		})
		for i := 0; i < len(routes); i++ {
			if i > 0 && routes[i].Metric == routes[i-1].Metric &&
				bytes.Equal(routes[i].NextHop, routes[i-1].NextHop) &&
				bytes.Equal(routes[i].Destination.IP, routes[i-1].Destination.IP) &&
				bytes.Equal(routes[i].Destination.Mask, routes[i-1].Destination.Mask) {
				continue
			}
			deduplicatedRoutes = append(deduplicatedRoutes, &routes[i])
		}

		err = luid.SetRoutesForFamily(family, deduplicatedRoutes)
		if err == windows.ERROR_NOT_FOUND && retryOnFailure {
			goto startOver
		} else if err != nil {
			return fmt.Errorf("unable to set routes: %w", err)
		}
	}

	err = luid.SetIPAddressesForFamily(family, addresses)
	if err == windows.ERROR_OBJECT_ALREADY_EXISTS {
		cleanupAddressesOnDisconnectedInterfaces(family, addresses)
		err = luid.SetIPAddressesForFamily(family, addresses)
	}
	if err == windows.ERROR_NOT_FOUND && retryOnFailure {
		goto startOver
	} else if err != nil {
		return fmt.Errorf("unable to set ips: %w", err)
	}

	var ipif *winipcfg.MibIPInterfaceRow
	ipif, err = luid.IPInterface(family)
	if err != nil {
		return err
	}
	ipif.RouterDiscoveryBehavior = winipcfg.RouterDiscoveryDisabled
	ipif.DadTransmits = 0
	ipif.ManagedAddressConfigurationSupported = false
	ipif.OtherStatefulConfigurationSupported = false
	if mtu > 0 {
		ipif.NLMTU = uint32(mtu)
	}
	if (family == windows.AF_INET && foundDefault4) || (family == windows.AF_INET6 && foundDefault6) {
		ipif.UseAutomaticMetric = false
		ipif.Metric = 0
	}
	err = ipif.Set()
	if err == windows.ERROR_NOT_FOUND && retryOnFailure {
		goto startOver
	} else if err != nil {
		return fmt.Errorf("unable to set metric and MTU: %w", err)
	}

	var ipif6 *winipcfg.MibIPInterfaceRow
	ipif6, err = luid.IPInterface(familyV6)
	if err != nil {
		return err
	}
	ipif6.RouterDiscoveryBehavior = winipcfg.RouterDiscoveryDisabled
	ipif6.DadTransmits = 0
	ipif6.ManagedAddressConfigurationSupported = false
	ipif6.OtherStatefulConfigurationSupported = false

	err = ipif6.Set()
	if err == windows.ERROR_NOT_FOUND && retryOnFailure {
		goto startOver
	} else if err != nil {
		return fmt.Errorf("unable to set v6 metric and MTU: %w", err)
	}

	err = luid.SetDNS(family, []net.IP{net.ParseIP("198.18.0.2")}, nil)
	if err == windows.ERROR_NOT_FOUND && retryOnFailure {
		goto startOver
	} else if err != nil {
		return fmt.Errorf("unable to set DNS %s %s: %w", "198.18.0.2", "nil", err)
	}
	return nil
}

func cleanupAddressesOnDisconnectedInterfaces(family winipcfg.AddressFamily, addresses []net.IPNet) {
	if len(addresses) == 0 {
		return
	}
	addrToStr := func(ip *net.IP) string {
		if ip4 := ip.To4(); ip4 != nil {
			return string(ip4)
		}
		return string(*ip)
	}
	addrHash := make(map[string]bool, len(addresses))
	for i := range addresses {
		addrHash[addrToStr(&addresses[i].IP)] = true
	}
	interfaces, err := winipcfg.GetAdaptersAddresses(family, winipcfg.GAAFlagDefault)
	if err != nil {
		return
	}
	for _, iface := range interfaces {
		if iface.OperStatus == winipcfg.IfOperStatusUp {
			continue
		}
		for address := iface.FirstUnicastAddress; address != nil; address = address.Next {
			ip := address.Address.IP()
			if addrHash[addrToStr(&ip)] {
				ipnet := net.IPNet{IP: ip, Mask: net.CIDRMask(int(address.OnLinkPrefixLength), 8*len(ip))}
				log.Infoln("Cleaning up stale address %s from interface ‘%s’", ipnet.String(), iface.FriendlyName())
				iface.LUID.DeleteIPAddress(ipnet)
			}
		}
	}
}

// GetAutoDetectInterface get ethernet interface
func GetAutoDetectInterface() (string, error) {
	ifname, err := getAutoDetectInterfaceByFamily(winipcfg.AddressFamily(windows.AF_INET))
	if err == nil {
		return ifname, err
	}

	return getAutoDetectInterfaceByFamily(winipcfg.AddressFamily(windows.AF_INET6))
}

func getAutoDetectInterfaceByFamily(family winipcfg.AddressFamily) (string, error) {
	interfaces, err := winipcfg.GetAdaptersAddresses(family, winipcfg.GAAFlagIncludeGateways)
	if err != nil {
		return "", fmt.Errorf("get ethernet interface failure. %w", err)
	}
	for _, iface := range interfaces {
		if iface.OperStatus != winipcfg.IfOperStatusUp {
			continue
		}

		ifname := iface.FriendlyName()
		if ifname == "Clash" {
			continue
		}

		for gatewayAddress := iface.FirstGatewayAddress; gatewayAddress != nil; gatewayAddress = gatewayAddress.Next {
			nextHop := gatewayAddress.Address.IP()

			var ipnet net.IPNet
			if family == windows.AF_INET {
				ipnet = net.IPNet{IP: net.IPv4zero, Mask: net.CIDRMask(0, 32)}
			} else {
				ipnet = net.IPNet{IP: net.IPv6zero, Mask: net.CIDRMask(0, 128)}
			}

			if _, err = iface.LUID.Route(ipnet, nextHop); err == nil {
				return ifname, nil
			}
		}
	}

	return "", errors.New("ethernet interface not found")
}
