package tailnet

import (
	"net/netip"

	"github.com/google/uuid"
	"golang.org/x/xerrors"
	"google.golang.org/protobuf/types/known/timestamppb"
	"tailscale.com/tailcfg"
	"tailscale.com/types/key"

	"github.com/coder/coder/v2/tailnet/proto"
)

func UUIDToByteSlice(u uuid.UUID) []byte {
	b := [16]byte(u)
	o := make([]byte, 16)
	copy(o, b[:]) // copy so that we can't mutate the original
	return o
}

func NodeToProto(n *Node) (*proto.Node, error) {
	k, err := n.Key.MarshalBinary()
	if err != nil {
		return nil, err
	}
	disco, err := n.DiscoKey.MarshalText()
	if err != nil {
		return nil, err
	}
	derpForcedWebsocket := make(map[int32]string)
	for i, s := range n.DERPForcedWebsocket {
		derpForcedWebsocket[int32(i)] = s
	}
	addresses := make([]string, len(n.Addresses))
	for i, prefix := range n.Addresses {
		s, err := prefix.MarshalText()
		if err != nil {
			return nil, err
		}
		addresses[i] = string(s)
	}
	allowedIPs := make([]string, len(n.AllowedIPs))
	for i, prefix := range n.AllowedIPs {
		s, err := prefix.MarshalText()
		if err != nil {
			return nil, err
		}
		allowedIPs[i] = string(s)
	}
	return &proto.Node{
		Id:                  int64(n.ID),
		AsOf:                timestamppb.New(n.AsOf),
		Key:                 k,
		Disco:               string(disco),
		PreferredDerp:       int32(n.PreferredDERP),
		DerpLatency:         n.DERPLatency,
		DerpForcedWebsocket: derpForcedWebsocket,
		Addresses:           addresses,
		AllowedIps:          allowedIPs,
		Endpoints:           n.Endpoints,
	}, nil
}

func ProtoToNode(p *proto.Node) (*Node, error) {
	k := key.NodePublic{}
	err := k.UnmarshalBinary(p.GetKey())
	if err != nil {
		return nil, err
	}
	disco := key.DiscoPublic{}
	err = disco.UnmarshalText([]byte(p.GetDisco()))
	if err != nil {
		return nil, err
	}
	derpForcedWebsocket := make(map[int]string)
	for i, s := range p.GetDerpForcedWebsocket() {
		derpForcedWebsocket[int(i)] = s
	}
	addresses := make([]netip.Prefix, len(p.GetAddresses()))
	for i, prefix := range p.GetAddresses() {
		err = addresses[i].UnmarshalText([]byte(prefix))
		if err != nil {
			return nil, err
		}
	}
	allowedIPs := make([]netip.Prefix, len(p.GetAllowedIps()))
	for i, prefix := range p.GetAllowedIps() {
		err = allowedIPs[i].UnmarshalText([]byte(prefix))
		if err != nil {
			return nil, err
		}
	}
	return &Node{
		ID:                  tailcfg.NodeID(p.GetId()),
		AsOf:                p.GetAsOf().AsTime(),
		Key:                 k,
		DiscoKey:            disco,
		PreferredDERP:       int(p.GetPreferredDerp()),
		DERPLatency:         p.GetDerpLatency(),
		DERPForcedWebsocket: derpForcedWebsocket,
		Addresses:           addresses,
		AllowedIPs:          allowedIPs,
		Endpoints:           p.Endpoints,
	}, nil
}

func OnlyNodeUpdates(resp *proto.CoordinateResponse) ([]*Node, error) {
	nodes := make([]*Node, 0, len(resp.GetPeerUpdates()))
	for _, pu := range resp.GetPeerUpdates() {
		if pu.Kind != proto.CoordinateResponse_PeerUpdate_NODE {
			continue
		}
		n, err := ProtoToNode(pu.Node)
		if err != nil {
			return nil, xerrors.Errorf("failed conversion from protobuf: %w", err)
		}
		nodes = append(nodes, n)
	}
	return nodes, nil
}

func SingleNodeUpdate(id uuid.UUID, node *Node, reason string) (*proto.CoordinateResponse, error) {
	p, err := NodeToProto(node)
	if err != nil {
		return nil, xerrors.Errorf("node failed conversion to protobuf: %w", err)
	}
	return &proto.CoordinateResponse{
		PeerUpdates: []*proto.CoordinateResponse_PeerUpdate{
			{
				Kind:   proto.CoordinateResponse_PeerUpdate_NODE,
				Id:     UUIDToByteSlice(id),
				Node:   p,
				Reason: reason,
			},
		},
	}, nil
}

func DERPMapToProto(derpMap *tailcfg.DERPMap) *proto.DERPMap {
	if derpMap == nil {
		return nil
	}

	regionScore := make(map[int64]float64)
	if derpMap.HomeParams != nil {
		for k, v := range derpMap.HomeParams.RegionScore {
			regionScore[int64(k)] = v
		}
	}

	regions := make(map[int64]*proto.DERPMap_Region, len(derpMap.Regions))
	for regionID, region := range derpMap.Regions {
		regions[int64(regionID)] = DERPRegionToProto(region)
	}

	return &proto.DERPMap{
		HomeParams: &proto.DERPMap_HomeParams{
			RegionScore: regionScore,
		},
		Regions: regions,
	}
}

func DERPRegionToProto(region *tailcfg.DERPRegion) *proto.DERPMap_Region {
	if region == nil {
		return nil
	}

	regionNodes := make([]*proto.DERPMap_Region_Node, len(region.Nodes))
	for i, node := range region.Nodes {
		regionNodes[i] = DERPNodeToProto(node)
	}

	return &proto.DERPMap_Region{
		RegionId:      int64(region.RegionID),
		EmbeddedRelay: region.EmbeddedRelay,
		RegionCode:    region.RegionCode,
		RegionName:    region.RegionName,
		Avoid:         region.Avoid,

		Nodes: regionNodes,
	}
}

func DERPNodeToProto(node *tailcfg.DERPNode) *proto.DERPMap_Region_Node {
	if node == nil {
		return nil
	}

	return &proto.DERPMap_Region_Node{
		Name:             node.Name,
		RegionId:         int64(node.RegionID),
		HostName:         node.HostName,
		CertName:         node.CertName,
		Ipv4:             node.IPv4,
		Ipv6:             node.IPv6,
		StunPort:         int32(node.STUNPort),
		StunOnly:         node.STUNOnly,
		DerpPort:         int32(node.DERPPort),
		InsecureForTests: node.InsecureForTests,
		ForceHttp:        node.ForceHTTP,
		StunTestIp:       node.STUNTestIP,
		CanPort_80:       node.CanPort80,
	}
}

func DERPMapFromProto(derpMap *proto.DERPMap) *tailcfg.DERPMap {
	if derpMap == nil {
		return nil
	}

	regionScore := make(map[int]float64, len(derpMap.HomeParams.RegionScore))
	for k, v := range derpMap.HomeParams.RegionScore {
		regionScore[int(k)] = v
	}

	regions := make(map[int]*tailcfg.DERPRegion, len(derpMap.Regions))
	for regionID, region := range derpMap.Regions {
		regions[int(regionID)] = DERPRegionFromProto(region)
	}

	return &tailcfg.DERPMap{
		HomeParams: &tailcfg.DERPHomeParams{
			RegionScore: regionScore,
		},
		Regions: regions,
	}
}

func DERPRegionFromProto(region *proto.DERPMap_Region) *tailcfg.DERPRegion {
	if region == nil {
		return nil
	}

	regionNodes := make([]*tailcfg.DERPNode, len(region.Nodes))
	for i, node := range region.Nodes {
		regionNodes[i] = DERPNodeFromProto(node)
	}

	return &tailcfg.DERPRegion{
		RegionID:      int(region.RegionId),
		EmbeddedRelay: region.EmbeddedRelay,
		RegionCode:    region.RegionCode,
		RegionName:    region.RegionName,
		Avoid:         region.Avoid,

		Nodes: regionNodes,
	}
}

func DERPNodeFromProto(node *proto.DERPMap_Region_Node) *tailcfg.DERPNode {
	if node == nil {
		return nil
	}

	return &tailcfg.DERPNode{
		Name:             node.Name,
		RegionID:         int(node.RegionId),
		HostName:         node.HostName,
		CertName:         node.CertName,
		IPv4:             node.Ipv4,
		IPv6:             node.Ipv6,
		STUNPort:         int(node.StunPort),
		STUNOnly:         node.StunOnly,
		DERPPort:         int(node.DerpPort),
		InsecureForTests: node.InsecureForTests,
		ForceHTTP:        node.ForceHttp,
		STUNTestIP:       node.StunTestIp,
		CanPort80:        node.CanPort_80,
	}
}
