package services

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
)

// ─── Tipos para OpenAI-compatible function calling ────────────────────────────

type agentToolParam struct {
	Type        string                    `json:"type"`
	Description string                    `json:"description,omitempty"`
	Enum        []string                  `json:"enum,omitempty"`
	Properties  map[string]agentToolParam `json:"properties,omitempty"`
	Required    []string                  `json:"required,omitempty"`
	Items       *agentToolParam           `json:"items,omitempty"`
}

type agentToolFunc struct {
	Name        string         `json:"name"`
	Description string         `json:"description"`
	Parameters  agentToolParam `json:"parameters"`
}

type agentTool struct {
	Type     string        `json:"type"`
	Function agentToolFunc `json:"function"`
}

type agentMessage struct {
	Role       string          `json:"role"`
	Content    interface{}     `json:"content"` // string o null
	ToolCalls  []agentToolCall `json:"tool_calls,omitempty"`
	ToolCallID string          `json:"tool_call_id,omitempty"`
	Name       string          `json:"name,omitempty"`
	// Adjunto opcional para que el modelo lea el archivo real (solo Anthropic).
	AttachmentBase64 string `json:"-"`
	AttachmentMime   string `json:"-"`
	// Raw guarda el JSON tal cual vino del proveedor cuando este mensaje se
	// parseó de una respuesta (no cuando lo armamos nosotros). Algunos
	// proveedores mandan campos propios fuera de nuestra struct — ej. Gemini
	// exige que el tool_call de un modelo "thinking" se reenvíe con su
	// thought_signature intacto, si no rechaza la siguiente ronda con 400.
	// Reenviar los bytes originales en vez de reserializar desde los campos
	// tipados preserva cualquier campo así sin que tengamos que conocerlo.
	Raw json.RawMessage `json:"-"`
}

// UnmarshalJSON guarda una copia de los bytes originales en Raw además de
// poblar los campos normales — así un mensaje que vino de la respuesta del
// proveedor se puede reenviar exactamente como llegó.
func (m *agentMessage) UnmarshalJSON(data []byte) error {
	type alias agentMessage
	var a alias
	if err := json.Unmarshal(data, &a); err != nil {
		return err
	}
	*m = agentMessage(a)
	m.Raw = append(json.RawMessage(nil), data...)
	return nil
}

// MarshalJSON reenvía Raw tal cual si el mensaje vino de un unmarshal
// previo; si lo armamos nosotros (Raw vacío), serializa los campos normales.
func (m agentMessage) MarshalJSON() ([]byte, error) {
	if len(m.Raw) > 0 {
		return m.Raw, nil
	}
	type alias agentMessage
	return json.Marshal(alias(m))
}

type agentToolCall struct {
	ID       string `json:"id"`
	Type     string `json:"type"`
	Function struct {
		Name      string `json:"name"`
		Arguments string `json:"arguments"`
	} `json:"function"`
}

type agentChatReq struct {
	Model      string         `json:"model"`
	Messages   []agentMessage `json:"messages"`
	Tools      []agentTool    `json:"tools,omitempty"`
	ToolChoice string         `json:"tool_choice,omitempty"`
	MaxTokens  int            `json:"max_tokens,omitempty"`
}

type agentChatResp struct {
	Choices []struct {
		Message      agentMessage `json:"message"`
		FinishReason string       `json:"finish_reason"`
	} `json:"choices"`
	Error *struct {
		Message string `json:"message"`
	} `json:"error"`
}

// ─── Definición de herramientas ───────────────────────────────────────────────

func agentTools() []agentTool {
	str := func(desc string) agentToolParam {
		return agentToolParam{Type: "string", Description: desc}
	}
	num := func(desc string) agentToolParam {
		return agentToolParam{Type: "number", Description: desc}
	}
	obj := func(props map[string]agentToolParam, required []string) agentToolParam {
		return agentToolParam{Type: "object", Properties: props, Required: required}
	}
	tool := func(name, desc string, params agentToolParam) agentTool {
		return agentTool{Type: "function", Function: agentToolFunc{Name: name, Description: desc, Parameters: params}}
	}

	return []agentTool{
		// ── Sistema ──────────────────────────────────────────────────────────
		tool("sistema_info", "Información general del sistema y recursos disponibles.", obj(nil, nil)),

		// ── Coolify ──────────────────────────────────────────────────────────
		tool("coolify_instancias", "Lista las instancias de Coolify configuradas. Úsala primero para saber qué config_id existe si el usuario no lo especifica.", obj(nil, nil)),
		tool("coolify_listar", "Lista recursos de una instancia Coolify: aplicaciones, servicios, bases de datos, servidores, proyectos, deployments recientes o miembros del equipo.",
			obj(map[string]agentToolParam{
				"config_id": num("ID de la instancia Coolify"),
				"tipo": agentToolParam{
					Type:        "string",
					Description: "Qué listar",
					Enum:        []string{"apps", "servicios", "bases_datos", "servidores", "proyectos", "deployments", "equipo"},
				},
			}, []string{"config_id", "tipo"})),
		tool("coolify_detalle", "Muestra el detalle de un recurso concreto de Coolify por su UUID.",
			obj(map[string]agentToolParam{
				"config_id": num("ID de la instancia Coolify"),
				"tipo": agentToolParam{
					Type:        "string",
					Description: "Tipo de recurso",
					Enum:        []string{"app", "servicio", "base_datos", "servidor", "proyecto", "deployment"},
				},
				"uuid": str("UUID del recurso"),
			}, []string{"config_id", "tipo", "uuid"})),
		tool("coolify_accion", "Ejecuta una acción sobre una aplicación, servicio o base de datos en Coolify: iniciar, detener, reiniciar o desplegar. 'deploy' solo aplica a aplicaciones.",
			obj(map[string]agentToolParam{
				"config_id": num("ID de la instancia Coolify"),
				"tipo": agentToolParam{
					Type:        "string",
					Description: "Sobre qué se ejecuta la acción",
					Enum:        []string{"app", "servicio", "base_datos"},
				},
				"uuid": str("UUID del recurso"),
				"accion": agentToolParam{
					Type:        "string",
					Description: "Acción a ejecutar",
					Enum:        []string{"start", "stop", "restart", "deploy"},
				},
				"force": agentToolParam{Type: "boolean", Description: "Solo para deploy: forzar rebuild desde cero sin usar caché"},
			}, []string{"config_id", "tipo", "uuid", "accion"})),
		tool("coolify_logs", "Obtiene los logs recientes de una aplicación en Coolify. Útil para diagnosticar por qué algo falla.",
			obj(map[string]agentToolParam{
				"config_id": num("ID de la instancia Coolify"),
				"uuid":      str("UUID de la aplicación"),
			}, []string{"config_id", "uuid"})),
		tool("coolify_deployments", "Lista los deployments recientes de una aplicación, con su estado (para saber si un despliegue terminó bien).",
			obj(map[string]agentToolParam{
				"config_id": num("ID de la instancia Coolify"),
				"uuid":      str("UUID de la aplicación"),
			}, []string{"config_id", "uuid"})),
		tool("coolify_envs", "Lista las variables de entorno de una aplicación o servicio en Coolify. Los valores pueden contener secretos: nunca los repitas completos en la respuesta salvo que el usuario lo pida explícitamente.",
			obj(map[string]agentToolParam{
				"config_id": num("ID de la instancia Coolify"),
				"tipo":      agentToolParam{Type: "string", Description: "Tipo de recurso", Enum: []string{"app", "servicio"}},
				"uuid":      str("UUID del recurso"),
			}, []string{"config_id", "tipo", "uuid"})),
		tool("coolify_servidor_info", "Consulta información de un servidor de Coolify: recursos desplegados, dominios configurados, o valida su conexión.",
			obj(map[string]agentToolParam{
				"config_id": num("ID de la instancia Coolify"),
				"uuid":      str("UUID del servidor"),
				"info": agentToolParam{
					Type:        "string",
					Description: "Qué consultar",
					Enum:        []string{"recursos", "dominios", "validar"},
				},
			}, []string{"config_id", "uuid", "info"})),

		// ── Facturas ─────────────────────────────────────────────────────────
		tool("listar_facturas", "Lista facturas. Puede filtrar por estado: pendiente, pagada, vencida, cancelada.",
			obj(map[string]agentToolParam{
				"page":   num("Página (default 1)"),
				"search": str("Búsqueda por número, cliente o descripción"),
				"estado": agentToolParam{Type: "string", Description: "pendiente | pagada | vencida | cancelada", Enum: []string{"pendiente", "pagada", "vencida", "cancelada", ""}},
			}, nil)),
		tool("adjuntar_factura", "Guarda como factura de VENTA el documento (PDF/foto) que el usuario acaba de enviar por Telegram: U-SITE es quien factura/cobra al cliente. Solo funciona si hay un archivo adjunto pendiente. Si el documento es al revés (un proveedor le factura a U-SITE), usa adjuntar_factura_compra en su lugar, no esta.",
			obj(map[string]agentToolParam{
				"cliente_id":  num("ID del cliente al que pertenece la factura (usa listar_clientes si no lo sabes)"),
				"numero":      str("Número de factura, si el usuario lo indicó"),
				"monto":       num("Monto de la factura, si el usuario lo indicó"),
				"descripcion": str("Descripción breve, ej: 'Factura mensual octubre'"),
			}, []string{"cliente_id"})),
		tool("adjuntar_factura_compra", "Guarda como factura de COMPRA (cuenta por pagar) el documento que el usuario acaba de enviar por Telegram: un proveedor le está facturando a U-SITE, no al revés. Busca o crea el proveedor por nombre. Solo funciona si hay un archivo adjunto pendiente.",
			obj(map[string]agentToolParam{
				"proveedor":   str("Nombre del proveedor que emitió la factura (quien la firma/factura, no U-SITE)"),
				"monto":       num("Monto de la factura"),
				"descripcion": str("Descripción breve de la compra, ej: 'Hosting AWS octubre'"),
			}, []string{"proveedor", "monto"})),

		// ── Clientes ─────────────────────────────────────────────────────────
		tool("listar_clientes", "Lista clientes con paginación y búsqueda.",
			obj(map[string]agentToolParam{
				"page":   num("Página (default 1)"),
				"search": str("Texto de búsqueda"),
			}, nil)),
		tool("crear_cliente", "Crea un nuevo cliente.",
			obj(map[string]agentToolParam{
				"nombre":    str("Nombre del cliente"),
				"empresa":   str("Empresa"),
				"email":     str("Correo electrónico"),
				"telefono":  str("Teléfono"),
				"documento": str("Documento de identidad"),
				"notas":     str("Notas adicionales"),
			}, []string{"nombre"})),

		// ── Contratos ────────────────────────────────────────────────────────
		tool("listar_contratos", "Lista contratos con filtros.",
			obj(map[string]agentToolParam{
				"page":   num("Página"),
				"search": str("Búsqueda"),
				"estado": str("Estado del contrato"),
			}, nil)),
		tool("renovar_contrato", "Renueva un contrato por su ID.",
			obj(map[string]agentToolParam{"id": num("ID del contrato")}, []string{"id"})),
		tool("enviar_correo_contrato", "Envía notificación de renovación por correo al cliente.",
			obj(map[string]agentToolParam{"id": num("ID del contrato")}, []string{"id"})),
		tool("crear_contrato", "Crea un contrato para un cliente con uno o más servicios y genera de una vez su PDF con cláusulas estándar.",
			obj(map[string]agentToolParam{
				"cliente_id":      num("ID del cliente (usa listar_clientes si no lo sabes)"),
				"servicio_ids":    agentToolParam{Type: "array", Description: "IDs de los servicios contratados", Items: &agentToolParam{Type: "number"}},
				"duracion_meses":  num("Duración del contrato en meses (default 12)"),
				"precio_acordado": num("Valor total acordado"),
				"moneda":          str("Moneda, ej: COP, USD (default COP)"),
				"notas":           str("Notas adicionales del contrato"),
			}, []string{"cliente_id", "servicio_ids", "precio_acordado"})),
		tool("generar_documento_contrato", "Genera (o regenera) el PDF de un contrato ya existente a partir de la plantilla activa.",
			obj(map[string]agentToolParam{"id": num("ID del contrato")}, []string{"id"})),

		// ── Contabilidad ─────────────────────────────────────────────────────
		tool("dashboard_contabilidad", "Resumen del mes: ingresos, egresos, pendientes.",
			obj(map[string]agentToolParam{
				"mes":  num("Mes (1-12)"),
				"anio": num("Año (ej: 2026)"),
			}, nil)),
		tool("listar_transacciones", "Lista transacciones contables.",
			obj(map[string]agentToolParam{
				"page": num("Página"),
				"tipo": agentToolParam{Type: "string", Description: "ingreso | egreso", Enum: []string{"ingreso", "egreso", ""}},
				"mes":  num("Mes (1-12)"),
				"anio": num("Año"),
			}, nil)),
		tool("crear_transaccion", "Registra una transacción de ingreso o egreso.",
			obj(map[string]agentToolParam{
				"tipo":        agentToolParam{Type: "string", Description: "ingreso | egreso", Enum: []string{"ingreso", "egreso"}},
				"descripcion": str("Descripción"),
				"valor":       num("Valor en COP"),
				"fecha":       str("Fecha YYYY-MM-DD (vacío = hoy)"),
				"notas":       str("Notas"),
			}, []string{"tipo", "descripcion", "valor"})),
		tool("listar_cuentas_cobro", "Lista cuentas por cobrar (lo que nos deben).",
			obj(map[string]agentToolParam{
				"page":   num("Página"),
				"estado": str("pendiente | pagada | vencida"),
			}, nil)),
		tool("crear_cuenta_cobro", "Crea una cuenta de cobro para un cliente/proyecto y genera de una vez su PDF de solicitud.",
			obj(map[string]agentToolParam{
				"cliente_id":  num("ID del cliente"),
				"descripcion": str("Descripción del cobro, ej: proyecto y periodo"),
				"valor":       num("Valor a cobrar"),
				"notas":       str("Notas adicionales"),
			}, []string{"cliente_id", "descripcion", "valor"})),
		tool("generar_documento_cuenta_cobro", "Genera (o regenera) el PDF de una cuenta de cobro ya existente.",
			obj(map[string]agentToolParam{"id": num("ID de la cuenta de cobro")}, []string{"id"})),
		tool("listar_cuentas_pagar", "Lista cuentas por pagar (lo que debemos).",
			obj(map[string]agentToolParam{
				"page":   num("Página"),
				"estado": str("pendiente | pagada | vencida"),
			}, nil)),

		// ── Automatización IA: cotizaciones y tarifas ───────────────────────────
		tool("listar_tarifas", "Lista las tarifas activas (valor por hora, licencias, VMs, márgenes) para armar cotizaciones.",
			obj(map[string]agentToolParam{
				"categoria": str("Filtrar por categoría: hora_servicio | licencia | vm_azure | margen | otro"),
			}, nil)),
		tool("crear_cotizacion", "Genera el PDF de una cotización para un cliente, a partir de la plantilla activa y los items con precio ya calculado (consulta listar_tarifas antes para saber los valores).",
			obj(map[string]agentToolParam{
				"cliente_id":    num("ID del cliente (usa listar_clientes si no lo sabes)"),
				"alcance":       str("Descripción del alcance del proyecto o servicio a cotizar"),
				"tipo_proyecto": str("Tipo de proyecto, ej: migracion_m365, vm_azure, soporte"),
				"items": agentToolParam{
					Type:        "array",
					Description: "Ítems de la cotización con precio ya calculado",
					Items: &agentToolParam{
						Type: "object",
						Properties: map[string]agentToolParam{
							"descripcion":    str("Descripción del ítem"),
							"cantidad":       num("Cantidad"),
							"valor_unitario": num("Valor unitario en COP"),
							"unidad":         str("hora | mes | unico"),
						},
						Required: []string{"descripcion", "cantidad", "valor_unitario"},
					},
				},
			}, []string{"cliente_id", "alcance", "items"})),

		// ── Automatización IA: arquitecturas ────────────────────────────────────
		tool("listar_arquitecturas_referencia", "Lista los patrones de arquitectura técnica ya resueltos (ej: AD redundante en Azure con VPN Gateway) para reutilizarlos en vez de improvisar.",
			obj(map[string]agentToolParam{"search": str("Búsqueda por nombre o tag")}, nil)),
		tool("generar_arquitectura", "Genera el PDF de una propuesta técnica ya redactada (consulta antes listar_arquitecturas_referencia para reutilizar patrones existentes).",
			obj(map[string]agentToolParam{
				"requerimiento":           str("Requerimiento del cliente en texto libre"),
				"propuesta":               str("Propuesta técnica ya redactada (HTML o texto) que se insertará en el documento"),
				"nombre":                  str("Título de la propuesta"),
				"cliente_id":              num("ID del cliente (opcional)"),
				"guardar_como_referencia": agentToolParam{Type: "boolean", Description: "Si true, guarda esta propuesta como patrón reutilizable para el futuro"},
			}, []string{"requerimiento", "propuesta"})),

		// ── Proyectos ────────────────────────────────────────────────────────
		tool("listar_proyectos", "Lista proyectos.",
			obj(map[string]agentToolParam{
				"page":   num("Página"),
				"search": str("Búsqueda"),
			}, nil)),
		tool("crear_proyecto", "Crea un nuevo proyecto para un cliente.",
			obj(map[string]agentToolParam{
				"cliente_id":  num("ID del cliente (usa listar_clientes si no lo sabes)"),
				"nombre":      str("Nombre del proyecto"),
				"stack":       str("Stack tecnológico, ej: Go + React + PostgreSQL"),
				"descripcion": str("Descripción adicional del proyecto"),
			}, []string{"cliente_id", "nombre"})),
		tool("crear_fase_proyecto", "Agrega una fase/etapa a un proyecto existente (ej: Diseño, Desarrollo, QA, Entrega). Se puede llamar varias veces seguidas para registrar todas las fases de un proyecto de una vez.",
			obj(map[string]agentToolParam{
				"proyecto_id":    num("ID del proyecto (usa listar_proyectos si no lo sabes)"),
				"nombre":         str("Nombre de la fase"),
				"descripcion":    str("Descripción de la fase"),
				"estado":         agentToolParam{Type: "string", Enum: []string{"pendiente", "en_progreso", "completado"}, Description: "Estado inicial (default pendiente)"},
				"orden":          num("Orden de la fase dentro del proyecto (0, 1, 2...)"),
				"fecha_estimada": str("Fecha estimada de entrega, formato YYYY-MM-DD"),
			}, []string{"proyecto_id", "nombre"})),
		tool("adjuntar_documento_proyecto", "Guarda como documento del proyecto el archivo (PDF, foto, Word, Excel o cualquier otro tipo) que el usuario acaba de enviar por Telegram en este chat. Solo funciona si hay un archivo adjunto pendiente.",
			obj(map[string]agentToolParam{
				"proyecto_id": num("ID del proyecto (usa listar_proyectos si no lo sabes)"),
				"tipo":        agentToolParam{Type: "string", Enum: []string{"contrato", "orden_servicio", "otro"}, Description: "Tipo de documento (default otro)"},
				"nombre":      str("Nombre del documento"),
				"descripcion": str("Descripción breve"),
			}, []string{"proyecto_id"})),

		// ── Tickets ──────────────────────────────────────────────────────────
		tool("listar_tickets", "Lista tickets de soporte de todos los proyectos.",
			obj(map[string]agentToolParam{
				"page":   num("Página"),
				"estado": str("abierto | cerrado | en_proceso"),
			}, nil)),
		tool("responder_ticket", "Responde un ticket de soporte.",
			obj(map[string]agentToolParam{
				"ticket_id": num("ID del ticket"),
				"mensaje":   str("Mensaje de respuesta"),
			}, []string{"ticket_id", "mensaje"})),

		// ── Tareas ───────────────────────────────────────────────────────────
		tool("listar_tareas", "Lista tareas del Kanban (resumen: título, estado, prioridad, asignado — sin descripción completa ni historial de comentarios). Para ver el detalle completo de una tarea puntual usa detalle_tarea.",
			obj(map[string]agentToolParam{
				"estado": str("pendiente | en_progreso | completada | cancelada"),
			}, nil)),
		tool("detalle_tarea", "Trae el detalle completo de una tarea puntual: descripción completa y el historial de comentarios/interacciones (quién comentó, qué dijo, cuándo, y archivos adjuntos si los hay). Úsala cuando el usuario pida el cuerpo, la descripción o el historial de una tarea específica — listar_tareas no trae eso.",
			obj(map[string]agentToolParam{
				"id": num("ID de la tarea (usa listar_tareas si no lo sabes)"),
			}, []string{"id"})),
		tool("listar_usuarios", "Lista los usuarios internos del sistema (staff), para poder asignarles tareas.",
			obj(map[string]agentToolParam{"search": str("Búsqueda por nombre")}, nil)),
		tool("crear_tarea", "Crea una nueva tarea, opcionalmente asignada a alguien desde el inicio.",
			obj(map[string]agentToolParam{
				"titulo":       str("Título de la tarea"),
				"descripcion":  str("Descripción"),
				"estado":       agentToolParam{Type: "string", Enum: []string{"por_hacer", "en_progreso"}, Description: "Estado inicial (default por_hacer). Debe coincidir con las columnas del tablero Kanban."},
				"prioridad":    agentToolParam{Type: "string", Enum: []string{"baja", "media", "alta"}, Description: "Prioridad"},
				"asignado_id":  num("ID del usuario responsable (usa listar_usuarios si no lo sabes)"),
				"fecha_limite": str("Fecha límite, formato YYYY-MM-DD"),
			}, []string{"titulo"})),
		tool("asignar_tarea", "Asigna (o reasigna) una tarea existente a un usuario. Notifica al usuario asignado.",
			obj(map[string]agentToolParam{
				"id":          num("ID de la tarea"),
				"asignado_id": num("ID del usuario responsable (usa listar_usuarios si no lo sabes)"),
			}, []string{"id", "asignado_id"})),
		tool("adjuntar_documento_tarea", "Guarda como comentario de una tarea el archivo (PDF, foto, Word, Excel o cualquier otro tipo) que el usuario acaba de enviar por Telegram en este chat. Úsala cuando el usuario mande un archivo junto con instrucciones sobre una tarea (nueva o existente), sin importar el formato. Solo funciona si hay un archivo adjunto pendiente. Si la tarea es nueva, primero créala con crear_tarea y usa el id que devuelve.",
			obj(map[string]agentToolParam{
				"id":         num("ID de la tarea a la que se adjunta el archivo"),
				"comentario": str("Comentario que acompaña al archivo (opcional)"),
			}, []string{"id"})),
		tool("actualizar_estado_tarea", "Cambia el estado de una tarea. Los valores deben coincidir con las columnas del tablero Kanban del dashboard.",
			obj(map[string]agentToolParam{
				"id": num("ID de la tarea"),
				"estado": agentToolParam{
					Type:        "string",
					Enum:        []string{"por_hacer", "en_progreso", "revision", "hecho"},
					Description: "por_hacer=nueva, en_progreso=en curso, revision=terminada pendiente de revisar, hecho=cerrada",
				},
			}, []string{"id", "estado"})),

		// ── Servidores ───────────────────────────────────────────────────────
		tool("listar_servidores", "Lista los servidores monitoreados.",
			obj(map[string]agentToolParam{"search": str("Búsqueda")}, nil)),

		// ── Monitor URLs ─────────────────────────────────────────────────────
		tool("listar_monitores", "Lista los monitores de URL activos.",
			obj(nil, nil)),
	}
}

// ─── Ejecución de herramientas ────────────────────────────────────────────────

func executeAgentTool(chatID int64, name string, args map[string]interface{}) string {
	result, err := runTool(chatID, name, args)
	if err != nil {
		return fmt.Sprintf(`{"error": %q}`, err.Error())
	}
	b, _ := json.Marshal(result)
	return string(b)
}

func runTool(chatID int64, name string, a map[string]interface{}) (interface{}, error) {
	getInt := func(key string, def int) int {
		if v, ok := a[key]; ok {
			switch x := v.(type) {
			case float64:
				return int(x)
			case int:
				return x
			}
		}
		return def
	}
	getStr := func(key string) string {
		if v, ok := a[key]; ok {
			if s, ok := v.(string); ok {
				return s
			}
		}
		return ""
	}
	getBool := func(key string) bool {
		if v, ok := a[key]; ok {
			if b, ok := v.(bool); ok {
				return b
			}
		}
		return false
	}

	switch name {

	case "sistema_info":
		cfgs, _ := models.GetAllCoolifyConfigs()
		names := make([]string, len(cfgs))
		for i, c := range cfgs {
			names[i] = fmt.Sprintf("#%d %s (%s)", c.ID, c.Nombre, c.BaseURL)
		}
		return map[string]interface{}{
			"sistema":            "U-Site Admin",
			"coolify_instancias": names,
			"timestamp":          time.Now().Format("2006-01-02 15:04:05"),
		}, nil

	// ── Coolify ────────────────────────────────────────────────────────────
	case "coolify_instancias":
		cfgs, err := models.GetAllCoolifyConfigs()
		if err != nil {
			return nil, err
		}
		type safe struct {
			ID      uint   `json:"id"`
			Nombre  string `json:"nombre"`
			BaseURL string `json:"base_url"`
			Activo  bool   `json:"activo"`
		}
		out := make([]safe, len(cfgs))
		for i, c := range cfgs {
			out[i] = safe{ID: c.ID, Nombre: c.Nombre, BaseURL: c.BaseURL, Activo: c.Activo}
		}
		return out, nil

	case "coolify_listar":
		cfg, err := coolifyConfigDe(getInt("config_id", 0))
		if err != nil {
			return nil, err
		}
		endpoint, ok := map[string]string{
			"apps":        "/applications",
			"servicios":   "/services",
			"bases_datos": "/databases",
			"servidores":  "/servers",
			"proyectos":   "/projects",
			"deployments": "/deployments",
			"equipo":      "/teams/current/members",
		}[getStr("tipo")]
		if !ok {
			return nil, fmt.Errorf("tipo inválido: usa apps, servicios, bases_datos, servidores, proyectos, deployments o equipo")
		}
		return coolifyCall("GET", endpoint, nil, cfg)

	case "coolify_detalle":
		cfg, err := coolifyConfigDe(getInt("config_id", 0))
		if err != nil {
			return nil, err
		}
		uuid := getStr("uuid")
		if uuid == "" {
			return nil, fmt.Errorf("uuid requerido")
		}
		base, ok := coolifyBasePorTipo(getStr("tipo"))
		if !ok {
			if getStr("tipo") == "deployment" {
				return coolifyCall("GET", "/deployments/"+uuid, nil, cfg)
			}
			return nil, fmt.Errorf("tipo inválido: usa app, servicio, base_datos, servidor, proyecto o deployment")
		}
		return coolifyCall("GET", base+"/"+uuid, nil, cfg)

	case "coolify_accion":
		cfg, err := coolifyConfigDe(getInt("config_id", 0))
		if err != nil {
			return nil, err
		}
		uuid := getStr("uuid")
		if uuid == "" {
			return nil, fmt.Errorf("uuid requerido")
		}
		tipo := getStr("tipo")
		accion := getStr("accion")

		if accion == "deploy" {
			if tipo != "app" {
				return nil, fmt.Errorf("'deploy' solo aplica a aplicaciones; para %s usa start, stop o restart", tipo)
			}
			return coolifyCall("POST", fmt.Sprintf("/deploy?uuid=%s&force=%v", uuid, getBool("force")), nil, cfg)
		}
		if accion != "start" && accion != "stop" && accion != "restart" {
			return nil, fmt.Errorf("acción inválida: usa start, stop, restart o deploy")
		}
		base, ok := coolifyBasePorTipo(tipo)
		if !ok || tipo == "servidor" || tipo == "proyecto" {
			return nil, fmt.Errorf("tipo inválido para una acción: usa app, servicio o base_datos")
		}
		return coolifyCall("GET", fmt.Sprintf("%s/%s/%s", base, uuid, accion), nil, cfg)

	case "coolify_logs", "coolify_deployments":
		cfg, err := coolifyConfigDe(getInt("config_id", 0))
		if err != nil {
			return nil, err
		}
		uuid := getStr("uuid")
		if uuid == "" {
			return nil, fmt.Errorf("uuid requerido")
		}
		sufijo := "/logs"
		if name == "coolify_deployments" {
			sufijo = "/deployments"
		}
		return coolifyCall("GET", "/applications/"+uuid+sufijo, nil, cfg)

	case "coolify_envs":
		cfg, err := coolifyConfigDe(getInt("config_id", 0))
		if err != nil {
			return nil, err
		}
		uuid := getStr("uuid")
		if uuid == "" {
			return nil, fmt.Errorf("uuid requerido")
		}
		base, ok := coolifyBasePorTipo(getStr("tipo"))
		if !ok || (getStr("tipo") != "app" && getStr("tipo") != "servicio") {
			return nil, fmt.Errorf("tipo inválido: las variables de entorno solo se consultan de app o servicio")
		}
		return coolifyCall("GET", base+"/"+uuid+"/envs", nil, cfg)

	case "coolify_servidor_info":
		cfg, err := coolifyConfigDe(getInt("config_id", 0))
		if err != nil {
			return nil, err
		}
		uuid := getStr("uuid")
		if uuid == "" {
			return nil, fmt.Errorf("uuid requerido")
		}
		sufijo, ok := map[string]string{
			"recursos": "/resources",
			"dominios": "/domains",
			"validar":  "/validate",
		}[getStr("info")]
		if !ok {
			return nil, fmt.Errorf("info inválida: usa recursos, dominios o validar")
		}
		return coolifyCall("GET", "/servers/"+uuid+sufijo, nil, cfg)

	// ── Clientes ───────────────────────────────────────────────────────────
	case "listar_facturas":
		page := getInt("page", 1)
		search := getStr("search")
		estado := getStr("estado")
		limit := 10
		offset := (page - 1) * limit
		all, total, err := models.GetAllFacturas(limit, offset, search)
		if err != nil {
			return nil, err
		}
		// Filtrar por estado si se especificó
		items := all
		if estado != "" {
			items = nil
			for _, f := range all {
				if f.Estado == estado {
					items = append(items, f)
				}
			}
			total = int64(len(items))
		}
		return map[string]interface{}{"items": items, "total": total, "page": page}, nil

	case "adjuntar_factura":
		att, ok := PopTelegramAttachment(chatID)
		if !ok {
			return nil, fmt.Errorf("no hay ningún documento pendiente en este chat; pide al usuario que lo envíe de nuevo")
		}
		clienteID := uint(getInt("cliente_id", 0))
		if clienteID == 0 {
			return nil, fmt.Errorf("cliente_id requerido")
		}
		f := &models.Factura{
			ClienteID:    clienteID,
			Numero:       getStr("numero"),
			Descripcion:  getStr("descripcion"),
			Monto:        float64(getInt("monto", 0)),
			Estado:       "pendiente",
			FechaEmision: time.Now(),
			Visible:      true,
		}
		if err := models.CreateFactura(f); err != nil {
			_ = os.Remove(att.Path)
			return nil, err
		}
		finalDir := "uploads/facturas"
		if err := os.MkdirAll(finalDir, 0755); err != nil {
			return nil, err
		}
		ext := filepath.Ext(att.OriginalName)
		finalPath := filepath.Join(finalDir, fmt.Sprintf("%d_telegram%s", f.ID, ext))
		if err := os.Rename(att.Path, finalPath); err != nil {
			return nil, fmt.Errorf("no se pudo guardar el archivo: %w", err)
		}
		if err := models.UpdateFacturaArchivo(f.ID, finalPath, att.OriginalName); err != nil {
			return nil, err
		}
		return map[string]interface{}{"ok": true, "factura_id": f.ID}, nil

	case "adjuntar_factura_compra":
		cp, err := AdjuntarFacturaCompra(chatID, getStr("proveedor"), getStr("descripcion"), float64(getInt("monto", 0)))
		if err != nil {
			return nil, err
		}
		return map[string]interface{}{"ok": true, "cuenta_pagar_id": cp.ID, "proveedor_id": cp.EntidadID}, nil

	case "listar_clientes":
		page := getInt("page", 1)
		search := getStr("search")
		limit := 10
		offset := (page - 1) * limit
		items, total, err := models.GetAllClientes(limit, offset, search)
		if err != nil {
			return nil, err
		}
		return map[string]interface{}{"items": items, "total": total, "page": page}, nil

	case "crear_cliente":
		c := models.Cliente{
			Nombre:    getStr("nombre"),
			Empresa:   getStr("empresa"),
			Email:     getStr("email"),
			Telefono:  getStr("telefono"),
			Documento: getStr("documento"),
			Notas:     getStr("notas"),
			Activo:    true,
		}
		if err := models.CreateCliente(c); err != nil {
			return nil, err
		}
		return map[string]interface{}{"ok": true, "nombre": c.Nombre}, nil

	// ── Contratos ──────────────────────────────────────────────────────────
	case "listar_contratos":
		page := getInt("page", 1)
		search := getStr("search")
		estado := getStr("estado")
		limit := 10
		offset := (page - 1) * limit
		items, total, err := models.GetAllContratos(limit, offset, search, estado)
		if err != nil {
			return nil, err
		}
		return map[string]interface{}{"items": items, "total": total, "page": page}, nil

	case "renovar_contrato":
		// La lógica de renovación está en el controller; aquí solo informamos que se debe hacer desde la UI
		id := uint(getInt("id", 0))
		if id == 0 {
			return nil, fmt.Errorf("id requerido")
		}
		contrato, err := models.GetContratoByID(id)
		if err != nil {
			return nil, err
		}
		return map[string]interface{}{
			"ok":                true,
			"info":              "Para renovar usa la UI o el endpoint /api/v2/contratos/:id/renovar",
			"contrato_id":       id,
			"cliente_id":        contrato.ClienteID,
			"fecha_vencimiento": contrato.FechaVencimiento,
		}, nil

	case "enviar_correo_contrato":
		id := uint(getInt("id", 0))
		if id == 0 {
			return nil, fmt.Errorf("id requerido")
		}
		contrato, err := models.GetContratoByID(id)
		if err != nil {
			return nil, err
		}
		return map[string]interface{}{
			"ok":          true,
			"info":        "Usa el endpoint /api/v2/contratos/:id/enviar-correo para enviar",
			"contrato_id": id,
			"cliente_id":  contrato.ClienteID,
		}, nil

	case "crear_contrato":
		idsRaw, _ := a["servicio_ids"].([]interface{})
		servicioIDs := make([]uint, 0, len(idsRaw))
		for _, v := range idsRaw {
			switch x := v.(type) {
			case float64:
				servicioIDs = append(servicioIDs, uint(x))
			case int:
				servicioIDs = append(servicioIDs, uint(x))
			}
		}
		contrato, doc, err := CrearContratoConDocumento(
			uint(getInt("cliente_id", 0)),
			servicioIDs,
			getInt("duracion_meses", 12),
			float64(getInt("precio_acordado", 0)),
			getStr("moneda"),
			getStr("notas"),
			"telegram",
		)
		if err != nil {
			return nil, err
		}
		result := map[string]interface{}{"ok": true, "contrato_id": contrato.ID}
		if doc != nil {
			result["documento_id"] = doc.ID
			result["descargar"] = fmt.Sprintf("/api/v2/documentos-generados/%d/download", doc.ID)
		}
		return result, nil

	case "generar_documento_contrato":
		id := uint(getInt("id", 0))
		if id == 0 {
			return nil, fmt.Errorf("id requerido")
		}
		doc, err := GenerarDocumentoContrato(id, "telegram")
		if err != nil {
			return nil, err
		}
		return map[string]interface{}{
			"ok":           true,
			"documento_id": doc.ID,
			"descargar":    fmt.Sprintf("/api/v2/documentos-generados/%d/download", doc.ID),
		}, nil

	// ── Contabilidad ───────────────────────────────────────────────────────
	case "dashboard_contabilidad":
		now := time.Now()
		mes := getInt("mes", int(now.Month()))
		anio := getInt("anio", now.Year())
		dash, err := models.GetDashboardData(mes, anio)
		if err != nil {
			return nil, err
		}
		return dash, nil

	case "listar_transacciones":
		page := getInt("page", 1)
		tipo := getStr("tipo")
		mes := getInt("mes", 0)
		anio := getInt("anio", 0)
		limit := 10
		offset := (page - 1) * limit
		items, total, err := models.GetAllTransacciones(limit, offset, "", tipo, mes, anio)
		if err != nil {
			return nil, err
		}
		return map[string]interface{}{"items": items, "total": total, "page": page}, nil

	case "crear_transaccion":
		t := &models.Transaccion{
			Tipo:        getStr("tipo"),
			Descripcion: getStr("descripcion"),
			Valor:       float64(getInt("valor", 0)),
			Notas:       getStr("notas"),
		}
		if err := models.CreateTransaccion(t); err != nil {
			return nil, err
		}
		return map[string]interface{}{"ok": true, "id": t.ID}, nil

	case "listar_cuentas_cobro":
		page := getInt("page", 1)
		estado := getStr("estado")
		limit := 10
		offset := (page - 1) * limit
		items, total, err := models.GetAllCuentasCobro(limit, offset, "", estado)
		if err != nil {
			return nil, err
		}
		return map[string]interface{}{"items": items, "total": total, "page": page}, nil

	case "crear_cuenta_cobro":
		cc, doc, err := CrearCuentaCobroConDocumento(
			uint(getInt("cliente_id", 0)),
			getStr("descripcion"),
			float64(getInt("valor", 0)),
			nil,
			getStr("notas"),
			"telegram",
		)
		if err != nil {
			return nil, err
		}
		result := map[string]interface{}{"ok": true, "cuenta_cobro_id": cc.ID}
		if doc != nil {
			result["documento_id"] = doc.ID
			result["descargar"] = fmt.Sprintf("/api/v2/documentos-generados/%d/download", doc.ID)
		}
		return result, nil

	case "generar_documento_cuenta_cobro":
		id := uint(getInt("id", 0))
		if id == 0 {
			return nil, fmt.Errorf("id requerido")
		}
		doc, err := GenerarDocumentoCuentaCobro(id, "telegram")
		if err != nil {
			return nil, err
		}
		return map[string]interface{}{
			"ok":           true,
			"documento_id": doc.ID,
			"descargar":    fmt.Sprintf("/api/v2/documentos-generados/%d/download", doc.ID),
		}, nil

	case "listar_cuentas_pagar":
		page := getInt("page", 1)
		estado := getStr("estado")
		limit := 10
		offset := (page - 1) * limit
		items, total, err := models.GetAllCuentasPagar(limit, offset, "", estado)
		if err != nil {
			return nil, err
		}
		return map[string]interface{}{"items": items, "total": total, "page": page}, nil

	// ── Automatización IA: cotizaciones y tarifas ───────────────────────────
	case "listar_tarifas":
		items, err := models.GetTarifasActivas(getStr("categoria"))
		if err != nil {
			return nil, err
		}
		return items, nil

	case "crear_cotizacion":
		itemsRaw, _ := a["items"].([]interface{})
		items := make([]ItemCotizacion, 0, len(itemsRaw))
		for _, raw := range itemsRaw {
			m, ok := raw.(map[string]interface{})
			if !ok {
				continue
			}
			toFloat := func(v interface{}) float64 {
				switch x := v.(type) {
				case float64:
					return x
				case int:
					return float64(x)
				}
				return 0
			}
			toStr := func(v interface{}) string {
				s, _ := v.(string)
				return s
			}
			items = append(items, ItemCotizacion{
				Descripcion:   toStr(m["descripcion"]),
				Cantidad:      toFloat(m["cantidad"]),
				ValorUnitario: toFloat(m["valor_unitario"]),
				Unidad:        toStr(m["unidad"]),
			})
		}
		doc, total, err := CrearCotizacion(uint(getInt("cliente_id", 0)), getStr("alcance"), getStr("tipo_proyecto"), items, "telegram")
		if err != nil {
			return nil, err
		}
		return map[string]interface{}{
			"ok":           true,
			"documento_id": doc.ID,
			"total":        total,
			"descargar":    fmt.Sprintf("/api/v2/documentos-generados/%d/download", doc.ID),
		}, nil

	// ── Automatización IA: arquitecturas ────────────────────────────────────
	case "listar_arquitecturas_referencia":
		items, _, err := models.GetAllArquitecturas(50, 0, getStr("search"), true)
		if err != nil {
			return nil, err
		}
		return items, nil

	case "generar_arquitectura":
		var clienteID *uint
		if id := getInt("cliente_id", 0); id > 0 {
			u := uint(id)
			clienteID = &u
		}
		doc, err := GenerarArquitectura(getStr("requerimiento"), getStr("propuesta"), getStr("nombre"), clienteID, getBool("guardar_como_referencia"), "telegram")
		if err != nil {
			return nil, err
		}
		return map[string]interface{}{
			"ok":           true,
			"documento_id": doc.ID,
			"descargar":    fmt.Sprintf("/api/v2/documentos-generados/%d/download", doc.ID),
		}, nil

	// ── Proyectos ──────────────────────────────────────────────────────────
	case "listar_proyectos":
		page := getInt("page", 1)
		search := getStr("search")
		limit := 10
		offset := (page - 1) * limit
		items, total, err := models.GetAllProyectos(limit, offset, search)
		if err != nil {
			return nil, err
		}
		return map[string]interface{}{"items": items, "total": total, "page": page}, nil

	case "crear_proyecto":
		p, err := CrearProyectoSimple(uint(getInt("cliente_id", 0)), getStr("nombre"), getStr("stack"), getStr("descripcion"))
		if err != nil {
			return nil, err
		}
		return map[string]interface{}{"ok": true, "id": p.ID, "slug": p.Slug}, nil

	case "crear_fase_proyecto":
		var fecha *time.Time
		if s := getStr("fecha_estimada"); s != "" {
			if t, errParse := time.Parse("2006-01-02", s); errParse == nil {
				fecha = &t
			}
		}
		f, err := CrearFaseProyecto(uint(getInt("proyecto_id", 0)), getStr("nombre"), getStr("descripcion"), getStr("estado"), getInt("orden", 0), fecha)
		if err != nil {
			return nil, err
		}
		return map[string]interface{}{"ok": true, "fase_id": f.ID}, nil

	case "adjuntar_documento_proyecto":
		d, err := AdjuntarDocumentoProyecto(chatID, uint(getInt("proyecto_id", 0)), getStr("tipo"), getStr("nombre"), getStr("descripcion"))
		if err != nil {
			return nil, err
		}
		return map[string]interface{}{"ok": true, "documento_id": d.ID}, nil

	// ── Tickets ────────────────────────────────────────────────────────────
	case "listar_tickets":
		estado := getStr("estado")
		items, err := models.GetAllTickets(estado)
		if err != nil {
			return nil, err
		}
		return items, nil

	case "responder_ticket":
		ticketID := uint(getInt("ticket_id", 0))
		mensaje := getStr("mensaje")
		if ticketID == 0 || mensaje == "" {
			return nil, fmt.Errorf("ticket_id y mensaje requeridos")
		}
		m := &models.TicketMensaje{
			TicketID:    ticketID,
			AutorNombre: "Agente Bot",
			Contenido:   mensaje,
			EsAdmin:     true,
		}
		if err := models.CreateTicketMensaje(m); err != nil {
			return nil, err
		}
		return map[string]interface{}{"ok": true}, nil

	// ── Tareas ─────────────────────────────────────────────────────────────
	case "listar_tareas":
		estado := getStr("estado")
		allItems, err := models.GetAllTareas()
		if err != nil {
			return nil, err
		}
		if estado == "" {
			return allItems, nil
		}
		var filtered []models.Tarea
		for _, t := range allItems {
			if t.Estado == estado {
				filtered = append(filtered, t)
			}
		}
		return filtered, nil

	case "detalle_tarea":
		id := uint(getInt("id", 0))
		if id == 0 {
			return nil, fmt.Errorf("id requerido")
		}
		tarea, err := models.GetTareaByID(id)
		if err != nil {
			return nil, fmt.Errorf("tarea no encontrada: %w", err)
		}
		comentarios, err := models.GetComentariosByTarea(id)
		if err != nil {
			return nil, err
		}
		return map[string]interface{}{"tarea": tarea, "comentarios": comentarios}, nil

	case "listar_usuarios":
		users, _, err := models.AllUsersSistema(50, 0, getStr("search"))
		if err != nil {
			return nil, err
		}
		type usuarioItem struct {
			ID   uint   `json:"id"`
			Name string `json:"name"`
		}
		out := make([]usuarioItem, len(users))
		for i, u := range users {
			out[i] = usuarioItem{ID: u.ID, Name: u.Name}
		}
		return out, nil

	case "crear_tarea":
		var asignadoID *uint
		if v := getInt("asignado_id", 0); v > 0 {
			u := uint(v)
			asignadoID = &u
		}
		var fechaLimite *time.Time
		if s := getStr("fecha_limite"); s != "" {
			if t, errParse := time.Parse("2006-01-02", s); errParse == nil {
				fechaLimite = &t
			}
		}
		t, err := CrearTareaAsignada(getStr("titulo"), getStr("descripcion"), getStr("estado"), getStr("prioridad"), asignadoID, fechaLimite)
		if err != nil {
			return nil, err
		}
		return map[string]interface{}{"ok": true, "id": t.ID, "titulo": t.Titulo}, nil

	case "asignar_tarea":
		id := uint(getInt("id", 0))
		asignadoIDVal := getInt("asignado_id", 0)
		if id == 0 || asignadoIDVal == 0 {
			return nil, fmt.Errorf("id y asignado_id requeridos")
		}
		asignadoID := uint(asignadoIDVal)
		t, err := AsignarTarea(id, &asignadoID)
		if err != nil {
			return nil, err
		}
		return map[string]interface{}{"ok": true, "id": t.ID, "asignado_id": asignadoID}, nil

	case "adjuntar_documento_tarea":
		c, err := AdjuntarDocumentoTarea(chatID, uint(getInt("id", 0)), getStr("comentario"))
		if err != nil {
			return nil, err
		}
		return map[string]interface{}{"ok": true, "comentario_id": c.ID, "tarea_id": c.TareaID}, nil

	case "actualizar_estado_tarea":
		id := uint(getInt("id", 0))
		estado := getStr("estado")
		if id == 0 || estado == "" {
			return nil, fmt.Errorf("id y estado requeridos")
		}
		if err := models.CambiarEstadoTarea(id, estado); err != nil {
			return nil, err
		}
		return map[string]interface{}{"ok": true, "id": id, "estado": estado}, nil

	// ── Servidores ─────────────────────────────────────────────────────────
	case "listar_servidores":
		search := getStr("search")
		items, _, err := models.GetAllServidores(50, 0, search)
		if err != nil {
			return nil, err
		}
		return items, nil

	// ── Monitor URLs ───────────────────────────────────────────────────────
	case "listar_monitores":
		items, err := models.GetAllUrlMonitors()
		if err != nil {
			return nil, err
		}
		return items, nil

	default:
		return nil, fmt.Errorf("herramienta desconocida: %s", name)
	}
}

// ─── Coolify helpers internos ─────────────────────────────────────────────────

func coolifyCall(method, endpoint string, body []byte, cfg *models.CoolifyConfig) (interface{}, error) {
	base := strings.TrimRight(cfg.BaseURL, "/")
	url := base + "/api/v1" + endpoint

	var reqBody io.Reader
	if body != nil {
		reqBody = bytes.NewReader(body)
	}
	req, err := http.NewRequest(method, url, reqBody)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+cfg.ApiToken)
	req.Header.Set("Accept", "application/json")
	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}

	client := &http.Client{Timeout: 20 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	raw, _ := io.ReadAll(io.LimitReader(resp.Body, 512*1024))

	// Coolify devuelve el detalle del error en el body; sin revisar el status,
	// un 401/404/500 se reportaría al usuario como si la acción hubiera salido bien.
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		detalle := strings.TrimSpace(string(raw))
		if len(detalle) > 300 {
			detalle = detalle[:300]
		}
		switch resp.StatusCode {
		case http.StatusUnauthorized, http.StatusForbidden:
			return nil, fmt.Errorf("Coolify rechazó la petición (%d): el API token de la instancia es inválido o no tiene permisos", resp.StatusCode)
		case http.StatusNotFound:
			return nil, fmt.Errorf("Coolify no encontró el recurso (404) en %s — revisa el uuid", endpoint)
		default:
			return nil, fmt.Errorf("Coolify respondió %d: %s", resp.StatusCode, detalle)
		}
	}

	var result interface{}
	if err := json.Unmarshal(raw, &result); err != nil {
		return map[string]string{"raw": string(raw)}, nil
	}
	return result, nil
}

// coolifyConfigDe resuelve la instancia de Coolify a usar. Si no se indicó
// config_id y solo hay una instancia activa configurada, la toma por defecto
// para que el usuario no tenga que saberse los IDs de memoria.
func coolifyConfigDe(configIDInt int) (*models.CoolifyConfig, error) {
	if configIDInt > 0 {
		cfg, err := models.GetCoolifyConfigByID(uint(configIDInt))
		if err != nil {
			return nil, fmt.Errorf("instancia Coolify #%d no encontrada", configIDInt)
		}
		return cfg, nil
	}
	cfgs, err := models.GetAllCoolifyConfigs()
	if err != nil {
		return nil, err
	}
	var activas []models.CoolifyConfig
	for _, c := range cfgs {
		if c.Activo {
			activas = append(activas, c)
		}
	}
	if len(activas) == 1 {
		return &activas[0], nil
	}
	if len(activas) == 0 {
		return nil, fmt.Errorf("no hay instancias de Coolify activas configuradas")
	}
	return nil, fmt.Errorf("hay varias instancias de Coolify: indica cuál usar con config_id (consulta coolify_instancias)")
}

// coolifyBasePorTipo mapea el tipo de recurso al prefijo de la API de Coolify.
func coolifyBasePorTipo(tipo string) (string, bool) {
	base, ok := map[string]string{
		"app":        "/applications",
		"servicio":   "/services",
		"base_datos": "/databases",
		"servidor":   "/servers",
		"proyecto":   "/projects",
	}[tipo]
	return base, ok
}

// ─── Tipos Anthropic ─────────────────────────────────────────────────────────

type anthropicTool struct {
	Name        string         `json:"name"`
	Description string         `json:"description"`
	InputSchema agentToolParam `json:"input_schema"`
}

type anthropicContentBlock struct {
	Type      string                `json:"type"`
	Text      string                `json:"text,omitempty"`
	ID        string                `json:"id,omitempty"`
	Name      string                `json:"name,omitempty"`
	Input     json.RawMessage       `json:"input,omitempty"`
	ToolUseID string                `json:"tool_use_id,omitempty"`
	Content   string                `json:"content,omitempty"`
	Source    *anthropicBlockSource `json:"source,omitempty"`
}

// anthropicBlockSource es el contenido en base64 de una imagen o PDF adjunto,
// para que Claude lea el archivo real en vez de solo un aviso en texto.
type anthropicBlockSource struct {
	Type      string `json:"type"` // "base64"
	MediaType string `json:"media_type"`
	Data      string `json:"data"`
}

type anthropicMsg struct {
	Role    string      `json:"role"`
	Content interface{} `json:"content"` // string o []anthropicContentBlock
}

type anthropicReq struct {
	Model     string          `json:"model"`
	MaxTokens int             `json:"max_tokens"`
	System    string          `json:"system,omitempty"`
	Messages  []anthropicMsg  `json:"messages"`
	Tools     []anthropicTool `json:"tools,omitempty"`
}

type anthropicResp struct {
	Content    []anthropicContentBlock `json:"content"`
	StopReason string                  `json:"stop_reason"`
	Error      *struct {
		Message string `json:"message"`
		Type    string `json:"type"`
	} `json:"error"`
}

// ─── Llamada al AI con function calling ──────────────────────────────────────

// callAI despacha al provider correcto (Anthropic o OpenAI-compatible).
func callAI(ai *models.AiConfig, messages []agentMessage, tools []agentTool) (*agentMessage, error) {
	if strings.ToLower(ai.Provider) == "anthropic" {
		return callAnthropicAI(ai, messages, tools)
	}
	return callOpenAICompatibleAI(ai, messages, tools)
}

// callOpenAICompatibleAI usa el formato de OpenAI (también vale para qwen, groq, deepseek, etc.)
func callOpenAICompatibleAI(ai *models.AiConfig, messages []agentMessage, tools []agentTool) (*agentMessage, error) {
	baseURL := ai.BaseURL
	if baseURL == "" {
		baseURL = ProviderDefaultURL(ai.Provider)
	}
	baseURL = strings.TrimRight(baseURL, "/")

	reqBody := agentChatReq{
		Model:      ai.ModelName,
		Messages:   messages,
		Tools:      tools,
		ToolChoice: "auto",
		MaxTokens:  4096,
	}

	payload, _ := json.Marshal(reqBody)
	req, err := http.NewRequest("POST", baseURL+"/chat/completions", bytes.NewReader(payload))
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+ai.ClaveEnClaro())
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: 120 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))

	var chatResp agentChatResp
	if err := json.Unmarshal(raw, &chatResp); err != nil {
		return nil, fmt.Errorf("respuesta inesperada del AI: %s", string(raw[:min(200, len(raw))]))
	}
	if chatResp.Error != nil {
		return nil, fmt.Errorf("error del AI: %s", chatResp.Error.Message)
	}
	if len(chatResp.Choices) == 0 {
		return nil, fmt.Errorf("el AI no devolvió respuesta")
	}
	msg := chatResp.Choices[0].Message
	return &msg, nil
}

// callAnthropicAI llama a la API nativa de Anthropic con tool use.
func callAnthropicAI(ai *models.AiConfig, messages []agentMessage, tools []agentTool) (*agentMessage, error) {
	// Convertir herramientas al formato Anthropic
	anthropicTools := make([]anthropicTool, len(tools))
	for i, t := range tools {
		anthropicTools[i] = anthropicTool{
			Name:        t.Function.Name,
			Description: t.Function.Description,
			InputSchema: t.Function.Parameters,
		}
	}

	// Extraer system prompt y convertir mensajes
	var systemPrompt string
	var anthropicMsgs []anthropicMsg

	for _, m := range messages {
		switch m.Role {
		case "system":
			if s, ok := m.Content.(string); ok {
				systemPrompt = s
			}

		case "user":
			content := ""
			if s, ok := m.Content.(string); ok {
				content = s
			}
			// Si el último mensaje ya es user, agregar tool_result como bloque adicional
			if len(anthropicMsgs) > 0 && anthropicMsgs[len(anthropicMsgs)-1].Role == "user" {
				last := anthropicMsgs[len(anthropicMsgs)-1]
				if blocks, ok := last.Content.([]anthropicContentBlock); ok {
					anthropicMsgs[len(anthropicMsgs)-1].Content = append(blocks, anthropicContentBlock{
						Type: "text",
						Text: content,
					})
					continue
				}
			}
			if m.AttachmentBase64 != "" {
				blockType := "image"
				if m.AttachmentMime == "application/pdf" {
					blockType = "document"
				}
				blocks := []anthropicContentBlock{{
					Type: blockType,
					Source: &anthropicBlockSource{
						Type:      "base64",
						MediaType: m.AttachmentMime,
						Data:      m.AttachmentBase64,
					},
				}}
				if content != "" {
					blocks = append(blocks, anthropicContentBlock{Type: "text", Text: content})
				}
				anthropicMsgs = append(anthropicMsgs, anthropicMsg{Role: "user", Content: blocks})
				continue
			}
			anthropicMsgs = append(anthropicMsgs, anthropicMsg{Role: "user", Content: content})

		case "assistant":
			if len(m.ToolCalls) > 0 {
				// Convertir tool_calls a bloques tool_use
				var blocks []anthropicContentBlock
				if s, ok := m.Content.(string); ok && s != "" {
					blocks = append(blocks, anthropicContentBlock{Type: "text", Text: s})
				}
				for _, tc := range m.ToolCalls {
					blocks = append(blocks, anthropicContentBlock{
						Type:  "tool_use",
						ID:    tc.ID,
						Name:  tc.Function.Name,
						Input: json.RawMessage(tc.Function.Arguments),
					})
				}
				anthropicMsgs = append(anthropicMsgs, anthropicMsg{Role: "assistant", Content: blocks})
			} else {
				content := ""
				if s, ok := m.Content.(string); ok {
					content = s
				}
				anthropicMsgs = append(anthropicMsgs, anthropicMsg{Role: "assistant", Content: content})
			}

		case "tool":
			// Los resultados de tool deben ir en un mensaje de usuario con tipo tool_result
			block := anthropicContentBlock{
				Type:      "tool_result",
				ToolUseID: m.ToolCallID,
				Content:   fmt.Sprintf("%v", m.Content),
			}
			// Agrupar en el último mensaje user si existe, o crear uno nuevo
			if len(anthropicMsgs) > 0 && anthropicMsgs[len(anthropicMsgs)-1].Role == "user" {
				last := anthropicMsgs[len(anthropicMsgs)-1]
				switch c := last.Content.(type) {
				case []anthropicContentBlock:
					anthropicMsgs[len(anthropicMsgs)-1].Content = append(c, block)
				default:
					anthropicMsgs[len(anthropicMsgs)-1].Content = []anthropicContentBlock{block}
				}
			} else {
				anthropicMsgs = append(anthropicMsgs, anthropicMsg{
					Role:    "user",
					Content: []anthropicContentBlock{block},
				})
			}
		}
	}

	reqBody := anthropicReq{
		Model:     ai.ModelName,
		MaxTokens: 4096,
		System:    systemPrompt,
		Messages:  anthropicMsgs,
		Tools:     anthropicTools,
	}

	payload, _ := json.Marshal(reqBody)
	req, err := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", bytes.NewReader(payload))
	if err != nil {
		return nil, err
	}
	req.Header.Set("x-api-key", ai.ClaveEnClaro())
	req.Header.Set("anthropic-version", "2023-06-01")
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: 120 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))

	var anthropicRsp anthropicResp
	if err := json.Unmarshal(raw, &anthropicRsp); err != nil {
		return nil, fmt.Errorf("respuesta inesperada de Anthropic: %s", string(raw[:min(200, len(raw))]))
	}
	if anthropicRsp.Error != nil {
		return nil, fmt.Errorf("error de Anthropic: %s", anthropicRsp.Error.Message)
	}

	// Convertir respuesta Anthropic → agentMessage (formato interno OpenAI)
	result := &agentMessage{Role: "assistant"}
	var textParts []string
	var toolCalls []agentToolCall

	for _, block := range anthropicRsp.Content {
		switch block.Type {
		case "text":
			if block.Text != "" {
				textParts = append(textParts, block.Text)
			}
		case "tool_use":
			inputJSON := "{}"
			if block.Input != nil {
				inputJSON = string(block.Input)
			}
			toolCalls = append(toolCalls, agentToolCall{
				ID:   block.ID,
				Type: "function",
				Function: struct {
					Name      string `json:"name"`
					Arguments string `json:"arguments"`
				}{Name: block.Name, Arguments: inputJSON},
			})
		}
	}

	if len(textParts) > 0 {
		result.Content = strings.Join(textParts, "\n")
	}
	result.ToolCalls = toolCalls
	return result, nil
}

func ProviderDefaultURL(provider string) string {
	switch strings.ToLower(provider) {
	case "openai":
		return "https://api.openai.com/v1"
	case "qwen", "dashscope":
		return "https://dashscope.aliyuncs.com/compatible-mode/v1"
	case "groq":
		return "https://api.groq.com/openai/v1"
	case "deepseek":
		return "https://api.deepseek.com/v1"
	case "gemini", "google":
		// Capa de compatibilidad OpenAI de Gemini — sin esto, "gemini" caía al
		// default (la URL de OpenAI) y fallaba siempre con la API key de Google.
		return "https://generativelanguage.googleapis.com/v1beta/openai"
	default:
		return "https://api.openai.com/v1"
	}
}

func min(a, b int) int {
	if a < b {
		return a
	}
	return b
}

// ─── Sistema prompt del agente ────────────────────────────────────────────────

func agentSystemPrompt() string {
	now := time.Now().Format("2006-01-02 15:04 MST")
	return fmt.Sprintf(`Eres el asistente administrador de U-Site SAS BIC. Fecha y hora actual: %s.

Tienes acceso total al sistema mediante herramientas. Puedes:
- Ver y administrar clientes, contratos, facturación y contabilidad
- Gestionar proyectos, tickets de soporte y tareas del equipo
- Controlar aplicaciones en las instancias de Coolify (deploy, restart, stop, logs)
- Monitorear servidores y URLs

COMPORTAMIENTO:
- Responde siempre en español, de forma clara y concisa
- Usa las herramientas para obtener datos reales antes de responder
- Cuando el usuario pida una acción (deploy, renovar, crear), hazla directamente sin pedir confirmación salvo que sea destructiva
- Para listar datos, muestra los más relevantes en formato legible
- Los valores monetarios son en COP (pesos colombianos)
- Si una herramienta falla, explica el error y sugiere alternativas
- Si el usuario solo saluda o pregunta qué puedes hacer (sin pedir una acción concreta), responde igual que el comando /ayuda: breve, organizado por categoría, sin inventar capacidades que no tienes
- En Coolify: si solo hay una instancia activa se usa automáticamente, no hace falta preguntar por config_id; si hay varias, consulta coolify_instancias y pregunta cuál. Para actuar sobre algo necesitas su UUID: búscalo con coolify_listar por nombre antes de llamar a coolify_accion. Reiniciar/desplegar es rutinario, hazlo directo; detener algo en producción sí confírmalo antes. Si un despliegue falla, revisa coolify_logs y coolify_deployments para explicar la causa concreta
- Cuando el mensaje empiece con "[Documento adjunto recibido: ...]", el usuario acaba de enviar un archivo por Telegram (puede ser cualquier formato: PDF, foto, Word, Excel, etc.). Si es PDF o foto, viene adjunto a este mismo mensaje y puedes leerlo directamente. Si es otro formato (Word, Excel, zip, etc.) no puedes ver su contenido, solo el nombre — de todas formas guárdalo con la herramienta que corresponda usando lo que el usuario haya escrito como contexto, no rechaces el archivo por el formato. Decide primero de qué se trata el archivo antes de guardarlo:
  · Si es una factura → primero determina la dirección leyendo quién emite y quién recibe: si el emisor/vendedor es "U-Site S.A.S BIC" (o similar) y el destinatario es un cliente, es una factura de VENTA → identifica cliente, monto y número, resuelve el cliente_id con listar_clientes, y llama a adjuntar_factura. Si es al revés — un proveedor (AWS, Coolify, un hosting, cualquier tercero) le está facturando A U-Site — es una factura de COMPRA → llama a adjuntar_factura_compra con el nombre de ese proveedor, no busques cliente_id. Ante la duda, mira a nombre de quién sale la plata: si U-Site cobra, es venta; si U-Site paga, es compra
  · Si es un documento de un proyecto (contrato, orden de servicio, entregable, etc.) → identifica a qué proyecto corresponde (usa listar_proyectos si hace falta) y llama a adjuntar_documento_proyecto
  · Si el mensaje habla de una tarea (ej: "asigna esto a Natalia", "esta es la tarea para el diseño") → usa adjuntar_documento_tarea. Si la tarea todavía no existe, créala primero con crear_tarea (con asignado_id si corresponde) y luego llama a adjuntar_documento_tarea con el id que te devolvió; si ya existe, resuélvela con listar_tareas
  Si logras identificar los datos necesarios (del documento o de lo que escribió el usuario), guarda directamente sin pedir confirmación — solo pregunta si de verdad no hay forma de determinar el cliente/proyecto/tarea o falta un dato imprescindible. No uses estas tools si el mensaje no menciona ningún documento adjunto
- Para registrar un proyecto nuevo con sus fases (el usuario puede describirlo en texto o mandarlo en un documento), primero usa crear_proyecto y luego llama a crear_fase_proyecto una vez por cada fase que corresponda — no hace falta preguntar confirmación entre cada fase
- Para asignar una tarea a alguien, resuelve el nombre con listar_usuarios y usa asignado_id en crear_tarea (si es nueva) o asignar_tarea (si ya existe) — la persona asignada recibe una notificación. Si además el usuario mandó un archivo junto con la instrucción de asignar, después de crear/asignar la tarea usa también adjuntar_documento_tarea para guardar ese archivo en la tarea
- Si preguntan por el detalle, la descripción completa o el historial/comentarios de una tarea específica, usa detalle_tarea (no listar_tareas, que solo da un resumen sin eso) — resuelve el id con listar_tareas primero si hace falta

COMANDOS ESPECIALES (el usuario puede escribirlos):
- /reset — olvidar el historial de esta conversación
- /instancias — listar instancias de Coolify
- /ayuda — mostrar qué puedes hacer`, now)
}

// ─── Motor principal del agente ───────────────────────────────────────────────

// ProcessAgentMessage procesa un mensaje de Telegram y retorna la respuesta del agente.
func ProcessAgentMessage(chatID int64, userText string, ai *models.AiConfig) (string, error) {
	// Comandos especiales (sin AI)
	switch strings.TrimSpace(userText) {
	case "/reset":
		if err := models.ClearAgentHistory(chatID); err != nil {
			return "", err
		}
		return "Historial borrado. ¿En qué puedo ayudarte?", nil
	case "/ayuda", "/start":
		return `¡Hola! 👋 Soy el asistente de administración de U-Site S.A.S BIC. Esto es lo que puedo hacer por ti:

✅ Clientes y facturación
Listar y crear clientes · facturas de venta y compra · cuentas por cobrar y pagar

✅ Contratos y cotizaciones
Crear, renovar y gestionar contratos · generar cotizaciones y propuestas técnicas

✅ Proyectos y soporte
Crear proyectos y fases · gestionar tickets · adjuntar documentos (PDF, fotos, Word, Excel, etc.)

✅ Tareas del equipo
Crear y asignar tareas · ver el tablero Kanban

✅ Coolify (deploy y control)
Listar apps, servicios y bases de datos · deploy, reinicio y detención · logs y despliegues

✅ Contabilidad
Dashboard de ingresos/egresos · transacciones · tarifas

✅ Monitoreo
Servidores y URLs vigiladas

¿Qué necesitas? Escríbeme en lenguaje natural, por ejemplo:
  "muéstrame los clientes de esta semana"
  "haz deploy de la app abc123 en coolify 1"
  "¿cuánto ingresamos este mes?"

Comandos: /reset · /instancias · /ayuda`, nil
	case "/instancias":
		cfgs, err := models.GetAllCoolifyConfigs()
		if err != nil {
			return "", err
		}
		if len(cfgs) == 0 {
			return "No hay instancias de Coolify configuradas.", nil
		}
		lines := make([]string, len(cfgs))
		for i, c := range cfgs {
			estado := "✓ activa"
			if !c.Activo {
				estado = "✗ inactiva"
			}
			lines[i] = fmt.Sprintf("• #%d **%s** — %s (%s)", c.ID, c.Nombre, c.BaseURL, estado)
		}
		return "**Instancias de Coolify:**\n" + strings.Join(lines, "\n"), nil
	}

	// Cargar historial (últimos 20 mensajes)
	// Solo cargar user + assistant (texto), sin tool_calls ni tool_results intermedios.
	// Esto mantiene el hilo conversacional sin reenviar JSON de datos que ya fueron procesados.
	history, _ := models.GetAgentHistory(chatID, 10)

	messages := []agentMessage{
		{Role: "system", Content: agentSystemPrompt()},
	}
	for _, h := range history {
		if h.Role != "user" && h.Role != "assistant" {
			continue
		}
		messages = append(messages, agentMessage{Role: h.Role, Content: h.Content})
	}
	userMsg := agentMessage{Role: "user", Content: userText}
	// Si hay un documento/foto pendiente en este chat, se lo pasamos al modelo
	// como archivo real (solo Anthropic lo lee de verdad) en vez de que la IA
	// tenga que adivinar cliente/monto a partir del nombre del archivo. Solo se
	// hace esto para tipos que la API de Claude sabe previsualizar (imagen o
	// PDF): un .docx/.xlsx/etc mandado como bloque "image" lo rechaza la API
	// (400) y tumba toda la respuesta. Para esos, el archivo se queda igual
	// adjunto/guardado (adjuntar_documento_tarea, etc.), solo que la IA no
	// puede "leer" su contenido, solo el nombre.
	if strings.ToLower(ai.Provider) == "anthropic" {
		if att, ok := PeekTelegramAttachment(chatID); ok {
			mime := ResolverMimeType(att.MimeType, att.OriginalName)
			if esMimePrevisualizablePorClaude(mime) {
				if data, err := os.ReadFile(att.Path); err == nil {
					userMsg.AttachmentBase64 = base64.StdEncoding.EncodeToString(data)
					userMsg.AttachmentMime = mime
				} else {
					log.Printf("[AGENT] No se pudo leer el adjunto pendiente (%s): %v", att.Path, err)
				}
			}
		}
	}
	messages = append(messages, userMsg)

	tools := agentTools()

	// Guardar mensaje del usuario en historial
	_ = models.SaveAgentMessage(chatID, "user", userText, "", "")

	// Loop de function calling (máximo 6 rondas)
	var finalResponse string
	for round := 0; round < 6; round++ {
		aiMsg, err := callAI(ai, messages, tools)
		if err != nil {
			log.Printf("[AGENT] Error llamando AI round %d: %v", round, err)
			return "Error al contactar el sistema de IA. Intenta de nuevo.", err
		}

		// Sin tool calls → respuesta final
		if len(aiMsg.ToolCalls) == 0 {
			content := ""
			if s, ok := aiMsg.Content.(string); ok {
				content = s
			}
			finalResponse = content
			_ = models.SaveAgentMessage(chatID, "assistant", content, "", "")
			break
		}

		// Hay tool calls → ejecutar y continuar
		// Agregar mensaje del AI con tool_calls al contexto (solo en memoria, no en BD)
		messages = append(messages, *aiMsg)

		// Ejecutar cada tool call
		for _, tc := range aiMsg.ToolCalls {
			var toolArgs map[string]interface{}
			_ = json.Unmarshal([]byte(tc.Function.Arguments), &toolArgs)

			log.Printf("[AGENT] Ejecutando tool: %s args: %s", tc.Function.Name, tc.Function.Arguments)
			toolResult := executeAgentTool(chatID, tc.Function.Name, toolArgs)

			// Agregar resultado al contexto (solo en memoria, no en BD)
			messages = append(messages, agentMessage{
				Role:       "tool",
				ToolCallID: tc.ID,
				Name:       tc.Function.Name,
				Content:    toolResult,
			})
		}
	}

	if finalResponse == "" {
		finalResponse = "El agente completó las acciones solicitadas."
	}
	return finalResponse, nil
}
