package ollama

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"reflect"

	"fiko-ai/internal/llm"
)

// Ollama'nın tool (function calling) API'si:
//
//	istek : {"model":…,"messages":[…],"tools":[{"type":"function","function":{"name","description","parameters"}}]}
//	yanıt : {"message":{"role":"assistant","tool_calls":[{"function":{"name":"…","arguments":{…}}}]}}
//
// arguments alanı Ollama sürümlerine göre NESNE veya JSON METNİ olabiliyor; ikisi de
// kabul edilir (sessizce boş girdiye düşmek en kötü hata olurdu).
var _ llm.ToolProvider = (*Client)(nil)

type ollamaTool struct {
	Type     string `json:"type"`
	Function struct {
		Name        string         `json:"name"`
		Description string         `json:"description,omitempty"`
		Parameters  map[string]any `json:"parameters,omitempty"`
	} `json:"function"`
}

type toolCallFn struct {
	Name      string          `json:"name"`
	Arguments json.RawMessage `json:"arguments"`
}

type toolCall struct {
	Function toolCallFn `json:"function"`
}

// toolMessage, tool çağrısı dönen akış satırının mesaj kısmıdır.
type toolMessage struct {
	chatMessage
	ToolCalls []toolCall `json:"tool_calls,omitempty"`
}

// toolChatRequest, tools alanı eklenmiş /api/chat gövdesidir.
type toolChatRequest struct {
	Model    string         `json:"model"`
	Messages []toolMessage  `json:"messages"`
	Stream   bool           `json:"stream"`
	Tools    []ollamaTool   `json:"tools,omitempty"`
	Options  map[string]any `json:"options,omitempty"`
}

// toolResponse, tool akışındaki bir NDJSON satırıdır.
type toolResponse struct {
	Model           string      `json:"model"`
	Message         toolMessage `json:"message"`
	Done            bool        `json:"done"`
	DoneReason      string      `json:"done_reason"`
	PromptEvalCount int         `json:"prompt_eval_count"`
	EvalCount       int         `json:"eval_count"`
	Error           string      `json:"error"`
}

// GenerateStreamWithTools tool tanımlarını modele verir ve akıştan tool_calls toplar.
// Tool çağrısı döngüseldir: çağrının sonucu mesaj geçmişine eklenip yeniden üretim yapılır
// (bu döngü ajan katmanındadır).
func (c *Client) GenerateStreamWithTools(ctx context.Context, req llm.Request, tools []llm.ToolSpec, onDelta llm.DeltaFunc) (llm.Result, error) {
	model := req.Model
	if model == "" {
		model = c.defaultModel
	}
	if model == "" {
		return llm.Result{}, llm.ErrNoModel
	}
	res := llm.Result{Model: model}
	if len(req.Messages) == 0 {
		return res, llm.ErrEmptyMessages
	}
	body, err := c.buildToolRequest(model, req, tools)
	if err != nil {
		return res, fmt.Errorf("ollama: istek oluşturulamadı: %w", err)
	}
	return c.streamToolChat(ctx, body, res, onDelta)
}

func (c *Client) buildToolRequest(model string, req llm.Request, tools []llm.ToolSpec) (toolChatRequest, error) {
	msgs := make([]toolMessage, 0, len(req.Messages))
	for _, m := range req.Messages {
		msgs = append(msgs, toolMessage{chatMessage: chatMessage{Role: string(m.Role), Content: m.Content}})
	}
	out := toolChatRequest{Model: model, Messages: msgs, Stream: true, Options: c.optionMap(req.Options)}
	for _, t := range tools {
		var ot ollamaTool
		ot.Type = "function"
		ot.Function.Name = t.Name
		ot.Function.Description = t.Description
		ot.Function.Parameters = t.InputSchema
		out.Tools = append(out.Tools, ot)
	}
	return out, nil
}

// streamToolChat gövdeyi yollar ve NDJSON akışını okur. Metin delta'ları onDelta'ya,
// tool çağrıları res.ToolCalls'a gider. GenerateStream ile AYNI hata/iptal semantiği.
func (c *Client) streamToolChat(ctx context.Context, body toolChatRequest, res llm.Result, onDelta llm.DeltaFunc) (llm.Result, error) {
	raw, err := json.Marshal(body)
	if err != nil {
		return res, fmt.Errorf("ollama: istek oluşturulamadı: %w", err)
	}
	// Retry yalnızca BAŞLIKLAR gelene kadar uygulanır (bkz. doWithRetry); akış başladıktan
	// sonra tekrar deneme olmaz, aksi halde kullanıcıya aynı içerik ikinci kez akardı.
	resp, err := c.doWithRetry(ctx, func() (*http.Request, error) {
		return newJSONRequest(ctx, c.baseURL+"/api/chat", bytes.NewReader(raw))
	})
	if err != nil {
		if ctx.Err() != nil {
			return res, ctx.Err()
		}
		return res, fmt.Errorf("ollama: istek başarısız: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return res, readHTTPError(resp)
	}

	var sb bytes.Buffer
	// Çağrılar SIRAYLA tutulur. Eskiden ad → çağrı haritasıydı: aynı turda aynı tool iki kez
	// çağrılınca (ör. iki ayrı bilgi için iki memory.remember) ikincisi birincinin ÜSTÜNE
	// yazılıyor, birincisi sessizce kayboluyordu. Yalnızca BİREBİR aynı çağrı (ad + argüman)
	// tekrar gelirse tek sayılır (akış aynı çağrıyı birden çok satırda yineleyebilir).
	var calls []llm.ToolCall
	br := bufio.NewReaderSize(resp.Body, 64*1024)
	finish := func(e error) (llm.Result, error) {
		res.Content = sb.String()
		res.ToolCalls = append(res.ToolCalls, calls...)
		return res, e
	}

	for {
		line, rerr := br.ReadBytes('\n')
		if trimmed := bytes.TrimSpace(line); len(trimmed) > 0 {
			var cr toolResponse
			if err := json.Unmarshal(trimmed, &cr); err != nil {
				return finish(fmt.Errorf("ollama: geçersiz akış satırı: %w", err))
			}
			if cr.Error != "" {
				return finish(&llm.ProviderError{Provider: providerName, Message: cr.Error})
			}
			if d := cr.Message.Content; d != "" {
				sb.WriteString(d)
				if onDelta != nil {
					if err := onDelta(d); err != nil {
						return finish(err)
					}
				}
			}
			for _, tc := range cr.Message.ToolCalls {
				if tc.Function.Name == "" {
					continue // adsız çağrı işe yaramaz; sessizce atlanır
				}
				args, err := parseArguments(tc.Function.Arguments)
				if err != nil {
					return finish(err)
				}
				call := llm.ToolCall{Name: tc.Function.Name, Arguments: args}
				dup := false
				for _, prev := range calls {
					if prev.Name == call.Name && reflect.DeepEqual(prev.Arguments, call.Arguments) {
						dup = true
						break
					}
				}
				if !dup {
					calls = append(calls, call)
				}
			}
			if cr.Done {
				res.DoneReason = cr.DoneReason
				res.PromptTokens = cr.PromptEvalCount
				res.CompletionTokens = cr.EvalCount
				// Model adı İSTENEN adla BİREBİR aynı olmalı (bkz. GenerateStream'deki
				// aynı düzeltme ve gerekçesi). Ollama BULUT modellerinde soneki düşer:
				// "gemma4:31b-cloud" isteğine "gemma4:31b" döner. Cevap adı buradan
				// alınırsa bellek öğrenme gibi sonraki çağrılar O SONEKSİZ adı
				// kullanır ve Ollama 404 döner ("model 'gemma4:31b' not found").
				// İstenen ad elimizde ve üretim ONUNLA yapıldı, dolayısıyla doğru olan odur.
				_ = cr.Model
				return finish(nil)
			}
		}
		if rerr != nil {
			if ctx.Err() != nil {
				return finish(ctx.Err())
			}
			return finish(streamReadError(rerr))
		}
	}
}

// parseArguments tool argümanlarını nesneye çevirir: metin (JSON) ya da nesne kabul edilir.
// Boş argüman {} demektir; bozuk metin ArgsError döner.
func parseArguments(raw json.RawMessage) (map[string]any, error) {
	trimmed := bytes.TrimSpace(raw)
	if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
		return map[string]any{}, nil
	}
	var obj map[string]any
	if err := json.Unmarshal(trimmed, &obj); err == nil {
		if obj == nil {
			return map[string]any{}, nil
		}
		return obj, nil
	}
	// Metin olarak gelmiş olabilir (bazı Ollama sürümleri): ikinci deneme.
	var asText string
	if err := json.Unmarshal(trimmed, &asText); err != nil {
		return nil, &llm.ArgsError{Raw: string(trimmed), Err: err}
	}
	if err := json.Unmarshal([]byte(asText), &obj); err != nil {
		return nil, &llm.ArgsError{Raw: asText, Err: err}
	}
	if obj == nil {
		return map[string]any{}, nil
	}
	return obj, nil
}
