package controllers

import (
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"time"

	"github.com/gofiber/fiber/v2"
	"github.com/sujit-baniya/fiber-boilerplate/app"
	"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
	"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
)

// AgentHeartbeat recibe métricas del agente instalado en un servidor.
// Ruta pública: POST /agent/heartbeat — autenticada por token en JSON body.
func AgentHeartbeat(c *fiber.Ctx) error {
	type HeartbeatRequest struct {
		Token        string `json:"token"`
		Metricas     string `json:"metricas"`
		ReniceAction string `json:"renice_action"`
	}

	var req HeartbeatRequest
	if err := c.BodyParser(&req); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"ok": false, "error": "body inválido"})
	}
	if req.Token == "" {
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"ok": false, "error": "token requerido"})
	}

	db := app.Http.Database.DB
	var servidor models.Servidor
	if err := db.Where("agent_token = ?", req.Token).First(&servidor).Error; err != nil {
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"ok": false, "error": "token inválido"})
	}

	now := time.Now()
	updates := map[string]interface{}{
		"agent_last_seen": &now,
		"ultimo_ping":     now.Format(time.RFC3339),
	}
	if req.Metricas != "" {
		updates["metricas_json"] = req.Metricas
		// Actualizar campos planos desde las métricas del agente (la verdad viene del agente)
		type agentRAM struct {
			TotalGB float64 `json:"total_gb"`
		}
		type agentCPU struct {
			Nucleos int `json:"nucleos"`
		}
		var rawMap map[string]any
		if jsonErr := json.Unmarshal([]byte(req.Metricas), &rawMap); jsonErr == nil {
			if os, _ := rawMap["os"].(string); os != "" {
				updates["so"] = os
			}
			if ram, _ := rawMap["ram"].(map[string]any); ram != nil {
				if total, _ := ram["total_gb"].(float64); total > 0 {
					updates["ram"] = fmt.Sprintf("%.0f GB", total)
				}
			}
			if cpu, _ := rawMap["cpu"].(map[string]any); cpu != nil {
				if n, _ := cpu["nucleos"].(float64); n > 0 {
					updates["nucleos"] = fmt.Sprintf("%d", int(n))
				}
			}
			// Compatibilidad: nuevo formato (discos array) y viejo (disco objeto)
			discos, hasArray := rawMap["discos"].([]any)
			if hasArray {
				var maxTotal float64
				for _, item := range discos {
					if d, _ := item.(map[string]any); d != nil {
						if t, _ := d["total_gb"].(float64); t > maxTotal {
							maxTotal = t
						}
					}
				}
				if maxTotal > 0 {
					updates["disco"] = fmt.Sprintf("%.0f GB", maxTotal)
				}
			} else if disco, _ := rawMap["disco"].(map[string]any); disco != nil {
				if total, _ := disco["total_gb"].(float64); total > 0 {
					updates["disco"] = fmt.Sprintf("%.0f GB", total)
				}
			}
		}
	}

	if err := db.Model(&servidor).Updates(updates).Error; err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"ok": false, "error": "no se pudo guardar"})
	}

	// Insertar punto en el historial de métricas
	if req.Metricas != "" {
		var raw map[string]any
		if json.Unmarshal([]byte(req.Metricas), &raw) == nil {
			getFloat := func(obj map[string]any, key string) float64 {
				if sub, ok := obj[key].(map[string]any); ok {
					if v, ok := sub["porcentaje"].(float64); ok {
						return v
					}
				}
				return 0
			}
			discoPct := 0.0
			if discos, ok := raw["discos"].([]any); ok && len(discos) > 0 {
				if d, ok := discos[0].(map[string]any); ok {
					if v, ok := d["porcentaje"].(float64); ok {
						discoPct = v
					}
				}
			}
			tcpConns := 0
			if v, ok := raw["tcp_conns"].(float64); ok {
				tcpConns = int(v)
			}
			load1 := 0.0
			if la, ok := raw["load_avg"].(map[string]any); ok {
				if v, ok := la["load1"].(float64); ok {
					load1 = v
				}
			}
			swapPct := 0.0
			if sw, ok := raw["swap"].(map[string]any); ok {
				if v, ok := sw["porcentaje"].(float64); ok {
					swapPct = v
				}
			}
			models.InsertMetricasHistory(servidor.ID,
				getFloat(raw, "cpu"),
				getFloat(raw, "ram"),
				swapPct,
				discoPct,
				tcpConns,
				load1,
			)
		}
	}

	// Notificar por Telegram si el agente aplicó un renice
	if req.ReniceAction != "" {
		go services.NotificarReniceAction(servidor.Nombre, req.ReniceAction)
	}

	return c.JSON(fiber.Map{"ok": true, "servidor_id": servidor.ID})
}

// GetMetricasHistory devuelve el historial de métricas de un servidor.
// GET /app/servidor/:id/metricas-history?horas=24
func GetMetricasHistory(c *fiber.Ctx) error {
	id := c.Params("id")
	horas := c.QueryInt("horas", 24)
	if horas <= 0 || horas > 168 {
		horas = 24
	}
	var srv models.Servidor
	if err := app.Http.Database.DB.First(&srv, id).Error; err != nil {
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "servidor no encontrado"})
	}
	items, err := models.GetMetricasHistory(srv.ID, horas)
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
	}
	return c.JSON(items)
}

// GenerateAgentToken genera o regenera el token de agente para un servidor.
// Ruta protegida: POST /app/servidor/:id/agent-token
func GenerateAgentToken(c *fiber.Ctx) error {
	id := c.Params("id")
	db := app.Http.Database.DB

	var servidor models.Servidor
	if err := db.First(&servidor, id).Error; err != nil {
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "servidor no encontrado"})
	}

	// Generar token aleatorio de 32 bytes (64 hex chars)
	tokenBytes := make([]byte, 32)
	if _, err := rand.Read(tokenBytes); err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "no se pudo generar token"})
	}
	token := hex.EncodeToString(tokenBytes)

	if err := db.Model(&servidor).Update("agent_token", token).Error; err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "no se pudo guardar token"})
	}

	return c.JSON(fiber.Map{
		"ok":          true,
		"agent_token": token,
		"servidor_id": servidor.ID,
	})
}
