// SPDX-License-Identifier: BSD-3-Clause
//go:build linux || freebsd || openbsd || darwin || solaris

package process

import (
	"context"
	"errors"
	"fmt"
	"os"
	"os/user"
	"path/filepath"
	"strconv"
	"strings"
	"syscall"

	"golang.org/x/sys/unix"

	"github.com/shirou/gopsutil/v4/internal/common"
)

type Signal = syscall.Signal

// POSIX
func getTerminalMap() (map[uint64]string, error) {
	ret := make(map[uint64]string)
	var termfiles []string

	devPath := common.HostDev()

	d, err := os.Open(devPath)
	if err != nil {
		return nil, err
	}
	defer d.Close()

	devnames, err := d.Readdirnames(-1)
	if err != nil {
		return nil, err
	}
	for _, devname := range devnames {
		if strings.HasPrefix(devname, "tty") {
			termfiles = append(termfiles, filepath.Join(devPath, devname))
		}
	}

	var ptsnames []string
	ptsPath := filepath.Join(devPath, "pts")
	ptsd, err := os.Open(ptsPath)
	if err != nil {
		ptsnames, _ = filepath.Glob(filepath.Join(devPath, "ttyp*"))
		if ptsnames == nil {
			return nil, err
		}
		termfiles = append(termfiles, ptsnames...)
	} else {
		defer ptsd.Close()
		ptsnames, err = ptsd.Readdirnames(-1)
		if err != nil {
			return nil, err
		}
		for _, ptsname := range ptsnames {
			termfiles = append(termfiles, filepath.Join(ptsPath, ptsname))
		}
	}

	for _, name := range termfiles {
		stat := unix.Stat_t{}
		err = unix.Stat(name, &stat)
		if err != nil {
			return nil, err
		}
		rdev := uint64(stat.Rdev)
		ret[rdev] = strings.TrimPrefix(name, devPath+string(os.PathSeparator))
	}
	return ret, nil
}

// isMount is a port of python's os.path.ismount()
// https://github.com/python/cpython/blob/08ff4369afca84587b1c82034af4e9f64caddbf2/Lib/posixpath.py#L186-L216
// https://docs.python.org/3/library/os.path.html#os.path.ismount
func isMount(path string) bool {
	// Check symlinkness with os.Lstat; unix.DT_LNK is not portable
	fileInfo, err := os.Lstat(path)
	if err != nil {
		return false
	}
	if fileInfo.Mode()&os.ModeSymlink != 0 {
		return false
	}
	var stat1 unix.Stat_t
	if err := unix.Lstat(path, &stat1); err != nil {
		return false
	}
	parent := filepath.Join(path, "..")
	var stat2 unix.Stat_t
	if err := unix.Lstat(parent, &stat2); err != nil {
		return false
	}
	return stat1.Dev != stat2.Dev || stat1.Ino == stat2.Ino
}

func PidExistsWithContext(ctx context.Context, pid int32) (bool, error) {
	if pid <= 0 {
		return false, fmt.Errorf("invalid pid %v", pid)
	}
	proc, err := os.FindProcess(int(pid))
	if err != nil {
		return false, err
	}
	defer proc.Release()

	if isMount(common.HostProcWithContext(ctx)) { // if /<HOST_PROC>/proc exists and is mounted, check if /<HOST_PROC>/proc/<PID> folder exists
		_, err := os.Stat(common.HostProcWithContext(ctx, strconv.Itoa(int(pid)))) //nolint:gosec // pid is int32, path traversal is not possible
		if os.IsNotExist(err) {
			return false, nil
		}
		return err == nil, err
	}

	// procfs does not exist or is not mounted, check PID existence by signalling the pid
	err = proc.Signal(syscall.Signal(0))
	if err == nil {
		return true, nil
	}
	if errors.Is(err, os.ErrProcessDone) {
		return false, nil
	}
	var errno syscall.Errno
	if !errors.As(err, &errno) {
		return false, err
	}
	switch errno {
	case syscall.ESRCH:
		return false, nil
	case syscall.EPERM:
		return true, nil
	}

	return false, err
}

func (p *Process) SendSignalWithContext(_ context.Context, sig syscall.Signal) error {
	process, err := os.FindProcess(int(p.Pid))
	if err != nil {
		return err
	}
	defer process.Release()

	err = process.Signal(sig)
	if err != nil {
		return err
	}

	return nil
}

func (p *Process) SuspendWithContext(ctx context.Context) error {
	return p.SendSignalWithContext(ctx, unix.SIGSTOP)
}

func (p *Process) ResumeWithContext(ctx context.Context) error {
	return p.SendSignalWithContext(ctx, unix.SIGCONT)
}

func (p *Process) TerminateWithContext(ctx context.Context) error {
	return p.SendSignalWithContext(ctx, unix.SIGTERM)
}

func (p *Process) KillWithContext(ctx context.Context) error {
	return p.SendSignalWithContext(ctx, unix.SIGKILL)
}

func (p *Process) UsernameWithContext(ctx context.Context) (string, error) {
	uids, err := p.UidsWithContext(ctx)
	if err != nil {
		return "", err
	}
	if len(uids) > 0 {
		u, err := user.LookupId(strconv.Itoa(int(uids[0])))
		if err != nil {
			return "", err
		}
		return u.Username, nil
	}
	return "", nil
}
