package providers

import (
	"chat-switchboard/capabilities"
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"
)

// OpenAIProvider handles any OpenAI-compatible API.
type OpenAIProvider struct{}

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

// ── Chat Completion (non-streaming) ─────────

func (p *OpenAIProvider) ChatCompletion(ctx context.Context, cfg ProviderConfig, req CompletionRequest) (*CompletionResponse, error) {
	req.Stream = false

	body, err := p.doRequest(ctx, cfg, req)
	if err != nil {
		return nil, err
	}
	defer body.Close()

	var resp openaiChatResponse
	if err := json.NewDecoder(body).Decode(&resp); err != nil {
		return nil, fmt.Errorf("decode response: %w", err)
	}

	if len(resp.Choices) == 0 {
		return nil, fmt.Errorf("no choices in response")
	}

	// Extract content string from response (responses are always string, not array)
	var contentStr string
	if s, ok := resp.Choices[0].Message.Content.(string); ok {
		contentStr = s
	}

	result := &CompletionResponse{
		Content:      contentStr,
		Model:        resp.Model,
		FinishReason: resp.Choices[0].FinishReason,
		InputTokens:  resp.Usage.PromptTokens,
		OutputTokens: resp.Usage.CompletionTokens,
	}

	// Cache tokens (OpenAI, Venice report these in prompt_tokens_details)
	if resp.Usage.PromptTokensDetails != nil {
		result.CacheReadTokens = resp.Usage.PromptTokensDetails.CachedTokens
	}

	// Extract tool calls if present
	if len(resp.Choices[0].Message.ToolCalls) > 0 {
		for _, tc := range resp.Choices[0].Message.ToolCalls {
			result.ToolCalls = append(result.ToolCalls, ToolCall{
				ID:   tc.ID,
				Type: tc.Type,
				Function: FunctionCall{
					Name:      tc.Function.Name,
					Arguments: tc.Function.Arguments,
				},
			})
		}
		if result.FinishReason == "" {
			result.FinishReason = "tool_calls"
		}
	}

	return result, nil
}

// ── Stream Completion ───────────────────────

func (p *OpenAIProvider) StreamCompletion(ctx context.Context, cfg ProviderConfig, req CompletionRequest) (<-chan StreamEvent, error) {
	req.Stream = true

	body, err := p.doRequest(ctx, cfg, req)
	if err != nil {
		return nil, err
	}

	ch := make(chan StreamEvent, 64)

	go func() {
		defer close(ch)
		defer body.Close()

		// Accumulate tool calls across stream chunks
		toolCallMap := map[int]*ToolCall{} // index → accumulated call

		// Usage arrives in a separate chunk (or on the final chunk).
		// OpenAI protocol order: content → finish_reason → usage → [DONE]
		// The usage chunk has choices=[] so we must hold the finish event
		// until usage arrives, otherwise tokens are always 0.
		var streamUsage *openaiUsage
		var pendingFinish *StreamEvent // deferred until usage arrives

		scanner := bufio.NewScanner(body)
		for scanner.Scan() {
			line := scanner.Text()

			if !strings.HasPrefix(line, "data: ") {
				continue
			}

			data := strings.TrimPrefix(line, "data: ")
			if data == "[DONE]" {
				// Flush any pending finish event (usage may or may not have arrived)
				if pendingFinish != nil {
					ch <- *pendingFinish
				} else {
					ch <- StreamEvent{Done: true}
				}
				return
			}

			var chunk openaiStreamChunk
			if err := json.Unmarshal([]byte(data), &chunk); err != nil {
				continue
			}

			// Capture usage — may arrive in a separate chunk with empty choices
			if chunk.Usage != nil {
				streamUsage = chunk.Usage
				// If finish already arrived, attach usage and flush immediately
				if pendingFinish != nil {
					pendingFinish.InputTokens = streamUsage.PromptTokens
					pendingFinish.OutputTokens = streamUsage.CompletionTokens
					if streamUsage.PromptTokensDetails != nil {
						pendingFinish.CacheReadTokens = streamUsage.PromptTokensDetails.CachedTokens
					}
					ch <- *pendingFinish
					return // Done — [DONE] will be handled by defer body.Close()
				}
			}

			if len(chunk.Choices) == 0 {
				continue
			}

			choice := chunk.Choices[0]

			// Accumulate tool call deltas
			for _, tc := range choice.Delta.ToolCalls {
				idx := 0
				if tc.Index != nil {
					idx = *tc.Index
				}

				if existing, ok := toolCallMap[idx]; ok {
					// Append arguments fragment
					existing.Function.Arguments += tc.Function.Arguments
				} else {
					// First chunk for this tool — has ID and name
					toolCallMap[idx] = &ToolCall{
						ID:   tc.ID,
						Type: "function",
						Function: FunctionCall{
							Name:      tc.Function.Name,
							Arguments: tc.Function.Arguments,
						},
					}
				}
			}

			ev := StreamEvent{
				Delta:        choice.Delta.Content,
				Reasoning:    choice.Delta.ReasoningContent,
				Model:        chunk.Model,
				FinishReason: choice.FinishReason,
			}

			if ev.FinishReason != "" {
				ev.Done = true
				// Attach accumulated tool calls on final event
				if ev.FinishReason == "tool_calls" && len(toolCallMap) > 0 {
					for _, tc := range toolCallMap {
						ev.ToolCalls = append(ev.ToolCalls, *tc)
					}
					// Tool calls need to flush immediately — the tool loop
					// needs the event to start executing tools
					if streamUsage != nil {
						ev.InputTokens = streamUsage.PromptTokens
						ev.OutputTokens = streamUsage.CompletionTokens
						if streamUsage.PromptTokensDetails != nil {
							ev.CacheReadTokens = streamUsage.PromptTokensDetails.CachedTokens
						}
					}
					ch <- ev
					continue
				}
				// Normal finish — defer until usage chunk arrives
				if streamUsage != nil {
					// Usage already captured (rare: same chunk or earlier)
					ev.InputTokens = streamUsage.PromptTokens
					ev.OutputTokens = streamUsage.CompletionTokens
					if streamUsage.PromptTokensDetails != nil {
						ev.CacheReadTokens = streamUsage.PromptTokensDetails.CachedTokens
					}
					ch <- ev
					continue
				}
				// Hold — usage chunk hasn't arrived yet
				pendingFinish = &ev
				continue
			}

			ch <- ev
		}

		if err := scanner.Err(); err != nil {
			ch <- StreamEvent{Error: err}
		} else if pendingFinish != nil {
			// Scanner ended without [DONE] — flush pending finish
			ch <- *pendingFinish
		}
	}()

	return ch, nil
}

// ── List Models ─────────────────────────────

func (p *OpenAIProvider) ListModels(ctx context.Context, cfg ProviderConfig) ([]Model, error) {
	endpoint := strings.TrimSuffix(cfg.Endpoint, "/") + "/models"

	httpReq, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil)
	if err != nil {
		return nil, err
	}
	if cfg.APIKey != "" {
		httpReq.Header.Set("Authorization", "Bearer "+cfg.APIKey)
	}
	for k, v := range cfg.CustomHeaders {
		httpReq.Header.Set(k, v)
	}

	resp, err := cfg.Client().Do(httpReq)
	if err != nil {
		return nil, fmt.Errorf("list models: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		b, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("list models: HTTP %d: %s", resp.StatusCode, string(b))
	}

	var result openaiModelsResponse
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("decode models: %w", err)
	}

	out := make([]Model, 0, len(result.Data))
	for _, m := range result.Data {
		// Heuristic inference from model ID
		caps := capabilities.InferCapabilities(m.ID)
		// Use context_length from API if available and we don't have it
		if m.ContextLength > 0 && caps.MaxContext == 0 {
			caps.MaxContext = m.ContextLength
		}

		out = append(out, Model{
			ID:           m.ID,
			Type:         m.Type, // passthrough from API if present (empty → "chat" at sync time)
			OwnedBy:      m.OwnedBy,
			Capabilities: caps,
		})
	}
	return out, nil
}

// ── Embeddings ─────────────────────────────

func (p *OpenAIProvider) Embed(ctx context.Context, cfg ProviderConfig, req EmbeddingRequest) (*EmbeddingResponse, error) {
	endpoint := strings.TrimSuffix(cfg.Endpoint, "/") + "/embeddings"

	oaiReq := openaiEmbeddingRequest{
		Model: req.Model,
		Input: req.Input,
	}
	body, err := json.Marshal(oaiReq)
	if err != nil {
		return nil, fmt.Errorf("marshal embedding request: %w", err)
	}

	httpReq, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	httpReq.Header.Set("Content-Type", "application/json")
	if cfg.APIKey != "" {
		httpReq.Header.Set("Authorization", "Bearer "+cfg.APIKey)
	}
	for k, v := range cfg.CustomHeaders {
		httpReq.Header.Set(k, v)
	}

	resp, err := cfg.Client().Do(httpReq)
	if err != nil {
		return nil, fmt.Errorf("embedding request: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		b, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("embedding error: HTTP %d: %s", resp.StatusCode, string(b))
	}

	var oaiResp openaiEmbeddingResponse
	if err := json.NewDecoder(resp.Body).Decode(&oaiResp); err != nil {
		return nil, fmt.Errorf("decode embedding response: %w", err)
	}

	result := &EmbeddingResponse{
		Model:       oaiResp.Model,
		InputTokens: oaiResp.Usage.PromptTokens,
		Embeddings:  make([][]float64, len(oaiResp.Data)),
	}
	for i, d := range oaiResp.Data {
		result.Embeddings[i] = d.Embedding
	}
	return result, nil
}

// ── HTTP Layer ──────────────────────────────

func (p *OpenAIProvider) doRequest(ctx context.Context, cfg ProviderConfig, req CompletionRequest) (io.ReadCloser, error) {
	endpoint := strings.TrimSuffix(cfg.Endpoint, "/") + "/chat/completions"

	// Build OpenAI-format request
	oaiReq := openaiChatRequest{
		Model:    req.Model,
		Stream:   req.Stream,
		Messages: make([]openaiMessage, 0, len(req.Messages)),
	}
	if req.Stream {
		oaiReq.StreamOptions = &openaiStreamOptions{IncludeUsage: true}
	}
	if req.MaxTokens > 0 {
		oaiReq.MaxTokens = req.MaxTokens
	}
	if req.Temperature != nil {
		oaiReq.Temperature = req.Temperature
	}
	if req.TopP != nil {
		oaiReq.TopP = req.TopP
	}

	// Convert tools
	for _, t := range req.Tools {
		oaiReq.Tools = append(oaiReq.Tools, openaiToolDef{
			Type: t.Type,
			Function: openaiToolDefFunction{
				Name:        t.Function.Name,
				Description: t.Function.Description,
				Parameters:  t.Function.Parameters,
			},
		})
	}

	// Convert messages — handle all roles including tool and multimodal
	for _, m := range req.Messages {
		oaiMsg := openaiMessage{
			Role: m.Role,
		}

		// Build content: multimodal parts or plain string
		if len(m.ContentParts) > 0 && m.Role == "user" {
			// Multimodal: convert to OpenAI content array format
			parts := make([]openaiContentPart, 0, len(m.ContentParts))
			for _, p := range m.ContentParts {
				switch p.Type {
				case "image_url":
					if p.ImageURL != nil {
						detail := p.ImageURL.Detail
						if detail == "" {
							detail = "auto"
						}
						parts = append(parts, openaiContentPart{
							Type: "image_url",
							ImageURL: &openaiImageURL{
								URL:    p.ImageURL.URL,
								Detail: detail,
							},
						})
					}
				default: // "text", "document" → text
					parts = append(parts, openaiContentPart{
						Type: "text",
						Text: p.Text,
					})
				}
			}
			oaiMsg.Content = parts
		} else {
			oaiMsg.Content = m.Content
		}

		// Assistant messages with tool calls
		if len(m.ToolCalls) > 0 {
			for _, tc := range m.ToolCalls {
				oaiMsg.ToolCalls = append(oaiMsg.ToolCalls, openaiToolCall{
					ID:   tc.ID,
					Type: tc.Type,
					Function: openaiToolCallFunction{
						Name:      tc.Function.Name,
						Arguments: tc.Function.Arguments,
					},
				})
			}
		}

		// Tool result messages
		if m.Role == "tool" {
			oaiMsg.ToolCallID = m.ToolCallID
			oaiMsg.Name = m.Name
		}

		// Named messages: pass through for speaker attribution (v0.23.0)
		// OpenAI supports "name" on any role to distinguish participants
		if m.Name != "" && m.Role != "tool" {
			oaiMsg.Name = m.Name
		}

		oaiReq.Messages = append(oaiReq.Messages, oaiMsg)
	}

	body, err := json.Marshal(oaiReq)
	if err != nil {
		return nil, fmt.Errorf("marshal request: %w", err)
	}

	// Merge hook-supplied extra fields into wire JSON (v0.22.1)
	if len(req.ExtraBody) > 0 {
		body, err = mergeExtraBody(body, req.ExtraBody)
		if err != nil {
			return nil, fmt.Errorf("merge extra body: %w", err)
		}
	}

	httpReq, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	httpReq.Header.Set("Content-Type", "application/json")
	if cfg.APIKey != "" {
		httpReq.Header.Set("Authorization", "Bearer "+cfg.APIKey)
	}
	// Inject provider-level custom headers (OpenRouter, Venice, etc.)
	for k, v := range cfg.CustomHeaders {
		httpReq.Header.Set(k, v)
	}

	resp, err := cfg.Client().Do(httpReq)
	if err != nil {
		return nil, fmt.Errorf("provider request: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		b, _ := io.ReadAll(resp.Body)
		resp.Body.Close()
		return nil, fmt.Errorf("provider error: HTTP %d: %s", resp.StatusCode, string(b))
	}

	return resp.Body, nil
}

// ── OpenAI Wire Types ───────────────────────

type openaiMessage struct {
	Role         string              `json:"role"`
	Content      interface{}         `json:"content,omitempty"`      // string or []openaiContentPart
	ToolCalls    []openaiToolCall    `json:"tool_calls,omitempty"`   // assistant requesting tools
	ToolCallID   string              `json:"tool_call_id,omitempty"` // role=tool result
	Name         string              `json:"name,omitempty"`         // speaker name (tool or persona attribution)
}

// openaiContentPart represents a single element in a multimodal content array.
// OpenAI format: [{"type":"text","text":"..."}, {"type":"image_url","image_url":{"url":"..."}}]
type openaiContentPart struct {
	Type     string          `json:"type"`
	Text     string          `json:"text,omitempty"`
	ImageURL *openaiImageURL `json:"image_url,omitempty"`
}

type openaiImageURL struct {
	URL    string `json:"url"`
	Detail string `json:"detail,omitempty"`
}

type openaiToolCallFunction struct {
	Name      string `json:"name,omitempty"`
	Arguments string `json:"arguments,omitempty"`
}

type openaiToolCall struct {
	Index    *int                   `json:"index,omitempty"` // present in streaming deltas
	ID       string                 `json:"id,omitempty"`
	Type     string                 `json:"type,omitempty"` // "function"
	Function openaiToolCallFunction `json:"function"`
}

type openaiToolDefFunction struct {
	Name        string          `json:"name"`
	Description string          `json:"description"`
	Parameters  json.RawMessage `json:"parameters"`
}

type openaiToolDef struct {
	Type     string               `json:"type"` // "function"
	Function openaiToolDefFunction `json:"function"`
}

type openaiChatRequest struct {
	Model         string               `json:"model"`
	Messages      []openaiMessage      `json:"messages"`
	MaxTokens     int                  `json:"max_tokens,omitempty"`
	Temperature   *float64             `json:"temperature,omitempty"`
	TopP          *float64             `json:"top_p,omitempty"`
	Stream        bool                 `json:"stream"`
	StreamOptions *openaiStreamOptions `json:"stream_options,omitempty"`
	Tools         []openaiToolDef      `json:"tools,omitempty"`
}

type openaiStreamOptions struct {
	IncludeUsage bool `json:"include_usage"`
}

type openaiChatResponse struct {
	Model   string `json:"model"`
	Choices []struct {
		Message      openaiMessage `json:"message"`
		FinishReason string        `json:"finish_reason"`
	} `json:"choices"`
	Usage openaiUsage `json:"usage"`
}

type openaiUsage struct {
	PromptTokens     int `json:"prompt_tokens"`
	CompletionTokens int `json:"completion_tokens"`
	PromptTokensDetails *struct {
		CachedTokens int `json:"cached_tokens"`
	} `json:"prompt_tokens_details,omitempty"`
	CompletionTokensDetails *struct {
		ReasoningTokens int `json:"reasoning_tokens"`
	} `json:"completion_tokens_details,omitempty"`
}

type openaiStreamChunk struct {
	Model   string `json:"model"`
	Choices []struct {
		Delta struct {
			Content          string           `json:"content"`
			ReasoningContent string           `json:"reasoning_content"`
			ToolCalls        []openaiToolCall `json:"tool_calls,omitempty"`
		} `json:"delta"`
		FinishReason string `json:"finish_reason"`
	} `json:"choices"`
	Usage *openaiUsage `json:"usage,omitempty"`
}

type openaiModelsResponse struct {
	Data []struct {
		ID            string `json:"id"`
		OwnedBy       string `json:"owned_by"`
		Type          string `json:"type,omitempty"`              // Some APIs (e.g. Venice-compat) include model type
		ContextLength int    `json:"context_length,omitempty"` // Ollama, OpenRouter
	} `json:"data"`
}

// ── Embedding Wire Types ──────────────────

type openaiEmbeddingRequest struct {
	Model string   `json:"model"`
	Input []string `json:"input"`
}

type openaiEmbeddingResponse struct {
	Data []struct {
		Embedding []float64 `json:"embedding"`
		Index     int       `json:"index"`
	} `json:"data"`
	Model string `json:"model"`
	Usage struct {
		PromptTokens int `json:"prompt_tokens"`
	} `json:"usage"`
}
