//go:build windows

package uvm

import (
	"context"
	"fmt"
	"net"
	"strings"

	"github.com/Microsoft/go-winio"
	"github.com/containerd/ttrpc"
	typeurl "github.com/containerd/typeurl/v2"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"github.com/Microsoft/hcsshim/hcn"
	"github.com/Microsoft/hcsshim/internal/computeagent"
	hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2"
	"github.com/Microsoft/hcsshim/internal/hns"
	"github.com/Microsoft/hcsshim/internal/log"
	ncproxynetworking "github.com/Microsoft/hcsshim/internal/ncproxy/networking"
	"github.com/Microsoft/hcsshim/internal/protocol/guestresource"
	"github.com/Microsoft/hcsshim/pkg/octtrpc"
)

func init() {
	typeurl.Register(&ncproxynetworking.Endpoint{}, "ncproxy/ncproxynetworking/Endpoint")
	typeurl.Register(&ncproxynetworking.Network{}, "ncproxy/ncproxynetworking/Network")
	typeurl.Register(&hcn.HostComputeEndpoint{}, "ncproxy/hcn/HostComputeEndpoint")
	typeurl.Register(&hcn.HostComputeNetwork{}, "ncproxy/hcn/HostComputeNetwork")
}

// This file holds the implementation of the Compute Agent service that is exposed for
// external network configuration.

const ComputeAgentAddrFmt = "\\\\.\\pipe\\computeagent-%s"

// create an interface here so we can mock out calls to the UtilityVM in our tests
type agentComputeSystem interface {
	AddEndpointToNSWithID(context.Context, string, string, *hns.HNSEndpoint) error
	UpdateNIC(context.Context, string, *hcsschema.NetworkAdapter) error
	RemoveEndpointFromNS(context.Context, string, *hns.HNSEndpoint) error
	AssignDevice(context.Context, string, uint16, string) (*VPCIDevice, error)
	RemoveDevice(context.Context, string, uint16) error
	AddNICInGuest(context.Context, *guestresource.LCOWNetworkAdapter) error
	RemoveNICInGuest(context.Context, *guestresource.LCOWNetworkAdapter) error
}

var _ agentComputeSystem = &UtilityVM{}

// mock hcn function for tests
var hnsGetHNSEndpointByName = hns.GetHNSEndpointByName

// computeAgent implements the ComputeAgent ttrpc service for adding and deleting NICs to a
// Utility VM.
type computeAgent struct {
	uvm agentComputeSystem
}

var _ computeagent.ComputeAgentService = &computeAgent{}

func (ca *computeAgent) AssignPCI(ctx context.Context, req *computeagent.AssignPCIInternalRequest) (*computeagent.AssignPCIInternalResponse, error) {
	log.G(ctx).WithFields(logrus.Fields{
		"containerID":          req.ContainerID,
		"deviceID":             req.DeviceID,
		"virtualFunctionIndex": req.VirtualFunctionIndex,
	}).Info("AssignPCI request")

	if req.DeviceID == "" {
		return nil, status.Error(codes.InvalidArgument, "received empty field in request")
	}

	dev, err := ca.uvm.AssignDevice(ctx, req.DeviceID, uint16(req.VirtualFunctionIndex), req.NicID)
	if err != nil {
		return nil, err
	}
	return &computeagent.AssignPCIInternalResponse{ID: dev.VMBusGUID}, nil
}

func (ca *computeAgent) RemovePCI(ctx context.Context, req *computeagent.RemovePCIInternalRequest) (*computeagent.RemovePCIInternalResponse, error) {
	log.G(ctx).WithFields(logrus.Fields{
		"containerID": req.ContainerID,
		"deviceID":    req.DeviceID,
	}).Info("RemovePCI request")

	if req.DeviceID == "" {
		return nil, status.Error(codes.InvalidArgument, "received empty field in request")
	}
	if err := ca.uvm.RemoveDevice(ctx, req.DeviceID, uint16(req.VirtualFunctionIndex)); err != nil {
		return nil, err
	}
	return &computeagent.RemovePCIInternalResponse{}, nil
}

// AddNIC will add a NIC to the computeagent services hosting UVM.
func (ca *computeAgent) AddNIC(ctx context.Context, req *computeagent.AddNICInternalRequest) (*computeagent.AddNICInternalResponse, error) {
	log.G(ctx).WithFields(logrus.Fields{
		"containerID": req.ContainerID,
		"endpoint":    req.Endpoint,
		"nicID":       req.NicID,
	}).Info("AddNIC request")

	if req.NicID == "" || req.Endpoint == nil {
		return nil, status.Error(codes.InvalidArgument, "received empty field in request")
	}

	endpoint, err := typeurl.UnmarshalAny(req.Endpoint)
	if err != nil {
		return nil, err
	}

	switch endpt := endpoint.(type) {
	case *ncproxynetworking.Endpoint:
		ipConfig := guestresource.LCOWIPConfig{
			IPAddress:    endpt.Settings.IPAddress,
			PrefixLength: uint8(endpt.Settings.IPAddressPrefixLength),
		}

		cfg := &guestresource.LCOWNetworkAdapter{
			NamespaceID:  endpt.NamespaceID,
			ID:           req.NicID,
			VPCIAssigned: true,
			IPConfigs:    []guestresource.LCOWIPConfig{ipConfig},
		}

		if endpt.Settings.DefaultGateway != "" {
			gw := net.ParseIP(endpt.Settings.DefaultGateway)
			if gw == nil {
				return nil, fmt.Errorf("invalid gateway address: %s", endpt.Settings.DefaultGateway)
			}
			route := guestresource.LCOWRoute{
				NextHop: endpt.Settings.DefaultGateway,
			}
			if gw.To4() != nil {
				route.DestinationPrefix = "0.0.0.0/0"
			} else if gw.To16() != nil {
				route.DestinationPrefix = "::/0"
			} else {
				return nil, fmt.Errorf("gateway address is neither IPv4 nor IPv6: %s", endpt.Settings.DefaultGateway)
			}
			cfg.Routes = []guestresource.LCOWRoute{route}
		}
		if err := ca.uvm.AddNICInGuest(ctx, cfg); err != nil {
			return nil, err
		}
	case *hcn.HostComputeEndpoint:
		hnsEndpoint, err := hnsGetHNSEndpointByName(endpt.Name)
		if err != nil {
			return nil, errors.Wrapf(err, "failed to get endpoint with name %q", endpt.Name)
		}
		if err := ca.uvm.AddEndpointToNSWithID(ctx, hnsEndpoint.Namespace.ID, req.NicID, hnsEndpoint); err != nil {
			return nil, err
		}
	default:
		return nil, status.Error(codes.InvalidArgument, "invalid request endpoint type")
	}

	return &computeagent.AddNICInternalResponse{}, nil
}

// ModifyNIC will modify a NIC from the computeagent services hosting UVM.
func (ca *computeAgent) ModifyNIC(ctx context.Context, req *computeagent.ModifyNICInternalRequest) (*computeagent.ModifyNICInternalResponse, error) {
	log.G(ctx).WithFields(logrus.Fields{
		"nicID":    req.NicID,
		"endpoint": req.Endpoint,
	}).Info("ModifyNIC request")

	if req.NicID == "" || req.Endpoint == nil || req.IovPolicySettings == nil {
		return nil, status.Error(codes.InvalidArgument, "received empty field in request")
	}

	endpoint, err := typeurl.UnmarshalAny(req.Endpoint)
	if err != nil {
		return nil, err
	}

	switch endpt := endpoint.(type) {
	case *ncproxynetworking.Endpoint:
		return nil, errors.New("modifying ncproxy networking endpoints is not supported")
	case *hcn.HostComputeEndpoint:
		hnsEndpoint, err := hnsGetHNSEndpointByName(endpt.Name)
		if err != nil {
			return nil, errors.Wrapf(err, "failed to get endpoint with name `%s`", endpt.Name)
		}

		moderationValue := hcsschema.InterruptModerationValue(req.IovPolicySettings.InterruptModeration)
		moderationName := hcsschema.InterruptModerationValueToName[moderationValue]

		iovSettings := &hcsschema.IovSettings{
			OffloadWeight:       &req.IovPolicySettings.IovOffloadWeight,
			QueuePairsRequested: &req.IovPolicySettings.QueuePairsRequested,
			InterruptModeration: &moderationName,
		}

		nic := &hcsschema.NetworkAdapter{
			EndpointId:  hnsEndpoint.Id,
			MacAddress:  hnsEndpoint.MacAddress,
			IovSettings: iovSettings,
		}

		if err := ca.uvm.UpdateNIC(ctx, req.NicID, nic); err != nil {
			return nil, errors.Wrap(err, "failed to update UVM's network adapter")
		}
	default:
		return nil, status.Error(codes.InvalidArgument, "invalid request endpoint type")
	}

	return &computeagent.ModifyNICInternalResponse{}, nil
}

// DeleteNIC will delete a NIC from the computeagent services hosting UVM.
func (ca *computeAgent) DeleteNIC(ctx context.Context, req *computeagent.DeleteNICInternalRequest) (*computeagent.DeleteNICInternalResponse, error) {
	log.G(ctx).WithFields(logrus.Fields{
		"containerID": req.ContainerID,
		"nicID":       req.NicID,
		"endpoint":    req.Endpoint,
	}).Info("DeleteNIC request")

	if req.NicID == "" || req.Endpoint == nil {
		return nil, status.Error(codes.InvalidArgument, "received empty field in request")
	}

	endpoint, err := typeurl.UnmarshalAny(req.Endpoint)
	if err != nil {
		return nil, err
	}

	switch endpt := endpoint.(type) {
	case *ncproxynetworking.Endpoint:
		cfg := &guestresource.LCOWNetworkAdapter{
			ID: req.NicID,
		}
		if err := ca.uvm.RemoveNICInGuest(ctx, cfg); err != nil {
			return nil, err
		}
	case *hcn.HostComputeEndpoint:
		hnsEndpoint, err := hnsGetHNSEndpointByName(endpt.Name)
		if err != nil {
			return nil, errors.Wrapf(err, "failed to get endpoint with name %q", endpt.Name)
		}
		if err := ca.uvm.RemoveEndpointFromNS(ctx, hnsEndpoint.Namespace.ID, hnsEndpoint); err != nil {
			return nil, err
		}
	default:
		return nil, status.Error(codes.InvalidArgument, "invalid request endpoint type")
	}

	return &computeagent.DeleteNICInternalResponse{}, nil
}

func setupAndServe(ctx context.Context, caAddr string, vm *UtilityVM) error {
	// Setup compute agent service
	l, err := winio.ListenPipe(caAddr, nil)
	if err != nil {
		return errors.Wrapf(err, "failed to listen on %s", caAddr)
	}
	s, err := ttrpc.NewServer(ttrpc.WithUnaryServerInterceptor(octtrpc.ServerInterceptor()))
	if err != nil {
		return err
	}
	computeagent.RegisterComputeAgentService(s, &computeAgent{vm})

	log.G(ctx).WithField("address", l.Addr().String()).Info("serving compute agent")
	go func() {
		defer l.Close()
		if err := trapClosedConnErr(s.Serve(ctx, l)); err != nil {
			log.G(ctx).WithError(err).Fatal("compute agent: serve failure")
		}
	}()

	return nil
}

func trapClosedConnErr(err error) error {
	if err == nil || strings.Contains(err.Error(), "use of closed network connection") {
		return nil
	}
	return err
}
