package services

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"
)

const (
	umindWebhookTimeout        = 10 * time.Second
	umindWebhookRespuestaLimit = 256 * 1024 // 256KB
)

// validarURLTool solo chequea forma (https + host presente) antes de
// intentar la llamada — la validación real de destino pasa por
// dialContextSeguro en cada conexión, no acá, para no dejar una ventana
// entre "resolver y validar" y "conectar" (DNS rebinding: el mismo hostname
// podría resolver a una IP pública en el primer lookup y a una interna
// milisegundos después, en el connect real).
func validarURLTool(rawURL string) (*url.URL, error) {
	u, err := url.Parse(strings.TrimSpace(rawURL))
	if err != nil {
		return nil, fmt.Errorf("URL inválida: %w", err)
	}
	if u.Scheme != "https" {
		return nil, fmt.Errorf("la URL de la tool debe ser https")
	}
	if u.Hostname() == "" {
		return nil, fmt.Errorf("URL sin host")
	}
	return u, nil
}

// dialContextSeguro resuelve el host en el momento de conectar (no antes) y
// rechaza cualquier IP interna justo antes de abrir la conexión TCP — cierra
// la ventana de DNS rebinding que tendría validar la URL una vez y confiar
// en que el cliente HTTP resuelva "lo mismo" después.
func dialContextSeguro(ctx context.Context, network, addr string) (net.Conn, error) {
	host, port, err := net.SplitHostPort(addr)
	if err != nil {
		return nil, err
	}
	ips, err := net.DefaultResolver.LookupIP(ctx, "ip", host)
	if err != nil {
		return nil, fmt.Errorf("no se pudo resolver %s: %w", host, err)
	}
	if len(ips) == 0 {
		return nil, fmt.Errorf("%s no resolvió a ninguna IP", host)
	}
	for _, ip := range ips {
		if ipEsInterna(ip) {
			return nil, fmt.Errorf("%s resuelve a una IP interna (%s), no permitido", host, ip)
		}
	}
	dialer := &net.Dialer{Timeout: umindWebhookTimeout}
	return dialer.DialContext(ctx, network, net.JoinHostPort(ips[0].String(), port))
}

// ipEsInterna centraliza qué se considera "red interna" — separado para
// poder testearlo sin red real.
func ipEsInterna(ip net.IP) bool {
	return ip.IsPrivate() ||
		ip.IsLoopback() ||
		ip.IsLinkLocalUnicast() ||
		ip.IsLinkLocalMulticast() ||
		ip.IsUnspecified() ||
		ip.IsMulticast()
}

var umindWebhookHTTPClient = &http.Client{
	Timeout: umindWebhookTimeout,
	Transport: &http.Transport{
		DialContext: dialContextSeguro,
	},
	CheckRedirect: func(req *http.Request, via []*http.Request) error {
		return fmt.Errorf("redirects no permitidos en tools custom")
	},
}

// LlamarHerramientaWebhook ejecuta una tool custom: POST a la URL configurada
// con los argumentos que decidió el modelo, con guardas SSRF y límites de
// tiempo/tamaño de respuesta. Devuelve el body de la respuesta tal cual (el
// modelo lo interpreta como resultado de la tool).
func LlamarHerramientaWebhook(rawURL string, headerNombre, headerValor string, argumentos map[string]interface{}) (string, error) {
	u, err := validarURLTool(rawURL)
	if err != nil {
		return "", err
	}

	body, err := json.Marshal(argumentos)
	if err != nil {
		return "", fmt.Errorf("no se pudieron serializar los argumentos: %w", err)
	}

	req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(body))
	if err != nil {
		return "", err
	}
	req.Header.Set("Content-Type", "application/json")
	if strings.TrimSpace(headerNombre) != "" {
		req.Header.Set(headerNombre, headerValor)
	}

	resp, err := umindWebhookHTTPClient.Do(req)
	if err != nil {
		return "", fmt.Errorf("no se pudo contactar la tool: %w", err)
	}
	defer resp.Body.Close()

	raw, _ := io.ReadAll(io.LimitReader(resp.Body, umindWebhookRespuestaLimit))
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		detalle := strings.TrimSpace(string(raw))
		if len(detalle) > 500 {
			detalle = detalle[:500]
		}
		return "", fmt.Errorf("la tool respondió %d: %s", resp.StatusCode, detalle)
	}
	return string(raw), nil
}
