package services

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"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"`
}

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.", obj(nil, nil)),
		tool("coolify_apps", "Lista las aplicaciones de una instancia Coolify.",
			obj(map[string]agentToolParam{"config_id": num("ID de la instancia Coolify")}, []string{"config_id"})),
		tool("coolify_servicios", "Lista los servicios (stacks) de una instancia Coolify.",
			obj(map[string]agentToolParam{"config_id": num("ID de la instancia Coolify")}, []string{"config_id"})),
		tool("coolify_servidores", "Lista los servidores de una instancia Coolify.",
			obj(map[string]agentToolParam{"config_id": num("ID de la instancia Coolify")}, []string{"config_id"})),
		tool("coolify_deploy", "Despliega una aplicación en Coolify.",
			obj(map[string]agentToolParam{
				"config_id": num("ID de la instancia Coolify"),
				"uuid":      str("UUID de la aplicación"),
				"force":     agentToolParam{Type: "boolean", Description: "Forzar rebuild desde cero"},
			}, []string{"config_id", "uuid"})),
		tool("coolify_restart", "Reinicia una aplicación en Coolify.",
			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_stop", "Detiene una aplicación en Coolify.",
			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_start", "Inicia una aplicación detenida en Coolify.",
			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_logs", "Obtiene los logs de una aplicación en Coolify.",
			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.",
			obj(map[string]agentToolParam{
				"config_id": num("ID de la instancia Coolify"),
				"uuid":      str("UUID de la aplicación"),
			}, []string{"config_id", "uuid"})),

		// ── 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)),

		// ── 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"})),

		// ── 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.",
			obj(map[string]agentToolParam{
				"estado": str("pendiente | en_progreso | completada | cancelada"),
			}, nil)),
		tool("crear_tarea", "Crea una nueva tarea.",
			obj(map[string]agentToolParam{
				"titulo":      str("Título de la tarea"),
				"descripcion": str("Descripción"),
				"estado":      agentToolParam{Type: "string", Enum: []string{"pendiente", "en_progreso"}, Description: "Estado inicial"},
				"prioridad":   agentToolParam{Type: "string", Enum: []string{"baja", "media", "alta"}, Description: "Prioridad"},
			}, []string{"titulo"})),
		tool("actualizar_estado_tarea", "Cambia el estado de una tarea.",
			obj(map[string]agentToolParam{
				"id":     num("ID de la tarea"),
				"estado": agentToolParam{Type: "string", Enum: []string{"pendiente", "en_progreso", "completada", "cancelada"}, Description: "Nuevo estado"},
			}, []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(name string, args map[string]interface{}) string {
	result, err := runTool(name, args)
	if err != nil {
		return fmt.Sprintf(`{"error": %q}`, err.Error())
	}
	b, _ := json.Marshal(result)
	return string(b)
}

func runTool(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_apps", "coolify_servicios", "coolify_servidores",
		"coolify_logs", "coolify_deployments":
		configID := uint(getInt("config_id", 0))
		if configID == 0 {
			return nil, fmt.Errorf("config_id requerido")
		}
		cfg, err := models.GetCoolifyConfigByID(configID)
		if err != nil {
			return nil, fmt.Errorf("instancia Coolify #%d no encontrada", configID)
		}
		endpoint := map[string]string{
			"coolify_apps":       "/applications",
			"coolify_servicios":  "/services",
			"coolify_servidores": "/servers",
		}[name]
		if endpoint == "" {
			uuid := getStr("uuid")
			if uuid == "" {
				return nil, fmt.Errorf("uuid requerido")
			}
			endpoint = map[string]string{
				"coolify_logs":        "/applications/" + uuid + "/logs",
				"coolify_deployments": "/applications/" + uuid + "/deployments",
			}[name]
		}
		return coolifyCall("GET", endpoint, nil, cfg)

	case "coolify_deploy":
		configID := uint(getInt("config_id", 0))
		uuid := getStr("uuid")
		force := getBool("force")
		if configID == 0 || uuid == "" {
			return nil, fmt.Errorf("config_id y uuid requeridos")
		}
		cfg, err := models.GetCoolifyConfigByID(configID)
		if err != nil {
			return nil, err
		}
		ep := fmt.Sprintf("/deploy?uuid=%s&force=%v", uuid, force)
		return coolifyCall("POST", ep, nil, cfg)

	case "coolify_restart":
		return coolifyAppAction(getInt("config_id", 0), getStr("uuid"), "restart")

	case "coolify_stop":
		return coolifyAppAction(getInt("config_id", 0), getStr("uuid"), "stop")

	case "coolify_start":
		return coolifyAppAction(getInt("config_id", 0), getStr("uuid"), "start")

	// ── 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 "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

	// ── 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 "crear_tarea":
		t := &models.Tarea{
			Titulo:      getStr("titulo"),
			Descripcion: getStr("descripcion"),
			Estado:      getStr("estado"),
			Prioridad:   getStr("prioridad"),
		}
		if t.Estado == "" {
			t.Estado = "pendiente"
		}
		if t.Prioridad == "" {
			t.Prioridad = "media"
		}
		if err := models.CreateTarea(t); err != nil {
			return nil, err
		}
		return map[string]interface{}{"ok": true, "id": t.ID, "titulo": t.Titulo}, 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))

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

func coolifyAppAction(configIDInt int, uuid, action string) (interface{}, error) {
	if configIDInt == 0 || uuid == "" {
		return nil, fmt.Errorf("config_id y uuid requeridos")
	}
	cfg, err := models.GetCoolifyConfigByID(uint(configIDInt))
	if err != nil {
		return nil, err
	}
	return coolifyCall("GET", "/applications/"+uuid+"/"+action, nil, cfg)
}

// ─── 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"`
}

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.ApiKey)
	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
				}
			}
			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.ApiKey)
	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"
	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
- Para acciones en Coolify, primero usa coolify_instancias para saber qué IDs hay disponibles si el usuario no lo especifica

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 `Soy el agente administrador de U-Site. Puedo ayudarte con:

• Clientes y contratos
• Contabilidad (ingresos, egresos, cuentas por cobrar/pagar)
• Proyectos y tickets
• Tareas del equipo
• Deploy y gestión de apps en Coolify
• Monitoreo de servidores y URLs

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 <b>%s</b> — %s (%s)", c.ID, c.Nombre, c.BaseURL, estado)
		}
		return "<b>Instancias de Coolify:</b>\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})
	}
	messages = append(messages, agentMessage{Role: "user", Content: userText})

	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(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
}
