// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package hoststats

import (
	"context"
	"fmt"
	"math"
	"runtime"
	"sync"
	"time"

	"github.com/armon/go-metrics"
	"github.com/hashicorp/go-hclog"
	"github.com/shirou/gopsutil/v3/disk"
	"github.com/shirou/gopsutil/v3/host"
	"github.com/shirou/gopsutil/v3/mem"
)

// Collector collects host resource usage stats
type Collector struct {
	numCores      int
	cpuCalculator map[string]*cpuStatsCalculator
	hostStats     *HostStats
	hostStatsLock sync.RWMutex
	dataDir       string

	metrics    Metrics
	baseLabels []metrics.Label

	logger hclog.Logger
}

// NewCollector returns a Collector. The dataDir is passed in
// so that we can present the disk related statistics for the mountpoint where the dataDir exists
func NewCollector(ctx context.Context, logger hclog.Logger, dataDir string, opts ...CollectorOption) *Collector {
	logger = logger.Named("host_stats")
	collector := initCollector(logger, dataDir)
	go collector.loop(ctx)
	return collector
}

// initCollector initializes the Collector but does not start the collection loop
func initCollector(logger hclog.Logger, dataDir string, opts ...CollectorOption) *Collector {
	numCores := runtime.NumCPU()
	statsCalculator := make(map[string]*cpuStatsCalculator)
	collector := &Collector{
		cpuCalculator: statsCalculator,
		numCores:      numCores,
		logger:        logger,
		dataDir:       dataDir,
	}

	for _, opt := range opts {
		opt(collector)
	}

	if collector.metrics == nil {
		collector.metrics = metrics.Default()
	}
	return collector
}

func (c *Collector) loop(ctx context.Context) {
	// Start collecting host stats right away and then keep collecting every
	// collection interval
	next := time.NewTimer(0)
	defer next.Stop()
	for {
		select {
		case <-next.C:
			c.collect()
			next.Reset(hostStatsCollectionInterval)
			c.Stats().Emit(c.metrics, c.baseLabels)

		case <-ctx.Done():
			return
		}
	}
}

// collect will collect stats related to resource usage of the host
func (c *Collector) collect() {
	hs := &HostStats{Timestamp: time.Now().UTC().UnixNano()}

	// Determine up-time
	uptime, err := host.Uptime()
	if err != nil {
		c.logger.Debug("failed to collect uptime stats", "error", err)
		uptime = 0
	}
	hs.Uptime = uptime

	// Collect memory stats
	mstats, err := c.collectMemoryStats()
	if err != nil {
		c.logger.Debug("failed to collect memory stats", "error", err)
		mstats = &MemoryStats{}
	}
	hs.Memory = mstats

	// Collect cpu stats
	cpus, err := c.collectCPUStats()
	if err != nil {
		c.logger.Debug("failed to collect cpu stats", "error", err)
		cpus = []*CPUStats{}
	}
	hs.CPU = cpus

	// Collect disk stats
	if c.dataDir != "" {
		diskStats, err := c.collectDiskStats(c.dataDir)
		if err != nil {
			c.logger.Debug("failed to collect dataDir disk stats", "error", err)
		}
		hs.DataDirStats = diskStats
	}

	// Update the collected status object.
	c.hostStatsLock.Lock()
	c.hostStats = hs
	c.hostStatsLock.Unlock()
}

func (c *Collector) collectDiskStats(dir string) (*DiskStats, error) {
	usage, err := disk.Usage(dir)
	if err != nil {
		return nil, fmt.Errorf("failed to collect disk usage stats: %w", err)
	}
	return c.toDiskStats(usage), nil
}

func (c *Collector) collectMemoryStats() (*MemoryStats, error) {
	memStats, err := mem.VirtualMemory()
	if err != nil {
		return nil, err
	}
	mem := &MemoryStats{
		Total:       memStats.Total,
		Available:   memStats.Available,
		Used:        memStats.Used,
		UsedPercent: memStats.UsedPercent,
		Free:        memStats.Free,
	}

	return mem, nil
}

// Stats returns the host stats that has been collected
func (c *Collector) Stats() *HostStats {
	c.hostStatsLock.RLock()
	defer c.hostStatsLock.RUnlock()

	if c.hostStats == nil {
		return &HostStats{}
	}

	return c.hostStats.Clone()
}

// toDiskStats merges UsageStat and PartitionStat to create a DiskStat
func (c *Collector) toDiskStats(usage *disk.UsageStat) *DiskStats {
	ds := DiskStats{
		Size:              usage.Total,
		Used:              usage.Used,
		Available:         usage.Free,
		UsedPercent:       usage.UsedPercent,
		InodesUsedPercent: usage.InodesUsedPercent,
		Path:              usage.Path,
	}
	if math.IsNaN(ds.UsedPercent) {
		ds.UsedPercent = 0.0
	}
	if math.IsNaN(ds.InodesUsedPercent) {
		ds.InodesUsedPercent = 0.0
	}

	return &ds
}

type CollectorOption func(c *Collector)

func WithMetrics(m *metrics.Metrics) CollectorOption {
	return func(c *Collector) {
		c.metrics = m
	}
}

func WithBaseLabels(labels []metrics.Label) CollectorOption {
	return func(c *Collector) {
		c.baseLabels = labels
	}
}
