//go:build !dae_stub_ebpf

/*
 * SPDX-License-Identifier: AGPL-3.0-only
 * Copyright (c) 2022-2026, daeuniverse Organization <dae@v2raya.org>
 */

package control

import (
	"bufio"
	"encoding/binary"
	"errors"
	"fmt"
	"net/netip"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"sync"

	"github.com/cilium/ebpf"
	"github.com/cilium/ebpf/asm"
	"github.com/cilium/ebpf/features"
	"github.com/daeuniverse/dae/common"
	"github.com/daeuniverse/dae/common/consts"
	daerrors "github.com/daeuniverse/dae/common/errors"
	internal "github.com/daeuniverse/dae/pkg/ebpf_internal"
	"github.com/sirupsen/logrus"
	"golang.org/x/sys/unix"
)

// ============================================================================
// BPF Type Synchronization
// ============================================================================
//
// Most BPF types are auto-generated by bpf2go in bpf_bpfel.go:
// - bpfTuplesKey (struct tuples_key) - auto-generated, five-tuple key
// - bpfDomainRouting (struct domain_routing)
// - bpfPortRange (struct port_range) - auto-generated via -type flag
// - bpfMatchSet, bpfPidPname, bpfRedirectEntry, etc.
//
// However, some types cannot be auto-generated and are defined here:
// - bpfRoutingResult (struct routing_result) - userspace routing metadata
// - _bpfLpmKey (struct lpm_key) - contains bpf_lpm_trie_key (kernel BPF type)
//
// Note: _bpfTuples was removed as it was unused in the codebase.
// Note: BPF LPM Trie requires the exact bpf_lpm_trie_key structure for
// kernel type recognition. Flattening breaks BPF map operations.
//
// To verify synchronization:
// 1. Check struct size matches (use unsafe.Sizeof in Go, sizeof in C)
// 2. Check field offsets match
// 3. Run BPF tests: go test -tags="linux dae_bpf_tests" ./control/kern/tests/...
// ============================================================================

type bpfRoutingResult struct {
	Mark     uint32
	Must     uint8
	Mac      [6]uint8
	Outbound uint8
	Pname    [16]uint8
	Pid      uint32
	Dscp     uint8
}

type _bpfLpmKey struct {
	PrefixLen uint32
	Data      [4]uint32
}

// bpfPortRange is auto-generated by bpf2go

func (r bpfPortRange) Encode() (b [16]byte) {
	binary.LittleEndian.PutUint16(b[:2], r.PortStart)
	binary.LittleEndian.PutUint16(b[2:], r.PortEnd)
	return b
}

func ParsePortRange(b []byte) (portStart, portEnd uint16) {
	portStart = binary.LittleEndian.Uint16(b[:2])
	portEnd = binary.LittleEndian.Uint16(b[2:])
	return portStart, portEnd
}

func (o *bpfObjects) newLpmMap(keys []_bpfLpmKey, values []uint32) (m *ebpf.Map, err error) {
	m, err = ebpf.NewMap(&ebpf.MapSpec{
		Type:       ebpf.LPMTrie,
		Flags:      o.UnusedLpmType.Flags(),
		MaxEntries: o.UnusedLpmType.MaxEntries(),
		KeySize:    o.UnusedLpmType.KeySize(),
		ValueSize:  o.UnusedLpmType.ValueSize(),
	})
	if err != nil {
		return nil, err
	}
	if _, err = BpfMapBatchUpdate(m, keys, values, &ebpf.BatchOptions{
		ElemFlags: uint64(ebpf.UpdateAny),
	}); err != nil {
		_ = m.Close()
		return nil, err
	}
	return m, nil
}

func cidrToBpfLpmKey(prefix netip.Prefix) _bpfLpmKey {
	bits := prefix.Bits()
	if prefix.Addr().Is4() {
		bits += 96
	}
	ip := prefix.Addr().As16()
	return _bpfLpmKey{
		PrefixLen: uint32(bits),
		Data:      common.Ipv6ByteSliceToUint32Array(ip[:]),
	}
}

var (
	CheckBatchUpdateFeatureOnce sync.Once
	SimulateBatchUpdate         bool
	SimulateBatchUpdateLpmTrie  bool

	CheckBatchDeleteFeatureOnce sync.Once
	SimulateBatchDelete         bool

	bpfMapBatchLookup = func(m *ebpf.Map, cursor *ebpf.MapBatchCursor, keysOut interface{}, valuesOut interface{}) (int, error) {
		return m.BatchLookup(cursor, keysOut, valuesOut, nil)
	}
)

const (
	bpfMapBatchDeleteAllLookupSize = 256
	bpfMapBatchDeleteAllChunkSize  = 1024
)

func initBatchDeleteFeatureFlags() {
	CheckBatchDeleteFeatureOnce.Do(func() {
		version, e := internal.KernelVersion()
		if e != nil {
			SimulateBatchDelete = true
			return
		}
		// BatchDelete requires kernel 5.6+ for BPF_MAP_TYPE_BATCH operations.
		if version.Less(consts.UserspaceBatchUpdateFeatureVersion) {
			SimulateBatchDelete = true
		}
	})
}

func isBatchLookupUnsupportedErr(err error) bool {
	if err == nil {
		return false
	}
	if errors.Is(err, unix.ENOTSUP) ||
		errors.Is(err, unix.EOPNOTSUPP) ||
		errors.Is(err, unix.ENOSYS) {
		return true
	}
	errStr := strings.ToLower(err.Error())
	return strings.Contains(errStr, "map batch api") && strings.Contains(errStr, "not supported")
}

func BpfMapBatchUpdate(m *ebpf.Map, keys interface{}, values interface{}, opts *ebpf.BatchOptions) (n int, err error) {
	CheckBatchUpdateFeatureOnce.Do(func() {
		version, e := internal.KernelVersion()
		if e != nil {
			SimulateBatchUpdate = true
			SimulateBatchUpdateLpmTrie = true
			return
		}
		if version.Less(consts.UserspaceBatchUpdateFeatureVersion) {
			SimulateBatchUpdate = true
		}
		if version.Less(consts.UserspaceBatchUpdateLpmTrieFeatureVersion) {
			SimulateBatchUpdateLpmTrie = true
		}
	})

	simulate := SimulateBatchUpdate
	if m.Type() == ebpf.LPMTrie {
		simulate = SimulateBatchUpdateLpmTrie
	}

	if !simulate {
		// Genuine BpfMapBatchUpdate
		return m.BatchUpdate(keys, values, opts)
	}

	// Simulate
	vKeys := reflect.ValueOf(keys)
	if vKeys.Kind() != reflect.Slice {
		return 0, fmt.Errorf("keys must be slice")
	}
	vVals := reflect.ValueOf(values)
	if vVals.Kind() != reflect.Slice {
		return 0, fmt.Errorf("values must be slice")
	}
	length := vKeys.Len()
	if vVals.Len() != length {
		return 0, fmt.Errorf("keys and values must have same length")
	}

	for i := range length {
		vKey := vKeys.Index(i)
		vVal := vVals.Index(i)
		if err = m.Update(vKey.Interface(), vVal.Interface(), ebpf.MapUpdateFlags(opts.ElemFlags)); err != nil {
			return i, fmt.Errorf("batch update map %s at index %d: %w", m.String(), i, err)
		}
	}
	return vKeys.Len(), nil
}

// BpfMapBatchDelete deletes keys and ignores ErrKeyNotExist.
// Uses kernel batch delete API when available (5.6+), otherwise falls back to loop.
func BpfMapBatchDelete(m *ebpf.Map, keys interface{}) (n int, err error) {
	initBatchDeleteFeatureFlags()

	if !SimulateBatchDelete {
		// Use kernel BatchDelete API - much faster for large batches
		n, err = m.BatchDelete(keys, &ebpf.BatchOptions{})
		// BatchDelete may return ErrKeyNotExist for some keys - that's ok
		if err != nil && !errors.Is(err, ebpf.ErrKeyNotExist) {
			return n, fmt.Errorf("batch delete map %s: %w", m.String(), err)
		}
		// Count successful deletions even with ErrKeyNotExist
		vKeys := reflect.ValueOf(keys)
		if vKeys.Kind() == reflect.Slice {
			return vKeys.Len(), nil
		}
		return n, nil
	}

	// Fallback: simulate batch delete with individual Delete calls
	vKeys := reflect.ValueOf(keys)
	if vKeys.Kind() != reflect.Slice {
		return 0, fmt.Errorf("keys must be slice")
	}
	length := vKeys.Len()

	for i := range length {
		vKey := vKeys.Index(i)
		if err = m.Delete(vKey.Interface()); err != nil && !errors.Is(err, ebpf.ErrKeyNotExist) {
			return i, fmt.Errorf("batch delete map %s at index %d: %w", m.String(), i, err)
		}
	}
	return vKeys.Len(), nil
}

// BpfMapDeleteAll deletes all entries in a map via iterator scan.
// It tolerates concurrent key disappearance during deletion.
func BpfMapDeleteAll[K any, V any](m *ebpf.Map) error {
	var (
		key K
		val V
	)

	iter := m.Iterate()
	for iter.Next(&key, &val) {
		if err := m.Delete(&key); err != nil && !errors.Is(err, ebpf.ErrKeyNotExist) {
			return fmt.Errorf("delete key in map %s: %w", m.String(), err)
		}
	}
	if err := iter.Err(); err != nil {
		return fmt.Errorf("iterate map %s: %w", m.String(), err)
	}
	return nil
}

// BpfMapBatchDeleteAll deletes all entries in a map using batch operations.
// It degrades safely to BpfMapDeleteAll on kernels without batch-map support.
// To avoid large temporary allocations, keys are deleted in fixed-size chunks.
func BpfMapBatchDeleteAll[K any, V any](m *ebpf.Map) error {
	if m == nil {
		return fmt.Errorf("nil map")
	}

	initBatchDeleteFeatureFlags()
	if SimulateBatchDelete {
		return BpfMapDeleteAll[K, V](m)
	}

	lookupKeys := make([]K, bpfMapBatchDeleteAllLookupSize)
	lookupValues := make([]V, bpfMapBatchDeleteAllLookupSize)
	keysToDelete := make([]K, 0, bpfMapBatchDeleteAllChunkSize)

	flushDeleteChunk := func() error {
		if len(keysToDelete) == 0 {
			return nil
		}
		if _, delErr := BpfMapBatchDelete(m, keysToDelete); delErr != nil {
			return fmt.Errorf("batch delete all in map %s: %w", m.String(), delErr)
		}
		keysToDelete = keysToDelete[:0]
		return nil
	}

	var cursor ebpf.MapBatchCursor
	for {
		count, err := bpfMapBatchLookup(m, &cursor, lookupKeys, lookupValues)
		if count > 0 {
			for i := range count {
				keysToDelete = append(keysToDelete, lookupKeys[i])
				if len(keysToDelete) >= bpfMapBatchDeleteAllChunkSize {
					if flushErr := flushDeleteChunk(); flushErr != nil {
						return flushErr
					}
				}
			}
		}
		if err != nil {
			if errors.Is(err, ebpf.ErrKeyNotExist) {
				break
			}
			if isBatchLookupUnsupportedErr(err) {
				return BpfMapDeleteAll[K, V](m)
			}
			return fmt.Errorf("batch lookup map %s: %w", m.String(), err)
		}
	}

	if flushErr := flushDeleteChunk(); flushErr != nil {
		return flushErr
	}
	return nil
}

// detectCgroupPath returns the first-found mount point of type cgroup2
// and stores it in the cgroupPath global variable.
// Copied from https://github.com/cilium/ebpf/blob/v0.10.0/examples/cgroup_skb/main.go
func detectCgroupPath() (string, error) {
	f, err := os.Open("/proc/mounts")
	if err != nil {
		return "", err
	}
	defer func() { _ = f.Close() }()

	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		// example fields: cgroup2 /sys/fs/cgroup/unified cgroup2 rw,nosuid,nodev,noexec,relatime 0 0
		fields := strings.Fields(scanner.Text())
		if len(fields) >= 3 && fields[2] == "cgroup2" {
			return fields[1], nil
		}
	}
	if err := scanner.Err(); err != nil {
		return "", fmt.Errorf("read /proc/mounts: %w", err)
	}

	return "", errors.New("cgroup2 not mounted")
}

type bpfIfParams struct {
	RxCksmOffload                  bool
	TxL4CksmIp4Offload             bool
	TxL4CksmIp6Offload             bool
	UseNonstandardOffloadAlgorithm bool
}

func (p bpfIfParams) CheckVersionRequirement(version *internal.Version) (err error) {
	if !p.TxL4CksmIp4Offload ||
		!p.TxL4CksmIp6Offload {
		// Need calc checksum on CPU. And need BPF_F_ADJ_ROOM_NO_CSUM_RESET.
		if version.Less(consts.ChecksumFeatureVersion) {
			return fmt.Errorf("your NIC does not support checksum offload and your kernel version %v does not support related BPF features; expect >=%v; upgrade your kernel and try again", version.String(),
				consts.ChecksumFeatureVersion.String())
		}
	}
	return nil
}

type loadBpfOptions struct {
	PinPath                string
	BigEndianTproxyPort    uint32
	CollectionOptions      *ebpf.CollectionOptions
	ConnStateMapMaxEntries uint32
}

const (
	defaultConnStateMapMaxEntries = 65536 * 4
	fastSockPlaceholderMaxEntries = 1
)

func loadBpfObjectsWithConstantsAndCustomizer(
	obj interface{},
	opts *ebpf.CollectionOptions,
	constants map[string]interface{},
	customize func(spec *ebpf.CollectionSpec) error,
) error {
	spec, err := loadBpf()
	if err != nil {
		return err
	}
	for name, value := range constants {
		if err := spec.Variables[name].Set(value); err != nil {
			return err
		}
	}
	if customize != nil {
		if err := customize(spec); err != nil {
			return err
		}
	}
	return spec.LoadAndAssign(obj, opts)
}

func disablePinnedConnStateMaps(spec *ebpf.CollectionSpec) error {
	if spec == nil {
		return fmt.Errorf("nil collection spec")
	}
	m, ok := spec.Maps["conn_state_map"]
	if !ok || m == nil {
		return fmt.Errorf("missing map spec %q", "conn_state_map")
	}
	m.Pinning = ebpf.PinNone
	return nil
}

func tuneConnStateBpfMap(spec *ebpf.CollectionSpec, maxEntries uint32) error {
	if spec == nil {
		return fmt.Errorf("nil collection spec")
	}
	if maxEntries == 0 {
		maxEntries = defaultConnStateMapMaxEntries
	}
	connState, ok := spec.Maps["conn_state_map"]
	if !ok || connState == nil {
		return fmt.Errorf("missing map spec %q", "conn_state_map")
	}
	connState.MaxEntries = maxEntries
	return nil
}

func tunePlaceholderBpfMaps(spec *ebpf.CollectionSpec) error {
	if spec == nil {
		return fmt.Errorf("nil collection spec")
	}

	fastSock, ok := spec.Maps["fast_sock"]
	if !ok || fastSock == nil {
		return fmt.Errorf("missing map spec %q", "fast_sock")
	}
	// fast_sock is retained only to keep the generated Go ABI stable while the
	// sockhash redirect path remains disabled, so a single placeholder slot is
	// sufficient.
	fastSock.MaxEntries = fastSockPlaceholderMaxEntries
	return nil
}

func customizeBpfMapSpecs(spec *ebpf.CollectionSpec, connStateMapMaxEntries uint32) error {
	if err := disablePinnedConnStateMaps(spec); err != nil {
		return err
	}
	if err := tuneConnStateBpfMap(spec, connStateMapMaxEntries); err != nil {
		return err
	}
	if err := tunePlaceholderBpfMaps(spec); err != nil {
		return err
	}
	return nil
}

func cleanupPinnedConnStateMapFiles(log *logrus.Logger, pinPath string) int {
	if pinPath == "" {
		return 0
	}

	removed := 0
	for _, mapName := range []string{"conn_state_map", "tcp_conn_state_map", "udp_conn_state_map"} {
		path := filepath.Join(pinPath, mapName)
		if err := os.Remove(path); err != nil {
			if !os.IsNotExist(err) && log != nil {
				log.Warnf("Failed to remove stale pinned conn-state map %s: %v", mapName, err)
			}
			continue
		}
		removed++
		if log != nil {
			log.Infof("Removed stale pinned conn-state map %s", mapName)
		}
	}
	return removed
}

func fullLoadBpfObjects(
	log *logrus.Logger,
	bpf *bpfObjects,
	opts *loadBpfOptions,
	soMarkFromDae uint32,
) (err error) {
retryLoadBpf:
	netnsID, err := GetDaeNetns().NetnsID()
	if err != nil {
		return fmt.Errorf("failed to get netns id: %w", err)
	}

	// bpf_redirect_peer() is only safe under specific conditions:
	// 1. Netkit device with scrub=NONE (preserves skb->mark across netns boundary)
	// 2. Kernel >= 6.8 (fixes CVE-2025-37959 skb metadata leak)
	// 3. TC ingress direction ONLY (egress must use bpf_redirect())
	//
	// When enabled, provides ~50% throughput improvement by bypassing CPU backlog.
	useRedirectPeer := uint8(0)
	if GetDaeNetns().IsUsingNetkit() && checkNetkitDeviceCanUseRedirectPeer(log, GetDaeNetns().Dae0().Attrs().Name) {
		// Check kernel version for CVE-2025-37959 fix
		kernelVersion, err := internal.KernelVersion()
		switch {
		case err != nil:
			log.Warnf("Failed to get kernel version: %v; bpf_redirect_peer() disabled", err)
		case kernelVersion.Less(consts.RedirectPeerSafeVersion):
			log.Debugf("Kernel %v < %v (CVE-2025-37959 fix); bpf_redirect_peer() disabled",
				kernelVersion, consts.RedirectPeerSafeVersion)
		default:
			useRedirectPeer = 1
			log.Infof("Safely enabled bpf_redirect_peer() (kernel %v, netkit+scrub=NONE)", kernelVersion)
		}
	}

	// Get peer MAC address. Netkit is created in L2 mode so this should be
	// populated, but keep the zero-MAC fallback for older or unusual kernels.
	peerMac := [6]byte{}
	hwAddr := GetDaeNetns().Dae0Peer().Attrs().HardwareAddr
	if len(hwAddr) == 6 {
		peerMac = [6]byte(hwAddr)
	}

	hasBpfGetCurrentTask := uint8(0)
	if err := features.HaveProgramHelper(ebpf.CGroupSockAddr, asm.FnGetCurrentTask); err == nil {
		hasBpfGetCurrentTask = 1
		log.Debugf("bpf_get_current_task is supported")
	} else {
		log.Warnf("Kernel does not support bpf_get_current_task helper: %v; process names may be truncated or less accurate (degraded to bpf_get_current_comm)", err)
	}

	constants := map[string]interface{}{
		"PARAM": struct {
			tproxyPort           uint32
			controlPlanePid      uint32
			dae0Ifindex          uint32
			daeNetnsId           uint32
			dae0peerMac          [6]byte
			paddingAfterMac      [2]uint8
			useRedirectPeer      uint8
			hasBpfGetCurrentTask uint8
			padding2             uint16
			daeSocketMark        uint32
		}{
			tproxyPort:           opts.BigEndianTproxyPort,
			controlPlanePid:      uint32(os.Getpid()),
			dae0Ifindex:          uint32(GetDaeNetns().Dae0().Attrs().Index),
			daeNetnsId:           uint32(netnsID),
			dae0peerMac:          peerMac,
			paddingAfterMac:      [2]uint8{0, 0},
			useRedirectPeer:      useRedirectPeer,
			hasBpfGetCurrentTask: hasBpfGetCurrentTask,
			padding2:             0,
			daeSocketMark:        soMarkFromDae,
		},
	}
	if err = loadBpfObjectsWithConstantsAndCustomizer(
		bpf,
		opts.CollectionOptions,
		constants,
		func(spec *ebpf.CollectionSpec) error {
			return customizeBpfMapSpecs(spec, opts.ConnStateMapMaxEntries)
		},
	); err != nil {
		if errors.Is(err, ebpf.ErrMapIncompatible) {
			// Map property is incompatible. Remove the old map and try again.
			prefix := "use pinned map "
			_, after, ok := strings.Cut(err.Error(), prefix)
			if !ok {
				return fmt.Errorf("loading objects: bad format: %w", err)
			}
			mapName, _, _ := strings.Cut(after, ":")
			_ = os.Remove(filepath.Join(opts.PinPath, mapName))
			log.Infof("Incompatible new map format with existing map %v detected; removed the old one.", mapName)
			goto retryLoadBpf
		}
		// Get detailed log from ebpf.internal.(*VerifierError)
		if log.Level == logrus.FatalLevel {
			if v := reflect.Indirect(reflect.ValueOf(errors.Unwrap(errors.Unwrap(err)))); v.Kind() == reflect.Struct {
				if _log := v.FieldByName("Log"); _log.IsValid() {
					if strSlice, ok := _log.Interface().([]string); ok {
						log.Fatalln(strings.Join(strSlice, "\n"))
					}
				}
			}
		}
		// Use daerrors.WrapBPFError to add helpful context to BPF errors.
		// This replaces string matching with structured error handling.
		err = daerrors.WrapBPFError(err)
		return err
	}
	return nil
}
