//go:build linux

package tc

import (
	"fmt"
	"io"
	"net/netip"
	"os"
	"path/filepath"

	"github.com/cilium/ebpf"
	"github.com/cilium/ebpf/rlimit"
	"github.com/sagernet/netlink"
	"golang.org/x/sys/unix"

	C "github.com/Dreamacro/clash/constant"
	"github.com/Dreamacro/clash/transport/socks5"
)

//go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS bpf ../bpf/tc.c

const (
	mapKey1 uint32 = 0
	mapKey2 uint32 = 1
)

type EBpfTC struct {
	objs   io.Closer
	qdisc  netlink.Qdisc
	filter netlink.Filter

	ifName     string
	ifIndex    int
	ifMark     uint32
	tunIfIndex uint32

	bpfPath string
}

func NewEBpfTc(ifName string, ifIndex int, ifMark uint32, tunIfIndex uint32) *EBpfTC {
	return &EBpfTC{
		ifName:     ifName,
		ifIndex:    ifIndex,
		ifMark:     ifMark,
		tunIfIndex: tunIfIndex,
	}
}

func (e *EBpfTC) Start() error {
	if err := rlimit.RemoveMemlock(); err != nil {
		return fmt.Errorf("remove memory lock: %w", err)
	}

	e.bpfPath = filepath.Join(C.BpfFSPath, e.ifName)
	if err := os.MkdirAll(e.bpfPath, os.ModePerm); err != nil {
		return fmt.Errorf("failed to create bpf fs subpath: %w", err)
	}

	var objs bpfObjects
	if err := loadBpfObjects(&objs, &ebpf.CollectionOptions{
		Maps: ebpf.MapOptions{
			PinPath: e.bpfPath,
		},
	}); err != nil {
		e.Close()
		return fmt.Errorf("loading objects: %w", err)
	}

	e.objs = &objs

	if err := objs.bpfMaps.TcParamsMap.Update(mapKey1, e.ifMark, ebpf.UpdateAny); err != nil {
		e.Close()
		return fmt.Errorf("storing objects: %w", err)
	}

	if err := objs.bpfMaps.TcParamsMap.Update(mapKey2, e.tunIfIndex, ebpf.UpdateAny); err != nil {
		e.Close()
		return fmt.Errorf("storing objects: %w", err)
	}

	attrs := netlink.QdiscAttrs{
		LinkIndex: e.ifIndex,
		Handle:    netlink.MakeHandle(0xffff, 0),
		Parent:    netlink.HANDLE_CLSACT,
	}

	qdisc := &netlink.GenericQdisc{
		QdiscAttrs: attrs,
		QdiscType:  "clsact",
	}

	e.qdisc = qdisc

	if err := netlink.QdiscAdd(qdisc); err != nil {
		if os.IsExist(err) {
			_ = netlink.QdiscDel(qdisc)
			err = netlink.QdiscAdd(qdisc)
		}

		if err != nil {
			e.Close()
			return fmt.Errorf("cannot add clsact qdisc: %w", err)
		}
	}

	filterAttrs := netlink.FilterAttrs{
		LinkIndex: e.ifIndex,
		Parent:    netlink.HANDLE_MIN_EGRESS,
		Handle:    netlink.MakeHandle(0, 1),
		Protocol:  unix.ETH_P_ALL,
		Priority:  1,
	}

	filter := &netlink.BpfFilter{
		FilterAttrs:  filterAttrs,
		Fd:           objs.bpfPrograms.TcTunFunc.FD(),
		Name:         "clash-tc-" + e.ifName,
		DirectAction: true,
	}

	if err := netlink.FilterAdd(filter); err != nil {
		e.Close()
		return fmt.Errorf("cannot attach ebpf object to filter: %w", err)
	}

	e.filter = filter

	return nil
}

func (e *EBpfTC) Close() {
	if e.filter != nil {
		_ = netlink.FilterDel(e.filter)
	}
	if e.qdisc != nil {
		_ = netlink.QdiscDel(e.qdisc)
	}
	if e.objs != nil {
		_ = e.objs.Close()
	}
	_ = os.Remove(filepath.Join(e.bpfPath, "tc_params_map"))
}

func (e *EBpfTC) Lookup(_ netip.AddrPort) (socks5.Addr, error) {
	return nil, fmt.Errorf("not supported")
}
