package controllers

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"

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

// ─── Auth ─────────────────────────────────────────────────────────────────────

func PortalLoginPage(c *fiber.Ctx) error {
	if user, err := auth.PortalUser(c); err == nil && user != nil {
		return c.Redirect("/portal/dashboard")
	}
	return c.Render("portal/login", fiber.Map{
		"error":   c.Query("error"),
		"success": c.Query("success"),
	}, "layouts/portal_public")
}

func PortalLoginPost(c *fiber.Ctx) error {
	email := strings.TrimSpace(c.FormValue("email"))
	password := c.FormValue("password")
	u, err := models.CheckPortalLogin(email, password)
	if err != nil {
		return c.Redirect("/portal/login?error=" + url.QueryEscape(err.Error()))
	}
	if err := auth.SetPortalSession(c, u.ID); err != nil {
		return c.Redirect("/portal/login?error=Error+interno")
	}
	return c.Redirect("/portal/dashboard")
}

func PortalLogout(c *fiber.Ctx) error {
	_ = auth.DestroyPortalSession(c)
	return c.Redirect("/portal/login")
}

// ─── Recuperar contraseña ────────────────────────────────────────────────────

func PortalForgotPasswordPage(c *fiber.Ctx) error {
	return c.Render("portal/forgot_password", fiber.Map{
		"success": c.Query("success"),
		"error":   c.Query("error"),
	}, "layouts/portal_public")
}

func PortalForgotPasswordPost(c *fiber.Ctx) error {
	email := strings.TrimSpace(c.FormValue("email"))

	// Respuesta genérica siempre para no revelar si el email existe
	successMsg := "Si el correo está registrado, recibirás un enlace para restablecer tu contraseña."

	u, err := models.GetPortalUserByEmail(email)
	if err == nil && u != nil && u.Activo {
		token, err := models.CreatePortalResetToken(u.ID)
		if err == nil {
			resetLink := services.GetPublicURL() + "/portal/reset-password?t=" + token
			services.SendPortalPasswordResetEmail(u.Email, u.Nombre, resetLink)
		} else {
			log.Printf("[Portal] Error creando token de reseteo para %s: %v", email, err)
		}
	}

	return c.Redirect("/portal/forgot-password?success=" + url.QueryEscape(successMsg))
}

func PortalResetPasswordPage(c *fiber.Ctx) error {
	token := c.Query("t")
	if token == "" {
		return c.Redirect("/portal/login")
	}
	_, err := models.GetValidPortalResetToken(token)
	if err != nil {
		return c.Render("portal/reset_password", fiber.Map{
			"error": err.Error(),
			"token": "",
		}, "layouts/portal_public")
	}
	return c.Render("portal/reset_password", fiber.Map{
		"token": token,
		"error": "",
	}, "layouts/portal_public")
}

func PortalResetPasswordPost(c *fiber.Ctx) error {
	token := c.FormValue("token")
	password := c.FormValue("password")
	confirm := c.FormValue("confirm")

	renderError := func(msg string) error {
		return c.Render("portal/reset_password", fiber.Map{
			"token": token,
			"error": msg,
		}, "layouts/portal_public")
	}

	if token == "" || password == "" || confirm == "" {
		return renderError("Todos los campos son obligatorios.")
	}
	if password != confirm {
		return renderError("Las contraseñas no coinciden.")
	}
	if len(password) < 8 {
		return renderError("La contraseña debe tener al menos 8 caracteres.")
	}

	record, err := models.GetValidPortalResetToken(token)
	if err != nil {
		return renderError(err.Error())
	}

	hashed, err := app.Http.Hash.Create(password)
	if err != nil {
		return renderError("Error al procesar la contraseña. Intenta de nuevo.")
	}
	if err := models.UpdatePortalUserPassword(record.PortalUserID, hashed); err != nil {
		return renderError("Error al actualizar la contraseña. Intenta de nuevo.")
	}
	_ = models.MarkPortalResetTokenUsed(record.ID)

	return c.Redirect("/portal/login?success=" + url.QueryEscape("Contraseña actualizada. Ya puedes ingresar."))
}

// ─── Dashboard ────────────────────────────────────────────────────────────────

func PortalDashboard(c *fiber.Ctx) error {
	u := middlewares.PortalUserFromLocals(c)
	if u == nil {
		return c.Redirect("/portal/login")
	}

	// Recargar con accesos
	fullUser, err := models.GetPortalUserByID(u.ID)
	if err != nil {
		return c.Redirect("/portal/login")
	}

	clienteIDs := models.GetClienteIDsForPortalUser(fullUser)
	proyectos, _ := models.GetProyectosByClienteIDs(clienteIDs)
	for i := range proyectos {
		_ = models.ActualizarProgresoProyecto(proyectos[i].ID)
	}

	// Si partner: agrupar proyectos por cliente
	type ClienteProyectos struct {
		Cliente   models.Cliente
		Proyectos []models.Proyecto
	}
	var grupos []ClienteProyectos
	clienteMap := map[uint]*ClienteProyectos{}
	for _, p := range proyectos {
		if _, ok := clienteMap[p.ClienteID]; !ok {
			clienteMap[p.ClienteID] = &ClienteProyectos{Cliente: p.Cliente}
		}
		clienteMap[p.ClienteID].Proyectos = append(clienteMap[p.ClienteID].Proyectos, p)
	}
	for _, g := range clienteMap {
		grupos = append(grupos, *g)
	}

	return c.Render("portal/dashboard", fiber.Map{
		"portalUser": fullUser,
		"proyectos":  proyectos,
		"grupos":     grupos,
		"isPartner":  fullUser.Rol == "partner",
	}, "layouts/portal")
}

// ─── Detalle de proyecto ──────────────────────────────────────────────────────

func PortalProyecto(c *fiber.Ctx) error {
	u := middlewares.PortalUserFromLocals(c)
	if u == nil {
		return c.Redirect("/portal/login")
	}

	slug := c.Params("slug")
	proy, err := models.GetProyectoBySlug(slug)
	if err != nil {
		return c.Status(404).Render("portal/404", fiber.Map{}, "layouts/portal")
	}

	// Verificar acceso
	fullUser, _ := models.GetPortalUserByID(u.ID)
	clienteIDs := models.GetClienteIDsForPortalUser(fullUser)
	hasAccess := false
	for _, cid := range clienteIDs {
		if cid == proy.ClienteID {
			hasAccess = true
			break
		}
	}
	if !hasAccess {
		return c.Status(403).Redirect("/portal/dashboard")
	}

	fases, _ := models.GetFasesByProyecto(proy.ID)
	avances, _ := models.GetAvancesByProyecto(proy.ID, true)
	entregables, _ := models.GetEntregablesByProyecto(proy.ID, true)
	tickets, _ := models.GetTicketsByProyecto(proy.ID)
	_ = models.ActualizarProgresoProyecto(proy.ID)

	return c.Render("portal/proyecto", fiber.Map{
		"portalUser":  fullUser,
		"proyecto":    proy,
		"fases":       fases,
		"avances":     avances,
		"entregables": entregables,
		"tickets":     tickets,
	}, "layouts/portal")
}

// ─── API del portal ───────────────────────────────────────────────────────────

// PortalGetProyectos devuelve los proyectos del usuario logueado (JSON para Alpine).
func PortalGetProyectos(c *fiber.Ctx) error {
	u := middlewares.PortalUserFromLocals(c)
	if u == nil {
		return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
	}
	fullUser, _ := models.GetPortalUserByID(u.ID)
	clienteIDs := models.GetClienteIDsForPortalUser(fullUser)
	proyectos, _ := models.GetProyectosByClienteIDs(clienteIDs)
	for i := range proyectos {
		_ = models.ActualizarProgresoProyecto(proyectos[i].ID)
	}
	return c.JSON(proyectos)
}

// PortalGetProyectoData devuelve todo el contenido de un proyecto (JSON).
func PortalGetProyectoData(c *fiber.Ctx) error {
	u := middlewares.PortalUserFromLocals(c)
	if u == nil {
		return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
	}
	slug := c.Params("slug")
	proy, err := models.GetProyectoBySlug(slug)
	if err != nil {
		return c.Status(404).JSON(fiber.Map{"error": "no encontrado"})
	}
	// Verificar acceso
	fullUser, _ := models.GetPortalUserByID(u.ID)
	clienteIDs := models.GetClienteIDsForPortalUser(fullUser)
	hasAccess := false
	for _, cid := range clienteIDs {
		if cid == proy.ClienteID {
			hasAccess = true
			break
		}
	}
	if !hasAccess {
		return c.Status(403).JSON(fiber.Map{"error": "sin acceso"})
	}

	fases, _ := models.GetFasesByProyecto(proy.ID)
	avances, _ := models.GetAvancesByProyecto(proy.ID, true)
	entregables, _ := models.GetEntregablesByProyecto(proy.ID, true)
	tickets, _ := models.GetTicketsByProyecto(proy.ID)
	facturas, _ := models.GetFacturasByCliente(proy.ClienteID, true)
	facturasNoLeidas := models.CountUnreadFacturaNotifs(u.ID)
	documentos, _ := models.GetDocumentosByProyecto(proy.ID)
	_ = models.ActualizarProgresoProyecto(proy.ID)

	return c.JSON(fiber.Map{
		"proyecto":         proy,
		"fases":            fases,
		"avances":          avances,
		"entregables":      entregables,
		"documentos":       documentos,
		"tickets":          tickets,
		"facturas":         facturas,
		"facturasNoLeidas": facturasNoLeidas,
	})
}

// PortalMarcarFacturasLeidas marca las notificaciones de facturas como leídas.
func PortalMarcarFacturasLeidas(c *fiber.Ctx) error {
	u := middlewares.PortalUserFromLocals(c)
	if u == nil {
		return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
	}
	_ = models.MarcarNotifsFacturasLeidas(u.ID)
	return c.JSON(fiber.Map{"ok": true})
}

// PortalCrearTicket crea un nuevo ticket de soporte.
func PortalCrearTicket(c *fiber.Ctx) error {
	u := middlewares.PortalUserFromLocals(c)
	if u == nil {
		return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
	}
	type Req struct {
		ProyectoSlug string `json:"proyecto_slug"`
		Titulo       string `json:"titulo"`
		Descripcion  string `json:"descripcion"`
		Prioridad    string `json:"prioridad"`
	}
	var req Req
	if err := c.BodyParser(&req); err != nil {
		return c.Status(400).JSON(fiber.Map{"error": err.Error()})
	}
	if strings.TrimSpace(req.Titulo) == "" {
		return c.Status(400).JSON(fiber.Map{"error": "El título es requerido"})
	}

	proy, err := models.GetProyectoBySlug(req.ProyectoSlug)
	if err != nil {
		return c.Status(404).JSON(fiber.Map{"error": "Proyecto no encontrado"})
	}

	t := &models.ProyectoTicket{
		ProyectoID:   &proy.ID,
		PortalUserID: &u.ID,
		AutorNombre:  u.Nombre,
		Titulo:       req.Titulo,
		Descripcion:  req.Descripcion,
		Prioridad:    req.Prioridad,
		Estado:       "abierto",
	}
	if t.Prioridad == "" {
		t.Prioridad = "media"
	}
	if err := models.CreateProyectoTicket(t); err != nil {
		return c.Status(500).JSON(fiber.Map{"error": err.Error()})
	}
	go services.DispatchTicketNuevo(t, u, proy.Nombre)
	return c.Status(201).JSON(t)
}

// PortalResponderTicket agrega un mensaje a un ticket.
func PortalResponderTicket(c *fiber.Ctx) error {
	u := middlewares.PortalUserFromLocals(c)
	if u == nil {
		return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
	}
	ticketID, _ := c.ParamsInt("id")
	type Req struct {
		Contenido string `json:"contenido"`
	}
	var req Req
	if err := c.BodyParser(&req); err != nil {
		return c.Status(400).JSON(fiber.Map{"error": err.Error()})
	}
	if strings.TrimSpace(req.Contenido) == "" {
		return c.Status(400).JSON(fiber.Map{"error": "El mensaje no puede estar vacío"})
	}
	ticket, err := models.GetTicketByID(uint(ticketID))
	if err != nil {
		return c.Status(404).JSON(fiber.Map{"error": "Ticket no encontrado"})
	}
	if ticket.PortalUserID == nil || *ticket.PortalUserID != u.ID {
		return c.Status(403).JSON(fiber.Map{"error": "Sin acceso"})
	}
	msg := &models.TicketMensaje{
		TicketID:    uint(ticketID),
		Contenido:   req.Contenido,
		EsAdmin:     false,
		AutorNombre: u.Nombre,
	}
	if err := models.CreateTicketMensaje(msg); err != nil {
		return c.Status(500).JSON(fiber.Map{"error": err.Error()})
	}
	go func() {
		proyNombre := ""
		if ticket.ProyectoID != nil {
			if proy, err := models.GetProyectoByID(*ticket.ProyectoID); err == nil {
				proyNombre = proy.Nombre
			}
		}
		services.DispatchTicketRespuestaCliente(ticket, req.Contenido, proyNombre)
	}()
	return c.Status(201).JSON(msg)
}

// PortalDownloadEntregable descarga un entregable del portal.
func PortalDownloadEntregable(c *fiber.Ctx) error {
	u := middlewares.PortalUserFromLocals(c)
	if u == nil {
		return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
	}
	entID, _ := c.ParamsInt("id")
	item, err := models.GetProyectoEntregableByID(uint(entID))
	if err != nil || !item.Visible {
		return c.Status(404).JSON(fiber.Map{"error": "No encontrado"})
	}
	// Verificar que el proyecto pertenece al cliente del portal user
	proy, err := models.GetProyectoByID(item.ProyectoID)
	if err != nil {
		return c.Status(404).JSON(fiber.Map{"error": "Proyecto no encontrado"})
	}
	fullUser, _ := models.GetPortalUserByID(u.ID)
	clienteIDs := models.GetClienteIDsForPortalUser(fullUser)
	hasAccess := false
	for _, cid := range clienteIDs {
		if cid == proy.ClienteID {
			hasAccess = true
			break
		}
	}
	if !hasAccess {
		return c.Status(403).JSON(fiber.Map{"error": "Sin acceso"})
	}
	clean := item.Archivo
	if !strings.HasPrefix(clean, "uploads/") {
		return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"})
	}
	c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, item.OriginalName))
	return c.SendFile(clean)
}

// PortalDownloadFactura descarga el PDF de una factura del portal.
func PortalDownloadFactura(c *fiber.Ctx) error {
	u := middlewares.PortalUserFromLocals(c)
	if u == nil {
		return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
	}
	factID, _ := c.ParamsInt("id")
	f, err := models.GetFacturaByID(uint(factID))
	if err != nil || !f.Visible || f.Archivo == "" {
		return c.Status(404).JSON(fiber.Map{"error": "No encontrado"})
	}
	// Verificar acceso
	fullUser, _ := models.GetPortalUserByID(u.ID)
	clienteIDs := models.GetClienteIDsForPortalUser(fullUser)
	hasAccess := false
	for _, cid := range clienteIDs {
		if cid == f.ClienteID {
			hasAccess = true
			break
		}
	}
	if !hasAccess {
		return c.Status(403).JSON(fiber.Map{"error": "Sin acceso"})
	}
	clean := f.Archivo
	if !strings.HasPrefix(clean, "uploads/") {
		return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"})
	}
	c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, f.OriginalName))
	return c.SendFile(clean)
}

// PortalDownloadDocumento descarga un documento del proyecto (contrato, orden de servicio, etc.).
func PortalDownloadDocumento(c *fiber.Ctx) error {
	u := middlewares.PortalUserFromLocals(c)
	if u == nil {
		return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
	}
	docID, _ := c.ParamsInt("id")
	doc, err := models.GetProyectoDocumentoByID(uint(docID))
	if err != nil || doc.Archivo == "" {
		return c.Status(404).JSON(fiber.Map{"error": "No encontrado"})
	}
	// Verificar que el proyecto pertenece al cliente del portal user
	proy, err := models.GetProyectoByID(doc.ProyectoID)
	if err != nil {
		return c.Status(404).JSON(fiber.Map{"error": "Proyecto no encontrado"})
	}
	fullUser, _ := models.GetPortalUserByID(u.ID)
	clienteIDs := models.GetClienteIDsForPortalUser(fullUser)
	hasAccess := false
	for _, cid := range clienteIDs {
		if cid == proy.ClienteID {
			hasAccess = true
			break
		}
	}
	if !hasAccess {
		return c.Status(403).JSON(fiber.Map{"error": "Sin acceso"})
	}
	clean := filepath.Clean(doc.Archivo)
	if !strings.HasPrefix(clean, "uploads/") {
		return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"})
	}
	nombre := doc.OriginalName
	if nombre == "" {
		nombre = doc.Nombre
	}
	safe := strings.Map(func(r rune) rune {
		if r > 127 || r == '"' || r == '\\' || r == '/' || r == '\n' || r == '\r' {
			return '_'
		}
		return r
	}, nombre)
	c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, safe, url.PathEscape(nombre)))
	return c.SendFile(clean)
}

// ─── Notificaciones del portal user ──────────────────────────────────────────

func PortalGetMisNotifs(c *fiber.Ctx) error {
	u := middlewares.PortalUserFromLocals(c)
	if u == nil {
		return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
	}
	soloNoLeidas := c.Query("no_leidas") == "1"
	items, err := models.GetSistemaNotifs("portal_user", u.ID, soloNoLeidas)
	if err != nil {
		return c.Status(500).JSON(fiber.Map{"error": err.Error()})
	}
	count := models.CountUnreadNotifs("portal_user", u.ID)
	return c.JSON(fiber.Map{"items": items, "unread": count})
}

func PortalMarcarNotifLeida(c *fiber.Ctx) error {
	u := middlewares.PortalUserFromLocals(c)
	if u == nil {
		return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
	}
	id, _ := c.ParamsInt("id")
	if err := models.MarcarNotifLeida(uint(id)); err != nil {
		return c.Status(500).JSON(fiber.Map{"error": err.Error()})
	}
	return c.JSON(fiber.Map{"ok": true})
}

func PortalMarcarTodasLeidas(c *fiber.Ctx) error {
	u := middlewares.PortalUserFromLocals(c)
	if u == nil {
		return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
	}
	if err := models.MarcarTodasLeidas("portal_user", u.ID); err != nil {
		return c.Status(500).JSON(fiber.Map{"error": err.Error()})
	}
	return c.JSON(fiber.Map{"ok": true})
}

// PortalMarcarTicketLeido marca como leídos todos los mensajes de admin del ticket.
func PortalMarcarTicketLeido(c *fiber.Ctx) error {
	u := middlewares.PortalUserFromLocals(c)
	if u == nil {
		return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
	}
	ticketID, _ := c.ParamsInt("id")
	// Verificar que el ticket pertenece al portal user
	ticket, err := models.GetTicketByID(uint(ticketID))
	if err != nil || ticket == nil {
		return c.Status(404).JSON(fiber.Map{"error": "ticket no encontrado"})
	}
	if ticket.PortalUserID == nil || *ticket.PortalUserID != u.ID {
		return c.Status(403).JSON(fiber.Map{"error": "sin permiso"})
	}
	_ = models.MarkTicketMessagesReadByPortal(uint(ticketID))
	return c.JSON(fiber.Map{"ok": true})
}

// ─── Mi Perfil ────────────────────────────────────────────────────────────────

// GET /portal/mi-perfil — devuelve datos del usuario autenticado
func PortalGetMiPerfil(c *fiber.Ctx) error {
	u := middlewares.PortalUserFromLocals(c)
	if u == nil {
		return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
	}
	full, err := models.GetPortalUserByID(u.ID)
	if err != nil {
		return c.Status(500).JSON(fiber.Map{"error": "error al obtener perfil"})
	}
	return c.JSON(fiber.Map{
		"id":                   full.ID,
		"nombre":               full.Nombre,
		"email":                full.Email,
		"telefono":             full.Telefono,
		"indicativo":           full.Indicativo,
		"pais":                 full.Pais,
		"ciudad":               full.Ciudad,
		"documento":            full.Documento,
		"empresa":              full.Empresa,
		"sitio_web":            full.SitioWeb,
		"documento_rut_file":   full.DocumentoRutFile,
		"documento_rut_nombre": full.DocumentoRutNombre,
	})
}

// PUT /portal/mi-perfil — actualiza datos del usuario autenticado
func PortalUpdateMiPerfil(c *fiber.Ctx) error {
	u := middlewares.PortalUserFromLocals(c)
	if u == nil {
		return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
	}
	type Req struct {
		Nombre     string `json:"nombre"`
		Email      string `json:"email"`
		Telefono   string `json:"telefono"`
		Indicativo string `json:"indicativo"`
		Pais       string `json:"pais"`
		Ciudad     string `json:"ciudad"`
		Documento  string `json:"documento"`
		Empresa    string `json:"empresa"`
		SitioWeb   string `json:"sitio_web"`
	}
	var req Req
	if err := c.BodyParser(&req); err != nil {
		return c.Status(400).JSON(fiber.Map{"error": "Datos inválidos"})
	}
	req.Nombre = strings.TrimSpace(req.Nombre)
	req.Email = strings.TrimSpace(req.Email)
	if req.Nombre == "" {
		return c.Status(400).JSON(fiber.Map{"error": "El nombre es requerido"})
	}
	if req.Email == "" {
		return c.Status(400).JSON(fiber.Map{"error": "El email es requerido"})
	}
	// Verificar que el email no lo use otro usuario
	existing, err := models.GetPortalUserByEmail(req.Email)
	if err == nil && existing.ID != u.ID {
		return c.Status(400).JSON(fiber.Map{"error": "El email ya está en uso"})
	}
	full, err := models.GetPortalUserByID(u.ID)
	if err != nil {
		return c.Status(500).JSON(fiber.Map{"error": "Usuario no encontrado"})
	}
	full.Nombre = req.Nombre
	full.Email = req.Email
	full.Telefono = req.Telefono
	full.Indicativo = req.Indicativo
	full.Pais = req.Pais
	full.Ciudad = req.Ciudad
	full.Documento = req.Documento
	full.Empresa = req.Empresa
	full.SitioWeb = req.SitioWeb
	if err := models.UpdatePortalUser(full); err != nil {
		return c.Status(500).JSON(fiber.Map{"error": err.Error()})
	}
	return c.JSON(fiber.Map{"ok": true})
}

// PUT /portal/mi-perfil/password — cambia la contraseña del usuario autenticado
func PortalCambiarPassword(c *fiber.Ctx) error {
	u := middlewares.PortalUserFromLocals(c)
	if u == nil {
		return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
	}
	type Req struct {
		Actual   string `json:"actual"`
		Nueva    string `json:"nueva"`
		Confirma string `json:"confirma"`
	}
	var req Req
	if err := c.BodyParser(&req); err != nil {
		return c.Status(400).JSON(fiber.Map{"error": "Datos inválidos"})
	}
	if strings.TrimSpace(req.Nueva) == "" {
		return c.Status(400).JSON(fiber.Map{"error": "La nueva contraseña no puede estar vacía"})
	}
	if req.Nueva != req.Confirma {
		return c.Status(400).JSON(fiber.Map{"error": "Las contraseñas no coinciden"})
	}
	if len(req.Nueva) < 8 {
		return c.Status(400).JSON(fiber.Map{"error": "La contraseña debe tener al menos 8 caracteres"})
	}
	full, err := models.GetPortalUserByID(u.ID)
	if err != nil {
		return c.Status(500).JSON(fiber.Map{"error": "Usuario no encontrado"})
	}
	if match, matchErr := app.Http.Hash.Match(req.Actual, full.Password); matchErr != nil || !match {
		return c.Status(400).JSON(fiber.Map{"error": "La contraseña actual es incorrecta"})
	}
	hashed, err := app.Http.Hash.Create(req.Nueva)
	if err != nil {
		return c.Status(500).JSON(fiber.Map{"error": "Error al procesar contraseña"})
	}
	if err := models.UpdatePortalUserPassword(u.ID, hashed); err != nil {
		return c.Status(500).JSON(fiber.Map{"error": err.Error()})
	}
	return c.JSON(fiber.Map{"ok": true})
}

// POST /portal/mi-perfil/rut-file — sube el documento RUT del usuario autenticado
func PortalSubirRutDocumento(c *fiber.Ctx) error {
	u := middlewares.PortalUserFromLocals(c)
	if u == nil {
		return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
	}
	file, err := c.FormFile("rut_file")
	if err != nil {
		return c.Status(400).JSON(fiber.Map{"error": "Archivo requerido"})
	}
	if file.Size > 10*1024*1024 {
		return c.Status(400).JSON(fiber.Map{"error": "Máximo 10 MB"})
	}
	ext := strings.ToLower(filepath.Ext(file.Filename))
	allowed := map[string]bool{".pdf": true, ".jpg": true, ".jpeg": true, ".png": true, ".webp": true}
	if !allowed[ext] {
		return c.Status(400).JSON(fiber.Map{"error": "Solo se permiten PDF, JPG, PNG o WEBP"})
	}
	dir := fmt.Sprintf("uploads/portal_users/%d", u.ID)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return c.Status(500).JSON(fiber.Map{"error": "Error al crear directorio"})
	}
	savePath := filepath.Join(dir, "rut"+ext)
	if err := c.SaveFile(file, savePath); err != nil {
		return c.Status(500).JSON(fiber.Map{"error": "Error al guardar archivo"})
	}
	if err := models.UpdatePortalUserRutFile(u.ID, savePath, file.Filename); err != nil {
		return c.Status(500).JSON(fiber.Map{"error": err.Error()})
	}
	return c.JSON(fiber.Map{"ok": true, "nombre": file.Filename})
}

// ─── Telegram portal ──────────────────────────────────────────────────────────

// PortalTelegramInit genera un código de vinculación temporal para el usuario del portal.
func PortalTelegramInit(c *fiber.Ctx) error {
	u := middlewares.PortalUserFromLocals(c)
	if u == nil {
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "No autenticado"})
	}
	token, err := models.GenerateTelegramPortalToken(u.ID)
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "No se pudo generar el código"})
	}

	// Obtener username del bot desde el primer TelegramConfig activo
	configs, _ := models.GetAllTelegramConfigs()
	botToken := ""
	for _, cfg := range configs {
		if cfg.Activo && cfg.BotToken != "" {
			botToken = cfg.BotToken
			break
		}
	}
	botUsername := services.GetBotUsername(botToken)

	return c.JSON(fiber.Map{
		"token":        token.Token,
		"bot_username": botUsername,
		"bot_link":     "https://t.me/" + botUsername,
	})
}

// PortalTelegramStatus devuelve si el usuario del portal tiene Telegram vinculado.
func PortalTelegramStatus(c *fiber.Ctx) error {
	u := middlewares.PortalUserFromLocals(c)
	if u == nil {
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "No autenticado"})
	}
	full, err := models.GetPortalUserByID(u.ID)
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
	}
	return c.JSON(fiber.Map{
		"linked":  full.TelegramChatID != "",
		"chat_id": full.TelegramChatID,
	})
}

// TelegramPortalWebhook recibe actualizaciones del bot de Telegram y vincula el chat_id
// al usuario del portal que envió el código de verificación.
// Este endpoint debe estar registrado como webhook en el bot: POST /setWebhook?url=.../webhooks/telegram-portal
func TelegramPortalWebhook(c *fiber.Ctx) error {
	var update struct {
		Message struct {
			Chat struct {
				ID int64 `json:"id"`
			} `json:"chat"`
			Text string `json:"text"`
		} `json:"message"`
	}
	if err := c.BodyParser(&update); err != nil {
		return c.SendStatus(fiber.StatusOK)
	}

	chatID := update.Message.Chat.ID
	text := strings.TrimSpace(update.Message.Text)
	log.Printf("[TelegramPortalWebhook] chatID=%d text=%q", chatID, text)
	if chatID == 0 || text == "" {
		return c.SendStatus(fiber.StatusOK)
	}

	// Buscar token en el texto: 6 caracteres hex mayúsculas
	// Acepta "/vincular A3F9B2" o solo "A3F9B2"
	var token string
	for _, part := range strings.Fields(text) {
		candidate := strings.ToUpper(strings.TrimPrefix(strings.TrimPrefix(part, "/vincular"), "/VINCULAR"))
		candidate = strings.TrimSpace(candidate)
		if len(candidate) == 6 {
			token = candidate
			break
		}
	}
	if token == "" {
		return c.SendStatus(fiber.StatusOK)
	}

	t, err := models.FindTelegramPortalToken(token)
	if err != nil {
		portalTelegramReply(chatID, "❌ Código inválido o expirado. Genera un nuevo código desde el portal.")
		return c.SendStatus(fiber.StatusOK)
	}

	if err := models.UpdatePortalUserTelegramChatID(t.PortalUserID, fmt.Sprintf("%d", chatID)); err != nil {
		return c.SendStatus(fiber.StatusOK)
	}
	models.DeleteTelegramPortalToken(t.PortalUserID)

	portalTelegramReply(chatID, "✅ ¡Tu Telegram ha sido vinculado al portal correctamente!\n\nRecibirás notificaciones importantes por este medio.")
	return c.SendStatus(fiber.StatusOK)
}

// portalTelegramReply envía un mensaje usando el primer bot activo configurado.
func portalTelegramReply(chatID int64, text string) {
	configs, _ := models.GetAllTelegramConfigs()
	for _, cfg := range configs {
		if cfg.Activo && cfg.BotToken != "" {
			svc := &services.TelegramService{BotToken: cfg.BotToken}
			_ = svc.SendMessage(chatID, text)
			return
		}
	}
}

// PortalTelegramValidar llama getUpdates en todos los bots activos, busca el mensaje
// "/vincular TOKEN" del usuario y vincula el chat_id si coincide.
// POST /portal/mi-perfil/telegram-validar
func PortalTelegramValidar(c *fiber.Ctx) error {
	u := middlewares.PortalUserFromLocals(c)
	if u == nil {
		return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "No autenticado"})
	}

	// Recuperar el token vigente del usuario
	tkn, err := models.GetTelegramPortalTokenByUser(u.ID)
	if err != nil || tkn == nil {
		return c.Status(400).JSON(fiber.Map{"error": "No tienes un código activo. Genera uno primero."})
	}

	configs, _ := models.GetAllTelegramConfigs()
	for _, cfg := range configs {
		if !cfg.Activo || cfg.BotToken == "" {
			continue
		}
		chatID, found := searchTokenInUpdates(cfg.BotToken, tkn.Token)
		if !found {
			continue
		}
		// Vincular
		if err := models.UpdatePortalUserTelegramChatID(u.ID, fmt.Sprintf("%d", chatID)); err != nil {
			return c.Status(500).JSON(fiber.Map{"error": "No se pudo vincular. Intenta de nuevo."})
		}
		models.DeleteTelegramPortalToken(u.ID)
		svc := &services.TelegramService{BotToken: cfg.BotToken}
		_ = svc.SendMessage(chatID, "✅ ¡Tu Telegram ha sido vinculado al portal correctamente!\n\nRecibirás notificaciones importantes por este medio.")
		log.Printf("[PortalTelegramValidar] usuario=%d chatID=%d vinculado", u.ID, chatID)
		return c.JSON(fiber.Map{"ok": true, "chat_id": fmt.Sprintf("%d", chatID)})
	}

	return c.Status(200).JSON(fiber.Map{"ok": false, "error": "No se encontró el mensaje. Asegúrate de enviar /vincular " + tkn.Token + " al bot."})
}

// searchTokenInUpdates llama getUpdates y busca un mensaje que contenga el token.
// Retorna el chat_id y true si lo encuentra.
func searchTokenInUpdates(botToken, token string) (int64, bool) {
	apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/getUpdates?limit=100", botToken)
	resp, err := http.Get(apiURL) //nolint:noctx
	if err != nil {
		log.Printf("[searchTokenInUpdates] error getUpdates: %v", err)
		return 0, false
	}
	defer resp.Body.Close()

	var result struct {
		OK     bool `json:"ok"`
		Result []struct {
			Message struct {
				Chat struct {
					ID int64 `json:"id"`
				} `json:"chat"`
				Text string `json:"text"`
			} `json:"message"`
		} `json:"result"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil || !result.OK {
		return 0, false
	}

	upperToken := strings.ToUpper(token)
	for _, upd := range result.Result {
		txt := strings.ToUpper(strings.TrimSpace(upd.Message.Text))
		if strings.Contains(txt, upperToken) && upd.Message.Chat.ID != 0 {
			log.Printf("[searchTokenInUpdates] token=%s chatID=%d", token, upd.Message.Chat.ID)
			return upd.Message.Chat.ID, true
		}
	}
	return 0, false
}
