package tool

import (
	"context"
	"errors"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"
)

// NewSafeClient, builtin web tool'larının (web.fetch, web.search...) kullandığı
// SSRF korumalı HTTP istemcisini döndürür: bağlanılan IP denetlenir (DNS rebinding'e kapalı),
// loopback/özel ağ/cloud metadata reddedilir, yönlendirme ve süre sınırlıdır.
//
// Politika, kind=http tool'larla AYNIDIR; böylece iki yol arasında güvenlik açığı farkı olmaz.
func NewSafeClient(p HTTPPolicy) *http.Client {
	return newSafeHTTPClient(p)
}

// NewPinnedClient, SADECE verilen allow listesindeki host:port adreslerine
// bağlanabilen bir istemci döndürür (allowlist / "pinned" istemci).
//
// Neden gerekiyor? İnternet tool'larının varsayılan istemcisi loopback'i reddeder
// (SSRF koruması). Arama sağlayıcısı olarak YERELDE çalışan bir SearXNG örneği
// (örn. http://127.0.0.1:8181) kullanıldığında bu koruma aramayı da engeller.
//
// Güvenli çözüm, korumayı tüm tool'lara açmak (AllowPrivate) DEĞİLDİR: burada
// izin YALNIZCA yapılandırmada sabitlenmiş (operatör kararı) host'a verilir.
// web.fetch kullanıcıdan GELEN adreslere çalıştığı için kendi istemcisini korur;
// web.search ise yalnızca bu tek, sabit adrese gidebilir. Böylece saldırgan
// model üzerinden "http://169.254.169.254/" tarzı bir adres aracılığıyla iç
// ağa ulaşamaz (adres kullanıcı girdisi değil, sunucu yapılandırmasıdır).
//
// Diğer tüm hedefler (izin listesinde olmayan her adres) reddedilir.
func NewPinnedClient(allowHosts []string, p HTTPPolicy) *http.Client {
	allowed := map[string]bool{}
	for _, h := range allowHosts {
		h = strings.ToLower(strings.TrimSpace(h))
		if h == "" {
			continue
		}
		allowed[h] = true
	}
	dialer := &net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}
	transport := &http.Transport{
		DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
			// addr daima "host:port" biçimindedir; izin listesiyle AYNı biçimde karşılaştır.
			// Yalnızca hostname'e bakmak "başka port" ayrımını kaybettirirdi.
			if !allowed[strings.ToLower(addr)] {
				return nil, fmt.Errorf("%w: %s (izinli arama adresi değil)", ErrHTTPBlocked, addr)
			}
			return dialer.DialContext(ctx, network, addr)
		},
		TLSHandshakeTimeout: 10 * time.Second,
	}
	timeout := p.Timeout
	if timeout <= 0 {
		timeout = DefaultHTTPTimeout
	}
	// Yönlendirme izlenmez: izinli adres başka bir yere yönlendirilirse
	// istemci yine de engellenir (CheckRedirect sadece sınırı uygular).
	return &http.Client{
		Transport: transport,
		Timeout:   timeout,
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			if len(via) > 2 {
				return errors.New("çok fazla yönlendirme")
			}
			return nil
		},
	}
}

// PinnedHost, bir arama sağlayıcı adresinden dial için "host:port" üretir.
// Boş dönüyorsa adres kullanılamaz.
func PinnedHost(rawURL string) string {
	u, err := url.Parse(strings.TrimSpace(rawURL))
	if err != nil || u.Host == "" {
		return ""
	}
	host := u.Hostname()
	port := u.Port()
	if port == "" {
		if u.Scheme == "https" {
			port = "443"
		} else {
			port = "80"
		}
	}
	return net.JoinHostPort(strings.ToLower(host), port)
}
