package services

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"regexp"
	"strconv"
	"strings"
	"time"

	_ "github.com/go-sql-driver/mysql"
	_ "github.com/lib/pq"
	_ "github.com/mattn/go-sqlite3"
	_ "github.com/microsoft/go-mssqldb"
	goredis "github.com/redis/go-redis/v9"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"

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

// connTimeout es el tiempo máximo para establecer una conexión a la DB externa.
const connTimeout = 8 * time.Second

// QueryResult contiene el resultado de una consulta SQL.
type QueryResult struct {
	Columns      []string         `json:"columns"`
	Rows         []map[string]any `json:"rows"`
	RowCount     int              `json:"row_count"`
	AffectedRows int64            `json:"affected_rows"`
	DurationMs   int64            `json:"duration_ms"`
	IsSelect     bool             `json:"is_select"`
	Error        string           `json:"error,omitempty"`
}

// openDynamicDB abre una conexión a la base de datos indicada por ConxDb.
func openDynamicDB(c models.ConxDb) (*sql.DB, error) {
	driver := strings.ToLower(c.TipoDb.Nombre)
	host := c.HostEfectivo()
	port := c.Puerto
	user := c.Usuario
	pass := c.Password

	var dsn string
	var driverName string

	switch {
	case strings.Contains(driver, "postgres"):
		driverName = "postgres"
		dsn = fmt.Sprintf("host=%s port=%s user=%s password=%s sslmode=disable connect_timeout=8", host, port, user, pass)
	case strings.Contains(driver, "mysql") || strings.Contains(driver, "mariadb"):
		driverName = "mysql"
		dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/?timeout=8s&readTimeout=20s&writeTimeout=20s", user, pass, host, port)
	case strings.Contains(driver, "sqlite"):
		driverName = "sqlite3"
		dsn = host // para sqlite el host es la ruta del archivo
	case strings.Contains(driver, "sqlserver") || strings.Contains(driver, "mssql"):
		driverName = "sqlserver"
		dsn = fmt.Sprintf("sqlserver://%s:%s@%s:%s?dial+timeout=8", user, pass, host, port)
	default:
		return nil, fmt.Errorf("driver no soportado: %s", driver)
	}

	db, err := sql.Open(driverName, dsn)
	if err != nil {
		return nil, err
	}
	db.SetConnMaxLifetime(30 * time.Second)
	db.SetMaxOpenConns(2)
	// Verificar conectividad inmediatamente para fallar rápido en lugar de bloquear al hacer la primera query
	ctx, cancel := context.WithTimeout(context.Background(), connTimeout)
	defer cancel()
	if err := db.PingContext(ctx); err != nil {
		db.Close()
		return nil, fmt.Errorf("no se pudo conectar al servidor de base de datos: %w", err)
	}
	return db, nil
}

// ExecuteSQL ejecuta SQL arbitrario contra la conexión y devuelve QueryResult.
// También guarda en query_history.
func ExecuteSQL(conx models.ConxDb, database, sqlText string, userID ...uint) QueryResult {
	uid := uint(0)
	if len(userID) > 0 {
		uid = userID[0]
	}
	if isMongoDriver(strings.ToLower(conx.TipoDb.Nombre)) {
		return mongoExecuteSQL(conx, database, sqlText, uid)
	}
	if isRedisDriver(strings.ToLower(conx.TipoDb.Nombre)) {
		return redisExecuteCommand(conx, database, sqlText, uid)
	}
	start := time.Now()

	db, err := openDynamicDB(conx)
	if err != nil {
		saveHistory(conx.ID, uid, sqlText, "error", err.Error(), 0, time.Since(start).Milliseconds())
		return QueryResult{Error: err.Error()}
	}
	defer db.Close()

	// Si se especifica una base de datos para seleccionar
	if database != "" {
		driver := strings.ToLower(conx.TipoDb.Nombre)
		if strings.Contains(driver, "postgres") {
			// En postgres se cambia con SET search_path o reconectando con dbname en DSN
			db2, err2 := openDynamicDBWithName(conx, database)
			if err2 == nil {
				db.Close()
				db = db2
			}
		} else {
			if _, err2 := db.Exec("USE " + quoteIdentifier(database, conx.TipoDb.Nombre)); err2 != nil {
				saveHistory(conx.ID, uid, sqlText, "error", err2.Error(), 0, time.Since(start).Milliseconds())
				return QueryResult{Error: err2.Error()}
			}
		}
	}

	trimmed := strings.TrimSpace(sqlText)
	isSelect := isSelectStatement(trimmed)

	var result QueryResult
	result.IsSelect = isSelect

	if isSelect {
		rows, err := db.Query(trimmed)
		if err != nil {
			elapsed := time.Since(start).Milliseconds()
			saveHistory(conx.ID, uid, sqlText, "error", err.Error(), 0, elapsed)
			return QueryResult{Error: err.Error(), IsSelect: true}
		}
		defer rows.Close()

		cols, _ := rows.Columns()
		result.Columns = cols

		for rows.Next() {
			vals := make([]any, len(cols))
			ptrs := make([]any, len(cols))
			for i := range vals {
				ptrs[i] = &vals[i]
			}
			rows.Scan(ptrs...)
			row := make(map[string]any, len(cols))
			for i, col := range cols {
				v := vals[i]
				if b, ok := v.([]byte); ok {
					row[col] = string(b)
				} else {
					row[col] = v
				}
			}
			result.Rows = append(result.Rows, row)
		}
		result.RowCount = len(result.Rows)
	} else {
		driver := strings.ToLower(conx.TipoDb.Nombre)
		// PostgreSQL: cuando se envían múltiples sentencias en un solo Exec, el servidor
		// las envuelve en una transacción implícita. CREATE DATABASE/DROP DATABASE no
		// pueden correr dentro de una transacción, así que dividimos y ejecutamos c/u por separado.
		if strings.Contains(driver, "postgres") {
			stmts := splitPostgresStatements(trimmed)
			if len(stmts) > 1 {
				var totalAffected int64
				for _, stmt := range stmts {
					res, execErr := db.Exec(stmt)
					if execErr != nil {
						elapsed := time.Since(start).Milliseconds()
						preview := stmt
						if len(preview) > 80 {
							preview = preview[:80] + "..."
						}
						saveHistory(conx.ID, uid, sqlText, "error", execErr.Error(), 0, elapsed)
						return QueryResult{Error: fmt.Sprintf("[%s]: %s", preview, execErr.Error())}
					}
					if affected, err2 := res.RowsAffected(); err2 == nil {
						totalAffected += affected
					}
				}
				elapsed := time.Since(start).Milliseconds()
				result.AffectedRows = totalAffected
				result.DurationMs = elapsed
				saveHistory(conx.ID, uid, sqlText, "ok", "", totalAffected, elapsed)
				return result
			}
		}

		res, err := db.Exec(trimmed)
		elapsed := time.Since(start).Milliseconds()
		if err != nil {
			saveHistory(conx.ID, uid, sqlText, "error", err.Error(), 0, elapsed)
			return QueryResult{Error: err.Error()}
		}
		affected, _ := res.RowsAffected()
		result.AffectedRows = affected
		result.DurationMs = elapsed
		saveHistory(conx.ID, uid, sqlText, "ok", "", affected, elapsed)
		return result
	}

	result.DurationMs = time.Since(start).Milliseconds()
	saveHistory(conx.ID, uid, sqlText, "ok", "", int64(result.RowCount), result.DurationMs)
	return result
}

// ListDatabases devuelve la lista de bases de datos del servidor.
func ListDatabases(conx models.ConxDb) ([]string, error) {
	driver := strings.ToLower(conx.TipoDb.Nombre)
	if isMongoDriver(driver) {
		return mongoListDatabases(conx)
	}
	if isRedisDriver(driver) {
		return redisListDatabases(conx)
	}
	db, err := openDynamicDB(conx)
	if err != nil {
		return nil, err
	}
	defer db.Close()

	var query string
	switch {
	case strings.Contains(driver, "postgres"):
		query = "SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname"
	case strings.Contains(driver, "mysql") || strings.Contains(driver, "mariadb"):
		query = "SHOW DATABASES"
	case strings.Contains(driver, "sqlserver") || strings.Contains(driver, "mssql"):
		query = "SELECT name FROM sys.databases ORDER BY name"
	default:
		return []string{"main"}, nil
	}

	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()
	rows, err := db.QueryContext(ctx, query)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var dbs []string
	for rows.Next() {
		var name string
		rows.Scan(&name)
		dbs = append(dbs, name)
	}
	return dbs, nil
}

// ListTables devuelve las tablas de una base de datos.
func ListTables(conx models.ConxDb, database string) ([]string, error) {
	driver := strings.ToLower(conx.TipoDb.Nombre)
	if isMongoDriver(driver) {
		return mongoListCollections(conx, database)
	}
	if isRedisDriver(driver) {
		return redisListKeys(conx, database)
	}

	var db *sql.DB
	var err error

	if strings.Contains(driver, "postgres") {
		db, err = openDynamicDBWithName(conx, database)
	} else {
		db, err = openDynamicDB(conx)
	}
	if err != nil {
		return nil, err
	}
	defer db.Close()

	var query string
	switch {
	case strings.Contains(driver, "postgres"):
		query = "SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY tablename"
	case strings.Contains(driver, "mysql") || strings.Contains(driver, "mariadb"):
		if _, err := db.Exec("USE " + quoteIdentifier(database, conx.TipoDb.Nombre)); err != nil {
			return nil, err
		}
		query = "SHOW TABLES"
	case strings.Contains(driver, "sqlserver") || strings.Contains(driver, "mssql"):
		query = fmt.Sprintf("USE [%s]; SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE' ORDER BY TABLE_NAME", database)
	default:
		query = "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
	}

	rows, err := db.Query(query)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var tables []string
	for rows.Next() {
		var name string
		rows.Scan(&name)
		tables = append(tables, name)
	}
	return tables, nil
}

// ColumnInfo describe una columna de tabla.
type ColumnInfo struct {
	Name     string `json:"name"`
	Type     string `json:"type"`
	Nullable string `json:"nullable"`
	Key      string `json:"key"`
	Default  string `json:"default"`
}

// GetTableColumns devuelve la info de columnas de una tabla.
func GetTableColumns(conx models.ConxDb, database, table string) ([]ColumnInfo, error) {
	driver := strings.ToLower(conx.TipoDb.Nombre)
	db, err := openDynamicDB(conx)
	if err != nil {
		return nil, err
	}
	defer db.Close()

	// Seleccionar base de datos si es necesario
	if database != "" {
		if strings.Contains(driver, "postgres") {
			db2, err2 := openDynamicDBWithName(conx, database)
			if err2 == nil {
				db.Close()
				db = db2
			}
		} else {
			if _, err2 := db.Exec("USE " + quoteIdentifier(database, conx.TipoDb.Nombre)); err2 != nil {
				// Intentar de todas formas
			}
		}
	}

	var query string
	switch {
	case strings.Contains(driver, "postgres"):
		query = fmt.Sprintf(`SELECT column_name, data_type, is_nullable, 
			COALESCE(column_default,'') as column_default,
			'' as column_key
			FROM information_schema.columns 
			WHERE table_name = '%s' AND table_schema = 'public'
			ORDER BY ordinal_position`, table)
	case strings.Contains(driver, "mysql") || strings.Contains(driver, "mariadb"):
		query = fmt.Sprintf("SHOW COLUMNS FROM `%s`", table)
	case strings.Contains(driver, "sqlserver") || strings.Contains(driver, "mssql"):
		query = fmt.Sprintf(`SELECT COLUMN_NAME as column_name, DATA_TYPE as data_type, 
			IS_NULLABLE as is_nullable, COALESCE(COLUMN_DEFAULT,'') as column_default,
			'' as column_key
			FROM INFORMATION_SCHEMA.COLUMNS 
			WHERE TABLE_NAME = '%s' ORDER BY ORDINAL_POSITION`, table)
	default:
		query = fmt.Sprintf("PRAGMA table_info('%s')", table)
	}

	rows, err := db.Query(query)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var cols []ColumnInfo
	driverLower := strings.ToLower(driver)
	isMySQL := strings.Contains(driverLower, "mysql") || strings.Contains(driverLower, "mariadb")

	for rows.Next() {
		var ci ColumnInfo
		if isMySQL {
			var field, colType, null, key, extra string
			var defaultVal *string
			if err := rows.Scan(&field, &colType, &null, &key, &defaultVal, &extra); err != nil {
				continue
			}
			ci.Name = field
			ci.Type = colType
			ci.Nullable = null
			ci.Key = key
			if defaultVal != nil {
				ci.Default = *defaultVal
			}
		} else {
			if err := rows.Scan(&ci.Name, &ci.Type, &ci.Nullable, &ci.Default, &ci.Key); err != nil {
				continue
			}
		}
		cols = append(cols, ci)
	}
	if cols == nil {
		cols = []ColumnInfo{}
	}
	return cols, nil
}

// TestConnection verifica si la conexión es válida.
func TestDBConnection(conx models.ConxDb) error {
	if isMongoDriver(strings.ToLower(conx.TipoDb.Nombre)) {
		return mongoTestConnection(conx)
	}
	if isRedisDriver(strings.ToLower(conx.TipoDb.Nombre)) {
		return redisTestConnection(conx)
	}
	db, err := openDynamicDB(conx)
	if err != nil {
		return err
	}
	defer db.Close()
	return db.Ping()
}

// ── helpers ──────────────────────────────────────────────────────────────────

func openDynamicDBWithName(c models.ConxDb, dbName string) (*sql.DB, error) {
	host := c.HostEfectivo()
	port := c.Puerto
	user := c.Usuario
	pass := c.Password
	driver := strings.ToLower(c.TipoDb.Nombre)

	var dsn string
	switch {
	case strings.Contains(driver, "postgres"):
		dsn = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable connect_timeout=8", host, port, user, pass, dbName)
		db, err := sql.Open("postgres", dsn)
		if err != nil {
			return nil, err
		}
		db.SetConnMaxLifetime(30 * time.Second)
		db.SetMaxOpenConns(2)
		ctx, cancel := context.WithTimeout(context.Background(), connTimeout)
		defer cancel()
		if err := db.PingContext(ctx); err != nil {
			db.Close()
			return nil, fmt.Errorf("no se pudo conectar a la base de datos '%s': %w", dbName, err)
		}
		return db, nil
	default:
		return openDynamicDB(c)
	}
}

// splitPostgresStatements divide SQL multi-sentencia en sentencias individuales
// respetando dollar-quoting ($$ ... $$), strings con comillas simples y comentarios.
// Esto permite ejecutar cada sentencia por separado evitando que PostgreSQL
// envuelva múltiples statements en una transacción implícita (que bloquea CREATE DATABASE).
func splitPostgresStatements(sqlText string) []string {
	var stmts []string
	var cur strings.Builder
	i, n := 0, len(sqlText)

	for i < n {
		ch := sqlText[i]

		// Dollar-quoting: $tag$ ... $tag$ (tag puede ser vacío: $$)
		if ch == '$' {
			j := i + 1
			for j < n && sqlText[j] != '$' && sqlText[j] != '\n' {
				j++
			}
			if j < n && sqlText[j] == '$' {
				tag := sqlText[i : j+1] // e.g. "$$" or "$func$"
				cur.WriteString(tag)
				i = j + 1
				closeIdx := strings.Index(sqlText[i:], tag)
				if closeIdx >= 0 {
					cur.WriteString(sqlText[i : i+closeIdx+len(tag)])
					i = i + closeIdx + len(tag)
				} else {
					cur.WriteString(sqlText[i:])
					i = n
				}
				continue
			}
		}

		// String con comillas simples
		if ch == '\'' {
			cur.WriteByte(ch)
			i++
			for i < n {
				c := sqlText[i]
				cur.WriteByte(c)
				i++
				if c == '\'' {
					if i < n && sqlText[i] == '\'' {
						cur.WriteByte(sqlText[i])
						i++
					} else {
						break
					}
				}
			}
			continue
		}

		// Comentario de línea (--)
		if ch == '-' && i+1 < n && sqlText[i+1] == '-' {
			cur.WriteByte(ch)
			i++
			for i < n && sqlText[i] != '\n' {
				cur.WriteByte(sqlText[i])
				i++
			}
			continue
		}

		// Comentario de bloque /* ... */
		if ch == '/' && i+1 < n && sqlText[i+1] == '*' {
			cur.WriteString("/*")
			i += 2
			for i < n {
				if sqlText[i] == '*' && i+1 < n && sqlText[i+1] == '/' {
					cur.WriteString("*/")
					i += 2
					break
				}
				cur.WriteByte(sqlText[i])
				i++
			}
			continue
		}

		// Punto y coma: fin de sentencia
		if ch == ';' {
			if stmt := strings.TrimSpace(cur.String()); stmt != "" {
				stmts = append(stmts, stmt)
			}
			cur.Reset()
			i++
			continue
		}

		cur.WriteByte(ch)
		i++
	}

	if stmt := strings.TrimSpace(cur.String()); stmt != "" {
		stmts = append(stmts, stmt)
	}
	return stmts
}

func isSelectStatement(sql string) bool {
	upper := strings.ToUpper(strings.TrimSpace(sql))
	keywords := []string{"SELECT ", "SHOW ", "DESCRIBE ", "EXPLAIN ", "WITH ", "PRAGMA "}
	for _, kw := range keywords {
		if strings.HasPrefix(upper, kw) {
			return true
		}
	}
	return false
}

func quoteIdentifier(name, driver string) string {
	d := strings.ToLower(driver)
	if strings.Contains(d, "postgres") {
		return `"` + strings.ReplaceAll(name, `"`, `""`) + `"`
	}
	return "`" + strings.ReplaceAll(name, "`", "``") + "`"
}

func saveHistory(conxID, userID uint, sqlText, status, errMsg string, rows, durationMs int64) {
	models.SaveQueryHistory(models.QueryHistory{
		ConxDbID:   conxID,
		UserID:     userID,
		SQL:        sqlText,
		Status:     status,
		ErrorMsg:   errMsg,
		RowsAffect: rows,
		DurationMs: durationMs,
		ExecutedAt: time.Now(),
	})
}

// ── Redis ─────────────────────────────────────────────────────────────────────

func isRedisDriver(driver string) bool {
	return strings.Contains(driver, "redis") || strings.Contains(driver, "valkey")
}

func redisConnect(c models.ConxDb, dbIndex int) (*goredis.Client, error) {
	host := c.HostEfectivo()
	port := c.Puerto
	pass := c.Password
	if port == "" {
		port = "6379"
	}
	client := goredis.NewClient(&goredis.Options{
		Addr:         fmt.Sprintf("%s:%s", host, port),
		Password:     pass,
		DB:           dbIndex,
		DialTimeout:  8 * time.Second,
		ReadTimeout:  15 * time.Second,
		WriteTimeout: 15 * time.Second,
	})
	ctx, cancel := context.WithTimeout(context.Background(), connTimeout)
	defer cancel()
	if err := client.Ping(ctx).Err(); err != nil {
		client.Close()
		return nil, fmt.Errorf("no se pudo conectar a Redis: %w", err)
	}
	return client, nil
}

func redisTestConnection(c models.ConxDb) error {
	client, err := redisConnect(c, 0)
	if err != nil {
		return err
	}
	client.Close()
	return nil
}

// redisListDatabases devuelve db0..dbN según CONFIG GET databases.
func redisListDatabases(c models.ConxDb) ([]string, error) {
	client, err := redisConnect(c, 0)
	if err != nil {
		return nil, err
	}
	defer client.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	total := 16 // default
	vals, err2 := client.ConfigGet(ctx, "databases").Result()
	if err2 == nil && len(vals) >= 2 {
		if n, parseErr := strconv.Atoi(fmt.Sprint(vals["databases"])); parseErr == nil && n > 0 {
			total = n
		}
	}

	dbs := make([]string, total)
	for i := range dbs {
		dbs[i] = fmt.Sprintf("db%d", i)
	}
	return dbs, nil
}

// redisListKeys devuelve hasta 200 keys del db seleccionado (SCAN con limit).
func redisListKeys(c models.ConxDb, database string) ([]string, error) {
	dbIndex := 0
	if strings.HasPrefix(database, "db") {
		if n, err := strconv.Atoi(database[2:]); err == nil {
			dbIndex = n
		}
	}
	client, err := redisConnect(c, dbIndex)
	if err != nil {
		return nil, err
	}
	defer client.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	var keys []string
	iter := client.Scan(ctx, 0, "*", 200).Iterator()
	for iter.Next(ctx) {
		keys = append(keys, iter.Val())
		if len(keys) >= 200 {
			break
		}
	}
	if err := iter.Err(); err != nil {
		return nil, err
	}
	return keys, nil
}

// redisExecuteCommand parsea y ejecuta un comando Redis.
// Los comandos se escriben como en redis-cli: GET key / SET key value / etc.
// Soporta múltiples líneas: cada línea no vacía es un comando independiente.
func redisExecuteCommand(conx models.ConxDb, database, cmdText string, userID ...uint) QueryResult {
	uid := uint(0)
	if len(userID) > 0 {
		uid = userID[0]
	}
	start := time.Now()

	dbIndex := 0
	if strings.HasPrefix(database, "db") {
		if n, err := strconv.Atoi(database[2:]); err == nil {
			dbIndex = n
		}
	}

	client, err := redisConnect(conx, dbIndex)
	if err != nil {
		saveHistory(conx.ID, uid, cmdText, "error", err.Error(), 0, time.Since(start).Milliseconds())
		return QueryResult{Error: err.Error()}
	}
	defer client.Close()

	// Dividir en líneas, ignorar vacías y comentarios (#)
	var lines []string
	for _, line := range strings.Split(cmdText, "\n") {
		trimmed := strings.TrimSpace(line)
		if trimmed == "" || strings.HasPrefix(trimmed, "#") {
			continue
		}
		lines = append(lines, trimmed)
	}

	if len(lines) == 0 {
		return QueryResult{Error: "comando vacío"}
	}

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	// Si hay múltiples comandos, ejecutarlos en pipeline y devolver tabla de resultados
	if len(lines) > 1 {
		var rows []map[string]any
		for _, line := range lines {
			args := redisParseArgs(line)
			if len(args) == 0 {
				continue
			}
			ifaces := make([]any, len(args))
			for i, a := range args {
				ifaces[i] = a
			}
			val, execErr := client.Do(ctx, ifaces...).Result()
			row := map[string]any{
				"command": line,
				"result":  redisValToString(val),
				"error":   "",
			}
			if execErr != nil && execErr != goredis.Nil {
				row["error"] = execErr.Error()
			}
			rows = append(rows, row)
		}
		elapsed := time.Since(start).Milliseconds()
		result := QueryResult{
			Columns:    []string{"command", "result", "error"},
			Rows:       rows,
			RowCount:   len(rows),
			IsSelect:   true,
			DurationMs: elapsed,
		}
		saveHistory(conx.ID, uid, cmdText, "ok", "", int64(len(rows)), elapsed)
		return result
	}

	// Comando único
	args := redisParseArgs(lines[0])
	if len(args) == 0 {
		return QueryResult{Error: "comando vacío"}
	}
	ifaces := make([]any, len(args))
	for i, a := range args {
		ifaces[i] = a
	}

	val, execErr := client.Do(ctx, ifaces...).Result()
	elapsed := time.Since(start).Milliseconds()

	if execErr != nil && execErr != goredis.Nil {
		saveHistory(conx.ID, uid, cmdText, "error", execErr.Error(), 0, elapsed)
		return QueryResult{Error: execErr.Error(), DurationMs: elapsed}
	}

	result := redisResultToQueryResult(val, lines[0])
	result.DurationMs = elapsed
	saveHistory(conx.ID, uid, cmdText, "ok", "", int64(result.RowCount), elapsed)
	return result
}

// redisResultToQueryResult convierte la respuesta de Redis en QueryResult presentable.
func redisResultToQueryResult(val any, cmd string) QueryResult {
	upper := strings.ToUpper(strings.Fields(cmd)[0])

	switch v := val.(type) {
	case nil:
		return QueryResult{
			IsSelect: true,
			Columns:  []string{"result"},
			Rows:     []map[string]any{{"result": "(nil)"}},
			RowCount: 1,
		}
	case string:
		return QueryResult{
			IsSelect: true,
			Columns:  []string{"result"},
			Rows:     []map[string]any{{"result": v}},
			RowCount: 1,
		}
	case int64:
		label := "result"
		if upper == "DEL" || upper == "EXISTS" || upper == "SREM" || upper == "LREM" {
			label = "affected"
		} else if upper == "TTL" || upper == "PTTL" {
			label = "ttl_seconds"
		} else if upper == "DBSIZE" || upper == "LLEN" || upper == "SCARD" || upper == "ZCARD" || upper == "HLEN" {
			label = "count"
		}
		return QueryResult{
			IsSelect:     false,
			AffectedRows: v,
			Columns:      []string{label},
			Rows:         []map[string]any{{label: v}},
			RowCount:     1,
		}
	case []any:
		// Lista o conjunto de valores
		if upper == "HGETALL" && len(v)%2 == 0 {
			// Alternar field/value → formato tabla
			var rows []map[string]any
			for i := 0; i+1 < len(v); i += 2 {
				rows = append(rows, map[string]any{
					"field": redisValToString(v[i]),
					"value": redisValToString(v[i+1]),
				})
			}
			return QueryResult{IsSelect: true, Columns: []string{"field", "value"}, Rows: rows, RowCount: len(rows)}
		}
		// KEYS, SMEMBERS, LRANGE, etc.
		var rows []map[string]any
		for _, item := range v {
			rows = append(rows, map[string]any{"value": redisValToString(item)})
		}
		return QueryResult{IsSelect: true, Columns: []string{"value"}, Rows: rows, RowCount: len(rows)}
	case map[any]any:
		var rows []map[string]any
		for k, mv := range v {
			rows = append(rows, map[string]any{
				"field": redisValToString(k),
				"value": redisValToString(mv),
			})
		}
		return QueryResult{IsSelect: true, Columns: []string{"field", "value"}, Rows: rows, RowCount: len(rows)}
	default:
		return QueryResult{
			IsSelect: true,
			Columns:  []string{"result"},
			Rows:     []map[string]any{{"result": fmt.Sprintf("%v", val)}},
			RowCount: 1,
		}
	}
}

func redisValToString(v any) string {
	if v == nil {
		return "(nil)"
	}
	return fmt.Sprintf("%v", v)
}

// redisParseArgs divide un comando Redis en tokens respetando comillas.
// Ej: SET mykey "hello world" → ["SET", "mykey", "hello world"]
func redisParseArgs(cmd string) []string {
	var args []string
	var cur strings.Builder
	inQ := false
	qChar := byte(0)

	for i := 0; i < len(cmd); i++ {
		ch := cmd[i]
		if inQ {
			if ch == qChar {
				inQ = false
			} else if ch == '\\' && i+1 < len(cmd) {
				i++
				cur.WriteByte(cmd[i])
			} else {
				cur.WriteByte(ch)
			}
		} else {
			if ch == '"' || ch == '\'' {
				inQ = true
				qChar = ch
			} else if ch == ' ' || ch == '\t' {
				if cur.Len() > 0 {
					args = append(args, cur.String())
					cur.Reset()
				}
			} else {
				cur.WriteByte(ch)
			}
		}
	}
	if cur.Len() > 0 {
		args = append(args, cur.String())
	}
	return args
}

// ── MongoDB ───────────────────────────────────────────────────────────────────

func isMongoDriver(driver string) bool {
	return strings.Contains(driver, "mongo")
}

func mongoURI(c models.ConxDb) string {
	host := c.HostEfectivo()
	port := c.Puerto
	user := c.Usuario
	pass := c.Password
	if user != "" && pass != "" {
		return fmt.Sprintf("mongodb://%s:%s@%s:%s/?directConnection=true&serverSelectionTimeoutMS=8000", user, pass, host, port)
	}
	return fmt.Sprintf("mongodb://%s:%s/?directConnection=true&serverSelectionTimeoutMS=8000", host, port)
}

func mongoConnect(c models.ConxDb) (*mongo.Client, error) {
	ctx, cancel := context.WithTimeout(context.Background(), connTimeout)
	defer cancel()
	client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI(c)))
	if err != nil {
		return nil, err
	}
	if err := client.Ping(ctx, nil); err != nil {
		client.Disconnect(context.Background()) //nolint
		return nil, fmt.Errorf("no se pudo conectar a MongoDB: %w", err)
	}
	return client, nil
}

func mongoTestConnection(c models.ConxDb) error {
	client, err := mongoConnect(c)
	if err != nil {
		return err
	}
	client.Disconnect(context.Background()) //nolint
	return nil
}

func mongoListDatabases(c models.ConxDb) ([]string, error) {
	client, err := mongoConnect(c)
	if err != nil {
		return nil, err
	}
	defer client.Disconnect(context.Background()) //nolint
	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()
	return client.ListDatabaseNames(ctx, bson.M{})
}

func mongoListCollections(c models.ConxDb, database string) ([]string, error) {
	client, err := mongoConnect(c)
	if err != nil {
		return nil, err
	}
	defer client.Disconnect(context.Background()) //nolint
	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()
	return client.Database(database).ListCollectionNames(ctx, bson.M{})
}

var mongoQueryRe = regexp.MustCompile(`(?s)^db\.(\w[\w\d_]*)\((.*)\)\s*$|^db\.(\w[\w\d_]*)\.(\w+)\((.*)\)\s*$`)

func mongoStripComments(text string) string {
	var lines []string
	for _, line := range strings.Split(text, "\n") {
		t := strings.TrimSpace(line)
		if t == "" || strings.HasPrefix(t, "--") || strings.HasPrefix(t, "//") || strings.HasPrefix(t, "#") {
			continue
		}
		lines = append(lines, t)
	}
	return strings.TrimSpace(strings.Join(lines, "\n"))
}

// Regexes para convertir sintaxis shell de Mongo a Extended JSON.
var (
	reNewDateEmpty  = regexp.MustCompile(`\bnew\s+Date\s*\(\s*\)`)
	reNewDateStr    = regexp.MustCompile(`\bnew\s+Date\s*\(\s*["']([^"']+)["']\s*\)`)
	reNewDateMs     = regexp.MustCompile(`\bnew\s+Date\s*\(\s*(\d+)\s*\)`)
	reISODate       = regexp.MustCompile(`\bISODate\s*\(\s*["']([^"']+)["']\s*\)`)
	reISODateEmpty  = regexp.MustCompile(`\bISODate\s*\(\s*\)`)
	reObjectId      = regexp.MustCompile(`\bObjectId\s*\(\s*["']([0-9a-fA-F]{24})["']\s*\)`)
	reNumberInt     = regexp.MustCompile(`\bNumberInt\s*\(\s*(\d+)\s*\)`)
	reNumberLong    = regexp.MustCompile(`\bNumberLong\s*\(\s*(\d+)\s*\)`)
	reNumberDecimal = regexp.MustCompile(`\bNumberDecimal\s*\(\s*["']?([0-9.eE+\-]+)["']?\s*\)`)
	reBinData       = regexp.MustCompile(`\bBinData\s*\(\s*(\d+)\s*,\s*["']([^"']*)["']\s*\)`)
	reTimestamp     = regexp.MustCompile(`\bTimestamp\s*\(\s*(\d+)\s*,\s*(\d+)\s*\)`)
	reMinKey        = regexp.MustCompile(`\bMinKey\b`)
	reMaxKey        = regexp.MustCompile(`\bMaxKey\b`)
	reUndefined     = regexp.MustCompile(`\bundefined\b`)
	reUnquotedKeys  = regexp.MustCompile(`([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)(\s*:)`)
)

// mongoShellToExtJSON convierte sintaxis shell de Mongo a Extended JSON strict
// para que bson.UnmarshalExtJSON pueda parsearlo correctamente.
func mongoShellToExtJSON(s string) string {
	now := time.Now().UTC().Format(time.RFC3339)

	// new Date() sin argumentos → fecha actual
	s = reNewDateEmpty.ReplaceAllStringFunc(s, func(_ string) string {
		return `{"$date":"` + now + `"}`
	})
	// new Date("iso-string")
	s = reNewDateStr.ReplaceAllString(s, `{"$$date":"$1"}`)
	// new Date(milliseconds)
	s = reNewDateMs.ReplaceAllString(s, `{"$$date":{"$$numberLong":"$1"}}`)
	// ISODate("iso-string")
	s = reISODate.ReplaceAllString(s, `{"$$date":"$1"}`)
	// ISODate() sin argumentos → fecha actual
	s = reISODateEmpty.ReplaceAllStringFunc(s, func(_ string) string {
		return `{"$date":"` + now + `"}`
	})
	// ObjectId("hex")
	s = reObjectId.ReplaceAllString(s, `{"$$oid":"$1"}`)
	// NumberInt(n)
	s = reNumberInt.ReplaceAllString(s, `{"$$numberInt":"$1"}`)
	// NumberLong(n)
	s = reNumberLong.ReplaceAllString(s, `{"$$numberLong":"$1"}`)
	// NumberDecimal(n)
	s = reNumberDecimal.ReplaceAllString(s, `{"$$numberDecimal":"$1"}`)
	// BinData(subtype, base64)
	s = reBinData.ReplaceAllString(s, `{"$$binary":{"base64":"$2","subType":"$1"}}`)
	// Timestamp(t, i)
	s = reTimestamp.ReplaceAllString(s, `{"$$timestamp":{"t":$1,"i":$2}}`)
	// MinKey / MaxKey
	s = reMinKey.ReplaceAllString(s, `{"$$minKey":1}`)
	s = reMaxKey.ReplaceAllString(s, `{"$$maxKey":1}`)
	// undefined → null
	s = reUndefined.ReplaceAllString(s, `null`)
	// Comillas simples → dobles (solo en valores string, no dentro de ya-convertidos)
	s = mongoSingleToDoubleQuotes(s)
	// Claves sin comillas → claves con comillas dobles
	s = reUnquotedKeys.ReplaceAllString(s, `$1"$2"$3`)
	return s
}

// mongoSingleToDoubleQuotes convierte 'valor' → "valor" respetando escapes.
func mongoSingleToDoubleQuotes(s string) string {
	var b strings.Builder
	inDouble := false
	inSingle := false
	runes := []rune(s)
	for i := 0; i < len(runes); i++ {
		c := runes[i]
		switch {
		case c == '\\' && (inDouble || inSingle):
			b.WriteRune(c)
			i++
			if i < len(runes) {
				b.WriteRune(runes[i])
			}
		case c == '"' && !inSingle:
			inDouble = !inDouble
			b.WriteRune(c)
		case c == '\'' && !inDouble:
			inSingle = !inSingle
			if inSingle {
				b.WriteRune('"')
			} else {
				b.WriteRune('"')
			}
		default:
			b.WriteRune(c)
		}
	}
	return b.String()
}

func mongoExecuteSQL(conx models.ConxDb, database, queryText string, userID ...uint) QueryResult {
	uid := uint(0)
	if len(userID) > 0 {
		uid = userID[0]
	}
	start := time.Now()
	client, err := mongoConnect(conx)
	if err != nil {
		saveHistory(conx.ID, uid, queryText, "error", err.Error(), 0, time.Since(start).Milliseconds())
		return QueryResult{Error: err.Error()}
	}
	defer client.Disconnect(context.Background()) //nolint
	db := client.Database(database)
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	result, err := mongoRunQuery(ctx, db, strings.TrimSpace(queryText))
	elapsed := time.Since(start).Milliseconds()
	if err != nil {
		saveHistory(conx.ID, uid, queryText, "error", err.Error(), 0, elapsed)
		return QueryResult{Error: err.Error()}
	}
	result.DurationMs = elapsed
	saveHistory(conx.ID, uid, queryText, "ok", "", int64(result.RowCount)+result.AffectedRows, elapsed)
	return *result
}

func mongoRunQuery(ctx context.Context, db *mongo.Database, query string) (*QueryResult, error) {
	// Eliminar comentarios y líneas vacías
	query = mongoStripComments(query)
	// Convertir sintaxis shell de Mongo (new Date(), ObjectId, etc.) a Extended JSON
	query = mongoShellToExtJSON(query)

	m := mongoQueryRe.FindStringSubmatch(query)
	if m == nil {
		return nil, fmt.Errorf("sintaxis no reconocida. Use: db.coleccion.metodo({...})")
	}

	var collName, method, argsRaw string
	// El regex tiene dos alternativas:
	// Alt 1 (m[1],m[2]): db.metodo(args)  → para db.runCommand(...)
	// Alt 2 (m[3],m[4],m[5]): db.coleccion.metodo(args)
	if m[1] != "" {
		// db.runCommand(args)
		collName = m[1]
		method = strings.ToLower(m[1])
		argsRaw = strings.TrimSpace(m[2])
	} else {
		collName = m[3]
		method = strings.ToLower(m[4])
		argsRaw = strings.TrimSpace(m[5])
	}

	if strings.ToLower(collName) == "runcommand" {
		return mongoRunCommand(ctx, db, argsRaw)
	}
	coll := db.Collection(collName)
	args := splitTopLevelArgs(argsRaw)
	switch method {
	case "find":
		return mongoFind(ctx, coll, args, false)
	case "findone":
		return mongoFind(ctx, coll, args, true)
	case "insertone":
		return mongoInsertOne(ctx, coll, args)
	case "insertmany":
		return mongoInsertMany(ctx, coll, args)
	case "updateone":
		return mongoUpdate(ctx, coll, args, false)
	case "updatemany":
		return mongoUpdate(ctx, coll, args, true)
	case "deleteone":
		return mongoDelete(ctx, coll, args, false)
	case "deletemany":
		return mongoDelete(ctx, coll, args, true)
	case "countdocuments":
		return mongoCount(ctx, coll, args)
	case "aggregate":
		return mongoAggregate(ctx, coll, argsRaw)
	case "drop":
		if err := coll.Drop(ctx); err != nil {
			return nil, err
		}
		return &QueryResult{IsSelect: false}, nil
	default:
		return nil, fmt.Errorf("método MongoDB no soportado: %s", m[2])
	}
}

func mongoFind(ctx context.Context, coll *mongo.Collection, args []string, one bool) (*QueryResult, error) {
	var filter bson.M
	if len(args) > 0 && args[0] != "" {
		if err := bson.UnmarshalExtJSON([]byte(args[0]), true, &filter); err != nil {
			return nil, fmt.Errorf("filtro inválido: %w", err)
		}
	} else {
		filter = bson.M{}
	}
	var docs []bson.M
	if one {
		var doc bson.M
		if err := coll.FindOne(ctx, filter).Decode(&doc); err != nil {
			if err == mongo.ErrNoDocuments {
				return &QueryResult{IsSelect: true, Columns: []string{}, Rows: []map[string]any{}}, nil
			}
			return nil, err
		}
		docs = []bson.M{doc}
	} else {
		cursor, err := coll.Find(ctx, filter)
		if err != nil {
			return nil, err
		}
		defer cursor.Close(ctx)
		if err := cursor.All(ctx, &docs); err != nil {
			return nil, err
		}
	}
	return bsonDocsToResult(docs), nil
}

func mongoInsertOne(ctx context.Context, coll *mongo.Collection, args []string) (*QueryResult, error) {
	if len(args) == 0 || args[0] == "" {
		return nil, fmt.Errorf("insertOne requiere un documento")
	}
	var doc bson.M
	if err := bson.UnmarshalExtJSON([]byte(args[0]), true, &doc); err != nil {
		return nil, fmt.Errorf("documento inválido: %w", err)
	}
	res, err := coll.InsertOne(ctx, doc)
	if err != nil {
		return nil, err
	}
	return &QueryResult{
		IsSelect: true, AffectedRows: 1,
		Columns:  []string{"insertedId"},
		Rows:     []map[string]any{{"insertedId": fmt.Sprintf("%v", res.InsertedID)}},
		RowCount: 1,
	}, nil
}

func mongoInsertMany(ctx context.Context, coll *mongo.Collection, args []string) (*QueryResult, error) {
	if len(args) == 0 || args[0] == "" {
		return nil, fmt.Errorf("insertMany requiere un array de documentos")
	}
	var arr bson.A
	if err := bson.UnmarshalExtJSON([]byte(args[0]), true, &arr); err != nil {
		return nil, fmt.Errorf("documentos inválidos: %w", err)
	}
	docs := make([]interface{}, len(arr))
	copy(docs, arr)
	res, err := coll.InsertMany(ctx, docs)
	if err != nil {
		return nil, err
	}
	return &QueryResult{IsSelect: false, AffectedRows: int64(len(res.InsertedIDs))}, nil
}

func mongoUpdate(ctx context.Context, coll *mongo.Collection, args []string, many bool) (*QueryResult, error) {
	if len(args) < 2 {
		return nil, fmt.Errorf("update requiere filtro y documento de actualización")
	}
	var filter, update bson.M
	if err := bson.UnmarshalExtJSON([]byte(args[0]), true, &filter); err != nil {
		return nil, fmt.Errorf("filtro inválido: %w", err)
	}
	if err := bson.UnmarshalExtJSON([]byte(args[1]), true, &update); err != nil {
		return nil, fmt.Errorf("update inválido: %w", err)
	}
	var affected int64
	if many {
		res, err := coll.UpdateMany(ctx, filter, update)
		if err != nil {
			return nil, err
		}
		affected = res.ModifiedCount
	} else {
		res, err := coll.UpdateOne(ctx, filter, update)
		if err != nil {
			return nil, err
		}
		affected = res.ModifiedCount
	}
	return &QueryResult{IsSelect: false, AffectedRows: affected}, nil
}

func mongoDelete(ctx context.Context, coll *mongo.Collection, args []string, many bool) (*QueryResult, error) {
	var filter bson.M
	if len(args) > 0 && args[0] != "" {
		if err := bson.UnmarshalExtJSON([]byte(args[0]), true, &filter); err != nil {
			return nil, fmt.Errorf("filtro inválido: %w", err)
		}
	} else {
		filter = bson.M{}
	}
	var deleted int64
	if many {
		res, err := coll.DeleteMany(ctx, filter)
		if err != nil {
			return nil, err
		}
		deleted = res.DeletedCount
	} else {
		res, err := coll.DeleteOne(ctx, filter)
		if err != nil {
			return nil, err
		}
		deleted = res.DeletedCount
	}
	return &QueryResult{IsSelect: false, AffectedRows: deleted}, nil
}

func mongoCount(ctx context.Context, coll *mongo.Collection, args []string) (*QueryResult, error) {
	var filter bson.M
	if len(args) > 0 && args[0] != "" {
		if err := bson.UnmarshalExtJSON([]byte(args[0]), true, &filter); err != nil {
			return nil, fmt.Errorf("filtro inválido: %w", err)
		}
	} else {
		filter = bson.M{}
	}
	count, err := coll.CountDocuments(ctx, filter)
	if err != nil {
		return nil, err
	}
	return &QueryResult{
		IsSelect: true,
		Columns:  []string{"count"},
		Rows:     []map[string]any{{"count": count}},
		RowCount: 1,
	}, nil
}

func mongoAggregate(ctx context.Context, coll *mongo.Collection, pipelineStr string) (*QueryResult, error) {
	var pipeline bson.A
	if err := bson.UnmarshalExtJSON([]byte(pipelineStr), true, &pipeline); err != nil {
		return nil, fmt.Errorf("pipeline inválido: %w", err)
	}
	cursor, err := coll.Aggregate(ctx, pipeline)
	if err != nil {
		return nil, err
	}
	defer cursor.Close(ctx)
	var docs []bson.M
	if err := cursor.All(ctx, &docs); err != nil {
		return nil, err
	}
	return bsonDocsToResult(docs), nil
}

func mongoRunCommand(ctx context.Context, db *mongo.Database, cmdStr string) (*QueryResult, error) {
	var cmd bson.D
	if err := bson.UnmarshalExtJSON([]byte(cmdStr), true, &cmd); err != nil {
		return nil, fmt.Errorf("comando inválido: %w", err)
	}
	var result bson.M
	if err := db.RunCommand(ctx, cmd).Decode(&result); err != nil {
		return nil, err
	}
	return bsonDocsToResult([]bson.M{result}), nil
}

func bsonDocsToResult(docs []bson.M) *QueryResult {
	if len(docs) == 0 {
		return &QueryResult{IsSelect: true, Columns: []string{}, Rows: []map[string]any{}}
	}
	keyIdx := make(map[string]int)
	for _, doc := range docs {
		for k := range doc {
			if _, seen := keyIdx[k]; !seen {
				keyIdx[k] = len(keyIdx)
			}
		}
	}
	cols := make([]string, len(keyIdx))
	for k, i := range keyIdx {
		cols[i] = k
	}
	rows := make([]map[string]any, len(docs))
	for i, doc := range docs {
		row := make(map[string]any, len(doc))
		for k, v := range doc {
			switch v.(type) {
			case bson.M, bson.A, bson.D:
				b, _ := json.Marshal(v)
				row[k] = string(b)
			default:
				row[k] = fmt.Sprintf("%v", v)
			}
		}
		rows[i] = row
	}
	return &QueryResult{IsSelect: true, Columns: cols, Rows: rows, RowCount: len(rows)}
}

func splitTopLevelArgs(s string) []string {
	var args []string
	depth := 0
	inStr := false
	start := 0
	s = strings.TrimSpace(s)
	for i := 0; i < len(s); i++ {
		ch := s[i]
		switch ch {
		case '"':
			if i == 0 || s[i-1] != '\\' {
				inStr = !inStr
			}
		case '{', '[', '(':
			if !inStr {
				depth++
			}
		case '}', ']', ')':
			if !inStr {
				depth--
			}
		case ',':
			if !inStr && depth == 0 {
				args = append(args, strings.TrimSpace(s[start:i]))
				start = i + 1
			}
		}
	}
	if last := strings.TrimSpace(s[start:]); last != "" {
		args = append(args, last)
	}
	return args
}
