package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"strings"
	"time"

	"github.com/sashabaranov/go-openai"
)

// LLM is an OpenAI LLM wrapper with tool-calling support
type LLM struct {
	client      *openai.Client
	model       string
	temperature float32
	maxTokens   int
	// Retry configuration
	maxRetries int
	retryDelay time.Duration
}

// NewLLM creates a new LLM instance
func NewLLM(apiKey, model string, temperature float32, baseURL string, maxTokens int) *LLM {
	return NewLLMWithRetry(apiKey, model, temperature, baseURL, maxTokens, 3, 1*time.Second)
}

// NewLLMWithRetry creates a new LLM instance with custom retry configuration
func NewLLMWithRetry(apiKey, model string, temperature float32, baseURL string, maxTokens int, maxRetries int, retryDelay time.Duration) *LLM {
	config := openai.DefaultConfig(apiKey)
	if baseURL != "" {
		config.BaseURL = baseURL
	}

	return &LLM{
		client:      openai.NewClientWithConfig(config),
		model:       model,
		temperature: temperature,
		maxTokens:   maxTokens,
		maxRetries:  maxRetries,
		retryDelay:  retryDelay,
	}
}

// ChatCompletionRequest is a request for chat completion
type ChatCompletionRequest struct {
	Messages []openai.ChatCompletionMessage
	Tools    []openai.Tool
}

// ChatCompletionResponse is a response from chat completion
type ChatCompletionResponse struct {
	Message openai.ChatCompletionMessage
}

// Chat sends a chat completion request with retry logic
func (l *LLM) Chat(ctx context.Context, messages []openai.ChatCompletionMessage, tools []openai.Tool) (*openai.ChatCompletionMessage, error) {
	var lastErr error
	delay := l.retryDelay

	for attempt := 0; attempt <= l.maxRetries; attempt++ {
		select {
		case <-ctx.Done():
			return nil, fmt.Errorf("context canceled: %w", ctx.Err())
		default:
		}

		req := openai.ChatCompletionRequest{
			Model:       l.model,
			Messages:    messages,
			Temperature: l.temperature,
			MaxTokens:   l.maxTokens,
		}

		if len(tools) > 0 {
			req.Tools = tools
		}

		resp, err := l.client.CreateChatCompletion(ctx, req)
		if err == nil {
			if len(resp.Choices) == 0 {
				return nil, fmt.Errorf("no response choices returned")
			}

			// Log warning if finish reason indicates an issue
			choice := resp.Choices[0]
			if choice.FinishReason == "length" {
				// Model hit token limit - may have incomplete response
				// This is common with reasoning models that need more tokens
				return nil, fmt.Errorf("response truncated: model hit token limit (finish_reason: length). Consider increasing OPENAI_MAX_TOKENS (current: %d). Usage: prompt=%d, completion=%d, total=%d",
					l.maxTokens, resp.Usage.PromptTokens, resp.Usage.CompletionTokens, resp.Usage.TotalTokens)
			}

			return &choice.Message, nil
		}

		lastErr = err

		// Check if this error is retryable
		if !isRetryableError(err) {
			return nil, fmt.Errorf("failed to create chat completion: %w", err)
		}

		// Don't wait after the last attempt
		if attempt < l.maxRetries {
			log.Printf("LLM request failed (attempt %d/%d): %v. Retrying in %v...",
				attempt+1, l.maxRetries, err, delay)
			select {
			case <-ctx.Done():
				return nil, fmt.Errorf("context canceled during retry wait: %w", ctx.Err())
			case <-time.After(delay):
			}
			// Exponential backoff
			delay *= 2
		}
	}

	return nil, fmt.Errorf("failed to create chat completion after %d retries: %w", l.maxRetries+1, lastErr)
}

// isRetryableError checks if an error is transient and worth retrying
func isRetryableError(err error) bool {
	if err == nil {
		return false
	}

	errStr := err.Error()

	// Context cancellation - retry if not explicitly canceled
	if strings.Contains(errStr, "context canceled") || strings.Contains(errStr, "context deadline exceeded") {
		// These can be transient if the context was canceled due to connection issues
		// But we should check if it's a genuine cancellation vs. a timeout
		return true
	}

	// Network-related errors
	retryablePatterns := []string{
		"connection refused",
		"connection reset",
		"connection closed",
		"network is unreachable",
		"no route to host",
		"timeout",
		"i/o timeout",
		"temporary failure",
		"server misbehaving",
		"service unavailable",
		"too many requests",
		"rate limit",
		"429",
		"500",
		"502",
		"503",
		"504",
	}

	lowerErr := strings.ToLower(errStr)
	for _, pattern := range retryablePatterns {
		if strings.Contains(lowerErr, strings.ToLower(pattern)) {
			return true
		}
	}

	return false
}

// ConvertMCPToolsToOpenAI converts MCP tools to OpenAI tool format
func ConvertMCPToolsToOpenAI(mcpTools []Tool) []openai.Tool {
	tools := make([]openai.Tool, len(mcpTools))
	for i, t := range mcpTools {
		// Convert InputSchema to JSON schema format using map[string]interface{}
		props := make(map[string]interface{})
		for name, prop := range t.InputSchema.Properties {
			propMap := map[string]interface{}{
				"type":        prop.Type,
				"description": prop.Description,
			}
			// For object types without explicit nested properties,
			// allow additionalProperties so the LLM can pass any key-value pairs
			// This is important for tools like 'query' and 'mutate' that accept
			// arbitrary variables objects
			if prop.Type == "object" {
				propMap["additionalProperties"] = true
			}
			props[name] = propMap
		}

		// Build parameters map, omitting empty required array
		params := map[string]interface{}{
			"type":       t.InputSchema.Type,
			"properties": props,
		}
		// Only include required if it has elements - empty slice marshals as null
		if len(t.InputSchema.Required) > 0 {
			params["required"] = t.InputSchema.Required
		}

		tools[i] = openai.Tool{
			Type: openai.ToolTypeFunction,
			Function: &openai.FunctionDefinition{
				Name:        t.Name,
				Description: t.Description,
				Parameters:  params,
			},
		}
	}
	return tools
}

// ParseToolCall parses a tool call from the LLM response
func ParseToolCall(toolCall openai.ToolCall) (string, map[string]interface{}, error) {
	name := toolCall.Function.Name

	var args map[string]interface{}
	if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {
		return name, nil, fmt.Errorf("failed to parse tool arguments: %w", err)
	}

	return name, args, nil
}

// TestConnection tests the connection to OpenAI API
func (l *LLM) TestConnection(ctx context.Context) error {
	// Simple test request - use enough tokens for reasoning models
	// Reasoning models need more tokens for their thinking process
	req := openai.ChatCompletionRequest{
		Model: l.model,
		Messages: []openai.ChatCompletionMessage{
			{
				Role:    openai.ChatMessageRoleUser,
				Content: "Hello",
			},
		},
		MaxTokens: 100,
	}

	_, err := l.client.CreateChatCompletion(ctx, req)
	if err != nil {
		return fmt.Errorf("failed to connect to OpenAI API: %w", err)
	}

	return nil
}
