package models

import (
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"net"
	"strings"
	"time"

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

// ApiKey es una credencial para /api/v2, alternativa al ADMIN_API_KEY único de
// entorno (que sigue funcionando como llave maestra para no romper lo que ya
// depende de él). A diferencia de esa llave maestra, cada ApiKey:
//   - Solo funciona desde la IP/CIDR que se le asignó (obligatoria, no opcional
//     — a diferencia del patrón de Pagos Externos, aquí se decidió exigirla
//     siempre porque esta llave puede dar acceso a datos internos, no solo a
//     pedir un cobro).
//   - Solo puede usar los scopes (grupos de endpoints) que se le habilitaron.
//   - Se puede revocar individualmente sin afectar a otras integraciones.
type ApiKey struct {
	gorm.Model
	Nombre       string     `json:"nombre" gorm:"column:nombre;size:150;not null"`
	TokenHash    string     `json:"-" gorm:"column:token_hash;uniqueIndex;size:64"`
	TokenPreview string     `json:"token_preview" gorm:"column:token_preview;size:12"`
	IPPermitida  string     `json:"ip_permitida" gorm:"column:ip_permitida;size:60;not null"` // IP exacta o CIDR, obligatoria
	Scopes       string     `json:"scopes" gorm:"column:scopes;size:300"`                     // comma-separated, ej: "oss,query_runner"
	Activa       bool       `json:"activa" gorm:"column:activa;default:true"`
	CreadoPorID  uint       `json:"creado_por_id" gorm:"column:creado_por_id"`
	UltimoUsoAt  *time.Time `json:"ultimo_uso_at" gorm:"column:ultimo_uso_at"`
	UltimoUsoIP  string     `json:"ultimo_uso_ip" gorm:"column:ultimo_uso_ip;size:60"`
}

func (ApiKey) TableName() string { return "api_keys" }

// GenerarApiKeyToken crea un token aleatorio de 32 bytes y su hash SHA-256,
// mismo esquema que ServicioPagoExterno: el token crudo se devuelve una sola
// vez, en la base solo queda el hash.
func GenerarApiKeyToken() (raw string, hash string, err error) {
	b := make([]byte, 32)
	if _, err := rand.Read(b); err != nil {
		return "", "", fmt.Errorf("no se pudo generar el token: %w", err)
	}
	raw = "sak_" + hex.EncodeToString(b)
	sum := sha256.Sum256([]byte(raw))
	hash = hex.EncodeToString(sum[:])
	return raw, hash, nil
}

func apiKeyTokenPreview(raw string) string {
	if len(raw) <= 8 {
		return raw
	}
	return "..." + raw[len(raw)-6:]
}

func CreateApiKey(k *ApiKey) (tokenPlano string, err error) {
	raw, hash, err := GenerarApiKeyToken()
	if err != nil {
		return "", err
	}
	k.TokenHash = hash
	k.TokenPreview = apiKeyTokenPreview(raw)
	if err := app.Http.Database.DB.Create(k).Error; err != nil {
		return "", err
	}
	return raw, nil
}

func RegenerarApiKeyToken(id uint) (tokenPlano string, err error) {
	raw, hash, err := GenerarApiKeyToken()
	if err != nil {
		return "", err
	}
	result := app.Http.Database.DB.Model(&ApiKey{}).Where("id = ?", id).
		Updates(map[string]interface{}{"token_hash": hash, "token_preview": apiKeyTokenPreview(raw)})
	if result.Error != nil {
		return "", result.Error
	}
	if result.RowsAffected == 0 {
		return "", fmt.Errorf("api key no encontrada")
	}
	return raw, nil
}

func GetAllApiKeys(limit, offset int) ([]ApiKey, int64, error) {
	var items []ApiKey
	var total int64
	db := app.Http.Database.DB.Model(&ApiKey{})
	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
}

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

// FindApiKeyActivaByToken resuelve la llave a partir del token crudo recibido
// en el header Authorization/X-API-Key. Solo hace match si está activa.
func FindApiKeyActivaByToken(rawToken string) (*ApiKey, error) {
	sum := sha256.Sum256([]byte(rawToken))
	hash := hex.EncodeToString(sum[:])
	var k ApiKey
	if err := app.Http.Database.DB.Where("token_hash = ? AND activa = ?", hash, true).First(&k).Error; err != nil {
		return nil, err
	}
	return &k, nil
}

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

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

// RegistrarUsoApiKey deja constancia de la última vez (y desde qué IP) que se
// usó la llave, para poder detectar llaves zombis o uso desde un origen raro.
func RegistrarUsoApiKey(id uint, ip string) {
	now := time.Now()
	app.Http.Database.DB.Model(&ApiKey{}).Where("id = ?", id).
		Updates(map[string]interface{}{"ultimo_uso_at": now, "ultimo_uso_ip": ip})
}

// IPPermitida valida la IP del caller contra el CIDR/IP exacta configurada.
// A diferencia de ServicioPagoExterno, aquí es obligatoria: una ApiKey sin
// IP válida configurada nunca debe dar acceso (fail-closed).
func (k *ApiKey) IPValida(ip string) bool {
	entrada := strings.TrimSpace(k.IPPermitida)
	if entrada == "" {
		return false
	}
	callerIP := net.ParseIP(ip)
	if callerIP == nil {
		return false
	}
	if strings.Contains(entrada, "/") {
		_, red, err := net.ParseCIDR(entrada)
		return err == nil && red.Contains(callerIP)
	}
	permitida := net.ParseIP(entrada)
	return permitida != nil && permitida.Equal(callerIP)
}

// ScopesList devuelve los scopes habilitados para esta llave.
func (k *ApiKey) ScopesList() []string {
	return SplitModulos(k.Scopes)
}

// TieneScope indica si la llave puede usar ese grupo de endpoints.
func (k *ApiKey) TieneScope(scope string) bool {
	for _, s := range k.ScopesList() {
		if s == scope {
			return true
		}
	}
	return false
}
