package ebpf

import (
	_ "embed"
	"fmt"
	"net"
	"sync"

	"github.com/cilium/ebpf/link"
	"github.com/cilium/ebpf/rlimit"
	log "github.com/sirupsen/logrus"
	"golang.org/x/sys/unix"

	"github.com/netbirdio/netbird/client/internal/ebpf/manager"
)

const (
	xdpProgName = "nb_xdp_prog"

	mapKeyFeatures uint32 = 0

	featureFlagWGProxy      = 0b00000001
	featureFlagDnsForwarder = 0b00000010
)

var (
	singleton     manager.Manager
	singletonLock = &sync.Mutex{}
)

// required packages libbpf-dev, libc6-dev-i386-amd64-cross

// GeneralManager is used to load multiple eBPF programs with a custom check (if then) done in prog.c
// The manager simply adds a feature (byte) of each program to a map that is shared between the userspace and kernel.
// When packet arrives, the C code checks for each feature (if it is set) and executes each enabled program (e.g., dns_fwd.c and wg_proxy.c).
//
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc clang-14 bpf src/prog.c -- -I /usr/x86_64-linux-gnu/include
type GeneralManager struct {
	lock         sync.Mutex
	link         link.Link
	featureFlags uint16
	bpfObjs      bpfObjects
}

// GetEbpfManagerInstance return a static eBpf Manager instance
func GetEbpfManagerInstance() manager.Manager {
	singletonLock.Lock()
	defer singletonLock.Unlock()
	if singleton != nil {
		return singleton
	}
	singleton = &GeneralManager{}
	return singleton
}

func (tf *GeneralManager) setFeatureFlag(feature uint16) {
	tf.featureFlags |= feature
}

func (tf *GeneralManager) loadXdp() error {
	if tf.link != nil {
		return nil
	}
	// it required for Docker
	err := rlimit.RemoveMemlock()
	if err != nil {
		return err
	}

	iFace, err := net.InterfaceByName("lo")
	if err != nil {
		return err
	}

	// lo has no native XDP, so the program runs in generic mode. Unless it
	// declares multi-buffer support the kernel must linearize every non-linear
	// skb before running it. Loopback packets are up to 64 KB, so that is a
	// contiguous GFP_ATOMIC allocation per packet, and when it fails the packet
	// is dropped before the program runs, stalling local TCP connections.
	// Multi-buffer XDP in generic mode requires kernel 6.3, so fall back to a
	// plain attach when the kernel rejects it.
	err = tf.attachXdp(iFace.Index, true)
	if err == nil {
		return nil
	}
	log.Debugf("failed to attach multi-buffer xdp program, retrying without it: %s", err)

	return tf.attachXdp(iFace.Index, false)
}

func (tf *GeneralManager) attachXdp(iFaceIndex int, multiBuffer bool) error {
	spec, err := loadBpf()
	if err != nil {
		return fmt.Errorf("load bpf spec: %w", err)
	}

	if multiBuffer {
		prog, ok := spec.Programs[xdpProgName]
		if !ok {
			return fmt.Errorf("program %s not found in bpf spec", xdpProgName)
		}
		prog.Flags |= unix.BPF_F_XDP_HAS_FRAGS
	}

	if err := spec.LoadAndAssign(&tf.bpfObjs, nil); err != nil {
		return fmt.Errorf("load bpf objects: %w", err)
	}

	tf.link, err = link.AttachXDP(link.XDPOptions{
		Program:   tf.bpfObjs.NbXdpProg,
		Interface: iFaceIndex,
	})
	if err != nil {
		if closeErr := tf.bpfObjs.Close(); closeErr != nil {
			log.Debugf("failed to close bpf objects after xdp attach error: %s", closeErr)
		}
		tf.link = nil
		return fmt.Errorf("attach xdp: %w", err)
	}
	return nil
}

func (tf *GeneralManager) unsetFeatureFlag(feature uint16) error {
	tf.lock.Lock()
	defer tf.lock.Unlock()
	tf.featureFlags &^= feature

	if tf.link == nil {
		return nil
	}

	if tf.featureFlags == 0 {
		return tf.close()
	}

	return tf.bpfObjs.NbFeatures.Put(mapKeyFeatures, tf.featureFlags)
}

func (tf *GeneralManager) close() error {
	log.Debugf("detach ebpf program ")
	err := tf.bpfObjs.Close()
	if err != nil {
		log.Warnf("failed to close eBpf objects: %s", err)
	}

	err = tf.link.Close()
	tf.link = nil
	return err
}
