/*
** Copyright (C) 2001-2025 Zabbix SIA
**
** This program is free software: you can redistribute it and/or modify it under the terms of
** the GNU Affero General Public License as published by the Free Software Foundation, version 3.
**
** This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
** without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
** See the GNU Affero General Public License for more details.
**
** You should have received a copy of the GNU Affero General Public License along with this program.
** If not, see <https://www.gnu.org/licenses/>.
**/

package proc

import (
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"syscall"
	"unsafe"

	"golang.org/x/sys/windows"
	"golang.zabbix.com/agent2/pkg/win32"
	"golang.zabbix.com/sdk/errs"
	"golang.zabbix.com/sdk/plugin"
)

const maxName = 256

// Plugin -
type Plugin struct {
	plugin.Base
}

var impl Plugin

func init() {
	err := plugin.RegisterMetrics(
		&impl, "Proc",
		"proc.num", "The number of processes.",
		"proc_info", "Various information about specific process(es).",
		"proc.get", "List of OS processes with statistics.",
	)
	if err != nil {
		panic(errs.Wrap(err, "failed to register metrics"))
	}
}

func getProcessUsername(pid uint32, wsid bool) (result, sidStr string, err error) {
	h, err := syscall.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid)
	if err != nil {
		return
	}
	defer syscall.CloseHandle(h)

	var tok syscall.Token
	if err = syscall.OpenProcessToken(h, windows.TOKEN_QUERY, &tok); err != nil {
		return
	}
	defer syscall.Close(syscall.Handle(tok))

	var size uint32
	err = syscall.GetTokenInformation(tok, syscall.TokenUser, nil, 0, &size)
	if err != nil && err.(syscall.Errno) != syscall.ERROR_INSUFFICIENT_BUFFER {
		return
	}
	b := make([]byte, size)
	err = syscall.GetTokenInformation(tok, syscall.TokenUser, &b[0], size, &size)
	if err != nil {
		return
	}
	sid := (*syscall.Tokenuser)(unsafe.Pointer(&b[0])).User.Sid

	nameLen := uint32(maxName)
	name := make([]uint16, nameLen)
	domainLen := uint32(maxName)
	domain := make([]uint16, domainLen)
	var use uint32
	if err = syscall.LookupAccountSid(nil, sid, &name[0], &nameLen, &domain[0], &domainLen, &use); err != nil {
		return
	}

	result = windows.UTF16ToString(name)

	if wsid == true {
		sidStr, err = sid.String()
	}

	return
}

type processEnumerator interface {
	inspect(p *syscall.ProcessEntry32)
}

func enumerateProcesses(name string, pv processEnumerator) (err error) {
	hs, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0)
	if err != nil {
		return
	}
	defer syscall.CloseHandle(hs)

	var procerr error
	var pe syscall.ProcessEntry32
	pe.Size = uint32(unsafe.Sizeof(pe))
	name = strings.ToUpper(name)

	for procerr = syscall.Process32First(hs, &pe); procerr == nil; procerr = syscall.Process32Next(hs, &pe) {
		if name == "" || name == strings.ToUpper(windows.UTF16ToString(pe.ExeFile[:])) {
			pv.inspect(&pe)
		}
	}
	if procerr.(syscall.Errno) != syscall.ERROR_NO_MORE_FILES {
		return procerr
	}

	return nil
}

type numEnumerator struct {
	user string
	num  int
}

func (e *numEnumerator) inspect(p *syscall.ProcessEntry32) {
	if e.user != "" {
		if procUser, _, err := getProcessUsername(p.ProcessID, false); err != nil ||
			e.user != strings.ToUpper(procUser) {
			return
		}
	}
	e.num++
}

func (p *Plugin) exportProcNum(params []string) (result interface{}, err error) {
	if len(params) > 2 {
		return nil, errors.New("Too many parameters.")
	}
	var name string
	if len(params) > 0 {
		name = params[0]
	}
	var e numEnumerator
	if len(params) > 1 {
		e.user = strings.ToUpper(params[1])
	}

	if err = enumerateProcesses(name, &e); err != nil {
		return
	}
	return e.num, nil
}

type infoAttr int

const (
	attrVmsize infoAttr = iota
	attrWkset
	attrPf
	attrKtime
	attrUtime
	attrGdiobj
	attrUserobj
	attrIoReadB
	attrIoReadOp
	attrIoWriteB
	attrIoWriteOp
	attrIoOtherB
	attrIoOtherOp
)

type procStatus struct {
	Pid           uint32  `json:"pid"`
	PPid          uint32  `json:"ppid"`
	Name          string  `json:"name"`
	User          string  `json:"user"`
	Sid           string  `json:"sid"`
	Vmsize        float64 `json:"vmsize"`
	Wkset         float64 `json:"wkset"`
	CpuTimeUser   float64 `json:"cputime_user"`
	CpuTimeSystem float64 `json:"cputime_system"`
	Threads       uint32  `json:"threads"`
	PageFaults    int64   `json:"page_faults"`
	Handles       int64   `json:"handles"`
	IoReadsB      int64   `json:"io_read_b"`
	IoWritesB     int64   `json:"io_write_b"`
	IoReadsOp     int64   `json:"io_read_op"`
	IoWritesOp    int64   `json:"io_write_op"`
	IoOtherB      int64   `json:"io_other_b"`
	IoOtherOp     int64   `json:"io_other_op"`
}

type procSummary struct {
	Name          string  `json:"name"`
	Processes     int     `json:"processes"`
	Vmsize        float64 `json:"vmsize"`
	Wkset         float64 `json:"wkset"`
	CpuTimeUser   float64 `json:"cputime_user"`
	CpuTimeSystem float64 `json:"cputime_system"`
	Threads       uint32  `json:"threads"`
	PageFaults    int64   `json:"page_faults"`
	Handles       int64   `json:"handles"`
	IoReadsB      int64   `json:"io_read_b"`
	IoWritesB     int64   `json:"io_write_b"`
	IoReadsOp     int64   `json:"io_read_op"`
	IoWritesOp    int64   `json:"io_write_op"`
	IoOtherB      int64   `json:"io_other_b"`
	IoOtherOp     int64   `json:"io_other_op"`
}

type thread struct {
	Pid  uint32 `json:"pid"`
	PPid uint32 `json:"ppid"`
	Name string `json:"name"`
	User string `json:"user"`
	Sid  string `json:"sid"`
	Tid  uint32 `json:"tid"`
}

var attrMap map[string]infoAttr = map[string]infoAttr{
	"":            attrVmsize,
	"vmsize":      attrVmsize,
	"wkset":       attrWkset,
	"pf":          attrPf,
	"ktime":       attrKtime,
	"utime":       attrUtime,
	"gdiobj":      attrGdiobj,
	"userobj":     attrUserobj,
	"io_read_b":   attrIoReadB,
	"io_read_op":  attrIoReadOp,
	"io_write_b":  attrIoWriteB,
	"io_write_op": attrIoWriteOp,
	"io_other_b":  attrIoOtherB,
	"io_other_op": attrIoOtherOp,
}

type infoStat int

const (
	statAvg infoStat = iota
	statMin
	statMax
	statSum
)

var statMap map[string]infoStat = map[string]infoStat{
	"":    statAvg,
	"avg": statAvg,
	"min": statMin,
	"max": statMax,
	"sum": statSum,
}

type infoEnumerator struct {
	attr  infoAttr
	stat  infoStat
	value float64
	num   int
}

func (e *infoEnumerator) inspect(p *syscall.ProcessEntry32) {
	h, err := syscall.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, p.ProcessID)
	if err != nil {
		return
	}
	defer syscall.CloseHandle(h)

	var value float64
	switch e.attr {
	case attrVmsize:
		m, err := win32.GetProcessMemoryInfo(h)
		if err != nil {
			return
		}
		// convert to kilobytes
		value = float64(m.PagefileUsage) / 1024
	case attrWkset:
		m, err := win32.GetProcessMemoryInfo(h)
		if err != nil {
			return
		}
		// convert to kilobytes
		value = float64(m.WorkingSetSize) / 1024
	case attrPf:
		m, err := win32.GetProcessMemoryInfo(h)
		if err != nil {
			return
		}
		value = float64(m.PageFaultCount)
	case attrKtime:
		var creationTime, exitTime, kernelTime, userTime syscall.Filetime
		if err = syscall.GetProcessTimes(h, &creationTime, &exitTime, &kernelTime, &userTime); err != nil {
			return
		}
		value = float64((uint64(kernelTime.HighDateTime)<<32 | uint64(kernelTime.LowDateTime)) / 1e4)
	case attrUtime:
		var creationTime, exitTime, kernelTime, userTime syscall.Filetime
		if err = syscall.GetProcessTimes(h, &creationTime, &exitTime, &kernelTime, &userTime); err != nil {
			return
		}
		value = float64((uint64(userTime.HighDateTime)<<32 | uint64(userTime.LowDateTime)) / 1e4)
	case attrGdiobj:
		value = float64(win32.GetGuiResources(h, win32.GR_GDIOBJECTS))
	case attrUserobj:
		value = float64(win32.GetGuiResources(h, win32.GR_USEROBJECTS))
	case attrIoReadB:
		if ioc, err := win32.GetProcessIoCounters(h); err != nil {
			return
		} else {
			value = float64(ioc.ReadTransferCount)
		}
	case attrIoReadOp:
		if ioc, err := win32.GetProcessIoCounters(h); err != nil {
			return
		} else {
			value = float64(ioc.ReadOperationCount)
		}
	case attrIoWriteB:
		if ioc, err := win32.GetProcessIoCounters(h); err != nil {
			return
		} else {
			value = float64(ioc.WriteTransferCount)
		}
	case attrIoWriteOp:
		if ioc, err := win32.GetProcessIoCounters(h); err != nil {
			return
		} else {
			value = float64(ioc.WriteOperationCount)
		}
	case attrIoOtherB:
		if ioc, err := win32.GetProcessIoCounters(h); err != nil {
			return
		} else {
			value = float64(ioc.OtherTransferCount)
		}
	case attrIoOtherOp:
		if ioc, err := win32.GetProcessIoCounters(h); err != nil {
			return
		} else {
			value = float64(ioc.OtherOperationCount)
		}
	}

	switch e.stat {
	case statAvg, statSum:
		e.value += value
	case statMin:
		if e.num == 0 || value < e.value {
			e.value = value
		}
	case statMax:
		if e.num == 0 || value > e.value {
			e.value = value
		}
	}
	e.num++
}

func (p *Plugin) exportProcInfo(params []string) (result interface{}, err error) {
	if len(params) > 3 {
		return nil, errors.New("Too many parameters.")
	}

	if len(params) == 0 || params[0] == "" {
		return nil, errors.New("Invalid first parameter.")
	}
	name := params[0]

	var e infoEnumerator
	if len(params) > 1 {
		var ok bool
		if e.attr, ok = attrMap[params[1]]; !ok {
			return nil, errors.New("Invalid second parameter.")
		}
	}
	if len(params) > 2 {
		var ok bool
		if e.stat, ok = statMap[params[2]]; !ok {
			return nil, errors.New("Invalid third parameter.")
		}
	}

	if err = enumerateProcesses(name, &e); err != nil {
		return
	}
	if e.stat == statAvg {
		e.value = e.value / float64(e.num)
	}
	return e.value, nil
}

func (p *Plugin) exportProcGet(params []string) (interface{}, error) {
	var name, userName, mode string
	switch len(params) {
	case 4:
		mode = params[3]
		switch mode {
		case "process", "", "thread", "summary":
		default:
			return nil, errors.New("Invalid fourth parameter")
		}
		fallthrough
	case 3:
		if params[2] != "" {
			return nil, errors.New("Invalid third parameter")
		}
		fallthrough
	case 2:
		userName = strings.ToUpper(params[1])
		fallthrough
	case 1:
		name = params[0]
	case 0:
	default:
		return nil, errors.New("Too many parameters.")
	}

	array := make([]procStatus, 0)
	threadArray := make([]thread, 0)
	summaryArray := make([]procSummary, 0)

	hs, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0)
	if err != nil {
		return nil, errors.New("Cannot get process table snapshot")
	}
	defer syscall.CloseHandle(hs)

	var procerr error
	var pe syscall.ProcessEntry32
	pe.Size = uint32(unsafe.Sizeof(pe))
	var pids []uint32

	for procerr = syscall.Process32First(hs, &pe); procerr == nil; procerr = syscall.Process32Next(hs, &pe) {
		procName := windows.UTF16ToString(pe.ExeFile[:])
		if name != "" && procName != name {
			continue
		}
		var uname string
		var sid string
		uname, sid, err = getProcessUsername(pe.ProcessID, true)

		if userName != "" && (err != nil || strings.ToUpper(uname) != userName) {
			continue
		}

		if err != nil {
			uname = "-1"
			sid = "-1"
		}

		proc := procStatus{
			Pid: pe.ProcessID, PPid: pe.ParentProcessID, Name: procName, Threads: pe.Threads,
			User: uname, Sid: sid,
		}

		// process might not exist anymore already, skipping silently
		h, err := syscall.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pe.ProcessID)
		if err != nil {
			continue
		}
		defer syscall.CloseHandle(h)

		if count, err := win32.GetProcessHandleCount(h); err == nil {
			proc.Handles = int64(count)
		} else {
			proc.Handles = -1
		}

		if m, err := win32.GetProcessMemoryInfo(h); err == nil {
			proc.Vmsize = float64(m.PagefileUsage) / 1024
			proc.Wkset = float64(m.WorkingSetSize) / 1024
			proc.PageFaults = int64(m.PageFaultCount)
		} else {
			proc.Vmsize = -1
			proc.Wkset = -1
			proc.PageFaults = -1
		}

		if ioc, err := win32.GetProcessIoCounters(h); err == nil {
			proc.IoReadsB = int64(ioc.ReadTransferCount)
			proc.IoWritesB = int64(ioc.WriteTransferCount)
			proc.IoOtherB = int64(ioc.OtherTransferCount)
			proc.IoReadsOp = int64(ioc.ReadOperationCount)
			proc.IoWritesOp = int64(ioc.WriteOperationCount)
			proc.IoOtherOp = int64(ioc.OtherOperationCount)
		} else {
			proc.IoReadsB = -1
			proc.IoWritesB = -1
			proc.IoOtherB = -1
			proc.IoReadsOp = -1
			proc.IoWritesOp = -1
			proc.IoOtherOp = -1
		}

		var creationTime, exitTime, kernelTime, userTime syscall.Filetime
		if err = syscall.GetProcessTimes(h, &creationTime, &exitTime, &kernelTime, &userTime); err == nil {
			proc.CpuTimeUser = float64(uint64(userTime.HighDateTime)<<32|uint64(userTime.LowDateTime)) / 1e7
			proc.CpuTimeSystem = float64(uint64(kernelTime.HighDateTime)<<32|uint64(kernelTime.LowDateTime)) / 1e7
		} else {
			proc.CpuTimeUser = -1
			proc.CpuTimeSystem = -1
		}

		array = append(array, proc)
		pids = append(pids, pe.ProcessID)
	}

	if mode == "thread" {
		ht, err := windows.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPTHREAD, 0)
		if err != nil {
			return nil, errors.New("Cannot get thread table snapshot")
		}

		var te windows.ThreadEntry32
		te.Size = uint32(unsafe.Sizeof(te))
		for procerr = windows.Thread32First(ht, &te); procerr == nil; procerr = windows.Thread32Next(ht, &te) {
			for _, proc := range array {
				if te.OwnerProcessID == proc.Pid {
					threadArray = append(threadArray, thread{
						proc.Pid, proc.PPid, proc.Name,
						proc.User, proc.Sid, te.ThreadID,
					})
					break
				}
			}
		}
		defer windows.CloseHandle(ht)
	}

	var jsonArray []byte
	switch mode {
	case "summary":
		var processed []string
	processes:
		for i, proc := range array {
			for _, j := range processed {
				if j == proc.Name {
					continue processes
				}
			}

			procSum := procSummary{
				proc.Name, 1, proc.Vmsize, proc.Wkset,
				proc.CpuTimeUser, proc.CpuTimeSystem, proc.Threads,
				proc.PageFaults, proc.Handles, proc.IoReadsB,
				proc.IoWritesB, proc.IoReadsOp, proc.IoWritesOp,
				proc.IoOtherB, proc.IoOtherOp,
			}

			if len(array) > i+1 {
				for _, procCmp := range array[i+1:] {
					if procCmp.Name != proc.Name {
						continue
					}
					procSum.Processes++
					procSum.Threads += procCmp.Threads
					addNonNegativeFloat(&procSum.Vmsize, procCmp.Vmsize)
					addNonNegativeFloat(&procSum.Wkset, procCmp.Wkset)
					addNonNegativeFloat(&procSum.CpuTimeUser, procCmp.CpuTimeUser)
					addNonNegativeFloat(&procSum.CpuTimeSystem, procCmp.CpuTimeSystem)
					addNonNegative(&procSum.PageFaults, procCmp.PageFaults)
					addNonNegative(&procSum.Handles, procCmp.Handles)
					addNonNegative(&procSum.IoReadsB, procCmp.IoReadsB)
					addNonNegative(&procSum.IoWritesB, procCmp.IoWritesB)
					addNonNegative(&procSum.IoOtherB, procCmp.IoOtherB)
					addNonNegative(&procSum.IoReadsOp, procCmp.IoReadsOp)
					addNonNegative(&procSum.IoWritesOp, procCmp.IoWritesOp)
					addNonNegative(&procSum.IoOtherOp, procCmp.IoOtherOp)
				}
			}
			processed = append(processed, proc.Name)
			summaryArray = append(summaryArray, procSum)
		}
		jsonArray, err = json.Marshal(summaryArray)
	case "thread":
		jsonArray, err = json.Marshal(threadArray)
	default:
		jsonArray, err = json.Marshal(array)
	}

	if err != nil {
		return nil, fmt.Errorf("Cannot create JSON array: %s", err)
	}

	return string(jsonArray), nil
}

// Export -
func (p *Plugin) Export(key string, params []string, ctx plugin.ContextProvider) (result interface{}, err error) {
	switch key {
	case "proc.num":
		return p.exportProcNum(params)
	case "proc_info":
		return p.exportProcInfo(params)
	case "proc.get":
		return p.exportProcGet(params)
	default:
		return nil, plugin.UnsupportedMetricError
	}
}
