package monitor

import (
	"os"
	"runtime"
	"sync"
	"sync/atomic"
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/gofiber/fiber/v2/internal/gopsutil/cpu"
	"github.com/gofiber/fiber/v2/internal/gopsutil/load"
	"github.com/gofiber/fiber/v2/internal/gopsutil/mem"
	"github.com/gofiber/fiber/v2/internal/gopsutil/net"
	"github.com/gofiber/fiber/v2/internal/gopsutil/process"
)

type stats struct {
	PID statsPID `json:"pid"`
	OS  statsOS  `json:"os"`
}

type statsPID struct {
	CPU   float64 `json:"cpu"`
	RAM   uint64  `json:"ram"`
	Conns int     `json:"conns"`
}

type statsOS struct {
	CPU      float64 `json:"cpu"`
	RAM      uint64  `json:"ram"`
	TotalRAM uint64  `json:"total_ram"`
	LoadAvg  float64 `json:"load_avg"`
	Conns    int     `json:"conns"`
}

var (
	monitPIDCPU   atomic.Value
	monitPIDRAM   atomic.Value
	monitPIDConns atomic.Value

	monitOSCPU      atomic.Value
	monitOSRAM      atomic.Value
	monitOSTotalRAM atomic.Value
	monitOSLoadAvg  atomic.Value
	monitOSConns    atomic.Value
)

var (
	mutex sync.RWMutex
	once  sync.Once
	data  = &stats{}
)

// New creates a new middleware handler
func New(config ...Config) fiber.Handler {
	// Set default config
	cfg := configDefault(config...)

	// Start routine to update statistics
	once.Do(func() {
		p, _ := process.NewProcess(int32(os.Getpid())) //nolint:errcheck // TODO: Handle error
		numcpu := runtime.NumCPU()
		updateStatistics(p, numcpu)

		go func() {
			for {
				time.Sleep(cfg.Refresh)

				updateStatistics(p, numcpu)
			}
		}()
	})

	// Return new handler
	//nolint:errcheck // Ignore the type-assertion errors
	return func(c *fiber.Ctx) error {
		// Don't execute middleware if Next returns true
		if cfg.Next != nil && cfg.Next(c) {
			return c.Next()
		}

		if c.Method() != fiber.MethodGet {
			return fiber.ErrMethodNotAllowed
		}
		if c.Get(fiber.HeaderAccept) == fiber.MIMEApplicationJSON || cfg.APIOnly {
			mutex.Lock()
			data.PID.CPU, _ = monitPIDCPU.Load().(float64)
			data.PID.RAM, _ = monitPIDRAM.Load().(uint64)
			data.PID.Conns, _ = monitPIDConns.Load().(int)

			data.OS.CPU, _ = monitOSCPU.Load().(float64)
			data.OS.RAM, _ = monitOSRAM.Load().(uint64)
			data.OS.TotalRAM, _ = monitOSTotalRAM.Load().(uint64)
			data.OS.LoadAvg, _ = monitOSLoadAvg.Load().(float64)
			data.OS.Conns, _ = monitOSConns.Load().(int)
			mutex.Unlock()
			return c.Status(fiber.StatusOK).JSON(data)
		}
		c.Set(fiber.HeaderContentType, fiber.MIMETextHTMLCharsetUTF8)
		return c.Status(fiber.StatusOK).SendString(cfg.index)
	}
}

func updateStatistics(p *process.Process, numcpu int) {
	pidCPU, err := p.Percent(0)
	if err == nil {
		monitPIDCPU.Store(pidCPU / float64(numcpu))
	}

	if osCPU, err := cpu.Percent(0, false); err == nil && len(osCPU) > 0 {
		monitOSCPU.Store(osCPU[0])
	}

	if pidRAM, err := p.MemoryInfo(); err == nil && pidRAM != nil {
		monitPIDRAM.Store(pidRAM.RSS)
	}

	if osRAM, err := mem.VirtualMemory(); err == nil && osRAM != nil {
		monitOSRAM.Store(osRAM.Used)
		monitOSTotalRAM.Store(osRAM.Total)
	}

	if loadAvg, err := load.Avg(); err == nil && loadAvg != nil {
		monitOSLoadAvg.Store(loadAvg.Load1)
	}

	pidConns, err := net.ConnectionsPid("tcp", p.Pid)
	if err == nil {
		monitPIDConns.Store(len(pidConns))
	}

	osConns, err := net.Connections("tcp")
	if err == nil {
		monitOSConns.Store(len(osConns))
	}
}
