package main

import (
	"bytes"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"net/http"
	"os"
	"runtime"
	"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"
	"gopkg.in/yaml.v3"
)

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

type Config struct {
	APIURL   string `yaml:"api_url"`  // https://admin.u-site.app
	Token    string `yaml:"token"`    // token generado desde el panel
	Interval int    `yaml:"interval"` // segundos entre reportes (default: 30)
	Debug    bool   `yaml:"debug"`
}

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
	}
	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"`
	CPU         CPUInfo   `json:"cpu"`
	Disco       DiscoInfo `json:"disco"`
	LoadAvg     LoadInfo  `json:"load_avg"`
	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 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"`
}

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
	if info, err := host.Info(); err == nil {
		m.Uptime = info.Uptime
		m.UptimeStr = uptimeStr(info.Uptime)
		if info.OS != "" {
			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),
		}
	}

	// 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])
	}

	// Disco (raíz /)
	root := "/"
	if runtime.GOOS == "windows" {
		root = "C:\\"
	}
	if d, err := disk.Usage(root); err == nil {
		gb := 1024.0 * 1024 * 1024
		m.Disco = DiscoInfo{
			TotalGB:    round2(float64(d.Total) / gb),
			UsadoGB:    round2(float64(d.Used) / gb),
			LibreGB:    round2(float64(d.Free) / gb),
			Porcentaje: round2(d.UsedPercent),
			Ruta:       root,
		}
	}

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

	return m, nil
}

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

type HeartbeatRequest struct {
	Token    string `json:"token"`
	Metricas string `json:"metricas"`
}

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

	payload := HeartbeatRequest{
		Token:    cfg.Token,
		Metricas: string(metJSON),
	}
	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 {
		// Si no hay config pero se pasaron flags, usarlos directamente
		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}
	}

	// Flags sobreescriben config
	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", cfg.APIURL, cfg.Interval)

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

	// Enviar inmediatamente al iniciar
	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))
		}
		if err := sendHeartbeat(cfg, m); err != nil {
			log.Printf("⚠️  Error enviando heartbeat: %v", err)
		} else {
			log.Printf("✅ Heartbeat enviado | RAM: %.1f%% | CPU: %.1f%% | Disco: %.1f%%",
				m.RAM.Porcentaje, m.CPU.Porcentaje, m.Disco.Porcentaje)
		}
	}

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