//go:build solaris

// Author: Jens Elkner (C) 2025

package metrics

import (
	"fmt"
	"io"
	"log"
	"math"
	"os"
	"strings"
	"syscall"
	"unsafe"
)

/** Solaris 11.3 types deduced from /usr/include/sys/procfs.h **/
// requires go v1.18+
type uchar_t uint8 // unsigned char
type char int8     // signed char
type short int16
type ushort_t uint16
type id_t int32
type pid_t int32
type uid_t uint32
type gid_t uid_t
type taskid_t id_t
type projid_t id_t
type zoneid_t id_t
type poolid_t id_t
type uintptr_t uint64
type long int64
type ulong_t uint64
type dev_t ulong_t
type size_t ulong_t
type time_t long
type sigset_t [16]char      // we do not need those struct, so just pad
type fltset_t [16]char      // we do not need those struct, so just pad
type sysset_t [64]char      // we do not need those struct, so just pad
type lwpstatus_t [1296]char // we do not need those struct, so just pad
type lwpsinfo_t [152]char   // we do not need those struct, so just pad

type timestruc_t struct {
	tv_sec  time_t
	tv_nsec long
}

/* process status file:  /proc/<pid>/status */
type pstatus_t struct {
	pr_flags   int32 /* flags (see below) */
	pr_nlwp    int32 /* number of active lwps in the process */
	pr_pid     pid_t /* process id */
	pr_ppid    pid_t /* parent process id */
	pr_pgid    pid_t /* process group id */
	pr_sid     pid_t /* session id */
	pr_aslwpid id_t  /* historical; now always zero */
	pr_agentid id_t  /* lwp id of the /proc agent lwp, if any */
	// 32
	pr_sigpend sigset_t  /* set of process pending signals */
	pr_brkbase uintptr_t /* address of the process heap */
	pr_brksize size_t    /* size of the process heap, in bytes */
	// 64
	pr_stkbase uintptr_t   /* address of the process stack */
	pr_stksize size_t      /* size of the process stack, in bytes */
	pr_utime   timestruc_t /* # process user cpu time */
	// 96
	pr_stime  timestruc_t /* # process system cpu time */
	pr_cutime timestruc_t /* # sum of children's user times */
	// 128
	pr_cstime   timestruc_t /* # sum of children's system times */
	pr_sigtrace sigset_t    /* sigset_t: set of traced signals */
	// 160
	pr_flttrace fltset_t /* set of traced faults */
	pr_sysentry sysset_t /* set of system calls traced on entry */
	// 240
	pr_sysexit sysset_t /* set of system calls traced on exit */
	// 304
	pr_dmodel    char    /* data model of the process (see below) */
	pr_va_mask   uchar_t /* VA masking bits, where supported */
	pr_adi_nbits uchar_t /* # of VA bits used by ADI when enabled */
	pr_pad       [1]char
	pr_taskid    taskid_t /* task id */
	// 312
	pr_projid projid_t /* project id */
	pr_nzomb  int32    /* number of zombie lwps in the process */
	pr_zoneid zoneid_t /* zone id */
	// 324
	pr_filler [15]int32 /* reserved for future use */
	// 384
	pr_lwp lwpstatus_t /* status of the representative lwp */
	// 1680
}

const PRARGSZ = 80 /* number of chars of arguments */
const PRFNSZ = 16  /* Maximum size of execed filename */

/* process ps(1) information file.  /proc/<pid>/psinfo */
type psinfo_t struct {
	pr_flag int32 /* process flags (DEPRECATED; do not use) */
	pr_nlwp int32 /* number of active lwps in the process */
	pr_pid  pid_t /* unique process id */
	pr_ppid pid_t /* process id of parent */
	pr_pgid pid_t /* pid of process group leader */
	pr_sid  pid_t /* session id */
	pr_uid  uid_t /* real user id */
	pr_euid uid_t /* effective user id */
	// 32
	pr_gid    gid_t     /* real group id */
	pr_egid   gid_t     /* effective group id */
	pr_addr   uintptr_t /* address of process */
	pr_size   size_t    /* size of process image in Kbytes */
	pr_rssize size_t    /* resident set size in Kbytes */
	// 64
	pr_rssizepriv size_t /* resident set size of private mappings */
	pr_ttydev     dev_t  /* controlling tty device (or PRNODEV) */
	/* The following percent numbers are 16-bit binary */
	/* fractions [0 .. 1] with the binary point to the */
	/* right of the high-order bit (1.0 == 0x8000) */
	pr_pctcpu ushort_t /* % of recent cpu time used by all lwps */
	pr_pctmem ushort_t /* % of system memory used by process */
	pr_dummy  int32    /* 8 byte alignment: GO doesn't do it automagically */
	// 84 + 4 = 88
	pr_start timestruc_t /* process start time, from the epoch */
	pr_time  timestruc_t /* usr+sys cpu time for this process */
	pr_ctime timestruc_t /* usr+sys cpu time for reaped children */
	// 136
	pr_fname  [PRFNSZ]char  /* name of execed file */
	pr_psargs [PRARGSZ]char /* initial characters of arg list */
	// 232
	pr_wstat  int32     /* if zombie, the wait() status */
	pr_argc   int32     /* initial argument count */
	pr_argv   uintptr_t /* address of initial argument vector */
	pr_envp   uintptr_t /* address of initial environment vector */
	pr_dmodel char      /* data model of the process */
	pr_pad2   [3]char
	pr_taskid taskid_t /* task id */
	// 264
	pr_projid   projid_t /* project id */
	pr_nzomb    int32    /* number of zombie lwps in the process */
	pr_poolid   poolid_t /* pool id */
	pr_zoneid   zoneid_t /* zone id */
	pr_contract id_t     /* process contract */
	pr_filler   [1]int32 /* reserved for future use */
	// 288
	pr_lwp lwpsinfo_t /* information for representative lwp */
	// 440
}

/* Resource usage.  /proc/<pid>/usage /proc/<pid>/lwp/<lwpid>/lwpusage */
type prusage_t struct {
	pr_lwpid id_t  /* lwp id.  0: process or defunct */
	pr_count int32 /* number of contributing lwps */
	// 8
	pr_tstamp timestruc_t /* current time stamp */
	pr_create timestruc_t /* process/lwp creation time stamp */
	pr_term   timestruc_t /* process/lwp termination time stamp */
	pr_rtime  timestruc_t /* total lwp real (elapsed) time */
	// 72
	pr_utime  timestruc_t /* user level cpu time */
	pr_stime  timestruc_t /* system call cpu time */
	pr_ttime  timestruc_t /* other system trap cpu time */
	pr_tftime timestruc_t /* text page fault sleep time */
	// 136
	pr_dftime  timestruc_t /* data page fault sleep time */
	pr_kftime  timestruc_t /* kernel page fault sleep time */
	pr_ltime   timestruc_t /* user lock wait sleep time */
	pr_slptime timestruc_t /* all other sleep time */
	// 200
	pr_wtime    timestruc_t /* wait-cpu (latency) time */
	pr_stoptime timestruc_t /* stopped time */
	// 232
	filltime [6]timestruc_t /* filler for future expansion */
	// 328
	pr_minf  ulong_t /* minor page faults */
	pr_majf  ulong_t /* major page faults */
	pr_nswap ulong_t /* swaps */
	pr_inblk ulong_t /* input blocks (JEL: disk events not always recorded, so perhaps usable as an indicator but not more) */
	// 360
	pr_oublk ulong_t /* output blocks (JEL: disk events not always recorded, so perhaps usable as an indicator but not more) */
	pr_msnd  ulong_t /* messages sent */
	pr_mrcv  ulong_t /* messages received */
	pr_sigs  ulong_t /* signals received */
	// 392
	pr_vctx ulong_t /* voluntary context switches */
	pr_ictx ulong_t /* involuntary context switches */
	pr_sysc ulong_t /* system calls */
	pr_ioch ulong_t /* chars read and written (JEL: no matter, whether to/from disk or somewhere else) */
	// 424
	filler [10]ulong_t /* filler for future expansion */
	// 504
}

/** End Of Solaris types **/

type ProcMetric uint32

const (
	PM_OPEN_FDS ProcMetric = iota
	PM_MAX_FDS
	PM_MINFLT
	PM_MAJFLT
	PM_CPU_UTIL
	PM_MEM_UTIL
	PM_CMINFLT // Linux, only
	PM_CMAJFLT // Linux, only
	PM_UTIME
	PM_STIME
	PM_TIME
	PM_CUTIME
	PM_CSTIME
	PM_CTIME
	PM_NUM_THREADS
	PM_STARTTIME
	PM_VSIZE
	PM_RSS
	PM_VCTX
	PM_ICTX
	PM_BLKIO // Linux, only
	PM_COUNT /* contract: must be the last one */
)

type MetricInfo struct {
	name, help, mtype string
}

/* process metric names and descriptions */
var pm_desc = [PM_COUNT]MetricInfo{
	{ // PM_OPEN_FDS
		"process_open_fds",
		"Number of open file descriptors",
		"gauge",
	}, { // PM_MAX_FDS
		"process_max_fds",
		"Max. number of open file descriptors (soft limit)",
		"gauge",
	}, { // PM_MINFLT
		"process_minor_pagefaults",
		"Number of minor faults of the process not caused a page load from disk",
		"counter",
	}, { // PM_MAJFLT
		"process_major_pagefaults",
		"Number of major faults of the process caused a page load from disk",
		"counter",
	}, { // PM_CPU_UTIL
		"process_cpu_utilization_percent",
		"Percent of recent cpu time used by all lwps",
		"gauge",
	}, { // PM_MEM_UTIL
		"process_mem_utilization_percent",
		"Percent of system memory used by process",
		"gauge",
	}, { // PM_CMINFLT
		"process_children_minor_pagefaults",
		"Number of minor faults of the process waited-for children not caused a page load from disk",
		"counter",
	}, { // PM_CMAJFLT
		"process_children_major_pagefaults",
		"Number of major faults of the process's waited-for children caused a page load from disk",
		"counter",
	}, { // PM_UTIME
		"process_user_cpu_seconds",
		"Total CPU time the process spent in user mode in seconds",
		"counter",
	}, { // PM_STIME
		"process_system_cpu_seconds",
		"Total CPU time the process spent in kernel mode in seconds",
		"counter",
	}, { // PM_TIME
		"process_total_cpu_seconds",
		"Total CPU time the process spent in user and kernel mode in seconds",
		"counter",
	}, { // PM_CUTIME
		"process_children_user_cpu_seconds",
		"Total CPU time the process's waited-for children spent in user mode in seconds",
		"counter",
	}, { // PM_CSTIME
		"process_children_system_cpu_seconds",
		"Total CPU time the process's waited-for children spent in kernel mode in seconds",
		"counter",
	}, { // PM_CTIME
		"process_children_total_cpu_seconds",
		"Total CPU time the process's waited-for children spent in user and in kernel mode in seconds",
		"counter",
	}, { // PM_NUM_THREADS
		"process_threads_total",
		"Number of threads in this process",
		"gauge",
	}, { // PM_STARTTIME
		"process_start_time_seconds",
		"The time the process has been started in seconds elapsed since Epoch",
		"counter",
	}, { // PM_VSIZE
		"process_virtual_memory_bytes",
		"Virtual memory size in bytes",
		"gauge",
	}, { // PM_RSS
		"process_resident_memory_bytes",
		"Resident set size of memory in bytes",
		"gauge",
	}, { // PM_VCTX
		"process_voluntary_ctxsw_total",
		"Number of voluntary context switches",
		"counter",
	}, { // PM_ICTX
		"process_involuntary_ctxsw_total",
		"Number of involuntary context switches",
		"counter",
	}, { // PM_BLKIO
		"process_delayacct_blkio_ticks",
		"Aggregated block I/O delays, measured in clock ticks (centiseconds)",
		"counter",
	},
}

type ProcFd uint32

const (
	FD_LIMITS ProcFd = iota
	FD_STAT
	FD_PSINFO // solaris/illumos, only
	FD_USAGE  // solaris/illumos, only
	FD_COUNT  /* contract: must be the last one */
)

/* emittable process metrics for solaris */
var activeProcMetrics = []ProcMetric{
	PM_MINFLT,
	PM_MAJFLT,
	PM_CPU_UTIL,
	PM_MEM_UTIL,
	PM_UTIME,
	PM_STIME,
	PM_TIME,
	PM_CUTIME,
	PM_CSTIME,
	PM_CTIME,
	PM_NUM_THREADS,
	PM_STARTTIME,
	PM_VSIZE,
	PM_RSS,
	PM_VCTX,
	PM_ICTX,
}

/* emittable fd metrics for solaris */
var activeFdMetrics = []ProcMetric{
	PM_OPEN_FDS,
	PM_MAX_FDS,
}

/*
process metrics related file descriptors for files we always need, and

	do not want to open/close all the time
*/
var pm_fd [FD_COUNT]int

/*
to avaid, that go closes the files in the background, which makes the FDs

	above useless, we need to keep the reference to them as well
*/
var pm_file [FD_COUNT]*os.File

/*
process metric values. TSDBs use internally always float64, so we do not

	need to make a difference between int and non-int values
*/
var pm_val [PM_COUNT]float64

/* path used to count open FDs */
var fd_path string

/* lazy init of this process related metrics */
func init() {
	var testdata_dir = ""
	var onTest = len(os.Args) > 1 && strings.HasSuffix(os.Args[0], ".test")
	if onTest {
		cwd, err := os.Getwd()
		if err != nil {
			panic("Unknwon directory: " + err.Error())
		}
		testdata_dir = cwd + "/testdata"
		fmt.Printf("Using test data in %s ...\n", testdata_dir)
	}

	// we preset all so that it is safe to use these vals even if the rest of
	// init fails
	for i := range int(PM_COUNT) {
		pm_val[i] = 0
	}
	for i := range int(FD_COUNT) {
		pm_fd[i] = -1
	}
	pid := os.Getpid()
	if onTest {
		fd_path = testdata_dir + "/fd"
	} else {
		fd_path = fmt.Sprintf("/proc/%d/fd", pid)
	}

	// NOTE: We do NOT close these FDs intentionally to avoid the open/close
	// overhead for each update.
	var path string
	if onTest {
		path = fmt.Sprintf(testdata_dir + "/solaris.ps_status")
	} else {
		path = fmt.Sprintf("/proc/%d/status", pid)
	}
	f, err := os.OpenFile(path, os.O_RDONLY, 0)
	if err != nil {
		log.Printf("ERROR: metrics: Unable to open %s (%v).", path, err)
	} else {
		pm_file[FD_STAT] = f
		pm_fd[FD_STAT] = int(f.Fd())
	}
	if onTest {
		path = fmt.Sprintf(testdata_dir + "/solaris.ps_info")
	} else {
		path = fmt.Sprintf("/proc/%d/psinfo", pid)
	}
	f, err = os.OpenFile(path, os.O_RDONLY, 0)
	if err != nil {
		log.Printf("ERROR: metrics: Unable to open %s (%v).", path, err)
	} else {
		pm_file[FD_PSINFO] = f
		pm_fd[FD_PSINFO] = int(f.Fd())
	}
	if onTest {
		path = fmt.Sprintf(testdata_dir + "/solaris.ps_usage")
	} else {
		path = fmt.Sprintf("/proc/%d/usage", pid)
	}
	f, err = os.OpenFile(path, os.O_RDONLY, 0)
	if err != nil {
		log.Printf("ERROR: metrics: Unable to open %s (%v).", path, err)
	} else {
		pm_file[FD_USAGE] = f
		pm_fd[FD_USAGE] = int(f.Fd())
	}

	/* usually an app does|cannot not change its own FD limits. So we handle
	it as a const - determine it once, only */
	var lim syscall.Rlimit
	err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &lim)
	if err == nil {
		pm_val[PM_MAX_FDS] = float64(lim.Cur)
	} else {
		log.Printf("ERROR: metrics: Unable determin max. fd limit.")
		pm_val[PM_MAX_FDS] = -1
	}
}

var nan = math.NaN()

func time2float(t timestruc_t) float64 {
	return float64(t.tv_sec) + float64(t.tv_nsec)*1e-9
}
func time2float2(a timestruc_t, b timestruc_t) float64 {
	return float64(a.tv_sec+b.tv_sec) + float64(a.tv_nsec+b.tv_nsec)*1e-9
}

func updateProcMetrics() {
	var status pstatus_t
	var psinfo psinfo_t
	var usage prusage_t

	var fail = pm_fd[FD_STAT] < 0
	if !fail {
		n, err := syscall.Pread(pm_fd[FD_STAT],
			(*(*[unsafe.Sizeof(status)]byte)(unsafe.Pointer(&status)))[:], 0)
		fail = (n < 324 || err != nil)
		if fail {
			fmt.Printf("WARNING: read %s@%d failed: %v\n",
				pm_file[FD_STAT].Name(), n, err)
		}
	}
	if fail {
		pm_val[PM_NUM_THREADS] = nan
		pm_val[PM_UTIME] = nan
		pm_val[PM_STIME] = nan
		pm_val[PM_TIME] = nan
		pm_val[PM_CUTIME] = nan
		pm_val[PM_CSTIME] = nan
		pm_val[PM_CTIME] = nan
	} else {
		pm_val[PM_NUM_THREADS] = float64(status.pr_nlwp + status.pr_nzomb)
		pm_val[PM_UTIME] = time2float(status.pr_utime)
		pm_val[PM_STIME] = time2float(status.pr_stime)
		pm_val[PM_TIME] = time2float2(status.pr_utime, status.pr_stime)
		pm_val[PM_CUTIME] = time2float(status.pr_cutime)
		pm_val[PM_CSTIME] = time2float(status.pr_cstime)
		pm_val[PM_CTIME] = time2float2(status.pr_cutime, status.pr_cstime)
	}
	fail = pm_fd[FD_PSINFO] < 0
	if !fail {
		n, err := syscall.Pread(pm_fd[FD_PSINFO],
			(*(*[unsafe.Sizeof(psinfo)]byte)(unsafe.Pointer(&psinfo)))[:], 0)
		fail = (n < 272 || err != nil)
		if fail {
			fmt.Printf("WARNING: read %s@%d failed: %v\n",
				pm_file[FD_PSINFO].Name(), n, err)
		}
	}
	if fail {
		pm_val[PM_VSIZE] = nan
		pm_val[PM_RSS] = nan
		pm_val[PM_CPU_UTIL] = nan
		pm_val[PM_MEM_UTIL] = nan
		pm_val[PM_STARTTIME] = nan
	} else {
		//num_threads = psinfo.pr_nlwp + psinfo.pr_nzomb	// already by status
		pm_val[PM_VSIZE] = float64(psinfo.pr_size << 10)
		pm_val[PM_RSS] = float64(psinfo.pr_rssize << 10)
		pm_val[PM_CPU_UTIL] = 100 * float64(psinfo.pr_pctcpu) / float64(0x8000)
		pm_val[PM_MEM_UTIL] = 100 * float64(psinfo.pr_pctmem) / float64(0x8000)
		pm_val[PM_STARTTIME] = float64(psinfo.pr_start.tv_sec)
	}
	fail = pm_fd[FD_USAGE] < 0
	if !fail {
		n, err := syscall.Pread(pm_fd[FD_USAGE],
			(*(*[unsafe.Sizeof(usage)]byte)(unsafe.Pointer(&usage)))[:], 0)
		fail = (n < 424 || err != nil)
		if fail {
			fmt.Printf("WARNING: read %s@%d failed: %v\n",
				pm_file[FD_USAGE].Name(), n, err)
		}
	}
	if fail {
		pm_val[PM_MINFLT] = nan
		pm_val[PM_MAJFLT] = nan
		pm_val[PM_VCTX] = nan
		pm_val[PM_ICTX] = nan
	} else {
		pm_val[PM_MINFLT] = float64(usage.pr_minf)
		pm_val[PM_MAJFLT] = float64(usage.pr_majf)
		pm_val[PM_VCTX] = float64(usage.pr_vctx)
		pm_val[PM_ICTX] = float64(usage.pr_ictx)
	}
}

func updateFdMetrics() {
	pm_val[PM_OPEN_FDS] = 0
	f, err := os.Open(fd_path)
	if err != nil {
		log.Printf("ERROR: metrics: Unable to open %s", fd_path)
		return
	}
	defer f.Close()
	for {
		names, err := f.Readdirnames(512)
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Printf("ERROR: metrics: Read error for %s: %s", fd_path, err)
			return
		}
		pm_val[PM_OPEN_FDS] += float64(len(names))
	}
}

func writeProcessMetrics(w io.Writer) {
	updateProcMetrics()
	if isMetadataEnabled() {
		for _, v := range activeProcMetrics {
			fmt.Fprintf(w, "# HELP %s %s\n# TYPE %s %s\n%s %.17g\n",
				pm_desc[v].name, pm_desc[v].help,
				pm_desc[v].name, pm_desc[v].mtype,
				pm_desc[v].name, pm_val[v])
		}
	} else {
		for _, v := range activeProcMetrics {
			fmt.Fprintf(w, "%s %.17g\n", pm_desc[v].name, pm_val[v])
		}
	}
}

func writeFDMetrics(w io.Writer) {
	updateFdMetrics()
	if isMetadataEnabled() {
		for _, v := range activeFdMetrics {
			fmt.Fprintf(w, "# HELP %s %s\n# TYPE %s %s\n%s %.17g\n",
				pm_desc[v].name, pm_desc[v].help,
				pm_desc[v].name, pm_desc[v].mtype,
				pm_desc[v].name, pm_val[v])
		}
	} else {
		for _, v := range activeFdMetrics {
			fmt.Fprintf(w, "%s %.17g\n", pm_desc[v].name, pm_val[v])
		}
	}
}
