//go:build linux
// +build linux

package netns

import (
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"sync"
	"syscall"

	"golang.org/x/sys/unix"

	"github.com/tidbcloud/aws-shared-provider/pkg/utils/closer"
)

const (
	// https://github.com/torvalds/linux/blob/master/include/uapi/linux/magic.h
	NSFS_MAGIC   = 0x6e736673
	PROCFS_MAGIC = 0x9fa0
)

func NewFromPath(path string) (NetNS, error) {
	return newFromPath(path)
}

func newFromPath(path string) (*netNs, error) {
	stat := syscall.Statfs_t{}
	if err := syscall.Statfs(path, &stat); err != nil {
		return nil, err
	}

	switch stat.Type {
	case PROCFS_MAGIC, NSFS_MAGIC:
		fd, err := os.Open(filepath.Clean(path))
		if err != nil {
			return nil, err
		}
		return &netNs{
			fd: fd,
		}, nil
	default:
		return nil, fmt.Errorf("unknown FS magic on %q: %x", path, stat.Type)
	}
}

func hostNetNs() (*netNs, error) {
	runtime.LockOSThread()
	defer runtime.UnlockOSThread()
	path := fmt.Sprintf("/proc/%d/task/%d/ns/net", os.Getpid(), unix.Gettid())
	return newFromPath(path)
}

type netNs struct {
	fd     *os.File
	closed bool
}

func (ns *netNs) Close() error {
	if err := ns.fd.Close(); err != nil {
		return err
	}
	ns.closed = true
	return nil
}

func (ns *netNs) enter() error {
	if ns.closed {
		return fmt.Errorf("netns %s is closed", ns.fd.Name())
	}

	if err := unix.Setns(int(ns.fd.Fd()), unix.CLONE_NEWNET); err != nil {
		return fmt.Errorf("can't switch to ns %v: %w", ns.fd.Name(), err)
	}

	return nil
}

func (ns *netNs) Do(fn func() error) error {
	if ns.closed {
		return fmt.Errorf("netns %s is closed", ns.fd.Name())
	}
	run := func() error {
		hostNetNs, err := hostNetNs()
		if err != nil {
			return fmt.Errorf("can't get host net ns: %w", err)
		}
		defer closer.Close(hostNetNs)

		if err := ns.enter(); err != nil {
			return err
		}

		defer func() {
			if err := hostNetNs.enter(); err != nil {
				// Switch back failed, just leave the thread locked
				// See https://github.com/containernetworking/plugins/blob/master/pkg/ns/ns_linux.go#L189
				return
			}
			runtime.UnlockOSThread()
		}()

		return fn()
	}

	wg := sync.WaitGroup{}
	wg.Add(1)
	var runErr error
	go func() {
		defer wg.Done()
		runtime.LockOSThread()

		runErr = run()
	}()

	wg.Wait()

	return runErr
}
