// Package statcollector provides system information collection
package statcollector

import (
	"log/slog"
	"os"
	"os/exec"
	"regexp"
	"strconv"
	"strings"
	"time"

	"git.eeqj.de/sneak/hdmistat/internal/netmon"
	"github.com/dustin/go-humanize"
	"github.com/shirou/gopsutil/v3/cpu"
	"github.com/shirou/gopsutil/v3/disk"
	"github.com/shirou/gopsutil/v3/host"
	"github.com/shirou/gopsutil/v3/mem"
	psnet "github.com/shirou/gopsutil/v3/net"
	"github.com/shirou/gopsutil/v3/process"
)

const (
	// Process collection constants
	maxProcesses       = 100
	processTimeout     = 50 * time.Millisecond
	processStableTime  = 100 * time.Millisecond
	msToSecondsDivisor = 1000

	// Network constants
	bitsPerMegabit = 1000 * 1000
)

// SystemInfo represents overall system information
type SystemInfo struct {
	Hostname    string
	Uptime      time.Duration
	MemoryTotal uint64
	MemoryUsed  uint64
	MemoryFree  uint64
	CPUPercent  []float64
	Temperature map[string]float64
	DiskUsage   []DiskInfo
	Network     []NetworkInfo
	Processes   []ProcessInfo
	CollectedAt time.Time
}

// DiskInfo represents disk usage information
type DiskInfo struct {
	Path        string
	Total       uint64
	Used        uint64
	Free        uint64
	UsedPercent float64
}

// NetworkInfo represents network interface information
type NetworkInfo struct {
	Name         string
	IPAddresses  []string
	LinkSpeed    uint64
	BytesSent    uint64
	BytesRecv    uint64
	BitsSentRate uint64 // bits per second
	BitsRecvRate uint64 // bits per second
}

// FormatSentRate returns the send rate as a human-readable string
func (n *NetworkInfo) FormatSentRate() string {
	return humanize.SI(float64(n.BitsSentRate), "bit/s")
}

// FormatRecvRate returns the receive rate as a human-readable string
func (n *NetworkInfo) FormatRecvRate() string {
	return humanize.SI(float64(n.BitsRecvRate), "bit/s")
}

// ProcessInfo represents process information
type ProcessInfo struct {
	PID        int32
	Name       string
	CPUPercent float64
	MemoryRSS  uint64
	MemoryVMS  uint64
	Username   string
}

// Collector interface for collecting system information
type Collector interface {
	Collect() (*SystemInfo, error)
}

// SystemCollector implements Collector
type SystemCollector struct {
	logger          *slog.Logger
	netMonitor      *netmon.Monitor
	lastCollectTime time.Time
}

// NewSystemCollector creates a new system collector
func NewSystemCollector(logger *slog.Logger) *SystemCollector {
	nm := netmon.New(logger)
	nm.Start()

	return &SystemCollector{
		logger:     logger,
		netMonitor: nm,
	}
}

// Stop stops the system collector
func (c *SystemCollector) Stop() {
	if c.netMonitor != nil {
		c.netMonitor.Stop()
	}
}

// Collect gathers system information
func (c *SystemCollector) Collect() (*SystemInfo, error) {
	info := &SystemInfo{
		CollectedAt: time.Now(),
		Temperature: make(map[string]float64),
	}

	// Hostname
	hostname, err := os.Hostname()
	if err != nil {
		c.logger.Warn("getting hostname", "error", err)
		info.Hostname = "unknown"
	} else {
		info.Hostname = hostname
	}

	// Uptime
	uptimeSecs, err := host.Uptime()
	if err != nil {
		c.logger.Warn("getting uptime", "error", err)
	} else {
		if uptimeSecs > 0 {
			// Convert uint64 to int64 safely to avoid overflow
			maxInt64 := ^uint64(0) >> 1
			if uptimeSecs <= maxInt64 {
				info.Uptime = time.Duration(int64(uptimeSecs)) * time.Second
			}
		}
	}

	// Memory
	vmStat, err := mem.VirtualMemory()
	if err != nil {
		c.logger.Warn("getting memory stats", "error", err)
	} else {
		info.MemoryTotal = vmStat.Total
		info.MemoryUsed = vmStat.Used
		info.MemoryFree = vmStat.Available
	}

	// CPU
	cpuPercent, err := cpu.Percent(time.Second, true)
	if err != nil {
		c.logger.Warn("getting cpu percent", "error", err)
	} else {
		info.CPUPercent = cpuPercent
	}

	// Temperature
	temps, err := host.SensorsTemperatures()
	if err != nil {
		c.logger.Warn("getting temperatures", "error", err)
	} else {
		for _, temp := range temps {
			if temp.Temperature > 0 {
				info.Temperature[temp.SensorKey] = temp.Temperature
			}
		}
	}

	// Disk usage
	partitions, err := disk.Partitions(false)
	if err != nil {
		c.logger.Warn("getting partitions", "error", err)
	} else {
		for _, partition := range partitions {
			if strings.HasPrefix(partition.Mountpoint, "/dev") ||
				strings.HasPrefix(partition.Mountpoint, "/sys") ||
				strings.HasPrefix(partition.Mountpoint, "/proc") {
				continue
			}

			usage, err := disk.Usage(partition.Mountpoint)
			if err != nil {
				continue
			}

			info.DiskUsage = append(info.DiskUsage, DiskInfo{
				Path:        partition.Mountpoint,
				Total:       usage.Total,
				Used:        usage.Used,
				Free:        usage.Free,
				UsedPercent: usage.UsedPercent,
			})
		}
	}

	// Network - get rates from network monitor
	netStats := c.netMonitor.GetStats()

	// Also get interface details for IP addresses
	interfaces, err := psnet.Interfaces()
	if err != nil {
		c.logger.Warn("getting network interfaces", "error", err)
	} else {
		// Create a map of interface names to IPs and link speeds
		ifaceIPs := make(map[string][]string)
		ifaceSpeeds := make(map[string]uint64)

		for _, iface := range interfaces {
			if iface.Name == "lo" || strings.HasPrefix(iface.Name, "docker") {
				continue
			}
			var ips []string
			for _, addr := range iface.Addrs {
				ips = append(ips, addr.Addr)
			}
			ifaceIPs[iface.Name] = ips

			// Try to get link speed with ethtool
			if speed := c.getLinkSpeed(iface.Name); speed > 0 {
				ifaceSpeeds[iface.Name] = speed
			}
		}

		// Combine network monitor stats with interface details
		for _, stat := range netStats {
			netInfo := NetworkInfo{
				Name:         stat.Name,
				BytesSent:    stat.BytesSent,
				BytesRecv:    stat.BytesRecv,
				BitsSentRate: stat.BitsSentRate,
				BitsRecvRate: stat.BitsRecvRate,
			}

			// Add IP addresses if available
			if ips, ok := ifaceIPs[stat.Name]; ok {
				netInfo.IPAddresses = ips
			}

			// Add link speed if available
			if speed, ok := ifaceSpeeds[stat.Name]; ok {
				netInfo.LinkSpeed = speed
			}

			info.Network = append(info.Network, netInfo)
		}
	}

	// Processes
	processes, err := process.Processes()
	if err != nil {
		c.logger.Warn("getting processes", "error", err)
	} else {
		// Limit to top processes to avoid hanging
		processCount := 0

		for _, p := range processes {
			if processCount >= maxProcesses {
				break
			}

			// Skip kernel threads and very short-lived processes
			name, err := p.Name()
			if err != nil || name == "" {
				continue
			}

			// Use CreateTime to skip very new processes that might not have stable stats
			createTime, err := p.CreateTime()
			if err != nil || time.Since(time.Unix(createTime/msToSecondsDivisor, 0)) < processStableTime {
				continue
			}

			// Get CPU percent with timeout - this is the call that can hang
			cpuPercent := 0.0
			cpuChan := make(chan float64, 1)
			go func() {
				cpu, _ := p.CPUPercent()
				cpuChan <- cpu
			}()

			select {
			case cpu := <-cpuChan:
				cpuPercent = cpu
			case <-time.After(processTimeout):
				// Skip this process if CPU sampling takes too long
				c.logger.Debug("skipping process due to CPU timeout", "pid", p.Pid, "name", name)
				continue
			}

			memInfo, _ := p.MemoryInfo()
			username, _ := p.Username()

			info.Processes = append(info.Processes, ProcessInfo{
				PID:        p.Pid,
				Name:       name,
				CPUPercent: cpuPercent,
				MemoryRSS:  memInfo.RSS,
				MemoryVMS:  memInfo.VMS,
				Username:   username,
			})

			processCount++
		}
	}

	c.lastCollectTime = time.Now()
	return info, nil
}

// getLinkSpeed gets the link speed for an interface using ethtool
func (c *SystemCollector) getLinkSpeed(ifaceName string) uint64 {
	// Run ethtool to get link speed
	output, err := exec.Command("ethtool", ifaceName).Output()
	if err != nil {
		return 0
	}

	// Parse the output for speed
	// Look for lines like "Speed: 1000Mb/s" or "Speed: 10000Mb/s"
	speedRegex := regexp.MustCompile(`Speed:\s+(\d+)Mb/s`)
	matches := speedRegex.FindSubmatch(output)
	if len(matches) < 2 {
		return 0
	}

	// Convert from Mb/s to bits/s
	mbps, err := strconv.ParseUint(string(matches[1]), 10, 64)
	if err != nil {
		return 0
	}

	return mbps * bitsPerMegabit // Convert to bits per second
}
