package controllers

import (
	"fmt"
	"math"
	"net"
	"strconv"
	"strings"
	"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"
)

// RENDER
func Servidor(c *fiber.Ctx) error {
	data := fiber.Map{
		"user":    c.Locals("user").(map[string]interface{}),
		"modules": c.Locals("userModules"),
	}

	if err := c.Render("servidor", data, "layouts/main"); err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"message": "Error rendering template",
			"error":   err.Error(),
		})
	}
	return nil
}

// ServidorDashboard renders the server dashboard view
func ServidorDashboard(c *fiber.Ctx) error {
	data := fiber.Map{
		"user":    c.Locals("user").(map[string]interface{}),
		"modules": c.Locals("userModules"),
	}

	if err := c.Render("servidor_dashboard", data, "layouts/main"); err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"message": "Error rendering template",
			"error":   err.Error(),
		})
	}
	return nil
}

func GetServidor(c *fiber.Ctx) error {
	pageStr := c.Query("page", "1")
	page, err := strconv.Atoi(pageStr)
	if err != nil || page < 1 {
		page = 1
	}
	searchQuery := c.Query("search", "")
	limitStr := c.Query("limit", "10")
	limit, _ := strconv.Atoi(limitStr)
	offset := (page - 1) * limit

	records, total, err := models.GetAllServidores(limit, offset, searchQuery)
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"message": "Error retrieving records",
			"error":   err.Error(),
		})
	}

	// Build connection count map per server
	type conxCount struct {
		ServidorID uint  `gorm:"column:servidor_id"`
		Count      int64 `gorm:"column:count"`
	}
	var counts []conxCount
	serverIDs := make([]uint, len(records))
	for i, s := range records {
		serverIDs[i] = s.ID
	}
	if len(serverIDs) > 0 {
		app.Http.Database.DB.Table("conx_db").
			Select("servidor_id, count(*) as count").
			Where("servidor_id IN ? AND deleted_at IS NULL", serverIDs).
			Group("servidor_id").
			Scan(&counts)
	}
	countMap := make(map[uint]int64)
	for _, cc := range counts {
		countMap[cc.ServidorID] = cc.Count
	}

	// Attach conx_count to each record
	type servidorWithCount struct {
		models.Servidor
		ConxCount int64 `json:"conx_count"`
	}
	enriched := make([]servidorWithCount, len(records))
	for i, s := range records {
		enriched[i] = servidorWithCount{Servidor: s, ConxCount: countMap[s.ID]}
	}

	totalPages := int(math.Ceil(float64(total) / float64(limit)))
	return c.JSON(fiber.Map{
		"registros":  enriched,
		"total":      total,
		"totalPages": totalPages,
		"page":       page,
		"limit":      limit,
	})
}

func GetServidorSelect(c *fiber.Ctx) error {
	records, err := models.GetAllServidoresSelect()
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"message": "Error retrieving records",
			"error":   err.Error(),
		})
	}
	return c.JSON(fiber.Map{
		"registros": records,
	})
}

func CreateServidor(c *fiber.Ctx) error {
	var m models.Servidor

	if err := c.BodyParser(&m); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"message": "Error parsing request body",
			"error":   err.Error(),
		})
	}

	if err := models.CreateServidor(m); err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"message": "Error creating servidor",
			"error":   err.Error(),
		})
	}

	return c.Status(fiber.StatusCreated).JSON(m)
}

func UpdateServidor(c *fiber.Ctx) error {
	uid, err := strconv.ParseUint(c.Params("id"), 10, 32)
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"message": "ID inválido",
			"error":   err.Error(),
		})
	}

	var m models.Servidor

	if err := c.BodyParser(&m); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"message": "Error al parsear el cuerpo de la solicitud",
			"error":   err.Error(),
		})
	}

	m.ID = uint(uid)

	// Cargar el registro actual para detectar cambios que deben propagarse a Hostinger
	var actual models.Servidor
	app.Http.Database.DB.First(&actual, uid)
	nombreAnterior := actual.Nombre

	// Usar map para Updates y así no perder campos con valor cero (incluido hostinger_vps_id=nil)
	updateMap := map[string]interface{}{
		"nombre":                   m.Nombre,
		"ip_servidor":              m.IpServidor,
		"so":                       m.So,
		"vencimiento":              m.Vencimiento,
		"ram":                      m.Ram,
		"nucleos":                  m.Nucleos,
		"disco":                    m.Disco,
		"ultimo_ping":              m.UltimoPing,
		"prov_servidor_id":         m.ProvServidorID,
		"tipo_servidor_id":         m.TipoServidorID,
		"hostinger_vps_id":         m.HostingerVpsID,
		"hostinger_subscription_id": m.HostingerSubscriptionID,
	}
	if err := app.Http.Database.DB.Model(&models.Servidor{}).Where("id = ?", uid).Updates(updateMap).Error; err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"message": "Error al actualizar el servidor",
			"error":   err.Error(),
		})
	}

	// Sincronizar hostname en Hostinger si está vinculado y el nombre cambió
	if actual.HostingerVpsID != nil && m.Nombre != "" && m.Nombre != nombreAnterior {
		if client, hErr := hostingerClient(); hErr == nil {
			_ = client.SetVPSHostname(*actual.HostingerVpsID, m.Nombre)
		}
	}

	return c.Status(fiber.StatusOK).JSON(m)
}

// SyncServidorFromHostinger sincroniza los datos del VPS de Hostinger hacia el servidor local.
// Actualiza: ip_servidor, ram, nucleos, disco, hostinger_state, vencimiento (si hay suscripción).
// La verdad de las specs físicas siempre viene de Hostinger.
func SyncServidorFromHostinger(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"})
	}
	if servidor.HostingerVpsID == nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "el servidor no tiene hostinger_vps_id configurado"})
	}

	client, err := hostingerClient()
	if err != nil {
		return c.Status(fiber.StatusFailedDependency).JSON(fiber.Map{"error": "No se encontró configuración activa de Hostinger"})
	}

	vps, err := client.GetVPSByID(*servidor.HostingerVpsID)
	if err != nil {
		return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.Error()})
	}
	if vps.ID == 0 {
		return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": "Hostinger no devolvio datos para ese VPS ID"})
	}

	updates := map[string]interface{}{
		"hostinger_state": vps.State,
	}

	// IP principal (primer IPv4)
	if len(vps.IPV4) > 0 && vps.IPV4[0].Address != "" {
		updates["ip_servidor"] = vps.IPV4[0].Address
	}

	// RAM: Hostinger devuelve en MB → convertir a GB
	if vps.RAMBytes > 0 {
		gbRam := float64(vps.RAMBytes) / 1024.0
		updates["ram"] = fmt.Sprintf("%.0f GB", gbRam)
	}

	// CPU cores
	if vps.CPU > 0 {
		updates["nucleos"] = fmt.Sprintf("%d", vps.CPU)
	}

	// Disco: Hostinger devuelve en MB (igual que memory) → convertir a GB
	if vps.DiskBytes > 0 {
		gbDisk := vps.DiskBytes / 1024
		updates["disco"] = fmt.Sprintf("%d GB", gbDisk)
	}

	// Vencimiento: resolución en cascada
	// 1) subscription_id que viene directo del VPS (el más confiable)
	// 2) subscription_id ya guardado en el servidor local
	// 3) fallback fuzzy por nombre
	subID := vps.SubscriptionID
	if subID == "" && servidor.HostingerSubscriptionID != "" {
		subID = servidor.HostingerSubscriptionID
	}

	if subID != "" {
		// Lookup directo — sin ambigüedad
		if sub, sErr := client.GetSubscriptionByID(subID); sErr == nil {
			// Para suscripciones activas con auto-renovación, expires_at viene vacío;
			// en ese caso usar next_billing_at como fecha de vencimiento.
			venc := sub.ExpiresAt
			if venc == "" {
				venc = sub.NextBillingAt
			}
			if venc != "" && len(venc) >= 10 {
				updates["vencimiento"] = venc[:10]
			}
			updates["hostinger_subscription_id"] = subID
		}
	} else {
		// Fallback: buscar por nombre si aún no tenemos subscription_id
		if subs, sErr := client.GetOrders(); sErr == nil {
			for _, sub := range subs {
				venc := sub.ExpiresAt
				if venc == "" {
					venc = sub.NextBillingAt
				}
				if venc != "" && len(venc) >= 10 &&
					strings.Contains(strings.ToLower(sub.Name), strings.ToLower(vps.Hostname)) {
					updates["vencimiento"] = venc[:10]
					updates["hostinger_subscription_id"] = sub.ID
					break
				}
			}
		}
	}

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

	db.First(&servidor, id)
	return c.JSON(fiber.Map{"ok": true, "servidor": servidor, "campos_actualizados": updates})
}

func DeleteServidor(c *fiber.Ctx) error {
	uid, err := strconv.ParseUint(c.Params("id"), 10, 32)
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"message": "ID inválido",
			"error":   err.Error(),
		})
	}

	var m models.Servidor
	m.ID = uint(uid)

	if err := models.DeleteServidor(m); err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"message": "Error al eliminar el servidor",
			"error":   err.Error(),
		})
	}

	return c.Status(fiber.StatusOK).JSON(fiber.Map{
		"message": "Servidor eliminado exitosamente",
	})
}

// GetServidorDashboard retorna un servidor con todas sus conexiones de BD
func GetServidorDashboard(c *fiber.Ctx) error {
	servidorID := c.Params("id")
	uid, err := strconv.ParseUint(servidorID, 10, 32)
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"error": "ID inválido",
		})
	}

	// Obtener servidor
	var servidor models.Servidor
	db := app.Http.Database.DB
	if err := db.Preload("ProvServidor").Preload("TipoServidor").First(&servidor, uid).Error; err != nil {
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
			"error": "Servidor no encontrado",
		})
	}

	// Obtener conexiones de BD asociadas
	var conexiones []models.ConxDb
	if err := db.Preload("Servidor").Preload("TipoDb").Where("servidor_id = ?", uid).Find(&conexiones).Error; err != nil {
		conexiones = []models.ConxDb{}
	}

	return c.JSON(fiber.Map{
		"servidor":   servidor,
		"conexiones": conexiones,
	})
}

// PingConexion tests the connection to a database and returns the response time
func PingConexion(c *fiber.Ctx) error {
	conexionID := c.Params("id")
	uid, err := strconv.ParseUint(conexionID, 10, 32)
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"exitoso": false,
			"error":   "ID inválido",
		})
	}

	var conexion models.ConxDb
	db := app.Http.Database.DB
	if err := db.Preload("TipoDb").Preload("Servidor").First(&conexion, uid).Error; err != nil {
		return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
			"exitoso": false,
			"error":   "Conexión no encontrada",
		})
	}

	host := conexion.HostEfectivo()
	puerto := conexion.Puerto

	// Primero intentar conexión real a la BD (más fiable que solo TCP)
	inicio := time.Now()
	testErr := services.TestDBConnection(conexion)
	if testErr == nil {
		return c.JSON(fiber.Map{
			"exitoso": true,
			"tiempo":  time.Since(inicio).Milliseconds(),
			"host":    host,
			"puerto":  puerto,
			"tipo":    conexion.TipoDb.Nombre,
			"error":   "",
		})
	}

	// Si la BD no conecta, probar al menos TCP (puerto abierto)
	tcpAddr := net.JoinHostPort(host, puerto)
	inicioTCP := time.Now()
	conn, errTCP := net.DialTimeout("tcp", tcpAddr, 3*time.Second)
	if errTCP == nil {
		conn.Close()
		return c.JSON(fiber.Map{
			"exitoso": false,
			"tiempo":  time.Since(inicioTCP).Milliseconds(),
			"host":    host,
			"puerto":  puerto,
			"tipo":    conexion.TipoDb.Nombre,
			// Puerto abierto pero la BD rechaza la conexión
			"error": fmt.Sprintf("Puerto %s abierto pero la BD no responde: %s", puerto, testErr.Error()),
		})
	}

	return c.JSON(fiber.Map{
		"exitoso": false,
		"tiempo":  int64(0),
		"host":    host,
		"puerto":  puerto,
		"tipo":    conexion.TipoDb.Nombre,
		"error":   fmt.Sprintf("No se puede alcanzar %s:%s — verifica que el host sea accesible desde este servidor. Tip: si el DB solo escucha en localhost del servidor, configura el campo 'Host' de la conexión.", host, puerto),
	})
}
