package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/exec"
	"runtime"
	"sort"
	"strings"
	"time"

	"github.com/shirou/gopsutil/v3/cpu"
	"github.com/shirou/gopsutil/v3/disk"
	"github.com/shirou/gopsutil/v3/host"
	"github.com/shirou/gopsutil/v3/load"
	"github.com/shirou/gopsutil/v3/mem"
	pnet "github.com/shirou/gopsutil/v3/net"
	"github.com/shirou/gopsutil/v3/process"
	"gopkg.in/yaml.v3"
)

// ── Config ────────────────────────────────────────────────────────────────────

type Config struct {
	APIURL            string  `yaml:"api_url"`
	Token             string  `yaml:"token"`
	Interval          int     `yaml:"interval"`
	Debug             bool    `yaml:"debug"`
	CPUThreshold      float64 `yaml:"cpu_threshold"`       // % para activar renice (default 90)
	ReniceEnabled     bool    `yaml:"renice_enabled"`      // activar renice automático
	ReniceConsecutive int     `yaml:"renice_consecutive"`  // reportes consecutivos requeridos (default 2)
}

func loadConfig(path string) (*Config, error) {
	f, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("no se pudo leer %s: %w", path, err)
	}
	var cfg Config
	if err := yaml.Unmarshal(f, &cfg); err != nil {
		return nil, fmt.Errorf("config YAML inválido: %w", err)
	}
	if cfg.Interval <= 0 {
		cfg.Interval = 30
	}
	if cfg.CPUThreshold <= 0 {
		cfg.CPUThreshold = 90
	}
	if cfg.ReniceConsecutive <= 0 {
		cfg.ReniceConsecutive = 2
	}
	return &cfg, nil
}

// ── Métricas ──────────────────────────────────────────────────────────────────

type Metricas struct {
	Hostname    string       `json:"hostname"`
	OS          string       `json:"os"`
	Arch        string       `json:"arch"`
	Uptime      uint64       `json:"uptime_seg"`
	UptimeStr   string       `json:"uptime"`
	RAM         RAMInfo      `json:"ram"`
	Swap        SwapInfo     `json:"swap"`
	CPU         CPUInfo      `json:"cpu"`
	Discos      []DiscoInfo  `json:"discos"`
	LoadAvg     LoadInfo     `json:"load_avg"`
	Red         []RedInfo    `json:"red"`
	TCPConns    int          `json:"tcp_conns"`
	TopProcs    []ProcesoInfo `json:"top_procs"`
	ReportadoEn string       `json:"reportado_en"`
}

type RAMInfo struct {
	TotalGB    float64 `json:"total_gb"`
	UsadoGB    float64 `json:"usado_gb"`
	LibreGB    float64 `json:"libre_gb"`
	Porcentaje float64 `json:"porcentaje"`
}

type SwapInfo struct {
	TotalGB    float64 `json:"total_gb"`
	UsadoGB    float64 `json:"usado_gb"`
	Porcentaje float64 `json:"porcentaje"`
}

type CPUInfo struct {
	Nucleos    int     `json:"nucleos"`
	Porcentaje float64 `json:"porcentaje"`
}

type DiscoInfo struct {
	TotalGB    float64 `json:"total_gb"`
	UsadoGB    float64 `json:"usado_gb"`
	LibreGB    float64 `json:"libre_gb"`
	Porcentaje float64 `json:"porcentaje"`
	Ruta       string  `json:"ruta"`
}

type LoadInfo struct {
	Load1  float64 `json:"load1"`
	Load5  float64 `json:"load5"`
	Load15 float64 `json:"load15"`
}

type RedInfo struct {
	Interface  string  `json:"interface"`
	BytesEnvMB float64 `json:"bytes_env_mb"`
	BytesRecMB float64 `json:"bytes_rec_mb"`
	PktEnv     uint64  `json:"pkt_env"`
	PktRec     uint64  `json:"pkt_rec"`
}

type ProcesoInfo struct {
	PID    int32   `json:"pid"`
	Nombre string  `json:"nombre"`
	CPU    float64 `json:"cpu"`
	RAMMB  float64 `json:"ram_mb"`
}

func round2(v float64) float64 {
	return float64(int(v*100)) / 100
}

func uptimeStr(secs uint64) string {
	d := secs / 86400
	h := (secs % 86400) / 3600
	m := (secs % 3600) / 60
	if d > 0 {
		return fmt.Sprintf("%dd %dh %dm", d, h, m)
	}
	return fmt.Sprintf("%dh %dm", h, m)
}

func collectMetrics() (*Metricas, error) {
	m := &Metricas{
		OS:          runtime.GOOS,
		Arch:        runtime.GOARCH,
		ReportadoEn: time.Now().UTC().Format(time.RFC3339),
	}

	// Hostname
	if hn, err := os.Hostname(); err == nil {
		m.Hostname = hn
	}

	// Uptime + OS
	if info, err := host.Info(); err == nil {
		m.Uptime = info.Uptime
		m.UptimeStr = uptimeStr(info.Uptime)
		if info.Platform != "" {
			m.OS = info.Platform + " " + info.PlatformVersion
		}
	}

	// RAM
	if v, err := mem.VirtualMemory(); err == nil {
		gb := 1024.0 * 1024 * 1024
		m.RAM = RAMInfo{
			TotalGB:    round2(float64(v.Total) / gb),
			UsadoGB:    round2(float64(v.Used) / gb),
			LibreGB:    round2(float64(v.Free) / gb),
			Porcentaje: round2(v.UsedPercent),
		}
	}

	// Swap
	if sv, err := mem.SwapMemory(); err == nil && sv.Total > 0 {
		gb := 1024.0 * 1024 * 1024
		m.Swap = SwapInfo{
			TotalGB:    round2(float64(sv.Total) / gb),
			UsadoGB:    round2(float64(sv.Used) / gb),
			Porcentaje: round2(sv.UsedPercent),
		}
	}

	// CPU
	m.CPU.Nucleos = runtime.NumCPU()
	if pcts, err := cpu.Percent(1*time.Second, false); err == nil && len(pcts) > 0 {
		m.CPU.Porcentaje = round2(pcts[0])
	}

	// Discos — incluir todo filesystem con espacio real (>100 MB), sin duplicar mountpoints
	{
		gb := 1024.0 * 1024 * 1024
		skipFS := map[string]bool{
			"proc": true, "sysfs": true, "devtmpfs": true, "devpts": true,
			"cgroup": true, "cgroup2": true, "hugetlbfs": true, "mqueue": true,
			"pstore": true, "securityfs": true, "debugfs": true, "tracefs": true,
			"autofs": true, "fusectl": true, "configfs": true, "efivarfs": true,
			"bpf": true, "rpc_pipefs": true, "nsfs": true, "ramfs": true,
			"squashfs": true, // snap packages: loop devices de solo lectura, siempre 100%
		}
		seen := map[string]bool{}
		parts, _ := disk.Partitions(true)
		// Asegurar que siempre se incluye la raíz
		type partEntry struct{ mountpoint, fstype string }
		entries := []partEntry{{"/", ""}}
		if runtime.GOOS == "windows" {
			entries = []partEntry{{"C:\\", ""}}
		}
		for _, p := range parts {
			entries = append(entries, partEntry{p.Mountpoint, p.Fstype})
		}
		for _, e := range entries {
			if seen[e.mountpoint] || skipFS[e.fstype] {
				continue
			}
			if strings.HasPrefix(e.mountpoint, "/snap/") {
				continue
			}
			d, err := disk.Usage(e.mountpoint)
			if err != nil || d.Total == 0 {
				continue
			}
			totalGB := float64(d.Total) / gb
			if totalGB < 0.1 { // ignorar < 100 MB (tmpfs pequeños, etc.)
				continue
			}
			seen[e.mountpoint] = true
			m.Discos = append(m.Discos, DiscoInfo{
				TotalGB:    round2(totalGB),
				UsadoGB:    round2(float64(d.Used) / gb),
				LibreGB:    round2(float64(d.Free) / gb),
				Porcentaje: round2(d.UsedPercent),
				Ruta:       e.mountpoint,
			})
		}
	}

	// Load average
	if la, err := load.Avg(); err == nil {
		m.LoadAvg = LoadInfo{Load1: round2(la.Load1), Load5: round2(la.Load5), Load15: round2(la.Load15)}
	}

	// Red (interfaces físicas, excluir loopback)
	if counters, err := pnet.IOCounters(true); err == nil {
		mb := 1024.0 * 1024
		for _, c := range counters {
			if c.Name == "lo" || c.Name == "lo0" {
				continue
			}
			m.Red = append(m.Red, RedInfo{
				Interface:  c.Name,
				BytesEnvMB: round2(float64(c.BytesSent) / mb),
				BytesRecMB: round2(float64(c.BytesRecv) / mb),
				PktEnv:     c.PacketsSent,
				PktRec:     c.PacketsRecv,
			})
		}
	}

	// TCP conexiones activas
	if conns, err := pnet.Connections("tcp"); err == nil {
		m.TCPConns = len(conns)
	}

	// Top 10 procesos: filtrar por RAM primero (barato), luego leer CPU solo en esos 10
	if procs, err := process.Processes(); err == nil {
		type pd struct {
			proc  *process.Process
			name  string
			ramMB float64
		}
		var lista []pd
		for _, p := range procs {
			name, _ := p.Name()
			if name == "" {
				continue
			}
			ramMB := 0.0
			if mi, err := p.MemoryInfo(); err == nil && mi != nil {
				ramMB = round2(float64(mi.RSS) / (1024 * 1024))
			}
			if ramMB < 1 {
				continue
			}
			lista = append(lista, pd{p, name, ramMB})
		}
		sort.Slice(lista, func(i, j int) bool {
			return lista[i].ramMB > lista[j].ramMB
		})
		limit := 10
		if len(lista) < limit {
			limit = len(lista)
		}
		for _, d := range lista[:limit] {
			cpuPct, _ := d.proc.CPUPercent() // solo 10 lecturas, no cientos
			m.TopProcs = append(m.TopProcs, ProcesoInfo{d.proc.Pid, d.name, round2(cpuPct), d.ramMB})
		}
	}

	return m, nil
}

// ── Reporte ───────────────────────────────────────────────────────────────────

type HeartbeatRequest struct {
	Token        string `json:"token"`
	Metricas     string `json:"metricas"`
	ReniceAction string `json:"renice_action,omitempty"` // descripción si se reniceó algo
}

func sendHeartbeat(cfg *Config, metricas *Metricas, reniceAction string) error {
	metJSON, err := json.Marshal(metricas)
	if err != nil {
		return err
	}

	payload := HeartbeatRequest{
		Token:        cfg.Token,
		Metricas:     string(metJSON),
		ReniceAction: reniceAction,
	}
	body, _ := json.Marshal(payload)

	url := cfg.APIURL + "/agent/heartbeat"
	req, err := http.NewRequest("POST", url, bytes.NewReader(body))
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", "usite-agent/1.0")

	client := &http.Client{Timeout: 10 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("API respondió %d", resp.StatusCode)
	}
	return nil
}

// ── Main ──────────────────────────────────────────────────────────────────────

func main() {
	configPath := flag.String("config", "agent.yml", "ruta al archivo de configuración")
	apiURL := flag.String("api-url", "", "URL del API (sobreescribe config)")
	token := flag.String("token", "", "token del servidor (sobreescribe config)")
	flag.Parse()

	cfg, err := loadConfig(*configPath)
	if err != nil {
		if *apiURL == "" || *token == "" {
			log.Fatalf("Error cargando config: %v\nUso: usite-agent --api-url=https://... --token=...", err)
		}
		cfg = &Config{APIURL: *apiURL, Token: *token, Interval: 30}
	}

	if *apiURL != "" {
		cfg.APIURL = *apiURL
	}
	if *token != "" {
		cfg.Token = *token
	}

	if cfg.APIURL == "" || cfg.Token == "" {
		log.Fatal("api_url y token son requeridos")
	}

	log.Printf("🚀 usite-agent iniciado | API: %s | Intervalo: %ds | Renice: %v (umbral %.0f%%, %d reportes)",
		cfg.APIURL, cfg.Interval, cfg.ReniceEnabled, cfg.CPUThreshold, cfg.ReniceConsecutive)

	ticker := time.NewTicker(time.Duration(cfg.Interval) * time.Second)
	defer ticker.Stop()

	consecAlto := 0 // contador de reportes consecutivos con CPU alta

	reportar := func() {
		m, err := collectMetrics()
		if err != nil {
			log.Printf("⚠️  Error recolectando métricas: %v", err)
			return
		}
		if cfg.Debug {
			b, _ := json.MarshalIndent(m, "", "  ")
			log.Printf("📊 Métricas:\n%s", string(b))
		}

		// ── Lógica de renice ──────────────────────────────────────────────────
		var reniceAction string
		if cfg.ReniceEnabled && m.CPU.Porcentaje >= cfg.CPUThreshold {
			consecAlto++
			log.Printf("⚠️  CPU alta: %.1f%% (reporte %d/%d)", m.CPU.Porcentaje, consecAlto, cfg.ReniceConsecutive)
			if consecAlto >= cfg.ReniceConsecutive && len(m.TopProcs) > 0 {
				// Ordenar top_procs por CPU para encontrar el culpable real
				procs := make([]ProcesoInfo, len(m.TopProcs))
				copy(procs, m.TopProcs)
				sort.Slice(procs, func(i, j int) bool { return procs[i].CPU > procs[j].CPU })
				culpable := procs[0]
				if culpable.CPU > 0 && culpable.PID > 0 {
					cmd := exec.Command("renice", "-n", "19", "-p", fmt.Sprintf("%d", culpable.PID))
					if rerr := cmd.Run(); rerr != nil {
						log.Printf("❌ renice falló para PID %d (%s): %v", culpable.PID, culpable.Nombre, rerr)
					} else {
						reniceAction = fmt.Sprintf("renice +19 aplicado a %s (PID %d, CPU %.1f%%)", culpable.Nombre, culpable.PID, culpable.CPU)
						log.Printf("🔧 %s", reniceAction)
						consecAlto = 0 // reiniciar tras actuar
					}
				}
			}
		} else {
			consecAlto = 0
		}

		if err := sendHeartbeat(cfg, m, reniceAction); err != nil {
			log.Printf("⚠️  Error enviando heartbeat: %v", err)
		} else {
			log.Printf("✅ Heartbeat | RAM: %.1f%% | CPU: %.1f%% | TCP: %d | Procs: %d",
				m.RAM.Porcentaje, m.CPU.Porcentaje, m.TCPConns, len(m.TopProcs))
		}
	}

	reportar()
	for range ticker.C {
		reportar()
	}
}
