package agent

import (
	"bytes"
	"encoding/json"
	"fmt"
	"gf-lt/config"
	"gf-lt/models"
	"io"
	"log/slog"
	"net/http"
	"strings"
)

var httpClient = &http.Client{}

var defaultProps = map[string]float32{
	"temperature":    0.8,
	"dry_multiplier": 0.0,
	"min_p":          0.05,
	"n_predict":      -1.0,
}

func detectAPI(api string) (isCompletion, isChat, isDeepSeek, isOpenRouter bool) {
	isCompletion = strings.Contains(api, "/completion") && !strings.Contains(api, "/chat/completions")
	isChat = strings.Contains(api, "/chat/completions")
	isDeepSeek = strings.Contains(api, "deepseek.com")
	isOpenRouter = strings.Contains(api, "openrouter.ai")
	return
}

type AgentClient struct {
	cfg       *config.Config
	getToken  func() string
	log       *slog.Logger
	chatBody  *models.ChatBody
	sysprompt string
	// lastToolCallID string
	tools []models.Tool
}

func NewAgentClient(cfg *config.Config, log *slog.Logger, gt func() string) *AgentClient {
	return &AgentClient{
		cfg:      cfg,
		getToken: gt,
		log:      log,
	}
}

func (ag *AgentClient) Log() *slog.Logger {
	return ag.log
}

func (ag *AgentClient) FormFirstMsg(sysprompt, msg string) (io.Reader, error) {
	ag.sysprompt = sysprompt
	ag.chatBody = &models.ChatBody{
		Messages: []models.RoleMsg{
			{Role: "system", Content: ag.sysprompt},
			{Role: "user", Content: msg},
		},
		Stream: false,
		Model:  ag.cfg.CurrentModel,
	}
	b, err := ag.buildRequest()
	if err != nil {
		return nil, err
	}
	return bytes.NewReader(b), nil
}

func (ag *AgentClient) FormMsg(msg string) (io.Reader, error) {
	m := models.RoleMsg{
		Role: "tool", Content: msg,
	}
	ag.chatBody.Messages = append(ag.chatBody.Messages, m)
	b, err := ag.buildRequest()
	if err != nil {
		return nil, err
	}
	return bytes.NewReader(b), nil
}

func (ag *AgentClient) FormMsgWithToolCallID(msg, toolCallID string) (io.Reader, error) {
	m := models.RoleMsg{
		Role:       "tool",
		Content:    msg,
		ToolCallID: toolCallID,
	}
	ag.chatBody.Messages = append(ag.chatBody.Messages, m)
	b, err := ag.buildRequest()
	if err != nil {
		return nil, err
	}
	return bytes.NewReader(b), nil
}

// buildRequest creates the appropriate LLM request based on the current API endpoint.
func (ag *AgentClient) buildRequest() ([]byte, error) {
	isCompletion, isChat, isDeepSeek, isOpenRouter := detectAPI(ag.cfg.CurrentAPI)
	ag.log.Debug("agent building request", "api", ag.cfg.CurrentAPI, "isCompletion", isCompletion, "isChat", isChat, "isDeepSeek", isDeepSeek, "isOpenRouter", isOpenRouter)
	// Build prompt for completion endpoints
	if isCompletion {
		var sb strings.Builder
		for i := range ag.chatBody.Messages {
			sb.WriteString(ag.chatBody.Messages[i].ToPrompt())
			sb.WriteString("\n")
		}
		prompt := strings.TrimSpace(sb.String())
		switch {
		case isDeepSeek:
			// DeepSeek completion
			req := models.NewDSCompletionReq(prompt, ag.chatBody.Model, defaultProps["temperature"], []string{})
			req.Stream = false // Agents don't need streaming
			return json.Marshal(req)
		case isOpenRouter:
			// OpenRouter completion
			req := models.NewOpenRouterCompletionReq(ag.chatBody.Model, prompt, defaultProps, []string{})
			req.Stream = false // Agents don't need streaming
			return json.Marshal(req)
		default:
			// Assume llama.cpp completion
			req := models.NewLCPReq(prompt, ag.chatBody.Model, nil, defaultProps, []string{})
			req.Stream = false // Agents don't need streaming
			return json.Marshal(req)
		}
	}
	switch {
	case isDeepSeek:
		// DeepSeek chat
		req := models.NewDSChatReq(*ag.chatBody)
		return json.Marshal(req)
	case isOpenRouter:
		// OpenRouter chat - agents don't use reasoning by default
		req := models.NewOpenRouterChatReq(*ag.chatBody, defaultProps, ag.cfg.ReasoningEffort)
		return json.Marshal(req)
	default:
		// Assume llama.cpp chat (OpenAI format)
		req := models.OpenAIReq{
			ChatBody: ag.chatBody,
			Tools:    ag.tools,
		}
		return json.Marshal(req)
	}
}

func (ag *AgentClient) LLMRequest(body io.Reader) ([]byte, error) {
	// Read the body for debugging (but we need to recreate it for the request)
	bodyBytes, err := io.ReadAll(body)
	if err != nil {
		ag.log.Error("failed to read request body", "error", err)
		return nil, err
	}
	req, err := http.NewRequest("POST", ag.cfg.CurrentAPI, bytes.NewReader(bodyBytes))
	if err != nil {
		ag.log.Error("failed to create request", "error", err)
		return nil, err
	}
	req.Header.Add("Accept", "application/json")
	req.Header.Add("Content-Type", "application/json")
	req.Header.Add("Authorization", "Bearer "+ag.getToken())
	req.Header.Set("Accept-Encoding", "gzip")
	ag.log.Debug("agent LLM request", "url", ag.cfg.CurrentAPI, "body_preview", string(bodyBytes[:min(len(bodyBytes), 500)]))
	resp, err := httpClient.Do(req)
	if err != nil {
		ag.log.Error("llamacpp api request failed", "error", err, "url", ag.cfg.CurrentAPI)
		return nil, err
	}
	defer resp.Body.Close()
	responseBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		ag.log.Error("failed to read response", "error", err)
		return nil, err
	}
	if resp.StatusCode >= 400 {
		ag.log.Error("agent LLM request failed", "status", resp.StatusCode, "response", string(responseBytes[:min(len(responseBytes), 1000)]))
		return responseBytes, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(responseBytes[:min(len(responseBytes), 200)]))
	}
	// Parse response and extract text content
	text, err := extractTextFromResponse(responseBytes)
	if err != nil {
		ag.log.Error("failed to extract text from response", "error", err, "response_preview", string(responseBytes[:min(len(responseBytes), 500)]))
		// Return raw response as fallback
		return responseBytes, nil
	}
	return []byte(text), nil
}

// extractTextFromResponse parses common LLM response formats and extracts the text content.
func extractTextFromResponse(data []byte) (string, error) {
	// Try to parse as generic JSON first
	var genericResp map[string]any
	if err := json.Unmarshal(data, &genericResp); err != nil {
		// Not JSON, return as string
		return string(data), nil
	}
	// Check for OpenAI chat completion format
	if choices, ok := genericResp["choices"].([]any); ok && len(choices) > 0 {
		if firstChoice, ok := choices[0].(map[string]any); ok {
			// Chat completion: choices[0].message.content
			if message, ok := firstChoice["message"].(map[string]any); ok {
				if content, ok := message["content"].(string); ok {
					return content, nil
				}
			}
			// Completion: choices[0].text
			if text, ok := firstChoice["text"].(string); ok {
				return text, nil
			}
			// Delta format for streaming (should not happen with stream: false)
			if delta, ok := firstChoice["delta"].(map[string]any); ok {
				if content, ok := delta["content"].(string); ok {
					return content, nil
				}
			}
		}
	}
	// Check for llama.cpp completion format
	if content, ok := genericResp["content"].(string); ok {
		return content, nil
	}
	// Unknown format, return pretty-printed JSON
	prettyJSON, err := json.MarshalIndent(genericResp, "", "  ")
	if err != nil {
		return string(data), nil
	}
	return string(prettyJSON), nil
}
