package llm

import (
	"context"
	"encoding/json"
	"fmt"
	"log/slog"
	"os"
	"time"

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

	coretypes "github.com/enmanuel/agents/pkg/llm"
	"github.com/enmanuel/agents/shell/logger"
)

// NewOpenAIComplete returns a CompleteFunc backed by the OpenAI-compatible API.
// Works with OpenAI, Ollama, vLLM, LMStudio — just change baseURL.
func NewOpenAIComplete(apiKeyEnv, baseURL string, log *slog.Logger) coretypes.CompleteFunc {
	return func(ctx context.Context, req coretypes.CompletionRequest) (coretypes.CompletionResponse, error) {
		apiKey := os.Getenv(apiKeyEnv)
		if apiKey == "" {
			apiKey = "ollama" // Ollama doesn't require a real key
		}

		cfg := openai.DefaultConfig(apiKey)
		if baseURL != "" {
			cfg.BaseURL = baseURL
		}
		client := openai.NewClientWithConfig(cfg)

		msgs := make([]openai.ChatCompletionMessage, 0, len(req.Messages)+1)
		if req.SystemPrompt != "" {
			msgs = append(msgs, openai.ChatCompletionMessage{
				Role:    openai.ChatMessageRoleSystem,
				Content: req.SystemPrompt,
			})
		}
		for _, m := range req.Messages {
			msgs = append(msgs, toOpenAIMessage(m))
		}

		openReq := openai.ChatCompletionRequest{
			Model:       req.Model,
			Messages:    msgs,
			MaxTokens:   req.MaxTokens,
			Temperature: float32(req.Temperature),
		}

		// Add tools if present
		if len(req.Tools) > 0 {
			openReq.Tools = toOpenAITools(req.Tools)
		}

		log.Info("llm_request",
			"provider", "openai",
			"model", req.Model,
			"messages", len(req.Messages),
			"tools", len(req.Tools),
		)

		start := time.Now()
		resp, err := client.CreateChatCompletion(ctx, openReq)
		if err != nil {
			ms := time.Since(start).Milliseconds()
			log.Error("llm_error", "provider", "openai", logger.FieldDurationMS, ms, "err", err)
			return coretypes.CompletionResponse{}, fmt.Errorf("openai completion: %w", err)
		}
		ms := time.Since(start).Milliseconds()

		if len(resp.Choices) == 0 {
			log.Error("llm_error", "provider", "openai", logger.FieldDurationMS, ms, "err", "empty choices")
			return coretypes.CompletionResponse{}, fmt.Errorf("openai: empty choices")
		}

		choice := resp.Choices[0]
		var toolCalls []coretypes.ToolCall
		for _, tc := range choice.Message.ToolCalls {
			toolCalls = append(toolCalls, coretypes.ToolCall{
				ID:        tc.ID,
				Name:      tc.Function.Name,
				Arguments: tc.Function.Arguments,
			})
		}

		log.Info("llm_response",
			"provider", "openai",
			"model", req.Model,
			logger.FieldDurationMS, ms,
			logger.FieldTokensUsed, resp.Usage.TotalTokens,
			"input_tokens", resp.Usage.PromptTokens,
			"output_tokens", resp.Usage.CompletionTokens,
			"tool_calls", len(toolCalls),
			"finish_reason", string(choice.FinishReason),
		)

		return coretypes.CompletionResponse{
			Content:      choice.Message.Content,
			ToolCalls:    toolCalls,
			FinishReason: string(choice.FinishReason),
			Usage: coretypes.TokenUsage{
				InputTokens:  resp.Usage.PromptTokens,
				OutputTokens: resp.Usage.CompletionTokens,
				TotalTokens:  resp.Usage.TotalTokens,
			},
		}, nil
	}
}

// toOpenAIMessage converts a core Message to an OpenAI ChatCompletionMessage.
func toOpenAIMessage(m coretypes.Message) openai.ChatCompletionMessage {
	role := openai.ChatMessageRoleUser
	switch m.Role {
	case coretypes.RoleAssistant:
		role = openai.ChatMessageRoleAssistant
	case coretypes.RoleSystem:
		role = openai.ChatMessageRoleSystem
	case coretypes.RoleTool:
		role = openai.ChatMessageRoleTool
	}

	msg := openai.ChatCompletionMessage{
		Role:       role,
		Content:    m.Content,
		ToolCallID: m.ToolCallID,
	}

	// Assistant messages with tool calls
	if m.Role == coretypes.RoleAssistant && len(m.ToolCalls) > 0 {
		msg.ToolCalls = make([]openai.ToolCall, len(m.ToolCalls))
		for i, tc := range m.ToolCalls {
			msg.ToolCalls[i] = openai.ToolCall{
				ID:   tc.ID,
				Type: openai.ToolTypeFunction,
				Function: openai.FunctionCall{
					Name:      tc.Name,
					Arguments: tc.Arguments,
				},
			}
		}
	}

	return msg
}

// toOpenAITools converts core ToolSpecs to OpenAI Tool format.
func toOpenAITools(specs []coretypes.ToolSpec) []openai.Tool {
	tools := make([]openai.Tool, len(specs))
	for i, s := range specs {
		tools[i] = openai.Tool{
			Type: openai.ToolTypeFunction,
			Function: &openai.FunctionDefinition{
				Name:        s.Name,
				Description: s.Description,
				Parameters:  json.RawMessage(marshalSchema(s.InputSchema)),
			},
		}
	}
	return tools
}

// marshalSchema marshals a JSON schema map to bytes. Falls back to empty object.
func marshalSchema(schema map[string]any) []byte {
	b, err := json.Marshal(schema)
	if err != nil {
		return []byte("{}")
	}
	return b
}
