package ollama

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"

	"fiko-ai/internal/llm"
)

// WithEmbedModel embedding için kullanılacak modeli ayarlar (ör. "nomic-embed-text") ve aynı Client'ı döndürür.
// Sohbet modelinden BAĞIMSIZDIR: sohbet modeli değişse de vektörler tutarlı kalır.
func (c *Client) WithEmbedModel(model string) *Client {
	c.embedModel = strings.TrimSpace(model)
	return c
}

// EmbedModel yapılandırılmış embedding modelini döndürür (boş olabilir).
func (c *Client) EmbedModel() string { return c.embedModel }

type embedRequest struct {
	Model string `json:"model"`
	Input string `json:"input"`
	// KeepAlive: embedding modeli de her çağrıda yeniden yüklenmesin diye
	// (4.7 GB'lık modellerde yükleme saniyeler sürüyor). Boşsa gönderilmez.
	KeepAlive string `json:"keep_alive,omitempty"`
	// Options bağlam penceresini SINIRLAR.
	//
	// Neden şart? Ollama, seçenek verilmezse model kartındaki bağlamı kullanır ve
	// embedding modeli KV cache için devasa yer ayırıyordu (qwen3-embedding:
	// num_ctx=40960 -> 12 GB VRAM). Aynı anda sohbet modeli + TTS + STT de
	// GPU'yu kullanınca bellek tükeniyor ve motorlar birbirini kovuyordu
	// (ölçüm: tek cümle TTS 1,6 sn yerine 131 sn). Embedding TEK KİLİTLİ bir
	// metni vektörleştirir; pencereye ihtiyacı yoktur.
	Options map[string]any `json:"options,omitempty"`
}

type embedResponse struct {
	Embeddings [][]float32 `json:"embeddings"`
	Error      string      `json:"error"`
}

// embedContext, embedding isteğinin bağlam penceresidir.
//
// Embedding tek kısa bir metni vektörleştirir; devasa pencere yalnızca KV cache
// için VRAM ayırır (bkz. embedRequest.Options). 512 token her metin için yeterlidir.
// 0 = gönderme (Ollama varsayılanı).
const embedContext = 512

// Embed metni Ollama /api/embed ile vektöre çevirir. ctx iptal edilirse istek kesilir.
func (c *Client) Embed(ctx context.Context, text string) ([]float32, error) {
	if c.embedModel == "" {
		return nil, llm.ErrNoEmbedModel
	}
	if strings.TrimSpace(text) == "" {
		return nil, llm.ErrEmptyText
	}
	ereq := embedRequest{Model: c.embedModel, Input: text, KeepAlive: c.keepAlive}
	if c.maxContext > 0 {
		ereq.Options = map[string]any{"num_ctx": embedContext}
	}
	body, err := json.Marshal(ereq)
	if err != nil {
		return nil, fmt.Errorf("ollama: embed isteği serileştirilemedi: %w", err)
	}
	resp, err := c.doWithRetry(ctx, func() (*http.Request, error) {
		req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/api/embed", bytes.NewReader(body))
		if err != nil {
			return nil, err
		}
		req.Header.Set("Content-Type", "application/json")
		return req, nil
	})
	if err != nil {
		if ctx.Err() != nil {
			return nil, ctx.Err()
		}
		return nil, fmt.Errorf("ollama: embed isteği başarısız: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return nil, readHTTPError(resp)
	}

	var er embedResponse
	if err := json.NewDecoder(resp.Body).Decode(&er); err != nil {
		return nil, fmt.Errorf("ollama: embed yanıtı çözümlenemedi: %w", err)
	}
	if er.Error != "" {
		return nil, &llm.ProviderError{Provider: providerName, Message: er.Error}
	}
	if len(er.Embeddings) == 0 || len(er.Embeddings[0]) == 0 {
		return nil, &llm.ProviderError{Provider: providerName, Message: "embed yanıtında vektör yok"}
	}
	return er.Embeddings[0], nil
}
