package models

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

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

// ServicioPagoExterno es una credencial que le entregamos a una aplicación
// externa para que pueda pedir cobros a través de nuestras pasarelas
// (Bold/dLocal/PayPal) sin darle acceso a nada más del sistema. El token solo
// se muestra en texto plano una vez, al crearlo o regenerarlo: en la base
// solo se guarda su hash (igual que una API key de Stripe/GitHub).
type ServicioPagoExterno struct {
	gorm.Model
	Nombre               string `json:"nombre" gorm:"column:nombre;size:150"`
	TokenHash            string `json:"-" gorm:"column:token_hash;uniqueIndex;size:64"`
	TokenPreview         string `json:"token_preview" gorm:"column:token_preview;size:12"`
	IPsPermitidas        string `json:"ips_permitidas" gorm:"column:ips_permitidas;type:text"`
	PasarelasHabilitadas string `json:"pasarelas_habilitadas" gorm:"column:pasarelas_habilitadas;size:100"`
	NotificarWebhook     bool   `json:"notificar_webhook" gorm:"column:notificar_webhook;default:false"`
	CallbackURL          string `json:"callback_url" gorm:"column:callback_url;type:text"`
	CallbackSecret       string `json:"-" gorm:"column:callback_secret;size:100"`
	NotificarTelegram    bool   `json:"notificar_telegram" gorm:"column:notificar_telegram;default:false"`
	TelegramChatID       string `json:"telegram_chat_id" gorm:"column:telegram_chat_id;size:50"`
	Activo               bool   `json:"activo" gorm:"column:activo;default:true"`
	CreadoPorID          uint   `json:"creado_por_id" gorm:"column:creado_por_id"`
}

func (ServicioPagoExterno) TableName() string { return "servicios_pago_externos" }

// SolicitudPagoExterna es cada cobro individual pedido por una app externa a
// través de un ServicioPagoExterno.
type SolicitudPagoExterna struct {
	gorm.Model
	ServicioID        uint                 `json:"servicio_id" gorm:"column:servicio_id;index"`
	Servicio          *ServicioPagoExterno `json:"servicio,omitempty" gorm:"foreignKey:ServicioID"`
	ReferenciaExterna string               `json:"referencia_externa" gorm:"column:referencia_externa;size:150;index"`
	ReferenciaInterna string               `json:"referencia_interna" gorm:"column:referencia_interna;uniqueIndex;size:60"`
	ClienteID         *uint                `json:"cliente_id" gorm:"column:cliente_id"`
	Cliente           *Cliente             `json:"cliente,omitempty" gorm:"foreignKey:ClienteID"`
	Pasarela          string               `json:"pasarela" gorm:"column:pasarela;size:20"`
	Monto             float64              `json:"monto" gorm:"column:monto"`
	Moneda            string               `json:"moneda" gorm:"column:moneda;size:10"`
	Descripcion       string               `json:"descripcion" gorm:"column:descripcion;type:text"`
	// Estado: pendiente | pagado | fallido | expirado
	Estado            string     `json:"estado" gorm:"column:estado;default:'pendiente';index"`
	EnlacePago        string     `json:"enlace_pago" gorm:"column:enlace_pago;type:text"`
	PasarelaLinkID    string     `json:"-" gorm:"column:pasarela_link_id;size:150"`
	PasarelaTxID      string     `json:"pasarela_tx_id" gorm:"column:pasarela_tx_id;size:150"`
	FechaPago         *time.Time `json:"fecha_pago" gorm:"column:fecha_pago"`
	CallbackEntregado bool       `json:"callback_entregado" gorm:"column:callback_entregado;default:false"`
	CallbackIntentos  int        `json:"callback_intentos" gorm:"column:callback_intentos;default:0"`
	TelegramEntregado bool       `json:"telegram_entregado" gorm:"column:telegram_entregado;default:false"`
}

func (SolicitudPagoExterna) TableName() string { return "solicitudes_pago_externas" }

// ─── Tokens ───────────────────────────────────────────────────────────────────

// GenerarTokenServicioPago crea un token aleatorio de 32 bytes (64 hex chars) y
// su hash SHA-256. El token crudo se devuelve una sola vez: solo el hash se
// guarda en base de datos, así una filtración de la BD no expone credenciales
// utilizables directamente.
func GenerarTokenServicioPago() (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 = "spx_" + hex.EncodeToString(b)
	hash = HashTokenServicioPago(raw)
	return raw, hash, nil
}

func HashTokenServicioPago(raw string) string {
	sum := sha256.Sum256([]byte(raw))
	return hex.EncodeToString(sum[:])
}

// tokenPreview devuelve los últimos caracteres del token para poder
// identificarlo en el panel sin volver a mostrarlo completo.
func tokenPreview(raw string) string {
	if len(raw) <= 8 {
		return raw
	}
	return "..." + raw[len(raw)-6:]
}

// ─── CRUD ServicioPagoExterno ───────────────────────────────────────────────

// CreateServicioPagoExterno genera el token, lo hashea y crea el registro.
// Devuelve el token en texto plano: es la única vez que estará disponible.
func CreateServicioPagoExterno(s *ServicioPagoExterno) (tokenPlano string, err error) {
	raw, hash, err := GenerarTokenServicioPago()
	if err != nil {
		return "", err
	}
	s.TokenHash = hash
	s.TokenPreview = tokenPreview(raw)
	if err := app.Http.Database.DB.Create(s).Error; err != nil {
		return "", err
	}
	return raw, nil
}

// RegenerarTokenServicioPago invalida el token anterior y genera uno nuevo.
func RegenerarTokenServicioPago(id uint) (tokenPlano string, err error) {
	raw, hash, err := GenerarTokenServicioPago()
	if err != nil {
		return "", err
	}
	result := app.Http.Database.DB.Model(&ServicioPagoExterno{}).Where("id = ?", id).
		Updates(map[string]interface{}{"token_hash": hash, "token_preview": tokenPreview(raw)})
	if result.Error != nil {
		return "", result.Error
	}
	if result.RowsAffected == 0 {
		return "", fmt.Errorf("servicio de pago no encontrado")
	}
	return raw, nil
}

func GetAllServiciosPagoExterno(limit, offset int) ([]ServicioPagoExterno, int64, error) {
	var items []ServicioPagoExterno
	var total int64
	db := app.Http.Database.DB.Model(&ServicioPagoExterno{})
	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 GetServicioPagoExternoByID(id uint) (*ServicioPagoExterno, error) {
	var s ServicioPagoExterno
	if err := app.Http.Database.DB.First(&s, id).Error; err != nil {
		return nil, err
	}
	return &s, nil
}

// FindServicioPagoExternoActivoByToken resuelve el servicio a partir del token
// crudo recibido en el header Authorization. Solo hace match si está activo.
func FindServicioPagoExternoActivoByToken(rawToken string) (*ServicioPagoExterno, error) {
	hash := HashTokenServicioPago(rawToken)
	var s ServicioPagoExterno
	if err := app.Http.Database.DB.Where("token_hash = ? AND activo = ?", hash, true).First(&s).Error; err != nil {
		return nil, err
	}
	return &s, nil
}

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

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

// IPPermitida valida la IP del caller contra la lista configurada en el
// servicio (coma-separada, admite IP exacta o CIDR). Lista vacía = sin
// restricción de IP.
func (s *ServicioPagoExterno) IPPermitida(ip string) bool {
	lista := strings.TrimSpace(s.IPsPermitidas)
	if lista == "" {
		return true
	}
	callerIP := net.ParseIP(ip)
	if callerIP == nil {
		return false
	}
	for _, entrada := range strings.Split(lista, ",") {
		entrada = strings.TrimSpace(entrada)
		if entrada == "" {
			continue
		}
		if strings.Contains(entrada, "/") {
			_, red, err := net.ParseCIDR(entrada)
			if err == nil && red.Contains(callerIP) {
				return true
			}
			continue
		}
		if net.ParseIP(entrada) != nil && net.ParseIP(entrada).Equal(callerIP) {
			return true
		}
	}
	return false
}

// PasarelasList devuelve las pasarelas habilitadas para este servicio.
func (s *ServicioPagoExterno) PasarelasList() []string {
	return SplitModulos(s.PasarelasHabilitadas)
}

// PasarelaHabilitada indica si el servicio puede usar esa pasarela.
func (s *ServicioPagoExterno) PasarelaHabilitada(pasarela string) bool {
	for _, p := range s.PasarelasList() {
		if p == pasarela {
			return true
		}
	}
	return false
}

// ─── SolicitudPagoExterna ───────────────────────────────────────────────────

// GenerarReferenciaInterna crea una referencia única con el prefijo "extpay-"
// que los webhooks de las pasarelas usan para distinguir estas solicitudes de
// las de un Contrato (que usan "contrato-{id}").
func GenerarReferenciaInterna() (string, error) {
	b := make([]byte, 8)
	if _, err := rand.Read(b); err != nil {
		return "", fmt.Errorf("no se pudo generar la referencia: %w", err)
	}
	return "extpay-" + hex.EncodeToString(b), nil
}

func CreateSolicitudPagoExterna(s *SolicitudPagoExterna) error {
	return app.Http.Database.DB.Create(s).Error
}

func GetSolicitudPagoExternaByReferenciaInterna(ref string) (*SolicitudPagoExterna, error) {
	var s SolicitudPagoExterna
	if err := app.Http.Database.DB.Preload("Servicio").Preload("Cliente").
		Where("referencia_interna = ?", ref).First(&s).Error; err != nil {
		return nil, err
	}
	return &s, nil
}

func GetAllSolicitudesPagoExterna(limit, offset int, servicioID uint) ([]SolicitudPagoExterna, int64, error) {
	var items []SolicitudPagoExterna
	var total int64
	db := app.Http.Database.DB.Model(&SolicitudPagoExterna{}).Preload("Servicio").Preload("Cliente")
	if servicioID > 0 {
		db = db.Where("servicio_id = ?", servicioID)
	}
	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
}

// MarcarSolicitudPagoExternaPagada transiciona pendiente→pagado de forma
// idempotente (igual patrón que MarcarContratoPagado): si ya estaba pagada,
// no vuelve a disparar la notificación. Devuelve ok=true solo en la
// transición real.
func MarcarSolicitudPagoExternaPagada(referenciaInterna, pasarelaTxID string) (ok bool, solicitud *SolicitudPagoExterna, err error) {
	now := time.Now()
	result := app.Http.Database.DB.Model(&SolicitudPagoExterna{}).
		Where("referencia_interna = ? AND estado = ?", referenciaInterna, "pendiente").
		Updates(map[string]interface{}{
			"estado":         "pagado",
			"fecha_pago":     now,
			"pasarela_tx_id": pasarelaTxID,
		})
	if result.Error != nil {
		return false, nil, result.Error
	}
	s, getErr := GetSolicitudPagoExternaByReferenciaInterna(referenciaInterna)
	if getErr != nil {
		return result.RowsAffected > 0, nil, getErr
	}
	return result.RowsAffected > 0, s, nil
}

// MarcarCallbackEntregado registra que la notificación saliente (webhook y/o
// Telegram) ya se intentó entregar, para no reintentar indefinidamente sin
// visibilidad.
func MarcarCallbackEntregado(id uint, webhookOk, telegramOk bool) error {
	updates := map[string]interface{}{}
	if webhookOk {
		updates["callback_entregado"] = true
	}
	if telegramOk {
		updates["telegram_entregado"] = true
	}
	if len(updates) == 0 {
		return app.Http.Database.DB.Model(&SolicitudPagoExterna{}).Where("id = ?", id).
			Update("callback_intentos", gorm.Expr("callback_intentos + 1")).Error
	}
	updates["callback_intentos"] = gorm.Expr("callback_intentos + 1")
	return app.Http.Database.DB.Model(&SolicitudPagoExterna{}).Where("id = ?", id).Updates(updates).Error
}
