//go:build windows

package systemops

import (
	"fmt"
	"net"
	"net/netip"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"sync"
	"time"

	log "github.com/sirupsen/logrus"
	"github.com/yusufpapurcu/wmi"

	"github.com/netbirdio/netbird/client/firewall/uspfilter"
	nbnet "github.com/netbirdio/netbird/util/net"
)

type MSFT_NetRoute struct {
	DestinationPrefix string
	NextHop           string
	InterfaceIndex    int32
	InterfaceAlias    string
	AddressFamily     uint16
}

type Route struct {
	Destination netip.Prefix
	Nexthop     netip.Addr
	Interface   *net.Interface
}

type MSFT_NetNeighbor struct {
	IPAddress        string
	LinkLayerAddress string
	State            uint8
	AddressFamily    uint16
	InterfaceIndex   uint32
	InterfaceAlias   string
}

type Neighbor struct {
	IPAddress        netip.Addr
	LinkLayerAddress string
	State            uint8
	AddressFamily    uint16
	InterfaceIndex   uint32
	InterfaceAlias   string
}

var prefixList []netip.Prefix
var lastUpdate time.Time
var mux = sync.Mutex{}

func (r *SysOps) SetupRouting(initAddresses []net.IP) (nbnet.AddHookFunc, nbnet.RemoveHookFunc, error) {
	return r.setupRefCounter(initAddresses)
}

func (r *SysOps) CleanupRouting() error {
	return r.cleanupRefCounter()
}

func (r *SysOps) addToRouteTable(prefix netip.Prefix, nexthop Nexthop) error {
	if nexthop.IP.Zone() != "" && nexthop.Intf == nil {
		zone, err := strconv.Atoi(nexthop.IP.Zone())
		if err != nil {
			return fmt.Errorf("invalid zone: %w", err)
		}
		nexthop.Intf = &net.Interface{Index: zone}
	}

	return addRouteCmd(prefix, nexthop)
}

func (r *SysOps) removeFromRouteTable(prefix netip.Prefix, nexthop Nexthop) error {
	args := []string{"delete", prefix.String()}
	if nexthop.IP.IsValid() {
		ip := nexthop.IP.WithZone("")
		args = append(args, ip.Unmap().String())
	}

	routeCmd := uspfilter.GetSystem32Command("route")

	out, err := exec.Command(routeCmd, args...).CombinedOutput()
	log.Tracef("route %s: %s", strings.Join(args, " "), out)

	if err != nil {
		return fmt.Errorf("remove route: %w", err)
	}
	return nil
}

func GetRoutesFromTable() ([]netip.Prefix, error) {
	mux.Lock()
	defer mux.Unlock()

	// If many routes are added at the same time this might block for a long time (seconds to minutes), so we cache the result
	if !isCacheDisabled() && time.Since(lastUpdate) < 2*time.Second {
		return prefixList, nil
	}

	routes, err := GetRoutes()
	if err != nil {
		return nil, fmt.Errorf("get routes: %w", err)
	}

	prefixList = nil
	for _, route := range routes {
		prefixList = append(prefixList, route.Destination)
	}

	lastUpdate = time.Now()
	return prefixList, nil
}

func GetRoutes() ([]Route, error) {
	var entries []MSFT_NetRoute

	query := `SELECT DestinationPrefix, Nexthop, InterfaceIndex, InterfaceAlias, AddressFamily FROM MSFT_NetRoute`
	if err := wmi.QueryNamespace(query, &entries, `ROOT\StandardCimv2`); err != nil {
		return nil, fmt.Errorf("get routes: %w", err)
	}

	var routes []Route
	for _, entry := range entries {
		dest, err := netip.ParsePrefix(entry.DestinationPrefix)
		if err != nil {
			log.Warnf("Unable to parse route destination %s: %v", entry.DestinationPrefix, err)
			continue
		}

		nexthop, err := netip.ParseAddr(entry.NextHop)
		if err != nil {
			log.Warnf("Unable to parse route next hop %s: %v", entry.NextHop, err)
			continue
		}

		var intf *net.Interface
		if entry.InterfaceIndex != 0 {
			intf = &net.Interface{
				Index: int(entry.InterfaceIndex),
				Name:  entry.InterfaceAlias,
			}

			if nexthop.Is6() && (nexthop.IsLinkLocalUnicast() || nexthop.IsLinkLocalMulticast()) {
				nexthop = nexthop.WithZone(strconv.Itoa(int(entry.InterfaceIndex)))
			}
		}

		routes = append(routes, Route{
			Destination: dest,
			Nexthop:     nexthop,
			Interface:   intf,
		})
	}

	return routes, nil
}

func GetNeighbors() ([]Neighbor, error) {
	var entries []MSFT_NetNeighbor
	query := `SELECT IPAddress, LinkLayerAddress, State, AddressFamily, InterfaceIndex, InterfaceAlias FROM MSFT_NetNeighbor`
	if err := wmi.QueryNamespace(query, &entries, `ROOT\StandardCimv2`); err != nil {
		return nil, fmt.Errorf("failed to query MSFT_NetNeighbor: %w", err)
	}

	var neighbors []Neighbor
	for _, entry := range entries {
		addr, err := netip.ParseAddr(entry.IPAddress)
		if err != nil {
			log.Warnf("Unable to parse neighbor IP address %s: %v", entry.IPAddress, err)
			continue
		}
		neighbors = append(neighbors, Neighbor{
			IPAddress:        addr,
			LinkLayerAddress: entry.LinkLayerAddress,
			State:            entry.State,
			AddressFamily:    entry.AddressFamily,
			InterfaceIndex:   entry.InterfaceIndex,
			InterfaceAlias:   entry.InterfaceAlias,
		})
	}

	return neighbors, nil
}

func addRouteCmd(prefix netip.Prefix, nexthop Nexthop) error {
	args := []string{"add", prefix.String()}

	if nexthop.IP.IsValid() {
		ip := nexthop.IP.WithZone("")
		args = append(args, ip.Unmap().String())
	} else {
		addr := "0.0.0.0"
		if prefix.Addr().Is6() {
			addr = "::"
		}
		args = append(args, addr)
	}

	if nexthop.Intf != nil {
		args = append(args, "if", strconv.Itoa(nexthop.Intf.Index))
	}

	routeCmd := uspfilter.GetSystem32Command("route")

	out, err := exec.Command(routeCmd, args...).CombinedOutput()
	log.Tracef("route %s: %s", strings.Join(args, " "), out)
	if err != nil {
		return fmt.Errorf("route add: %w", err)
	}

	return nil
}

func isCacheDisabled() bool {
	return os.Getenv("NB_DISABLE_ROUTE_CACHE") == "true"
}
