//go:build windows

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"net"
	"strings"

	"github.com/Microsoft/hcsshim/hcn"
	"github.com/Microsoft/hcsshim/internal/log"
	ncproxygrpc "github.com/Microsoft/hcsshim/pkg/ncproxy/ncproxygrpc/v1"
	"github.com/pkg/errors"
)

func hcnEndpointToEndpointResponse(ep *hcn.HostComputeEndpoint) (_ *ncproxygrpc.GetEndpointResponse, err error) {
	hcnEndpointResp := &ncproxygrpc.HcnEndpointSettings{
		Name:        ep.Name,
		Macaddress:  ep.MacAddress,
		NetworkName: ep.HostComputeNetwork,
		DnsSetting: &ncproxygrpc.DnsSetting{
			ServerIpAddrs: ep.Dns.ServerList,
			Domain:        ep.Dns.Domain,
			Search:        ep.Dns.Search,
		},
	}

	policies, err := parseEndpointPolicies(ep.Policies)
	if err != nil {
		return nil, err
	}
	hcnEndpointResp.Policies = policies

	ipConfigInfos := ep.IpConfigurations
	// there may be one ipv4 and/or one ipv6 configuration for an endpoint
	if len(ipConfigInfos) == 0 || len(ipConfigInfos) > 2 {
		return nil, errors.Errorf("invalid number (%v) of ip configuration information for endpoint %v", len(ipConfigInfos), ep.Name)
	}
	for _, ipConfig := range ipConfigInfos {
		ip := net.ParseIP(ipConfig.IpAddress)
		if ip == nil {
			return nil, errors.Errorf("failed to parse IP address %v", ipConfig.IpAddress)
		}
		if ip.To4() != nil {
			// this is an IPv4 address
			hcnEndpointResp.Ipaddress = ipConfig.IpAddress
			hcnEndpointResp.IpaddressPrefixlength = uint32(ipConfig.PrefixLength)
		} else {
			// this is an IPv6 address
			hcnEndpointResp.Ipv6Address = ipConfig.IpAddress
			hcnEndpointResp.Ipv6AddressPrefixlength = uint32(ipConfig.PrefixLength)
		}
	}

	return &ncproxygrpc.GetEndpointResponse{
		Namespace: ep.HostComputeNamespace,
		ID:        ep.Id,
		Endpoint: &ncproxygrpc.EndpointSettings{
			Settings: &ncproxygrpc.EndpointSettings_HcnEndpoint{
				HcnEndpoint: hcnEndpointResp,
			},
		},
	}, nil
}

func modifyEndpoint(ctx context.Context, id string, policies []hcn.EndpointPolicy, requestType hcn.RequestType) error {
	endpointRequest := hcn.PolicyEndpointRequest{
		Policies: policies,
	}

	settingsJSON, err := json.Marshal(endpointRequest)
	if err != nil {
		return err
	}

	requestMessage := &hcn.ModifyEndpointSettingRequest{
		ResourceType: hcn.EndpointResourceTypePolicy,
		RequestType:  requestType,
		Settings:     settingsJSON,
	}

	return hcn.ModifyEndpointSettings(id, requestMessage)
}

func parseEndpointPolicies(policies []hcn.EndpointPolicy) (*ncproxygrpc.HcnEndpointPolicies, error) {
	results := &ncproxygrpc.HcnEndpointPolicies{}
	for _, policy := range policies {
		switch policy.Type {
		case hcn.PortMapping:
			portMapSettings := &hcn.PortnameEndpointPolicySetting{}
			if err := json.Unmarshal(policy.Settings, portMapSettings); err != nil {
				return nil, err
			}
			results.PortnamePolicySetting = &ncproxygrpc.PortNameEndpointPolicySetting{
				PortName: portMapSettings.Name,
			}
		case hcn.IOV:
			iovSettings := &hcn.IovPolicySetting{}
			if err := json.Unmarshal(policy.Settings, iovSettings); err != nil {
				return nil, err
			}
			results.IovPolicySettings = &ncproxygrpc.IovEndpointPolicySetting{
				IovOffloadWeight:    iovSettings.IovOffloadWeight,
				QueuePairsRequested: iovSettings.QueuePairsRequested,
				InterruptModeration: iovSettings.InterruptModeration,
			}
		}
	}
	return results, nil
}

func constructEndpointPolicies(req *ncproxygrpc.HcnEndpointPolicies) ([]hcn.EndpointPolicy, error) {
	policies := []hcn.EndpointPolicy{}
	if req.IovPolicySettings != nil {
		iovSettings := hcn.IovPolicySetting{
			IovOffloadWeight:    req.IovPolicySettings.IovOffloadWeight,
			QueuePairsRequested: req.IovPolicySettings.QueuePairsRequested,
			InterruptModeration: req.IovPolicySettings.InterruptModeration,
		}
		iovJSON, err := json.Marshal(iovSettings)
		if err != nil {
			return []hcn.EndpointPolicy{}, errors.Wrap(err, "failed to marshal IovPolicySettings")
		}
		policy := hcn.EndpointPolicy{
			Type:     hcn.IOV,
			Settings: iovJSON,
		}
		policies = append(policies, policy)
	}

	if req.PortnamePolicySetting != nil {
		portPolicy := hcn.PortnameEndpointPolicySetting{
			Name: req.PortnamePolicySetting.PortName,
		}
		portPolicyJSON, err := json.Marshal(portPolicy)
		if err != nil {
			return []hcn.EndpointPolicy{}, errors.Wrap(err, "failed to marshal portname")
		}
		policy := hcn.EndpointPolicy{
			Type:     hcn.PortName,
			Settings: portPolicyJSON,
		}
		policies = append(policies, policy)
	}

	return policies, nil
}

func createHCNNetwork(ctx context.Context, req *ncproxygrpc.HostComputeNetworkSettings) (*hcn.HostComputeNetwork, error) {
	// Check if the network already exists, and if so return error.
	_, err := hcn.GetNetworkByName(req.Name)
	if err == nil {
		return nil, errors.Errorf("network with name %q already exists", req.Name)
	}

	policies := []hcn.NetworkPolicy{}
	if req.SwitchName != "" {
		// Get the layer ID from the external switch. HNS will create a transparent network for
		// any external switch that is created not through HNS so this is what we're
		// searching for here. If the network exists, the vSwitch with this name exists.
		extSwitch, err := hcn.GetNetworkByName(req.SwitchName)
		if err != nil {
			if _, ok := err.(hcn.NetworkNotFoundError); ok { //nolint:errorlint
				return nil, errors.Errorf("no network/switch with name `%s` found", req.SwitchName)
			}
			return nil, errors.Wrapf(err, "failed to get network/switch with name %q", req.SwitchName)
		}

		// Get layer ID and use this as the basis for what to layer the new network over.
		if extSwitch.Health.Extra.LayeredOn == "" {
			return nil, errors.Errorf("no layer ID found for network %q found", extSwitch.Id)
		}

		layerPolicy := hcn.LayerConstraintNetworkPolicySetting{LayerId: extSwitch.Health.Extra.LayeredOn}
		data, err := json.Marshal(layerPolicy)
		if err != nil {
			return nil, errors.Wrap(err, "failed to marshal layer policy")
		}

		netPolicy := hcn.NetworkPolicy{
			Type:     hcn.LayerConstraint,
			Settings: data,
		}
		policies = append(policies, netPolicy)
	}

	subnets := make([]hcn.Subnet, 0, len(req.SubnetIpaddressPrefix)+len(req.SubnetIpaddressPrefixIpv6))
	for _, addrPrefix := range req.SubnetIpaddressPrefix {
		subnet := hcn.Subnet{
			IpAddressPrefix: addrPrefix,
			Routes: []hcn.Route{
				{
					NextHop:           req.DefaultGateway,
					DestinationPrefix: "0.0.0.0/0",
				},
			},
		}
		subnets = append(subnets, subnet)
	}

	if len(req.SubnetIpaddressPrefixIpv6) != 0 {
		if err := hcn.IPv6DualStackSupported(); err != nil {
			// a request was made for an IPv6 address on a system that doesn't support IPv6
			return nil, fmt.Errorf("IPv6 address requested but not supported: %w", err)
		}
	}

	for _, ipv6AddrPrefix := range req.SubnetIpaddressPrefixIpv6 {
		subnet := hcn.Subnet{
			IpAddressPrefix: ipv6AddrPrefix,
			Routes: []hcn.Route{
				{
					NextHop:           req.DefaultGatewayIpv6,
					DestinationPrefix: "::/0",
				},
			},
		}
		subnets = append(subnets, subnet)
	}

	ipam := hcn.Ipam{
		Type:    req.IpamType.String(),
		Subnets: subnets,
	}

	network := &hcn.HostComputeNetwork{
		Name:     req.Name,
		Type:     hcn.NetworkType(req.Mode.String()),
		Ipams:    []hcn.Ipam{ipam},
		Policies: policies,
		SchemaVersion: hcn.SchemaVersion{
			Major: 2,
			Minor: 2,
		},
	}

	network, err = network.Create()
	if err != nil {
		return nil, errors.Wrapf(err, "failed to create HNS network %q", req.Name)
	}

	return network, nil
}

func hcnNetworkToNetworkResponse(ctx context.Context, network *hcn.HostComputeNetwork) (*ncproxygrpc.GetNetworkResponse, error) {
	var (
		ipamType                  int32
		defaultGateway            string
		defaultGatewayIPv6        string
		switchName                string
		subnetIPAddressPrefixes   []string
		subnetIPv6AddressPrefixes []string
	)

	for _, ipam := range network.Ipams {
		// all ipams should have the same type so just keep the last one
		ipamType = ncproxygrpc.HostComputeNetworkSettings_IpamType_value[ipam.Type]
		for _, subnet := range ipam.Subnets {
			// split prefix off string so we can check if this is ipv4 or ipv6
			ipParts := strings.Split(subnet.IpAddressPrefix, "/")
			ipPrefix := net.ParseIP(ipParts[0])
			if ipPrefix == nil {
				return nil, fmt.Errorf("failed to parse IP address %v", ipPrefix)
			}
			if ipPrefix.To4() != nil {
				// this is an IPv4 address
				subnetIPAddressPrefixes = append(subnetIPAddressPrefixes, subnet.IpAddressPrefix)
				if len(subnet.Routes) != 0 {
					defaultGateway = subnet.Routes[0].NextHop
				}
			} else {
				// this is an IPv6 address
				subnetIPv6AddressPrefixes = append(subnetIPv6AddressPrefixes, subnet.IpAddressPrefix)
				if len(subnet.Routes) != 0 {
					defaultGatewayIPv6 = subnet.Routes[0].NextHop
				}
			}
		}
	}

	mode := ncproxygrpc.HostComputeNetworkSettings_NetworkMode_value[string(network.Type)]

	if network.Health.Extra.SwitchGuid != "" {
		extSwitch, err := hcn.GetNetworkByID(network.Health.Extra.SwitchGuid)
		if err != nil {
			return nil, err
		}
		switchName = extSwitch.Name
	}

	settings := &ncproxygrpc.HostComputeNetworkSettings{
		Name:                      network.Name,
		Mode:                      ncproxygrpc.HostComputeNetworkSettings_NetworkMode(mode),
		SwitchName:                switchName,
		IpamType:                  ncproxygrpc.HostComputeNetworkSettings_IpamType(ipamType),
		SubnetIpaddressPrefix:     subnetIPAddressPrefixes,
		DefaultGateway:            defaultGateway,
		SubnetIpaddressPrefixIpv6: subnetIPv6AddressPrefixes,
		DefaultGatewayIpv6:        defaultGatewayIPv6,
	}

	var startMac, endMac string
	switch n := len(network.MacPool.Ranges); {
	case n < 1:
		return nil, fmt.Errorf("network %s(%s) MAC pool is empty", network.Name, network.Id)
	case n > 1:
		log.G(ctx).WithField("networkName", network.Name).Warn("network has multiple MAC pools, only returning the first")
		fallthrough
	default:
		startMac = network.MacPool.Ranges[0].StartMacAddress
		endMac = network.MacPool.Ranges[0].EndMacAddress
	}

	return &ncproxygrpc.GetNetworkResponse{
		ID: network.Id,
		Network: &ncproxygrpc.Network{
			Settings: &ncproxygrpc.Network_HcnNetwork{
				HcnNetwork: settings,
			},
		},
		MacRange: &ncproxygrpc.MacRange{
			StartMacAddress: startMac,
			EndMacAddress:   endMac,
		},
	}, nil
}

func createHCNEndpoint(ctx context.Context, network *hcn.HostComputeNetwork, req *ncproxygrpc.HcnEndpointSettings) (*hcn.HostComputeEndpoint, error) {
	// Construct ip config.
	ipConfigs := []hcn.IpConfig{}
	if req.Ipaddress != "" && req.IpaddressPrefixlength != 0 {
		ipv4Config := hcn.IpConfig{
			IpAddress:    req.Ipaddress,
			PrefixLength: uint8(req.IpaddressPrefixlength),
		}
		ipConfigs = append(ipConfigs, ipv4Config)
	}

	if req.Ipv6Address != "" && req.Ipv6AddressPrefixlength != 0 {
		if err := hcn.IPv6DualStackSupported(); err != nil {
			// a request was made for an IPv6 address on a system that doesn't support IPv6
			return nil, fmt.Errorf("IPv6 address requested but not supported: %w", err)
		}
		ipv6Config := hcn.IpConfig{
			IpAddress:    req.Ipv6Address,
			PrefixLength: uint8(req.Ipv6AddressPrefixlength),
		}
		ipConfigs = append(ipConfigs, ipv6Config)
	}

	var err error
	policies := []hcn.EndpointPolicy{}
	if req.Policies != nil {
		policies, err = constructEndpointPolicies(req.Policies)
		if err != nil {
			return nil, errors.Wrap(err, "failed to construct endpoint policies")
		}
	}

	endpoint := &hcn.HostComputeEndpoint{
		Name:               req.Name,
		HostComputeNetwork: network.Id,
		MacAddress:         req.Macaddress,
		IpConfigurations:   ipConfigs,
		Policies:           policies,
		SchemaVersion: hcn.SchemaVersion{
			Major: 2,
			Minor: 0,
		},
	}

	if req.DnsSetting != nil {
		endpoint.Dns = hcn.Dns{
			ServerList: req.DnsSetting.ServerIpAddrs,
			Domain:     req.DnsSetting.Domain,
			Search:     req.DnsSetting.Search,
		}
	}
	endpoint, err = endpoint.Create()
	if err != nil {
		return nil, errors.Wrap(err, "failed to create HNS endpoint")
	}

	return endpoint, nil
}

// getHostDefaultNamespace returns the first namespace found that has type [hcn.NamespaceTypeHostDefault],
// or an error if none is found.
func getHostDefaultNamespace() (string, error) {
	namespaces, err := hcn.ListNamespaces()
	if err != nil {
		return "", errors.Wrapf(err, "failed list namespaces")
	}

	for _, ns := range namespaces {
		if ns.Type == hcn.NamespaceTypeHostDefault {
			return ns.Id, nil
		}
	}
	return "", errors.New("unable to find default host namespace to attach to")
}
