/*
** 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 cpu

import (
	"errors"
	"sync"
	"time"
	"unsafe"

	"golang.zabbix.com/agent2/pkg/win32"
	"golang.zabbix.com/sdk/errs"
	"golang.zabbix.com/sdk/plugin"
	"golang.zabbix.com/sdk/zbxerr"
)

const (
	modeParam = 2
	cpuParam  = 1
	noParam   = 0

	defaultIndex = 60
)

// Plugin -
type Plugin struct {
	plugin.Base
	cpus      []*cpuUnit
	collector *pdhCollector
	cpusMu    sync.Mutex
	stop      chan struct{}
}

func init() {
	impl.collector = newPdhCollector(&impl)

	err := plugin.RegisterMetrics(
		&impl, pluginName,
		"system.cpu.discovery", "List of detected CPUs/CPU cores, used for low-level discovery.",
		"system.cpu.load", "CPU load.",
		"system.cpu.num", "Number of CPUs.",
		"system.cpu.util", "CPU utilization percentage.",
	)
	if err != nil {
		panic(errs.Wrap(err, "failed to register metrics"))
	}
}

func numCPUOnline() int {
	return numCPU()
}

func numCPUConf() int {
	// unsupported on Windows
	return 0
}

func numCPU() (numCpu int) {
	size, err := win32.GetLogicalProcessorInformationEx(win32.RelationProcessorCore, nil)
	if err != nil {
		return
	}

	b := make([]byte, size)
	size, err = win32.GetLogicalProcessorInformationEx(win32.RelationProcessorCore, b)
	if err != nil {
		return
	}

	var sinfo *win32.SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX
	for i := uint32(0); i < size; i += sinfo.Size {
		sinfo = (*win32.SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX)(unsafe.Pointer(&b[i]))
		pinfo := (*win32.PROCESSOR_RELATIONSHIP)(unsafe.Pointer(&sinfo.Data[0]))
		groups := (*win32.RGGROUP_AFFINITY)(unsafe.Pointer(&pinfo.GroupMask[0]))[:pinfo.GroupCount:pinfo.GroupCount]
		for _, group := range groups {
			for mask := group.Mask; mask != 0; mask >>= 1 {
				numCpu += int(mask & 1)
			}
		}
	}
	return
}

func (p *Plugin) getCPULoad(params []string) (result any, err error) {
	split := 1

	period := historyIndex(defaultIndex)
	switch len(params) {
	case modeParam: // mode parameter
		if period = periodByMode(params[1]); period < 0 {
			return nil, errors.New("Invalid first parameter.")
		}

		fallthrough
	case cpuParam: // all, cpu number or per cpu
		switch params[0] {
		case "", "all":
		case "percpu":
			split = numCPUOnline()
		default:
			return nil, errors.New("Invalid second parameter.")
		}
	case noParam:
	default:
		return nil, zbxerr.ErrorTooManyParameters
	}

	p.cpusMu.Lock()
	defer p.cpusMu.Unlock()

	return p.cpus[0].counterAverage(counterLoad, period, split), nil
}

func (p *Plugin) collectCpuData() (err error) {
	ok, err := p.collector.collect()
	if err != nil || !ok {
		return
	}

	for i, cpu := range p.cpus {
		slot := &cpu.history[cpu.tail]
		cpu.status = cpuStatusOnline
		if i == 0 {
			// gather cpu load into 'total' slot
			slot.load += p.collector.cpuLoad()
		}
		slot.util += p.collector.cpuUtil(i)

		p.cpusMu.Lock()
		if cpu.tail = cpu.tail.inc(); cpu.tail == cpu.head {
			cpu.head = cpu.head.inc()
		}
		// write the current value into next slot so next time the new value
		// can be added to it resulting in incrementing counter
		nextSlot := &cpu.history[cpu.tail]
		*nextSlot = *slot
		p.cpusMu.Unlock()
	}
	return
}

func (p *Plugin) Start() {
	numCpus := numCPU()
	numGroups := getNumaNodeCount()
	if numCpus == 0 || numGroups == 0 {
		p.Warningf("cannot calculate the number of CPUs per group, only total values will be available")
	}
	p.cpus = p.newCpus(numCpus)
	p.collector.open(numCpus, numGroups)

	p.stop = make(chan struct{})

	go func() {
		t := time.NewTicker(1 * time.Second)
		defer t.Stop()
		for {
			select {
			case <-p.stop:
				return
			case <-t.C:
				p.Debugf("starting to collect CPU performance data")

				err := p.collectCpuData()
				if err != nil {
					p.Warningf("failed to get CPU performance data: '%s'", err)
					continue
				}

				p.Debugf("collected CPU performance data")
			}
		}
	}()
}

func (p *Plugin) Stop() {
	p.collector.close()
	p.cpus = nil

	p.stop <- struct{}{}
}

func (p *Plugin) Export(key string, params []string, ctx plugin.ContextProvider) (result interface{}, err error) {
	if p.cpus == nil || p.cpus[0].head == p.cpus[0].tail {
		// no data gathered yet
		return
	}
	switch key {
	case "system.cpu.discovery":
		return p.getCpuDiscovery(params)
	case "system.cpu.load":
		return p.getCPULoad(params)
	case "system.cpu.num":
		if len(params) > 0 && params[0] == "max" {
			return nil, errors.New("Invalid first parameter.")
		}
		return p.getCpuNum(params)
	case "system.cpu.util":
		return p.getCpuUtil(params)
	default:
		return nil, plugin.UnsupportedMetricError
	}
}

func (p *Plugin) getCounterAverage(cpu *cpuUnit, counter cpuCounter, period historyIndex) any {
	p.cpusMu.Lock()
	defer p.cpusMu.Unlock()
	return cpu.counterAverage(counter, period, 1)
}