//go:build linux

package proc

import (
	"encoding/binary"
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"
)

const linuxAuxvClockTicks = 17

func processByPIDNative(pid int) (ProcessInfo, bool) {
	data, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
	if err != nil {
		return ProcessInfo{}, false
	}
	ppid, pgid, startTicks, ok := parseLinuxProcStat(string(data))
	if !ok {
		return ProcessInfo{}, false
	}
	bootTime, ok := parseLinuxBootTime()
	if !ok {
		return ProcessInfo{}, false
	}
	bootID, ok := parseLinuxBootID()
	if !ok {
		return ProcessInfo{}, false
	}
	clockTicksPerSecond, ok := linuxClockTicksPerSecond()
	if !ok {
		return ProcessInfo{}, false
	}
	startedAt := linuxStartedAt(bootTime, startTicks, clockTicksPerSecond)
	if startedAt.IsZero() {
		return ProcessInfo{}, false
	}
	return ProcessInfo{
		PID:       pid,
		PPID:      ppid,
		PGID:      pgid,
		StartedAt: startedAt,
		StartID:   linuxProcessStartID(bootID, startTicks),
	}, true
}

func parseLinuxProcStat(stat string) (ppid int, pgid int, startTicks uint64, ok bool) {
	// comm is parenthesized but may itself contain ')', so the final ") "
	// separates it from field 3 (state).
	end := strings.LastIndex(stat, ") ")
	if end < 0 {
		return 0, 0, 0, false
	}
	fields := strings.Fields(stat[end+2:])
	// fields starts at proc(5) field 3: state, ppid, pgrp, ... starttime.
	if len(fields) < 20 {
		return 0, 0, 0, false
	}
	ppid, err := strconv.Atoi(fields[1])
	if err != nil {
		return 0, 0, 0, false
	}
	pgid, err = strconv.Atoi(fields[2])
	if err != nil {
		return 0, 0, 0, false
	}
	startTicks, err = strconv.ParseUint(fields[19], 10, 64)
	if err != nil {
		return 0, 0, 0, false
	}
	return ppid, pgid, startTicks, true
}

func parseLinuxBootTime() (time.Time, bool) {
	data, err := os.ReadFile("/proc/stat")
	if err != nil {
		return time.Time{}, false
	}
	for _, line := range strings.Split(string(data), "\n") {
		fields := strings.Fields(line)
		if len(fields) != 2 || fields[0] != "btime" {
			continue
		}
		seconds, err := strconv.ParseInt(fields[1], 10, 64)
		if err != nil || seconds <= 0 {
			return time.Time{}, false
		}
		return time.Unix(seconds, 0).UTC(), true
	}
	return time.Time{}, false
}

func parseLinuxBootID() (string, bool) {
	data, err := os.ReadFile("/proc/sys/kernel/random/boot_id")
	if err != nil {
		return "", false
	}
	return normalizeLinuxBootID(string(data))
}

func normalizeLinuxBootID(raw string) (string, bool) {
	bootID := strings.ToLower(strings.TrimSpace(raw))
	if len(bootID) != 36 {
		return "", false
	}
	for index, value := range bootID {
		switch index {
		case 8, 13, 18, 23:
			if value != '-' {
				return "", false
			}
		default:
			if (value < '0' || value > '9') && (value < 'a' || value > 'f') {
				return "", false
			}
		}
	}
	return bootID, true
}

func linuxProcessStartID(bootID string, startTicks uint64) string {
	return "linux:" + bootID + ":" + strconv.FormatUint(startTicks, 10)
}

func linuxClockTicksPerSecond() (uint64, bool) {
	data, err := os.ReadFile("/proc/self/auxv")
	if err != nil {
		return 0, false
	}
	return parseLinuxAuxvClockTicks(data, strconv.IntSize/8, binary.NativeEndian)
}

func parseLinuxAuxvClockTicks(data []byte, wordSize int, byteOrder binary.ByteOrder) (uint64, bool) {
	if wordSize != 4 && wordSize != 8 {
		return 0, false
	}
	entrySize := wordSize * 2
	for len(data) >= entrySize {
		key := linuxAuxvWord(data[:wordSize], wordSize, byteOrder)
		value := linuxAuxvWord(data[wordSize:entrySize], wordSize, byteOrder)
		if key == 0 {
			return 0, false
		}
		if key == linuxAuxvClockTicks {
			return value, value > 0
		}
		data = data[entrySize:]
	}
	return 0, false
}

func linuxAuxvWord(data []byte, wordSize int, byteOrder binary.ByteOrder) uint64 {
	if wordSize == 4 {
		return uint64(byteOrder.Uint32(data))
	}
	return byteOrder.Uint64(data)
}

func linuxStartedAt(bootTime time.Time, startTicks uint64, clockTicksPerSecond uint64) time.Time {
	if bootTime.IsZero() || clockTicksPerSecond == 0 {
		return time.Time{}
	}
	seconds := startTicks / clockTicksPerSecond
	remainder := startTicks % clockTicksPerSecond
	nanoseconds := remainder * uint64(time.Second) / clockTicksPerSecond
	return bootTime.UTC().Add(time.Duration(seconds)*time.Second + time.Duration(nanoseconds))
}
