package systeminfo

import (
	"context"
	"encoding/json"
	"errors"
	"time"

	"github.com/shirou/gopsutil/v4/cpu"
	"github.com/shirou/gopsutil/v4/disk"
	"github.com/shirou/gopsutil/v4/mem"
	"github.com/shirou/gopsutil/v4/net"
	"github.com/shirou/gopsutil/v4/sensors"
	"github.com/yusing/go-proxy/internal/common"
	"github.com/yusing/go-proxy/internal/gperr"
	"github.com/yusing/go-proxy/internal/logging"
	"github.com/yusing/go-proxy/internal/metrics/period"
	"github.com/yusing/go-proxy/internal/utils/strutils"
)

type SystemInfo struct {
	Timestamp   time.Time
	CPUAverage  float64
	Memory      *mem.VirtualMemoryStat
	Disk        *disk.UsageStat
	NetworkIO   *net.IOCountersStat
	NetworkUp   float64
	NetworkDown float64
	Sensors     []sensors.TemperatureStat
}

var Poller = period.NewPoller("system_info", getSystemInfo)

func init() {
	Poller.Start()
}

func _() { // check if this behavior is not changed
	var _ sensors.Warnings = disk.Warnings{}
}

func getSystemInfo(ctx context.Context, lastResult *SystemInfo) (*SystemInfo, error) {
	errs := gperr.NewBuilder("failed to get system info")
	var systemInfo SystemInfo

	if !common.MetricsDisableCPU {
		cpuAverage, err := cpu.PercentWithContext(ctx, 150*time.Millisecond, false)
		if err != nil {
			errs.Add(err)
		} else {
			systemInfo.CPUAverage = cpuAverage[0]
		}
	}

	if !common.MetricsDisableMemory {
		memoryInfo, err := mem.VirtualMemory()
		if err != nil {
			errs.Add(err)
		}
		systemInfo.Memory = memoryInfo
	}

	if !common.MetricsDisableDisk {
		diskInfo, err := disk.Usage("/")
		if err != nil {
			errs.Add(err)
		}
		systemInfo.Disk = diskInfo
	}

	if !common.MetricsDisableNetwork {
		networkIO, err := net.IOCounters(false)
		if err != nil {
			errs.Add(err)
		} else {
			networkIO := networkIO[0]
			systemInfo.NetworkIO = &networkIO
			var networkUp, networkDown float64
			if lastResult != nil {
				interval := time.Since(lastResult.Timestamp).Seconds()
				networkUp = float64(networkIO.BytesSent-lastResult.NetworkIO.BytesSent) / interval
				networkDown = float64(networkIO.BytesRecv-lastResult.NetworkIO.BytesRecv) / interval
			}
			systemInfo.NetworkUp = networkUp
			systemInfo.NetworkDown = networkDown
		}
	}

	if !common.MetricsDisableSensors {
		sensorsInfo, err := sensors.SensorsTemperatures()
		if err != nil {
			errs.Add(err)
		}
		systemInfo.Sensors = sensorsInfo
	}

	if errs.HasError() {
		allWarnings := gperr.NewBuilder("")
		allErrors := gperr.NewBuilder("failed to get system info")
		errs.ForEach(func(err error) {
			// disk.Warnings has the same type
			// all Warnings are alias of common.Warnings from "github.com/shirou/gopsutil/v4/internal/common"
			// see line 37
			warnings := new(sensors.Warnings)
			if errors.As(err, &warnings) {
				for _, warning := range warnings.List {
					allWarnings.Add(warning)
				}
			} else {
				allErrors.Add(err)
			}
		})
		if allWarnings.HasError() {
			logging.Warn().Msg(allWarnings.String())
		}
		if allErrors.HasError() {
			return nil, allErrors.Error()
		}
	}

	return &systemInfo, nil
}

func (s *SystemInfo) MarshalJSON() ([]byte, error) {
	return json.Marshal(map[string]any{
		"timestamp":   s.Timestamp.Unix(),
		"time":        strutils.FormatTime(s.Timestamp),
		"cpu_average": s.CPUAverage,
		"memory": map[string]any{
			"total":        s.Memory.Total,
			"available":    s.Memory.Available,
			"used":         s.Memory.Used,
			"used_percent": s.Memory.UsedPercent,
		},
		"disk": map[string]any{
			"path":         s.Disk.Path,
			"fstype":       s.Disk.Fstype,
			"total":        s.Disk.Total,
			"used":         s.Disk.Used,
			"used_percent": s.Disk.UsedPercent,
			"free":         s.Disk.Free,
		},
		"network": map[string]any{
			"name":           s.NetworkIO.Name,
			"bytes_sent":     s.NetworkIO.BytesSent,
			"bytes_recv":     s.NetworkIO.BytesRecv,
			"upload_speed":   s.NetworkUp,
			"download_speed": s.NetworkDown,
		},
		"sensors": s.Sensors,
	})
}