package controllers

import (
	"bytes"
	"encoding/csv"
	"encoding/json"
	"fmt"
	"log"
	"math"
	"net/http"
	"strconv"
	"strings"
	"time"

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

// QueryRunnerPage renderiza la vista del editor SQL.
// GET /app/query-runner?conx_db_id=1
func QueryRunnerPage(c *fiber.Ctx) error {
	conxIDStr := c.Query("conx_db_id", "")
	var conx models.ConxDb
	if conxIDStr != "" {
		id, _ := strconv.ParseUint(conxIDStr, 10, 32)
		app.Http.Database.DB.Preload("TipoDb").Preload("Servidor").First(&conx, id)
	}

	data := fiber.Map{
		"user":       c.Locals("user").(map[string]interface{}),
		"modules":    c.Locals("userModules"),
		"conx_db_id": conxIDStr,
		"conx":       conx,
	}
	if err := c.Render("query_runner", data, "layouts/main"); err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
			"error": err.Error(),
		})
	}
	return nil
}

// GetConxDbList devuelve las conexiones DB disponibles (según el rol del usuario).
// Para API calls (Hermes) sin sesión retorna todas.
func GetConxDbList(c *fiber.Ctx) error {
	user, err := auth.User(c)
	if err != nil || user == nil {
		var all []models.ConxDb
		app.Http.Database.DB.Preload("TipoDb").Preload("Servidor").Find(&all)
		return c.JSON(fiber.Map{"data": all})
	}

	var items []models.ConxDb
	if user.IsAdmin {
		app.Http.Database.DB.Preload("TipoDb").Preload("Servidor").Find(&items)
	} else {
		ids := make([]uint, 0)
		for _, db := range user.Role.ConxDBs {
			ids = append(ids, db.ID)
		}
		if len(ids) == 0 {
			return c.JSON(fiber.Map{"data": []models.ConxDb{}})
		}
		app.Http.Database.DB.Preload("TipoDb").Preload("Servidor").Where("id IN ?", ids).Find(&items)
	}
	return c.JSON(fiber.Map{"data": items})
}

// GetDatabases lista las bases de datos de una conexión.
// GET /app/query-runner/databases?conx_db_id=1
func GetDatabases(c *fiber.Ctx) error {
	conx, err := loadConxDb(c.Query("conx_db_id", "0"))
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
	}
	dbs, err := services.ListDatabases(conx)
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
	}
	return c.JSON(fiber.Map{"data": dbs})
}

// GetTables lista las tablas de una base de datos.
// GET /app/query-runner/tables?conx_db_id=1&db=mydb
func GetTables(c *fiber.Ctx) error {
	conx, err := loadConxDb(c.Query("conx_db_id", "0"))
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
	}
	dbName := c.Query("db", "")
	tables, err := services.ListTables(conx, dbName)
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
	}
	return c.JSON(fiber.Map{"data": tables})
}

// RunQuery ejecuta una consulta SQL.
// POST /app/query-runner/run
// Body: { conx_db_id, database, sql }
func RunQuery(c *fiber.Ctx) error {
	var body struct {
		ConxDbID uint   `json:"conx_db_id"`
		Database string `json:"database"`
		SQL      string `json:"sql"`
	}
	if err := c.BodyParser(&body); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cuerpo inválido"})
	}
	if strings.TrimSpace(body.SQL) == "" {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "La consulta está vacía"})
	}

	conx, err := loadConxDb(strconv.Itoa(int(body.ConxDbID)))
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
	}

	uid := extractUserID(c)
	result := services.ExecuteSQL(conx, body.Database, body.SQL, uid)
	return c.JSON(result)
}

// RunBatchQuery ejecuta múltiples consultas en lote.
// POST /app/query-runner/run-batch
// Body: { conx_db_id, database, sqls: ["...", "..."] }
// También soporta multipart/form-data con file .sql
func RunBatchQuery(c *fiber.Ctx) error {
	uid := extractUserID(c)
	conxDbIDStr := c.FormValue("conx_db_id", c.Query("conx_db_id"))
	database := c.FormValue("database", c.Query("database"))

	// Obtener statements del body o del archivo subido
	var statements []string

	// 1. Intentar leer archivo .sql subido
	if file, err := c.FormFile("file"); err == nil {
		f, err := file.Open()
		if err == nil {
			defer f.Close()
			buf := new(bytes.Buffer)
			buf.ReadFrom(f)
			content := buf.String()
			// Dividir por ;
			for _, stmt := range strings.Split(content, ";") {
				stmt = strings.TrimSpace(stmt)
				if stmt != "" {
					statements = append(statements, stmt)
				}
			}
		}
	}

	// 2. Si no hay archivo, leer del body JSON
	if len(statements) == 0 {
		var body struct {
			ConxDbID uint     `json:"conx_db_id"`
			Database string   `json:"database"`
			SQLs     []string `json:"sqls"`
		}
		if err := c.BodyParser(&body); err == nil {
			conxDbIDStr = strconv.Itoa(int(body.ConxDbID))
			if body.Database != "" {
				database = body.Database
			}
			for _, s := range body.SQLs {
				s = strings.TrimSpace(s)
				if s != "" {
					statements = append(statements, s)
				}
			}
		}
	}

	if len(statements) == 0 {
		return c.Status(400).JSON(fiber.Map{"error": "No hay consultas para ejecutar. Envía sqls[] o un archivo .sql"})
	}

	conx, err := loadConxDb(conxDbIDStr)
	if err != nil {
		return c.Status(400).JSON(fiber.Map{"error": err.Error()})
	}

	type batchResult struct {
		Index    int         `json:"index"`
		SQL      string      `json:"sql"`
		Status   string      `json:"status"`
		Duration string      `json:"duration"`
		Rows     int         `json:"rows"`
		Columns  []string    `json:"columns,omitempty"`
		Data     []fiber.Map `json:"data,omitempty"`
		Error    string      `json:"error,omitempty"`
	}
	results := make([]batchResult, 0, len(statements))

	for i, stmt := range statements {
		r := services.ExecuteSQL(conx, database, stmt, uid)
		br := batchResult{
			Index:    i,
			SQL:      stmt,
			Status:   "ok",
			Duration: fmt.Sprintf("%dms", r.DurationMs),
			Rows:     r.RowCount,
		}
		if r.Error != "" {
			br.Status = "error"
			br.Error = r.Error
		} else {
			br.Columns = r.Columns
			br.Data = make([]fiber.Map, len(r.Rows))
			for j, row := range r.Rows {
				br.Data[j] = fiber.Map(row)
			}
		}
		results = append(results, br)
	}

	return c.JSON(fiber.Map{"results": results, "total": len(results)})
}

// TestConnection verifica que la conexión funciona.
// GET /app/query-runner/test?conx_db_id=1
func TestConnection(c *fiber.Ctx) error {
	conx, err := loadConxDb(c.Query("conx_db_id", "0"))
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
	}
	if err := services.TestDBConnection(conx); err != nil {
		return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"ok": false, "error": err.Error()})
	}
	return c.JSON(fiber.Map{"ok": true})
}

// GetHistory devuelve el historial de una conexión.
// GET /app/query-runner/history?conx_db_id=1&page=1
func GetHistory(c *fiber.Ctx) error {
	conxIDStr := c.Query("conx_db_id", "0")
	conxID, _ := strconv.ParseUint(conxIDStr, 10, 32)
	page, _ := strconv.Atoi(c.Query("page", "1"))
	if page < 1 {
		page = 1
	}
	limit := 50
	offset := (page - 1) * limit

	user, _ := auth.User(c)
	var items []models.QueryHistory
	var total int64
	var err error
	if user != nil && user.IsAdmin {
		items, total, err = models.GetQueryHistoryAdmin(uint(conxID), limit, offset)
	} else {
		uid := uint(0)
		if user != nil {
			uid = user.ID
		}
		items, total, err = models.GetQueryHistory(uint(conxID), uid, limit, offset)
	}
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
	}
	totalPages := int(math.Ceil(float64(total) / float64(limit)))
	return c.JSON(fiber.Map{
		"data":       items,
		"total":      total,
		"totalPages": totalPages,
		"page":       page,
	})
}

// ClearHistory borra el historial de una conexión.
// DELETE /app/query-runner/history?conx_db_id=1
func ClearHistory(c *fiber.Ctx) error {
	conxIDStr := c.Query("conx_db_id", "0")
	conxID, _ := strconv.ParseUint(conxIDStr, 10, 32)

	user, _ := auth.User(c)
	var err error
	if user != nil && user.IsAdmin {
		err = models.DeleteQueryHistoryAdmin(uint(conxID))
	} else {
		uid := uint(0)
		if user != nil {
			uid = user.ID
		}
		err = models.DeleteQueryHistory(uint(conxID), uid)
	}
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
	}
	return c.JSON(fiber.Map{"ok": true})
}

// ExportCSV exporta el resultado de una consulta como CSV.
// POST /app/query-runner/export/csv
func ExportCSV(c *fiber.Ctx) error {
	var body struct {
		ConxDbID uint   `json:"conx_db_id"`
		Database string `json:"database"`
		SQL      string `json:"sql"`
	}
	if err := c.BodyParser(&body); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cuerpo inválido"})
	}
	conx, err := loadConxDb(strconv.Itoa(int(body.ConxDbID)))
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
	}

	uid := extractUserID(c)
	result := services.ExecuteSQL(conx, body.Database, body.SQL, uid)
	if result.Error != "" {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": result.Error})
	}

	var buf bytes.Buffer
	w := csv.NewWriter(&buf)
	w.Write(result.Columns)
	for _, row := range result.Rows {
		rec := make([]string, len(result.Columns))
		for i, col := range result.Columns {
			v := row[col]
			if v == nil {
				rec[i] = ""
			} else {
				rec[i] = fmt.Sprintf("%v", v)
			}
		}
		w.Write(rec)
	}
	w.Flush()

	filename := fmt.Sprintf("query_%s.csv", time.Now().Format("20060102_150405"))
	c.Set("Content-Disposition", "attachment; filename="+filename)
	c.Set("Content-Type", "text/csv; charset=utf-8")
	return c.SendStream(bytes.NewReader(buf.Bytes()), buf.Len())
}

// ExportJSON exporta el resultado como JSON.
// POST /app/query-runner/export/json
func ExportJSON(c *fiber.Ctx) error {
	var body struct {
		ConxDbID uint   `json:"conx_db_id"`
		Database string `json:"database"`
		SQL      string `json:"sql"`
	}
	if err := c.BodyParser(&body); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cuerpo inválido"})
	}
	conx, err := loadConxDb(strconv.Itoa(int(body.ConxDbID)))
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
	}

	uid := extractUserID(c)
	result := services.ExecuteSQL(conx, body.Database, body.SQL, uid)
	if result.Error != "" {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": result.Error})
	}

	data, _ := json.MarshalIndent(result.Rows, "", "  ")
	filename := fmt.Sprintf("query_%s.json", time.Now().Format("20060102_150405"))
	c.Set("Content-Disposition", "attachment; filename="+filename)
	c.Set("Content-Type", "application/json; charset=utf-8")
	return c.SendStream(bytes.NewReader(data), len(data))
}

// GetTableColumnsHandler devuelve info de columnas de una tabla.
// GET /app/query-runner/columns?conx_db_id=1&db=mydb&table=users
func GetTableColumnsHandler(c *fiber.Ctx) error {
	conx, err := loadConxDb(c.Query("conx_db_id", "0"))
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
	}
	dbName := c.Query("db", "")
	table := c.Query("table", "")
	if table == "" {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "table es requerido"})
	}
	cols, err := services.GetTableColumns(conx, dbName, table)
	if err != nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
	}
	return c.JSON(fiber.Map{"data": cols})
}

// UpdateCellHandler actualiza una celda específica.
// POST /app/query-runner/update-cell
// Body: { conx_db_id, database, table, column, pk_column, pk_value, value }
func UpdateCellHandler(c *fiber.Ctx) error {
	var body struct {
		ConxDbID uint   `json:"conx_db_id"`
		Database string `json:"database"`
		Table    string `json:"table"`
		Column   string `json:"column"`
		PkColumn string `json:"pk_column"`
		PkValue  string `json:"pk_value"`
		Value    string `json:"value"`
	}
	if err := c.BodyParser(&body); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cuerpo inválido"})
	}
	if body.Table == "" || body.Column == "" || body.PkColumn == "" || body.PkValue == "" {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "table, column, pk_column y pk_value son requeridos"})
	}

	conx, err := loadConxDb(strconv.Itoa(int(body.ConxDbID)))
	if err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
	}

	driver := strings.ToLower(conx.TipoDb.Nombre)
	if strings.Contains(driver, "mongo") || strings.Contains(driver, "redis") {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Solo soportado para SQL"})
	}

	// Construir la consulta de actualización
	qCol := quoteIdent(body.Column, driver)
	qTable := quoteIdent(body.Table, driver)
	qPkCol := quoteIdent(body.PkColumn, driver)

	// Determinar si el valor debe ir como string o literal
	isNumeric := false
	if _, err := strconv.ParseFloat(body.Value, 64); err == nil {
		isNumeric = true
	}
	isBool := strings.ToLower(body.Value) == "true" || strings.ToLower(body.Value) == "false"
	isNull := strings.ToLower(body.Value) == "null"

	var valueSQL string
	switch {
	case isNull:
		valueSQL = "NULL"
	case isNumeric:
		valueSQL = body.Value
	case isBool:
		valueSQL = body.Value
	default:
		valueSQL = "'" + strings.ReplaceAll(body.Value, "'", "''") + "'"
	}

	// Determinar si pk_value es numérico para no entrecomillarlo
	pkValueSQL := "'" + strings.ReplaceAll(body.PkValue, "'", "''") + "'"
	if _, err := strconv.ParseFloat(body.PkValue, 64); err == nil {
		pkValueSQL = body.PkValue
	}

	sqlText := fmt.Sprintf("UPDATE %s SET %s = %s WHERE %s = %s",
		qTable, qCol, valueSQL, qPkCol, pkValueSQL)

	uid := extractUserID(c)
	result := services.ExecuteSQL(conx, body.Database, sqlText, uid)
	if result.Error != "" {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": result.Error})
	}
	return c.JSON(fiber.Map{"ok": true, "affected": result.AffectedRows})
}

// quoteIdent envuelve un identificador con las comillas adecuadas según el driver.
func quoteIdent(name, driver string) string {
	d := strings.ToLower(driver)
	if strings.Contains(d, "postgres") {
		return `"` + strings.ReplaceAll(name, `"`, `""`) + `"`
	}
	if strings.Contains(d, "sqlserver") || strings.Contains(d, "mssql") {
		return "[" + name + "]"
	}
	return "`" + strings.ReplaceAll(name, "`", "``") + "`"
}

// NL2SQLHandler convierte lenguaje natural a SQL usando IA.
// POST /app/query-runner/nl2sql
// Body: { conx_db_id, database, text, schema }
// schema es un array de { table, columns: [{name, type}] } para dar contexto a la IA.
func NL2SQLHandler(c *fiber.Ctx) error {
	var body struct {
		ConxDbID uint   `json:"conx_db_id"`
		Database string `json:"database"`
		Text     string `json:"text"`
		Schema   []struct {
			Table   string `json:"table"`
			Columns []struct {
				Name string `json:"name"`
				Type string `json:"type"`
			} `json:"columns"`
		} `json:"schema"`
	}
	if err := c.BodyParser(&body); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cuerpo inválido"})
	}
	if strings.TrimSpace(body.Text) == "" {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "El texto está vacío"})
	}

	// Construir representación del esquema para el prompt
	var schemaLines []string
	for _, t := range body.Schema {
		var cols []string
		for _, col := range t.Columns {
			cols = append(cols, col.Name+" ("+col.Type+")")
		}
		if len(cols) > 0 {
			schemaLines = append(schemaLines, fmt.Sprintf("  %s: %s", t.Table, strings.Join(cols, ", ")))
		} else {
			schemaLines = append(schemaLines, "  "+t.Table)
		}
	}
	schemaStr := ""
	if len(schemaLines) > 0 {
		schemaStr = "Esquema disponible:\n" + strings.Join(schemaLines, "\n")
	}

	aiConfig, err := models.GetAiConfigForService("query_runner")
	if err != nil {
		return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
			"error": "No hay configuración de IA activa. Configura una en /app/ai-config",
		})
	}

	prompt := fmt.Sprintf(`Eres un experto en SQL. Convierte la siguiente descripción en lenguaje natural a una consulta SQL válida.

%s

Descripción: %s

Reglas:
- Devuelve SOLO el SQL, sin explicaciones ni bloques de código markdown.
- Usa los nombres de tablas y columnas exactamente como aparecen en el esquema.
- Si la base de datos es PostgreSQL, usa comillas dobles para identificadores.
- Termina siempre con punto y coma.
- Si no hay suficiente información para generar el SQL, devuelve el SQL más razonable posible.

SQL:`, schemaStr, body.Text)

	result := callAISQLSuggestion(aiConfig, prompt, "nl2sql", body.ConxDbID, body.Database)
	if result == nil {
		return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "La IA no pudo generar el SQL"})
	}
	return c.JSON(result)
}

// CorrectQueryHandler usa IA o heurística para corregir/sugerir queries.
// POST /app/query-runner/suggest
// Body: { conx_db_id, database, sql, action: "correct"|"complete"|"optimize" }
func CorrectQueryHandler(c *fiber.Ctx) error {
	var body struct {
		ConxDbID uint   `json:"conx_db_id"`
		Database string `json:"database"`
		SQL      string `json:"sql"`
		Action   string `json:"action"` // "correct" | "complete" | "optimize"
	}
	if err := c.BodyParser(&body); err != nil {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cuerpo inválido"})
	}
	if strings.TrimSpace(body.SQL) == "" {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "La consulta está vacía"})
	}
	if body.Action == "" {
		body.Action = "correct"
	}

	// 1. Si hay una configuración de IA activa para este servicio, usarla
	aiConfig, aiErr := models.GetAiConfigForService("query_runner")
	if aiErr == nil && aiConfig != nil {
		aiResult := callAISQLSuggestion(aiConfig, body.SQL, body.Action, body.ConxDbID, body.Database)
		if aiResult != nil {
			return c.JSON(aiResult)
		}
	}

	// 2. Fallback: corrección heurística local
	suggestion := heuristicSQLFix(body.SQL)
	return c.JSON(fiber.Map{
		"suggestion": suggestion,
		"source":     "local",
	})
}

// heuristicSQLFix aplica correcciones básicas sin IA.
func heuristicSQLFix(sql string) string {
	original := sql

	// Palabras clave de SQL
	// nolint:unused
	keywords := map[string]string{
		"SLECT": "SELECT", "SELCT": "SELECT", "SELCET": "SELECT", "SElECT": "SELECT",
		"SELC": "SELECT", "SELEKT": "SELECT",
		"FORM": "FROM", "FOM": "FROM", "FRO": "FROM",
		"WHER": "WHERE", "WHRE": "WHERE", "WHARE": "WHERE", "WHEERE": "WHERE",
		"UPDTE": "UPDATE", "UPDAT": "UPDATE", "UDATE": "UPDATE", "UPDETA": "UPDATE",
		"DELTE": "DELETE", "DELET": "DELETE", "DELT": "DELETE",
		"INSRT": "INSERT", "INSER": "INSERT", "NSERT": "INSERT",
		"INOT": "INTO", "IINTO": "INTO",
		"VALUS": "VALUES", "VLAUES": "VALUES",
		"GRUP": "GROUP", "GROU": "GROUP", "GRUOP": "GROUP",
		"ORDR": "ORDER", "ORDRE": "ORDER", "ORBER": "ORDER",
		"HAVNG": "HAVING", "HAVIN": "HAVING", "HVING": "HAVING",
		"LIMT": "LIMIT", "LIIMT": "LIMIT",
		"JON":  "JOIN", "JOUN": "JOIN",
		"LEF JOIN": "LEFT JOIN", "LEFTJ OIN": "LEFT JOIN",
		"RIGTH": "RIGHT",
		"CRATE": "CREATE", "CREARE": "CREATE",
		"TABEL": "TABLE", "TBALE": "TABLE",
		"ALTR": "ALTER", "ALTE": "ALTER",
		"DRO":  "DROP", "DROPP": "DROP",
		"IDNEX": "INDEX", "INEX": "INDEX",
		"PRIMRY": "PRIMARY", "PRIMAR": "PRIMARY", "PRMARY": "PRIMARY",
		"FORIGN": "FOREIGN", "FOREIN": "FOREIGN", "FORIEGN": "FOREIGN",
		"REFERNCES": "REFERENCES", "REFERECES": "REFERENCES", "REFRENCES": "REFERENCES",
		"CONSTRINT": "CONSTRAINT", "CONSTRAIN": "CONSTRAINT",
		"TRUNC": "TRUNCATE", "TRUNCAT": "TRUNCATE",
		"TRIGER": "TRIGGER", "TRGIGER": "TRIGGER",
		"FUNCTON": "FUNCTION", "FUNCTIN": "FUNCTION", "FUNTION": "FUNCTION",
		"PROCEDRE": "PROCEDURE", "PROCDURE": "PROCEDURE",
		"BEGN": "BEGIN", "BEGIIN": "BEGIN",
		"COMIT": "COMMIT", "COMIIT": "COMMIT",
		"ROLLBCK": "ROLLBACK", "ROLLBAK": "ROLLBACK", "ROLBACK": "ROLLBACK",
	}

	// Reemplazar palabras mal escritas (case-insensitive)
	words := strings.Fields(sql)
	for i, word := range words {
		upperWord := strings.ToUpper(word)
		if corrected, ok := keywords[upperWord]; ok {
			if upperWord != corrected {
				// Mantener case original si empezaba con mayúscula
				if word != "" && word[0] >= 'A' && word[0] <= 'Z' {
					words[i] = corrected
				} else {
					words[i] = strings.ToLower(corrected)
				}
			}
		}
	}
	fixed := strings.Join(words, " ")

	// Agregar punto y coma si falta
	fixed = strings.TrimSpace(fixed)
	if !strings.HasSuffix(fixed, ";") && !strings.HasSuffix(fixed, "\n") {
		fixed = fixed + ";"
	}

	if fixed != original {
		return fixed
	}
	return ""
}

// callAISQLSuggestion llama a un LLM para ayudar con SQL.
func callAISQLSuggestion(config *models.AiConfig, sql, action string, conxDbID uint, database string) *fiber.Map {
	// Construir prompt según la acción
	var prompt string
	switch action {
	case "correct":
		prompt = fmt.Sprintf(`Eres un experto en SQL. Corrige los errores de esta consulta SQL. Devuelve SOLO el SQL corregido, sin explicaciones.

SQL original:
%s

SQL corregido:`, sql)
	case "complete":
		prompt = fmt.Sprintf(`Eres un experto en SQL. Completa esta consulta SQL. Devuelve SOLO el SQL completo, sin explicaciones.

SQL incompleto:
%s

SQL completo:`, sql)
	case "optimize":
		prompt = fmt.Sprintf(`Eres un experto en optimización de SQL. Optimiza esta consulta añadiendo índices sugeridos, mejorando JOINs y filtrando mejor. Devuelve SOLO el SQL optimizado, sin explicaciones.

SQL original:
%s

SQL optimizado:`, sql)
	case "nl2sql":
		// El prompt ya viene construido desde NL2SQLHandler (incluye esquema + descripción)
		prompt = sql
	default:
		prompt = fmt.Sprintf(`Eres un experto en SQL. Ayuda con esta consulta SQL. Devuelve SOLO el SQL resultante, sin explicaciones.

%s`, sql)
	}

	// Hacer la llamada HTTP al proveedor de IA
	client := &http.Client{Timeout: 60 * time.Second}

	baseURL := config.BaseURL
	if baseURL == "" {
		switch config.Provider {
		case "openai":
			baseURL = "https://api.openai.com/v1"
		case "qwen":
			baseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
		case "anthropic":
			baseURL = "https://api.anthropic.com/v1"
		default:
			log.Printf("[AI_SQL] Provider '%s' requiere BaseURL configurada", config.Provider)
			return nil
		}
	}

	modelName := config.ModelName
	if modelName == "" {
		switch config.Provider {
		case "openai":
			modelName = "gpt-4o-mini"
		case "qwen":
			modelName = "qwen2.5-72b-instruct"
		case "anthropic":
			modelName = "claude-3-haiku-20240307"
		case "ollama":
			modelName = "gemma3:1b"
		default:
			modelName = "gpt-4o-mini"
		}
	}

	requestBody := map[string]interface{}{
		"model": modelName,
		"messages": []map[string]string{
			{"role": "system", "content": "Eres un experto en SQL. Responde SOLO con el SQL, sin explicaciones adicionales."},
			{"role": "user", "content": prompt},
		},
		"temperature": 0.1,
		"max_tokens":  1024,
	}

	jsonBody, _ := json.Marshal(requestBody)

	var url string
	var authHeader string
	switch config.Provider {
	case "anthropic":
		url = baseURL + "/messages"
		authHeader = "x-api-key"
	default:
		url = baseURL + "/chat/completions"
		authHeader = "Authorization"
	}

	req, _ := http.NewRequest("POST", url, bytes.NewReader(jsonBody))
	req.Header.Set("Content-Type", "application/json")
	switch config.Provider {
	case "anthropic":
		req.Header.Set(authHeader, config.ApiKey)
		req.Header.Set("anthropic-version", "2023-06-01")
	case "ollama":
		// Sin auth para red interna; para URL pública el api_key es "usuario:token"
		if config.ApiKey != "" && config.ApiKey != "ollama" {
			req.SetBasicAuth("ollama", config.ApiKey)
		}
	default:
		req.Header.Set(authHeader, "Bearer "+config.ApiKey)
	}

	resp, err := client.Do(req)
	if err != nil {
		log.Printf("[AI_SQL] Error llamando a %s: %v", config.Provider, err)
		return nil
	}
	defer resp.Body.Close()

	var apiResult map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&apiResult); err != nil {
		log.Printf("[AI_SQL] Error decodificando respuesta: %v", err)
		return nil
	}

	// Extraer texto de diferentes formatos de respuesta
	var suggestion string
	if config.Provider == "anthropic" {
		if content, ok := apiResult["content"].([]interface{}); ok && len(content) > 0 {
			if first, ok := content[0].(map[string]interface{}); ok {
				if text, ok := first["text"].(string); ok {
					suggestion = text
				}
			}
		}
	} else {
		if choices, ok := apiResult["choices"].([]interface{}); ok && len(choices) > 0 {
			if first, ok := choices[0].(map[string]interface{}); ok {
				if msg, ok := first["message"].(map[string]interface{}); ok {
					if content, ok := msg["content"].(string); ok {
						suggestion = content
					}
				}
			}
		}
	}

	// Limpiar bloques de código markdown
	suggestion = strings.TrimSpace(suggestion)
	suggestion = strings.TrimPrefix(suggestion, "```sql")
	suggestion = strings.TrimPrefix(suggestion, "```")
	suggestion = strings.TrimSuffix(suggestion, "```")
	suggestion = strings.TrimSpace(suggestion)

	if suggestion == "" {
		return nil
	}

	return &fiber.Map{
		"suggestion": suggestion,
		"source":     config.Provider,
	}
}

// ── helper ───────────────────────────────────────────────────────────────────

func loadConxDb(idStr string) (models.ConxDb, error) {
	id, err := strconv.ParseUint(idStr, 10, 32)
	if err != nil || id == 0 {
		return models.ConxDb{}, fmt.Errorf("conx_db_id inválido")
	}
	var conx models.ConxDb
	if err := app.Http.Database.DB.Preload("TipoDb").Preload("Servidor").First(&conx, id).Error; err != nil {
		return models.ConxDb{}, fmt.Errorf("conexión no encontrada: %w", err)
	}
	return conx, nil
}

func extractUserID(c *fiber.Ctx) uint {
	user, err := auth.User(c)
	if err != nil || user == nil {
		return 0
	}
	return user.ID
}
