package netns

import (
	"os"
	"os/exec"
	"runtime"
	"strconv"
	"strings"
	"syscall"

	"github.com/sagernet/netlink"
	"github.com/sagernet/sing-box/adapter"
	C "github.com/sagernet/sing-box/constant"
	"github.com/sagernet/sing-box/option"
	E "github.com/sagernet/sing/common/exceptions"
	"github.com/sagernet/sing/common/logger"

	vnetns "github.com/vishvananda/netns"
)

type Manager struct {
	logger     logger.ContextLogger
	namespaces []option.NetworkNamespace
	holderArgs []string
	paths      map[string]string
	holders    []*holder
}

type holder struct {
	command    *exec.Cmd
	pipeWriter *os.File
	pidFile    string
}

func NewManager(logger logger.ContextLogger, namespaces []option.NetworkNamespace, holderArgs []string) (*Manager, error) {
	paths := make(map[string]string)
	for _, namespace := range namespaces {
		if namespace.Tag == "" {
			return nil, E.New("network namespace: missing tag")
		}
		_, duplicated := paths[namespace.Tag]
		if duplicated {
			return nil, E.New("network namespace: duplicated tag: ", namespace.Tag)
		}
		switch namespace.Type {
		case C.NetNsTypeDefault:
			if namespace.DefaultOptions.Path == "" {
				return nil, E.New("network namespace[", namespace.Tag, "]: missing path")
			}
			paths[namespace.Tag] = namespace.DefaultOptions.Path
		case C.NetNsTypeUnshare:
			paths[namespace.Tag] = ""
		}
	}
	return &Manager{
		logger:     logger,
		namespaces: namespaces,
		holderArgs: holderArgs,
		paths:      paths,
	}, nil
}

func (m *Manager) Name() string {
	return "netns"
}

func (m *Manager) Start(stage adapter.StartStage) error {
	if stage != adapter.StartStateInitialize {
		return nil
	}
	for _, namespace := range m.namespaces {
		switch namespace.Type {
		case C.NetNsTypeDefault:
			path := namespace.DefaultOptions.Path
			if !strings.HasPrefix(path, "/") {
				path = "/run/netns/" + path
			}
			_, err := os.Stat(path)
			if err != nil {
				return E.Cause(err, "network namespace[", namespace.Tag, "]")
			}
		case C.NetNsTypeUnshare:
			err := m.startNamespace(namespace)
			if err != nil {
				return E.Cause(err, "network namespace[", namespace.Tag, "]")
			}
		}
	}
	return nil
}

func (m *Manager) startNamespace(namespace option.NetworkNamespace) error {
	if len(m.holderArgs) == 0 {
		return E.New("unshare network namespace is only supported in `sing-box run`")
	}
	created, err := m.startHolder(namespace.UnshareOptions.PidFile)
	if err != nil {
		return err
	}
	m.holders = append(m.holders, created)
	pid := created.command.Process.Pid
	if created.pidFile != "" {
		err = os.WriteFile(created.pidFile, []byte(strconv.Itoa(pid)+"\n"), 0o644)
		if err != nil {
			return E.Cause(err, "write pid file")
		}
	}
	m.paths[namespace.Tag] = netnsPath(pid)
	m.logger.Info("created network namespace[", namespace.Tag, "], holder pid: ", pid)
	if os.Geteuid() == 0 {
		m.logger.Info("enter network namespace[", namespace.Tag, "] with: nsenter -n -t ", pid)
	} else {
		m.logger.Info("enter network namespace[", namespace.Tag, "] with: nsenter -U --preserve-credentials -n -t ", pid)
	}
	return nil
}

func (m *Manager) Close() error {
	for _, created := range m.holders {
		created.pipeWriter.Close()
		if created.pidFile != "" {
			os.Remove(created.pidFile)
		}
	}
	m.holders = nil
	return nil
}

func (m *Manager) ResolvePath(nameOrPath string) string {
	path, loaded := m.paths[nameOrPath]
	if loaded && path != "" {
		return path
	}
	return nameOrPath
}

func netnsPath(pid int) string {
	return "/proc/" + strconv.Itoa(pid) + "/ns/net"
}

func (m *Manager) startHolder(pidFile string) (*holder, error) {
	pipeReader, pipeWriter, err := os.Pipe()
	if err != nil {
		return nil, err
	}
	command := exec.Command(m.holderArgs[0], m.holderArgs[1:]...)
	command.Stdin = pipeReader
	command.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWNET,
	}
	err = command.Start()
	pipeReader.Close()
	if err != nil {
		pipeWriter.Close()
		return nil, E.Cause(err, "start holder process")
	}
	go command.Wait()
	err = setupNamespace(command.Process.Pid)
	if err != nil {
		pipeWriter.Close()
		return nil, err
	}
	return &holder{command: command, pipeWriter: pipeWriter, pidFile: pidFile}, nil
}

func setupNamespace(pid int) error {
	resultChannel := make(chan error, 1)
	go func() {
		runtime.LockOSThread()
		resultChannel <- setupNamespaceThread(pid)
	}()
	return <-resultChannel
}

func setupNamespaceThread(pid int) error {
	targetNs, err := vnetns.GetFromPath(netnsPath(pid))
	if err != nil {
		return E.Cause(err, "open created netns")
	}
	defer targetNs.Close()
	err = vnetns.Set(targetNs)
	if err != nil {
		return E.Cause(err, "enter created netns")
	}
	loopbackLink, err := netlink.LinkByName("lo")
	if err != nil {
		return E.Cause(err, "find lo")
	}
	err = netlink.LinkSetUp(loopbackLink)
	if err != nil {
		return E.Cause(err, "set lo up")
	}
	return nil
}
