package sysinfo

import (
	"context"
	"os"
	"path"
	"strings"

	"github.com/containerd/cgroups/v3"
	cgroupsV2 "github.com/containerd/cgroups/v3/cgroup2"
	"github.com/containerd/log"
	"github.com/moby/sys/userns"
)

func newV2(options ...Opt) *SysInfo {
	sysInfo := &SysInfo{
		CgroupUnified: true,
		cg2GroupPath:  "/",
	}
	for _, o := range options {
		o(sysInfo)
	}

	ops := []infoCollector{
		applyNetworkingInfo,
		applyAppArmorInfo,
		applySeccompInfo,
		applyCgroupNsInfo,
	}

	m, err := cgroupsV2.Load(sysInfo.cg2GroupPath)
	if err != nil {
		log.G(context.TODO()).Warn(err)
	} else {
		sysInfo.cg2Controllers = make(map[string]struct{})
		controllers, err := m.Controllers()
		if err != nil {
			log.G(context.TODO()).Warn(err)
		}
		for _, c := range controllers {
			sysInfo.cg2Controllers[c] = struct{}{}
		}
		ops = append(ops,
			applyMemoryCgroupInfoV2,
			applyCPUCgroupInfoV2,
			applyIOCgroupInfoV2,
			applyCPUSetCgroupInfoV2,
			applyPIDSCgroupInfoV2,
			applyDevicesCgroupInfoV2,
		)
	}

	for _, o := range ops {
		o(sysInfo)
	}
	return sysInfo
}

func getSwapLimitV2() bool {
	_, g, err := cgroups.ParseCgroupFileUnified("/proc/self/cgroup")
	if err != nil {
		return false
	}

	if g == "" {
		return false
	}

	cGroupPath := path.Join("/sys/fs/cgroup", g, "memory.swap.max")
	if _, err = os.Stat(cGroupPath); os.IsNotExist(err) {
		return false
	}
	return true
}

func applyMemoryCgroupInfoV2(info *SysInfo) {
	if _, ok := info.cg2Controllers["memory"]; !ok {
		info.Warnings = append(info.Warnings, "Unable to find memory controller")
		return
	}

	info.MemoryLimit = true
	info.SwapLimit = getSwapLimitV2()
	info.MemoryReservation = true
	info.OomKillDisable = false
	info.MemorySwappiness = false
	info.KernelMemory = false
	info.KernelMemoryTCP = false
}

func applyCPUCgroupInfoV2(info *SysInfo) {
	if _, ok := info.cg2Controllers["cpu"]; !ok {
		info.Warnings = append(info.Warnings, "Unable to find cpu controller")
		return
	}
	info.CPUShares = true
	info.CPUCfs = true
	info.CPURealtime = false
}

func applyIOCgroupInfoV2(info *SysInfo) {
	if _, ok := info.cg2Controllers["io"]; !ok {
		info.Warnings = append(info.Warnings, "Unable to find io controller")
		return
	}

	info.BlkioWeight = true
	info.BlkioWeightDevice = true
	info.BlkioReadBpsDevice = true
	info.BlkioWriteBpsDevice = true
	info.BlkioReadIOpsDevice = true
	info.BlkioWriteIOpsDevice = true
}

func applyCPUSetCgroupInfoV2(info *SysInfo) {
	if _, ok := info.cg2Controllers["cpuset"]; !ok {
		info.Warnings = append(info.Warnings, "Unable to find cpuset controller")
		return
	}
	info.Cpuset = true

	cpus, err := os.ReadFile(path.Join("/sys/fs/cgroup", info.cg2GroupPath, "cpuset.cpus.effective"))
	if err != nil {
		return
	}
	info.Cpus = strings.TrimSpace(string(cpus))

	cpuSets, err := parseUintList(info.Cpus, 0)
	if err != nil {
		info.Warnings = append(info.Warnings, "Unable to parse cpuset cpus: "+err.Error())
		return
	}
	info.CPUSets = cpuSets

	mems, err := os.ReadFile(path.Join("/sys/fs/cgroup", info.cg2GroupPath, "cpuset.mems.effective"))
	if err != nil {
		return
	}
	info.Mems = strings.TrimSpace(string(mems))

	memSets, err := parseUintList(info.Cpus, 0)
	if err != nil {
		info.Warnings = append(info.Warnings, "Unable to parse cpuset mems: "+err.Error())
		return
	}
	info.MemSets = memSets
}

func applyPIDSCgroupInfoV2(info *SysInfo) {
	if _, ok := info.cg2Controllers["pids"]; !ok {
		info.Warnings = append(info.Warnings, "Unable to find pids controller")
		return
	}
	info.PidsLimit = true
}

func applyDevicesCgroupInfoV2(info *SysInfo) {
	info.CgroupDevicesEnabled = !userns.RunningInUserNS()
}
