package services

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
	"strings"
	"text/template"
	"time"

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

// DispatchPayload contiene las variables disponibles en el PayloadTemplate.
type DispatchPayload struct {
	ContratoID   uint
	Referencia   string
	Email        string
	Monto        float64
	Moneda       string
	SaasID       uint
	SaasSlug     string
	Fuente       string // dlocal | bold | manual
	WebhookToken string // token único de la integración (para incluirlo en el payload saliente)
}

// DispatchSaasPaymentNotification notifica a todos los SaaS externos activos
// asociados al contrato. Se ejecuta en goroutine separada desde los webhooks.
//
// Cadena de vinculación:
//   Contrato → contrato_servicios (m2m) → servicios.id
//   servicios.id ↔ saas_productos.servicio_id → saas_productos.id
//   saas_productos.id → saas_api_configs.saas_id (activo = true)
func DispatchSaasPaymentNotification(contratoID uint, payerEmail, fuente string, monto float64, moneda string) {
	referencia := fmt.Sprintf("contrato-%d", contratoID)

	// 1. Cargar contrato con sus servicios
	contrato, err := models.GetContratoByID(contratoID)
	if err != nil {
		log.Printf("[DISPATCH] Error cargando contrato %d: %v", contratoID, err)
		return
	}
	if len(contrato.Servicios) == 0 {
		log.Printf("[DISPATCH] Contrato %d sin servicios — nada que despachar", contratoID)
		return
	}

	// 2. Recopilar IDs de servicios del contrato
	servicioIDs := make([]uint, len(contrato.Servicios))
	for i, s := range contrato.Servicios {
		servicioIDs[i] = s.ID
	}

	// 3. Buscar SaasProductos cuyo servicio_id esté en los servicios del contrato
	var saasProductos []models.SaasProducto
	if err := app.Http.Database.DB.
		Where("servicio_id IN ? AND activo = ?", servicioIDs, true).
		Find(&saasProductos).Error; err != nil {
		log.Printf("[DISPATCH] Error buscando SaasProductos para contrato %d: %v", contratoID, err)
		return
	}
	if len(saasProductos) == 0 {
		log.Printf("[DISPATCH] Contrato %d: ningún SaasProducto vinculado a sus servicios", contratoID)
		return
	}

	// 4. Recopilar IDs de SaaS
	saasIDs := make([]uint, len(saasProductos))
	saasMap := make(map[uint]models.SaasProducto, len(saasProductos))
	for i, sp := range saasProductos {
		saasIDs[i] = sp.ID
		saasMap[sp.ID] = sp
	}

	// 5. Obtener configuraciones API activas para esos SaaS, filtrando por pasarela
	configs, err := models.GetSaasApiConfigsBySaasIDs(saasIDs, fuente) // fuente = "dlocal" | "bold"
	if err != nil {
		log.Printf("[DISPATCH] Error obteniendo SaasApiConfigs: %v", err)
		return
	}
	if len(configs) == 0 {
		log.Printf("[DISPATCH] Contrato %d: ninguna SaasApiConfig activa encontrada", contratoID)
		return
	}

	// 6. Enviar notificación a cada endpoint
	for _, cfg := range configs {
		saas := saasMap[cfg.SaasID]
		payload := DispatchPayload{
			ContratoID:   contratoID,
			Referencia:   referencia,
			Email:        payerEmail,
			Monto:        monto,
			Moneda:       moneda,
			SaasID:       saas.ID,
			SaasSlug:     saas.Slug,
			Fuente:       fuente,
			WebhookToken: cfg.WebhookToken,
		}
		dispatchOne(cfg, payload)
	}
}

// dispatchOne realiza la llamada HTTP a un endpoint e inserta el log correspondiente.
func dispatchOne(cfg models.SaasApiConfig, payload DispatchPayload) {
	logEntry := models.SaasDispatchLog{
		SaasApiConfigID: cfg.ID,
		ContratoID:      payload.ContratoID,
		Referencia:      payload.Referencia,
		PayerEmail:      payload.Email,
		Fuente:          payload.Fuente,
		Intentos:        1,
	}

	// Renderizar payload template
	body, err := renderTemplate(cfg.PayloadTemplate, payload)
	if err != nil {
		logEntry.Estado = "failed"
		logEntry.ErrorMsg = fmt.Sprintf("template error: %v", err)
		_ = models.SaveSaasDispatchLog(&logEntry)
		log.Printf("[DISPATCH] Config %d — error de template: %v", cfg.ID, err)
		return
	}

	// Construir request
	metodo := strings.ToUpper(cfg.Metodo)
	if metodo == "" {
		metodo = "POST"
	}

	timeout := cfg.TimeoutSeg
	if timeout <= 0 {
		timeout = 10
	}
	client := &http.Client{Timeout: time.Duration(timeout) * time.Second}

	var reqBody io.Reader
	if metodo != "GET" {
		reqBody = bytes.NewBufferString(body)
	}

	req, err := http.NewRequest(metodo, cfg.EndpointURL, reqBody)
	if err != nil {
		logEntry.Estado = "failed"
		logEntry.ErrorMsg = fmt.Sprintf("error creando request: %v", err)
		_ = models.SaveSaasDispatchLog(&logEntry)
		log.Printf("[DISPATCH] Config %d — error creando request: %v", cfg.ID, err)
		return
	}

	if metodo != "GET" {
		req.Header.Set("Content-Type", "application/json")
	}
	if cfg.ApiKeyHeader != "" && cfg.ApiKeyValue != "" {
		req.Header.Set(cfg.ApiKeyHeader, cfg.ApiKeyValue)
	}

	// Ejecutar llamada
	resp, err := client.Do(req)
	if err != nil {
		logEntry.Estado = "failed"
		logEntry.ErrorMsg = fmt.Sprintf("error HTTP: %v", err)
		_ = models.SaveSaasDispatchLog(&logEntry)
		log.Printf("[DISPATCH] Config %d — error llamando %s: %v", cfg.ID, cfg.EndpointURL, err)
		return
	}
	defer resp.Body.Close()

	respBytes, _ := io.ReadAll(resp.Body)
	respBody := truncateStr(string(respBytes), 2000)

	logEntry.HttpStatus = resp.StatusCode
	logEntry.Respuesta = respBody
	if resp.StatusCode >= 200 && resp.StatusCode < 300 {
		logEntry.Estado = "success"
	} else {
		logEntry.Estado = "failed"
	}

	_ = models.SaveSaasDispatchLog(&logEntry)
	log.Printf("[DISPATCH] Config %d → %s %s — HTTP %d", cfg.ID, metodo, cfg.EndpointURL, resp.StatusCode)
}

// renderTemplate aplica text/template sobre el PayloadTemplate del config.
func renderTemplate(tmplStr string, data DispatchPayload) (string, error) {
	if tmplStr == "" {
		// Si no hay template, enviar payload JSON mínimo
		return fmt.Sprintf(`{"contrato_id":%d,"referencia":"%s","email":"%s","monto":%.2f,"moneda":"%s","fuente":"%s"}`,
			data.ContratoID, data.Referencia, data.Email, data.Monto, data.Moneda, data.Fuente), nil
	}
	t, err := template.New("payload").Parse(tmplStr)
	if err != nil {
		return "", err
	}
	var buf bytes.Buffer
	if err := t.Execute(&buf, data); err != nil {
		return "", err
	}
	return buf.String(), nil
}

func truncateStr(s string, max int) string {
	if len(s) <= max {
		return s
	}
	return s[:max] + "…"
}
