package ollama

import (
	"context"
	"errors"
	"io"
	"net"
	"net/http"
	"strconv"
	"strings"
	"syscall"
	"time"
)

// retry.go: geçici (transient) sağlayıcı hataları için SINIRLI tekrar deneme.
//
// CANLI HATA (04.10.2026): kullanıcı cloud modelini (gemma4:31b-cloud) seçmişti.
// Ollama bu modeli https://ollama.com/api/chat adresine PROXY'ler ve upstream
// bağlantıları arada "HTTP 502: ... connection error: PROTOCOL_ERROR" ile kopuyor.
// Bu GEÇİCİ bir hatadır: aynı istek birkaç saniye sonra 200 dönüyor (doğrulandı).
// İstemcide tek deneme olduğu için TEK bir ağ sıçraması kullanıcının turunu tümden
// öldürüyor ve sohbette "HATA" balonu bırakıyordu. Buradaki tekrar deneme bunu
// görünmez kılar: kullanıcı farkında olmadan bir kez daha sorar ve cevabı alır.
//
// GÜVENLİK — bu dosyanın en önemli kuralı:
// Tekrar yalnızca HTTP BAŞLIKLARI gelmeden ÖNCE yapılır. Akış (NDJSON) başladıktan
// sonra ASLA tekrar denenmez. Aksi halde kullanıcıya zaten parça parça gösterilmiş
// içerik ikinci kez akar ve sohbet bozulur (bkz. streamReadError).

const (
	// defaultMaxAttempts toplam deneme sayısıdır: 1 ilk + 2 tekrar.
	defaultMaxAttempts = 3

	// defaultRetryBaseDelay ilk tekrar arasındaki beklemedir; sonraki denemelerde ikiye katlanır.
	defaultRetryBaseDelay = 300 * time.Millisecond

	// maxRetryDelay bekleme tavanıdır. Cloud sağlayıcı yavaşladığında kullanıcıyı
	// gereksiz yere bekletmemek için gereklidir.
	maxRetryDelay = 3 * time.Second
)

// retryableStatus, tekrar denemeye DEĞER hataları ayırır.
//
//   - 502/503/504: upstream (Ollama -> ollama.com proxy'si) geçici olarak erişilemiyor.
//     Sunucu açıkça "başaramadım" dediği için modeli çalıştırmamıştır; tekrar
//     denemek token harcatmaz, yalnızca aynı cevabı ister.
//   - 500: sağlayıcı iç hatası; kısa süre içinde düzelebilir.
//   - 429: hız sınırı. Retry-After başlığına uyulur (bkz. retryDelay).
//   - 408: istek zaman aşımı.
//
// 400 / 401 / 403 / 404 / 413 KESİNLİKLE tekrar DENENMEZ: model yok, yetkisiz,
// bozuk gövde gibi kalıcı hatalar. Bunları tekrarlamak yalnızca zaman kaybıdır.
func retryableStatus(code int) bool {
	switch code {
	case http.StatusRequestTimeout, // 408
		http.StatusTooManyRequests,     // 429
		http.StatusInternalServerError, // 500
		http.StatusBadGateway,          // 502
		http.StatusServiceUnavailable,  // 503
		http.StatusGatewayTimeout:      // 504
		return true
	}
	return false
}

// isDialFailure, isteğin karşıya HİÇ GİTMEDİĞI bağlantı hatalarını tanır
// (Ollama kapalı ya da yeniden başlıyor).
//
// Böyle bir istek sunucuya ulaşmadığı için tekrar denemek YAN ETKISIZDIR: model
// çalışmadı, token harcanmadı. Buna karşılık bağlantı kurulduktan SONRA koptuğunda
// istek sunucuya ulaşmış OLABİLİR (üretim başlamış olabilir); o durumda tekrar
// denemek yanlış olur, bu yüzden kasıtlı olarak tekrar denenmez.
func isDialFailure(err error) bool {
	var opErr *net.OpError
	if errors.As(err, &opErr) && opErr.Op == "dial" {
		return true
	}
	return errors.Is(err, syscall.ECONNREFUSED)
}

// retryDelay, n. denemeden önce beklenecek süreyi hesaplar (300ms, 600ms, 1200ms...).
// Sunucu Retry-After verdiyse O UYGUNLUR (yok sayılmaz); aksi halde üst sınıra kırpılır.
func (c *Client) retryDelay(attempt int, resp *http.Response) time.Duration {
	if resp != nil {
		if d, ok := parseRetryAfter(resp.Header.Get("Retry-After"), time.Now()); ok {
			return clampDelay(d)
		}
	}
	base := c.retryBase
	if base <= 0 {
		base = defaultRetryBaseDelay
	}
	d := base
	for i := 1; i < attempt && d < maxRetryDelay; i++ {
		d *= 2
	}
	return clampDelay(d)
}

// clampDelay, süreyi [0, maxRetryDelay] aralığına indirger.
func clampDelay(d time.Duration) time.Duration {
	if d <= 0 {
		return 0
	}
	if d > maxRetryDelay {
		return maxRetryDelay
	}
	return d
}

// parseRetryAfter, RFC 9110 Retry-After değerini çözer: saniye ya da HTTP tarihi.
// Tanınmayan biçim (ör. boş) için false döner.
func parseRetryAfter(v string, now time.Time) (time.Duration, bool) {
	v = strings.TrimSpace(v)
	if v == "" {
		return 0, false
	}
	if secs, err := strconv.Atoi(v); err == nil {
		if secs < 0 {
			return 0, false
		}
		return time.Duration(secs) * time.Second, true
	}
	if t, err := http.ParseTime(v); err == nil {
		if d := t.Sub(now); d > 0 {
			return d, true
		}
		return 0, true // tarih geçmişte: bekleme yok
	}
	return 0, false
}

// doWithRetry, isteği en çok maxAttempts kez yollar; yalnızca GEÇİCİ hatalarda
// (retryableStatus ya da bağlantının hiç kurulamadığı durum) tekrar dener.
// ctx iptal edilirse beklemeden döner.
//
// build HER DENEMEDE çağılır. Bunun nedeni gövde (body) tek kullanımlıktır:
// ikinci denemede boş gövde gitmesin diye istek baştan kurulur.
func (c *Client) doWithRetry(ctx context.Context, build func() (*http.Request, error)) (*http.Response, error) {
	attempts := c.maxAttempts
	if attempts < 1 {
		attempts = 1
	}
	for attempt := 1; ; attempt++ {
		if err := ctx.Err(); err != nil {
			return nil, err
		}
		req, err := build()
		if err != nil {
			return nil, err // istek kurulamadı: tekrarlanacak bir hata değil
		}
		resp, err := c.http.Do(req)
		if err != nil {
			if ctx.Err() != nil {
				return nil, ctx.Err()
			}
			// Bağlantı KURULAMADIYSA istek sunucuya ulaşmadı: tekrar denemek yan etkisiz.
			// Kurulduktan sonra koptuysa istek sunucuya ulaşmış olabilir; tekrarlanmaz.
			if attempt >= attempts || !isDialFailure(err) {
				return nil, err
			}
			if werr := sleepCtx(ctx, c.retryDelay(attempt, nil)); werr != nil {
				return nil, werr
			}
			continue
		}
		if !retryableStatus(resp.StatusCode) || attempt >= attempts {
			return resp, nil
		}
		// Tekrar denenecekse gövde tüketilip kapatılır: bağlantı havuza geri dönsün
		// (akside yavaş yavaş sızıntı ve port tükenmesi olur).
		_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4096))
		_ = resp.Body.Close()
		if werr := sleepCtx(ctx, c.retryDelay(attempt, resp)); werr != nil {
			return nil, werr
		}
	}
}

// sleepCtx, iptal edilebilir bekleme. Normalde nil döner; ctx iptal edilirse
// ctx.Err() döner (böylece çağıran "kullanıcı vazgeçti" ile "sağlayıcı çöktü"
// ayrımını korur).
func sleepCtx(ctx context.Context, d time.Duration) error {
	if d <= 0 {
		return nil
	}
	t := time.NewTimer(d)
	defer t.Stop()
	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-t.C:
		return nil
	}
}
