package provider

import (
	"bytes"
	"compress/gzip"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/seifghazi/claude-code-monitor/internal/config"
	"github.com/seifghazi/claude-code-monitor/internal/model"
	"github.com/seifghazi/claude-code-monitor/internal/sse"
)

type OpenAIProvider struct {
	client *http.Client
	config *config.OpenAIProviderConfig
}

func NewOpenAIProvider(cfg *config.OpenAIProviderConfig) Provider {
	return &OpenAIProvider{
		client: &http.Client{
			// No timeout set here - we rely on context cancellation for timeouts.
			// Setting Timeout here would apply to the entire request+response cycle,
			// which causes "context canceled" errors for long-running streaming
			// requests with large "thinking" content blocks.
			// The server's WriteTimeout handles individual write operations,
			// and the context passed to ForwardRequest controls the overall timeout.
			Transport: &http.Transport{
				// Connection timeouts
				DialContext: (&net.Dialer{
					Timeout:   30 * time.Second,
					KeepAlive: 30 * time.Second,
				}).DialContext,
				TLSHandshakeTimeout:   30 * time.Second,
				ResponseHeaderTimeout: 300 * time.Second, // Time to wait for response headers (high for 1M context)
				ExpectContinueTimeout: 1 * time.Second,
				// Connection pooling
				MaxIdleConns:        100,
				MaxIdleConnsPerHost: 10,
				IdleConnTimeout:     90 * time.Second,
			},
		},
		config: cfg,
	}
}

func (p *OpenAIProvider) Name() string {
	return "openai"
}

func (p *OpenAIProvider) ForwardRequest(ctx context.Context, originalReq *http.Request) (*http.Response, error) {
	// First, we need to convert the Anthropic request to OpenAI format
	bodyBytes, err := io.ReadAll(originalReq.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read request body: %w", err)
	}
	originalReq.Body = io.NopCloser(bytes.NewReader(bodyBytes))

	var anthropicReq model.AnthropicRequest
	if err := json.Unmarshal(bodyBytes, &anthropicReq); err != nil {
		return nil, fmt.Errorf("failed to parse anthropic request: %w", err)
	}

	// Convert to OpenAI format
	openAIReq := convertAnthropicToOpenAI(&anthropicReq)
	newBodyBytes, err := json.Marshal(openAIReq)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal openai request: %w", err)
	}

	// Clone the request with new body
	proxyReq := originalReq.Clone(ctx)
	proxyReq.Body = io.NopCloser(bytes.NewReader(newBodyBytes))
	proxyReq.ContentLength = int64(len(newBodyBytes))

	// Parse the configured base URL
	baseURL, err := url.Parse(p.config.BaseURL)
	if err != nil {
		return nil, fmt.Errorf("failed to parse base URL '%s': %w", p.config.BaseURL, err)
	}

	// Update the destination URL for OpenAI
	proxyReq.URL.Scheme = baseURL.Scheme
	proxyReq.URL.Host = baseURL.Host
	proxyReq.URL.Path = "/v1/chat/completions" // OpenAI endpoint

	// Update request headers
	proxyReq.RequestURI = ""
	proxyReq.Host = baseURL.Host

	// Remove Anthropic-specific headers
	proxyReq.Header.Del("anthropic-version")
	proxyReq.Header.Del("x-api-key")
	proxyReq.Header.Del("Authorization")

	// Determine which API key to use
	apiKey := p.config.APIKey

	// Check for client-provided API key if allowed
	if p.config.AllowClientAPIKey && p.config.ClientAPIKeyHeader != "" {
		if clientKey := originalReq.Header.Get(p.config.ClientAPIKeyHeader); clientKey != "" {
			apiKey = clientKey
		}
	}

	// Add OpenAI headers
	if apiKey != "" {
		proxyReq.Header.Set("Authorization", "Bearer "+apiKey)
	}
	proxyReq.Header.Set("Content-Type", "application/json")

	// Remove the client API key header from the proxied request
	if p.config.ClientAPIKeyHeader != "" {
		proxyReq.Header.Del(p.config.ClientAPIKeyHeader)
	}

	// Forward the request
	resp, err := p.client.Do(proxyReq)
	if err != nil {
		return nil, fmt.Errorf("failed to forward request: %w", err)
	}

	// Check for error responses
	if resp.StatusCode >= 400 {
		// Read the error body for debugging
		errorBody, _ := io.ReadAll(resp.Body)
		resp.Body.Close()

		// Log the error details
		// OpenAI API error - will be returned to client

		// Create an error response in Anthropic format
		errorResp := map[string]interface{}{
			"type": "error",
			"error": map[string]interface{}{
				"type":    "api_error",
				"message": fmt.Sprintf("OpenAI API error: %s", string(errorBody)),
			},
		}
		errorJSON, _ := json.Marshal(errorResp)

		// Create a new response with the error
		resp.Body = io.NopCloser(bytes.NewReader(errorJSON))
		resp.Header.Set("Content-Type", "application/json")
		resp.Header.Del("Content-Encoding")
		resp.ContentLength = int64(len(errorJSON))

		return resp, nil
	}

	// Handle gzip-encoded responses
	var bodyReader io.ReadCloser = resp.Body
	if resp.Header.Get("Content-Encoding") == "gzip" {
		gzReader, err := gzip.NewReader(resp.Body)
		if err != nil {
			resp.Body.Close()
			return nil, fmt.Errorf("failed to create gzip reader: %w", err)
		}
		bodyReader = gzReader
		resp.Header.Del("Content-Encoding")
		resp.Header.Del("Content-Length")
	}

	// For streaming responses, we need to convert back to Anthropic format
	if anthropicReq.Stream {
		// Create a pipe to transform the response
		pr, pw := io.Pipe()

		// Start a goroutine to transform the stream
		go func() {
			defer bodyReader.Close()
			if err := transformOpenAIStreamToAnthropic(bodyReader, pw); err != nil {
				_ = pw.CloseWithError(err)
				return
			}
			_ = pw.Close()
		}()

		// Replace the response body with our transformed stream
		resp.Body = pr
	} else {
		// For non-streaming, read and convert the response
		respBody, err := io.ReadAll(bodyReader)
		bodyReader.Close()
		if err != nil {
			return nil, fmt.Errorf("failed to read response body: %w", err)
		}

		// Convert OpenAI response back to Anthropic format
		transformedBody := transformOpenAIResponseToAnthropic(respBody)
		resp.Body = io.NopCloser(bytes.NewReader(transformedBody))
		resp.ContentLength = int64(len(transformedBody))
		resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(transformedBody)))
	}

	return resp, nil
}

// extractSystemMessages combines all system messages into a single string for OpenAI.
func extractSystemMessages(system []model.AnthropicSystemMessage) string {
	if len(system) == 0 {
		return ""
	}
	var parts []string
	for _, sysMsg := range system {
		parts = append(parts, sysMsg.Text)
	}
	return strings.Join(parts, "\n\n")
}

// convertToolResultContent converts the various formats of tool result content to a string.
func convertToolResultContent(content interface{}) string {
	if content == nil {
		return ""
	}

	switch v := content.(type) {
	case string:
		return v
	case []interface{}:
		var result string
		for _, c := range v {
			if contentMap, ok := c.(map[string]interface{}); ok {
				if contentMap["type"] == "text" {
					if text, ok := contentMap["text"].(string); ok {
						result += text + "\n"
					}
				} else if text, hasText := contentMap["text"]; hasText {
					result += fmt.Sprintf("%v\n", text)
				} else {
					if jsonBytes, err := json.Marshal(contentMap); err == nil {
						result += string(jsonBytes) + "\n"
					} else {
						result += fmt.Sprintf("%v\n", contentMap)
					}
				}
			}
		}
		return result
	case map[string]interface{}:
		if v["type"] == "text" {
			if text, ok := v["text"].(string); ok {
				return text
			}
		}
		if jsonBytes, err := json.Marshal(v); err == nil {
			return string(jsonBytes)
		}
		return fmt.Sprintf("%v", v)
	default:
		if jsonBytes, err := json.Marshal(content); err == nil {
			return string(jsonBytes)
		}
		return fmt.Sprintf("%v", content)
	}
}

// convertMessageContent converts an Anthropic message's content to a plain text string for OpenAI.
// It handles both content arrays (with possible tool results) and simple string content.
func convertMessageContent(msg model.AnthropicMessage) string {
	contentArray, ok := msg.Content.([]interface{})
	if !ok {
		// Handle simple string content via GetContentBlocks
		contentBlocks := msg.GetContentBlocks()
		var parts []string
		for _, block := range contentBlocks {
			if block.Type == "text" {
				parts = append(parts, block.Text)
			}
		}
		content := strings.Join(parts, "\n")
		if content == "" {
			content = "..."
		}
		return content
	}

	// Check if this message contains tool results
	hasToolResults := false
	for _, item := range contentArray {
		if block, ok := item.(map[string]interface{}); ok {
			if blockType, _ := block["type"].(string); blockType == "tool_result" {
				hasToolResults = true
				break
			}
		}
	}

	if hasToolResults {
		return convertContentArrayWithToolResults(contentArray)
	}
	return convertRegularContentArray(contentArray)
}

// convertContentArrayWithToolResults handles content arrays that contain tool_result blocks.
func convertContentArrayWithToolResults(contentArray []interface{}) string {
	textContent := ""

	for _, item := range contentArray {
		block, ok := item.(map[string]interface{})
		if !ok {
			continue
		}
		blockType, _ := block["type"].(string)
		switch blockType {
		case "text":
			if text, ok := block["text"].(string); ok {
				textContent += text + "\n"
			}
		case "tool_result":
			toolID := ""
			if id, ok := block["tool_use_id"].(string); ok {
				toolID = id
			}
			resultContent := convertToolResultContent(block["content"])
			textContent += fmt.Sprintf("Tool result for %s:\n%s\n", toolID, resultContent)
		}
	}

	if textContent == "" {
		return "..."
	}
	return strings.TrimSpace(textContent)
}

// convertRegularContentArray handles content arrays with only text blocks.
func convertRegularContentArray(contentArray []interface{}) string {
	var parts []string
	for _, item := range contentArray {
		if block, ok := item.(map[string]interface{}); ok {
			if blockType, _ := block["type"].(string); blockType == "text" {
				if text, ok := block["text"].(string); ok {
					parts = append(parts, text)
				}
			}
		}
	}
	content := strings.Join(parts, "\n")
	if content == "" {
		content = "..."
	}
	return content
}

// convertToolsToOpenAI converts Anthropic tool definitions to OpenAI format.
func convertToolsToOpenAI(tools []model.Tool) []map[string]interface{} {
	result := make([]map[string]interface{}, 0, len(tools))
	for _, tool := range tools {
		if tool.Name == "" {
			continue
		}

		parameters := make(map[string]interface{})
		if tool.InputSchema.Type != nil {
			parameters["type"] = tool.InputSchema.Type
		} else {
			parameters["type"] = "object"
		}

		if tool.InputSchema.Properties != nil {
			fixedProperties := make(map[string]interface{})
			for propName, propValue := range tool.InputSchema.Properties {
				if prop, ok := propValue.(map[string]interface{}); ok {
					if propType, hasType := prop["type"]; hasType && propType == "array" {
						if _, hasItems := prop["items"]; !hasItems {
							prop["items"] = map[string]interface{}{"type": "string"}
						}
					}
					fixedProperties[propName] = prop
				} else {
					fixedProperties[propName] = propValue
				}
			}
			parameters["properties"] = fixedProperties
		} else {
			parameters["properties"] = make(map[string]interface{})
		}

		if len(tool.InputSchema.Required) > 0 {
			parameters["required"] = tool.InputSchema.Required
		}

		functionDef := map[string]interface{}{
			"name":       tool.Name,
			"parameters": parameters,
		}
		if tool.Description != "" {
			functionDef["description"] = tool.Description
		}

		result = append(result, map[string]interface{}{
			"type":     "function",
			"function": functionDef,
		})
	}
	return result
}

// convertToolChoice converts Anthropic tool_choice to OpenAI format.
func convertToolChoice(toolChoice interface{}) interface{} {
	if toolChoice == nil {
		return nil
	}
	toolChoiceMap, ok := toolChoice.(map[string]interface{})
	if !ok {
		return nil
	}
	switch toolChoiceMap["type"] {
	case "auto":
		return "auto"
	case "any":
		return "required"
	case "tool":
		if name, ok := toolChoiceMap["name"].(string); ok {
			return map[string]interface{}{
				"type": "function",
				"function": map[string]interface{}{
					"name": name,
				},
			}
		}
		return "auto"
	default:
		return "auto"
	}
}

func convertAnthropicToOpenAI(req *model.AnthropicRequest) map[string]interface{} {
	messages := []map[string]interface{}{}

	// Add system message if present
	if systemContent := extractSystemMessages(req.System); systemContent != "" {
		messages = append(messages, map[string]interface{}{
			"role":    "system",
			"content": systemContent,
		})
	}

	// Convert conversation messages
	for _, msg := range req.Messages {
		messages = append(messages, map[string]interface{}{
			"role":    msg.Role,
			"content": convertMessageContent(msg),
		})
	}

	// Get model-specific max token limit
	maxTokensLimit := getModelMaxTokens(req.Model)
	if maxTokensLimit > 0 && req.MaxTokens > maxTokensLimit {
		req.MaxTokens = maxTokensLimit
	}

	openAIReq := map[string]interface{}{
		"model":                 req.Model,
		"messages":              messages,
		"stream":                req.Stream,
		"max_completion_tokens": req.MaxTokens,
	}

	if req.Stream {
		openAIReq["stream_options"] = map[string]interface{}{
			"include_usage": true,
		}
	}

	// o-series models don't support temperature
	isOSeriesModel := strings.HasPrefix(req.Model, "o1") || strings.HasPrefix(req.Model, "o3")
	if !isOSeriesModel {
		openAIReq["temperature"] = req.Temperature
	}

	// Convert tools and tool_choice
	if len(req.Tools) > 0 {
		openAIReq["tools"] = convertToolsToOpenAI(req.Tools)

		if req.ToolChoice != nil {
			if choice := convertToolChoice(req.ToolChoice); choice != nil {
				openAIReq["tool_choice"] = choice
			}
		}
	}

	return openAIReq
}

func getMapKeys(m map[string]interface{}) []string {
	keys := make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	return keys
}

// getModelMaxTokens returns the max output tokens for known models
// Returns 0 for unknown models, letting the API handle validation
func getModelMaxTokens(model string) int {
	// Model-specific max completion token limits
	modelLimits := map[string]int{
		// GPT-4 Turbo and GPT-4o models
		"gpt-4-turbo":         4096,
		"gpt-4-turbo-preview": 4096,
		"gpt-4o":              16384,
		"gpt-4o-mini":         16384,
		"gpt-4o-2024-05-13":   16384,
		"gpt-4o-2024-08-06":   16384,
		// GPT-4 models
		"gpt-4":      8192,
		"gpt-4-32k":  8192,
		"gpt-4-0613": 8192,
		// GPT-3.5 models
		"gpt-3.5-turbo":      4096,
		"gpt-3.5-turbo-16k":  4096,
		"gpt-3.5-turbo-0125": 4096,
		"gpt-3.5-turbo-1106": 4096,
		// o1 reasoning models
		"o1":         100000,
		"o1-preview": 32768,
		"o1-mini":    65536,
		// o3 reasoning models (estimated based on o1 patterns)
		"o3":      100000,
		"o3-mini": 65536,
	}

	// Check for exact match first
	if limit, ok := modelLimits[model]; ok {
		return limit
	}

	// Check for prefix matches for versioned models
	for prefix, limit := range modelLimits {
		if strings.HasPrefix(model, prefix) {
			return limit
		}
	}

	// Return 0 for unknown models - let the API validate
	return 0
}

func transformOpenAIResponseToAnthropic(respBody []byte) []byte {
	// This is a simplified transformation
	// In production, you'd want to handle all fields properly
	var openAIResp map[string]interface{}
	if err := json.Unmarshal(respBody, &openAIResp); err != nil {
		return respBody // Return as-is if we can't parse
	}

	// Extract the assistant's message
	var contentBlocks []map[string]interface{}

	if choices, ok := openAIResp["choices"].([]interface{}); ok && len(choices) > 0 {
		if choice, ok := choices[0].(map[string]interface{}); ok {
			if msg, ok := choice["message"].(map[string]interface{}); ok {
				// Handle regular text content
				if content, ok := msg["content"].(string); ok && content != "" {
					contentBlocks = append(contentBlocks, map[string]interface{}{
						"type": "text",
						"text": content,
					})
				}

				// Handle tool calls
				if toolCalls, ok := msg["tool_calls"].([]interface{}); ok {
					// Since this proxy forwards to Claude/Anthropic API, we should always
					// use tool_use blocks so Claude can execute the tools properly
					// (regardless of which model generated the response)
					for _, tc := range toolCalls {
						if toolCall, ok := tc.(map[string]interface{}); ok {
							if function, ok := toolCall["function"].(map[string]interface{}); ok {
								// Convert OpenAI tool call to Anthropic tool_use format
								anthropicToolUse := map[string]interface{}{
									"type": "tool_use",
									"id":   toolCall["id"],
									"name": function["name"],
								}

								// Parse the arguments JSON string
								if argsStr, ok := function["arguments"].(string); ok {
									var args map[string]interface{}
									if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
										anthropicToolUse["input"] = args
									} else {
										// If parsing fails, wrap in a raw field like Python does
										// Failed to parse tool arguments - skip
										anthropicToolUse["input"] = map[string]interface{}{"raw": argsStr}
									}
								} else if args, ok := function["arguments"].(map[string]interface{}); ok {
									// Already a map, use directly
									anthropicToolUse["input"] = args
								} else {
									// Fallback for any other type
									anthropicToolUse["input"] = map[string]interface{}{"raw": fmt.Sprintf("%v", function["arguments"])}
								}

								contentBlocks = append(contentBlocks, anthropicToolUse)
							}
						}
					}
				}
			}
		}
	}

	// If no content blocks were created, add a default empty text block
	if len(contentBlocks) == 0 {
		contentBlocks = []map[string]interface{}{
			{"type": "text", "text": ""},
		}
	}

	// Build Anthropic-style response
	anthropicResp := map[string]interface{}{
		"id":      openAIResp["id"],
		"type":    "message",
		"role":    "assistant",
		"content": contentBlocks,
		"model":   openAIResp["model"],
	}

	// Convert OpenAI usage format to Anthropic format
	if usage, ok := openAIResp["usage"].(map[string]interface{}); ok {
		anthropicUsage := map[string]interface{}{}

		// Map prompt_tokens to input_tokens
		if promptTokens, ok := usage["prompt_tokens"].(float64); ok {
			anthropicUsage["input_tokens"] = int(promptTokens)
		}

		// Map completion_tokens to output_tokens
		if completionTokens, ok := usage["completion_tokens"].(float64); ok {
			anthropicUsage["output_tokens"] = int(completionTokens)
		}

		// Include total_tokens if needed (though Anthropic format doesn't typically use it)
		if totalTokens, ok := usage["total_tokens"].(float64); ok {
			anthropicUsage["total_tokens"] = int(totalTokens)
		}

		anthropicResp["usage"] = anthropicUsage
	}

	result, _ := json.Marshal(anthropicResp)
	return result
}

func transformOpenAIStreamToAnthropic(openAIStream io.Reader, anthropicStream io.Writer) error {
	var messageStarted bool
	var contentStarted bool
	var sawDone bool

	err := sse.ForEachLine(openAIStream, func(line string) error {

		// Skip empty lines
		if line == "" {
			return nil
		}

		// Handle SSE data lines
		if strings.HasPrefix(line, "data: ") {
			data := strings.TrimPrefix(line, "data: ")

			// Handle end of stream
			if data == "[DONE]" {
				sawDone = true
				// Send Anthropic-style completion
				if contentStarted {
					if _, err := fmt.Fprintf(anthropicStream, "data: {\"type\":\"content_block_stop\",\"index\":0}\n\n"); err != nil {
						return err
					}
				}
				if messageStarted {
					if _, err := fmt.Fprintf(anthropicStream, "data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null}}\n\n"); err != nil {
						return err
					}
					if _, err := fmt.Fprintf(anthropicStream, "data: {\"type\":\"message_stop\"}\n\n"); err != nil {
						return err
					}
				}
				return nil
			}

			// Parse OpenAI response
			var openAIChunk map[string]interface{}
			if err := json.Unmarshal([]byte(data), &openAIChunk); err != nil {
				return nil
			}

			// Check for usage data BEFORE processing choices
			// According to OpenAI docs, usage is sent in the final chunk with empty choices array
			if usage, hasUsage := openAIChunk["usage"].(map[string]interface{}); hasUsage {
				// Convert OpenAI usage to Anthropic format
				anthropicUsage := map[string]interface{}{}

				// Handle both float64 and int types
				if promptTokens, ok := usage["prompt_tokens"].(float64); ok {
					anthropicUsage["input_tokens"] = int(promptTokens)
				} else if promptTokens, ok := usage["prompt_tokens"].(int); ok {
					anthropicUsage["input_tokens"] = promptTokens
				}

				if completionTokens, ok := usage["completion_tokens"].(float64); ok {
					anthropicUsage["output_tokens"] = int(completionTokens)
				} else if completionTokens, ok := usage["completion_tokens"].(int); ok {
					anthropicUsage["output_tokens"] = completionTokens
				}

				if len(anthropicUsage) > 0 {
					// Send usage data in a message_delta event
					usageDelta := map[string]interface{}{
						"type":  "message_delta",
						"delta": map[string]interface{}{},
						"usage": anthropicUsage,
					}
					usageJSON, _ := json.Marshal(usageDelta)
					if _, err := fmt.Fprintf(anthropicStream, "data: %s\n\n", usageJSON); err != nil {
						return err
					}
				}
			}

			// Extract choices array
			choices, ok := openAIChunk["choices"].([]interface{})
			if !ok || len(choices) == 0 {
				// Skip further processing if no choices, but we already handled usage above
				return nil
			}

			choice, ok := choices[0].(map[string]interface{})
			if !ok {
				return nil
			}

			delta, ok := choice["delta"].(map[string]interface{})
			if !ok {
				return nil
			}

			// Handle first chunk - send message_start
			if !messageStarted {
				messageStarted = true
				messageStart := map[string]interface{}{
					"type": "message_start",
					"message": map[string]interface{}{
						"id":            openAIChunk["id"],
						"type":          "message",
						"role":          "assistant",
						"model":         openAIChunk["model"],
						"content":       []interface{}{},
						"stop_reason":   nil,
						"stop_sequence": nil,
						"usage":         map[string]interface{}{
							// Empty usage - will be updated in final chunk
						},
					},
				}
				startJSON, _ := json.Marshal(messageStart)
				if _, err := fmt.Fprintf(anthropicStream, "data: %s\n\n", startJSON); err != nil {
					return err
				}
			}

			// Handle content
			if content, hasContent := delta["content"].(string); hasContent && content != "" {
				if !contentStarted {
					contentStarted = true
					// Send content_block_start
					blockStart := map[string]interface{}{
						"type":  "content_block_start",
						"index": 0,
						"content_block": map[string]interface{}{
							"type": "text",
							"text": "",
						},
					}
					blockStartJSON, _ := json.Marshal(blockStart)
					if _, err := fmt.Fprintf(anthropicStream, "data: %s\n\n", blockStartJSON); err != nil {
						return err
					}
				}

				// Send content_block_delta
				contentDelta := map[string]interface{}{
					"type":  "content_block_delta",
					"index": 0,
					"delta": map[string]interface{}{
						"type": "text_delta",
						"text": content,
					},
				}
				deltaJSON, _ := json.Marshal(contentDelta)
				if _, err := fmt.Fprintf(anthropicStream, "data: %s\n\n", deltaJSON); err != nil {
					return err
				}
			}

		}
		return nil
	})

	if err != nil {
		return err
	}

	if !sawDone {
		return io.ErrUnexpectedEOF
	}

	return nil
}
