package models

import (
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"strings"

	"github.com/sujit-baniya/fiber-boilerplate/app"
	"gorm.io/gorm"
)

// UmindTenant representa un sitio/cliente de uMind — dueño de los dominios
// permitidos y el nombre del negocio que se muestra al visitante. Un tenant
// puede tener varios UmindAgente independientes (cada uno con su propia
// config de IA, base de conocimiento, tools y canales); lo que antes vivía
// acá (SiteKey, AiConfigID, Tono, MensajeBienvenida, Color) se movió a
// UmindAgente — ver pkg/models/umind_agente.go y migrations.MigrarUmindAgentes.
type UmindTenant struct {
	gorm.Model
	Nombre             string `json:"nombre" gorm:"column:nombre;size:150;not null"`
	DominiosPermitidos string `json:"dominios_permitidos" gorm:"column:dominios_permitidos;type:text"` // coma-separado, ej: u-site.app,www.u-site.app
	Activo             bool   `json:"activo" gorm:"column:activo;default:true"`
	CreadoPorID        uint   `json:"creado_por_id" gorm:"column:creado_por_id"`
	// ClienteID es lo que le permite a un PortalUser llegar a sus tenants
	// (ver GetClienteIDsForPortalUser). Puntero y sin not null: los tenants
	// creados antes de que existiera el portal quedan sin cliente hasta que
	// el staff los asigne, y el ALTER TABLE no falla sobre datos existentes.
	ClienteID *uint `json:"cliente_id" gorm:"column:cliente_id;index"`
	PlanID    *uint `json:"plan_id" gorm:"column:plan_id;index"`
}

func (UmindTenant) TableName() string { return "umind_tenants" }

// GenerarSiteKey crea un identificador público único para el widget de un
// agente. No se hashea (a diferencia de un token de API) porque no es un
// secreto: viaja en el HTML público del sitio del cliente.
func GenerarSiteKey() (string, error) {
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		return "", fmt.Errorf("no se pudo generar la site_key: %w", err)
	}
	return "umk_" + hex.EncodeToString(b), nil
}

func CreateUmindTenant(t *UmindTenant) error {
	return app.Http.Database.DB.Create(t).Error
}

func GetAllUmindTenants(limit, offset int) ([]UmindTenant, int64, error) {
	var items []UmindTenant
	var total int64
	db := app.Http.Database.DB.Model(&UmindTenant{})
	if err := db.Count(&total).Error; err != nil {
		return nil, 0, err
	}
	if err := db.Order("id DESC").Limit(limit).Offset(offset).Find(&items).Error; err != nil {
		return nil, 0, err
	}
	return items, total, nil
}

// GetUmindTenantsByClientes lista los tenants que pertenecen a alguno de los
// clientes dados — es la base del alcance del portal. Lista vacía devuelve
// vacío sin consultar (fail-closed: un portal user sin clientes no ve nada).
func GetUmindTenantsByClientes(clienteIDs []uint) ([]UmindTenant, error) {
	if len(clienteIDs) == 0 {
		return []UmindTenant{}, nil
	}
	var items []UmindTenant
	err := app.Http.Database.DB.Where("cliente_id IN ?", clienteIDs).Order("id DESC").Find(&items).Error
	return items, err
}

func GetUmindTenantByID(id uint) (*UmindTenant, error) {
	var t UmindTenant
	if err := app.Http.Database.DB.First(&t, id).Error; err != nil {
		return nil, err
	}
	return &t, nil
}

func UpdateUmindTenant(id uint, updates map[string]interface{}) error {
	return app.Http.Database.DB.Model(&UmindTenant{}).Where("id = ?", id).Updates(updates).Error
}

func DeleteUmindTenant(id uint) error {
	return app.Http.Database.DB.Delete(&UmindTenant{}, id).Error
}

// DominioPermitido valida el host de un Origin/Referer contra la lista
// configurada. Admite dominio exacto o comodín "*.dominio.com" para
// subdominios. Lista vacía = no permite nada (fail-closed): un tenant recién
// creado sin dominios configurados no debe poder ser usado desde ningún sitio.
func (t *UmindTenant) DominioPermitido(host string) bool {
	host = strings.ToLower(strings.TrimSpace(host))
	if host == "" {
		return false
	}
	lista := strings.TrimSpace(t.DominiosPermitidos)
	if lista == "" {
		return false
	}
	for _, entrada := range strings.Split(lista, ",") {
		entrada = strings.ToLower(strings.TrimSpace(entrada))
		if entrada == "" {
			continue
		}
		if strings.HasPrefix(entrada, "*.") {
			sufijo := entrada[1:] // ".dominio.com"
			if strings.HasSuffix(host, sufijo) {
				return true
			}
			continue
		}
		if entrada == host {
			return true
		}
	}
	return false
}

// ─── Documentos y chunks de conocimiento ────────────────────────────────────

// UmindDocumento es una fuente de conocimiento de un agente: una URL
// crawleada o un archivo subido. Se trocea en UmindChunk para la búsqueda
// por similitud.
type UmindDocumento struct {
	gorm.Model
	AgenteID    uint   `json:"agente_id" gorm:"column:agente_id;index"`
	Tipo        string `json:"tipo" gorm:"column:tipo;size:20"`                 // url | archivo
	Origen      string `json:"origen" gorm:"column:origen;type:text"`           // la URL crawleada, o el nombre del archivo
	Estado      string `json:"estado" gorm:"column:estado;default:'pendiente'"` // pendiente | procesando | listo | error
	Error       string `json:"error" gorm:"column:error;type:text"`
	TotalChunks int    `json:"total_chunks" gorm:"column:total_chunks;default:0"`
}

func (UmindDocumento) TableName() string { return "umind_documentos" }

func CreateUmindDocumento(d *UmindDocumento) error {
	return app.Http.Database.DB.Create(d).Error
}

func GetUmindDocumentosByAgente(agenteID uint) ([]UmindDocumento, error) {
	var items []UmindDocumento
	err := app.Http.Database.DB.Where("agente_id = ?", agenteID).Order("id DESC").Find(&items).Error
	return items, err
}

func GetUmindDocumentoByID(id uint) (*UmindDocumento, error) {
	var d UmindDocumento
	if err := app.Http.Database.DB.First(&d, id).Error; err != nil {
		return nil, err
	}
	return &d, nil
}

func UpdateUmindDocumentoEstado(id uint, estado, errMsg string, totalChunks int) error {
	return app.Http.Database.DB.Model(&UmindDocumento{}).Where("id = ?", id).Updates(map[string]interface{}{
		"estado":       estado,
		"error":        errMsg,
		"total_chunks": totalChunks,
	}).Error
}

func DeleteUmindDocumento(id uint) error {
	if err := app.Http.Database.DB.Where("documento_id = ?", id).Delete(&UmindChunk{}).Error; err != nil {
		return err
	}
	return app.Http.Database.DB.Delete(&UmindDocumento{}, id).Error
}

// UmindChunk es un fragmento de texto con su embedding, listo para búsqueda
// por similitud. Sin pgvector por ahora: el embedding se guarda como JSON de
// []float32 y la similitud se calcula en memoria (suficiente para el volumen
// de un piloto de un solo agente; si el volumen crece, se migra a pgvector
// sin cambiar la interfaz de búsqueda).
type UmindChunk struct {
	gorm.Model
	AgenteID      uint   `json:"agente_id" gorm:"column:agente_id;index"`
	DocumentoID   uint   `json:"documento_id" gorm:"column:documento_id;index;not null"`
	Contenido     string `json:"contenido" gorm:"column:contenido;type:text;not null"`
	EmbeddingJSON string `json:"-" gorm:"column:embedding_json;type:text"`
}

func (UmindChunk) TableName() string { return "umind_chunks" }

// EmbeddingToJSON / EmbeddingFromJSON convierten el vector a/desde el formato
// de almacenamiento en texto.
func EmbeddingToJSON(v []float32) (string, error) {
	b, err := json.Marshal(v)
	if err != nil {
		return "", err
	}
	return string(b), nil
}

func EmbeddingFromJSON(s string) ([]float32, error) {
	var v []float32
	if err := json.Unmarshal([]byte(s), &v); err != nil {
		return nil, err
	}
	return v, nil
}

func CreateUmindChunks(chunks []UmindChunk) error {
	if len(chunks) == 0 {
		return nil
	}
	return app.Http.Database.DB.CreateInBatches(chunks, 50).Error
}

// GetUmindChunksByAgente retorna todos los chunks del agente, para la
// búsqueda por similitud en memoria.
func GetUmindChunksByAgente(agenteID uint) ([]UmindChunk, error) {
	var items []UmindChunk
	err := app.Http.Database.DB.Where("agente_id = ?", agenteID).Find(&items).Error
	return items, err
}

// ─── Historial de conversación del widget ───────────────────────────────────

// UmindMensaje guarda el historial de conversación del widget, por agente y
// sesión de navegador (no hay usuario autenticado del lado del visitante).
type UmindMensaje struct {
	gorm.Model
	AgenteID  uint   `json:"agente_id" gorm:"column:agente_id;index"`
	SessionID string `json:"session_id" gorm:"column:session_id;index;not null"`
	Role      string `json:"role" gorm:"column:role;not null"` // user | assistant
	Content   string `json:"content" gorm:"column:content;type:text;not null"`
}

func (UmindMensaje) TableName() string { return "umind_mensajes" }

func SaveUmindMensaje(agenteID uint, sessionID, role, content string) error {
	m := &UmindMensaje{AgenteID: agenteID, SessionID: sessionID, Role: role, Content: content}
	return app.Http.Database.DB.Create(m).Error
}

// GetUmindHistorial retorna los últimos n mensajes de una sesión, en orden cronológico.
func GetUmindHistorial(agenteID uint, sessionID string, n int) ([]UmindMensaje, error) {
	var items []UmindMensaje
	err := app.Http.Database.DB.
		Where("agente_id = ? AND session_id = ?", agenteID, sessionID).
		Order("created_at DESC").
		Limit(n).
		Find(&items).Error
	for i, j := 0, len(items)-1; i < j; i, j = i+1, j-1 {
		items[i], items[j] = items[j], items[i]
	}
	return items, err
}

// GetUmindSesiones lista las sesiones de conversación recientes de un agente
// (para el panel admin), con el último mensaje como resumen.
func GetUmindSesiones(agenteID uint, limit int) ([]UmindMensaje, error) {
	var items []UmindMensaje
	err := app.Http.Database.DB.Raw(`
		SELECT * FROM (
			SELECT DISTINCT ON (session_id) *
			FROM umind_mensajes
			WHERE agente_id = ? AND deleted_at IS NULL
			ORDER BY session_id, created_at DESC
		) ultimos
		ORDER BY created_at DESC
		LIMIT ?
	`, agenteID, limit).Scan(&items).Error
	return items, err
}
