package openai

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"net/http"
	"net/url"
	"path"
	"strings"
	"time"

	"github.com/nomadcode/nomadcode-core/internal/model"
)

const (
	defaultTimeoutSec = 900
	responsesPath     = "/v1/responses"
)

type Config struct {
	BaseURL              string
	APIKey               string
	Model                string
	ContextSize          int
	TimeoutSec           int
	ModelResponsesStream bool
}

type Client struct {
	cfg        Config
	httpClient *http.Client
	logger     *slog.Logger
}

func NewClient(cfg Config, logger *slog.Logger) *Client {
	if cfg.TimeoutSec <= 0 {
		cfg.TimeoutSec = defaultTimeoutSec
	}
	return &Client{
		cfg:        cfg,
		httpClient: &http.Client{Timeout: time.Duration(cfg.TimeoutSec) * time.Second},
		logger:     logger,
	}
}

func (c *Client) Generate(ctx context.Context, input model.GenerateInput) (model.GenerateResult, error) {
	endpoint, err := responsesURL(c.cfg.BaseURL)
	if err != nil {
		return model.GenerateResult{}, err
	}

	modelName := strings.TrimSpace(input.Model)
	if modelName == "" {
		modelName = c.cfg.Model
	}
	if strings.TrimSpace(modelName) == "" {
		return model.GenerateResult{}, fmt.Errorf("model name is required")
	}
	if strings.TrimSpace(input.Input) == "" {
		return model.GenerateResult{}, fmt.Errorf("model input is required")
	}

	streamRequested := c.cfg.ModelResponsesStream
	respResult, err := c.executeGenerate(ctx, input, endpoint, modelName, streamRequested)
	return respResult, err
}

func (c *Client) executeGenerate(ctx context.Context, input model.GenerateInput, endpoint, modelName string, stream bool) (model.GenerateResult, error) {
	reqBody := responsesRequest{
		Model:           modelName,
		Input:           input.Input,
		Instructions:    input.Instructions,
		Metadata:        buildRequestMetadata(input),
		Stream:          stream,
		Temperature:     input.Temperature,
		TopP:            input.TopP,
		MaxOutputTokens: input.MaxOutputTokens,
	}
	if c.cfg.ContextSize > 0 {
		reqBody.Options = &responsesOptions{NumCtx: c.cfg.ContextSize}
	}
	body, err := json.Marshal(reqBody)
	if err != nil {
		return model.GenerateResult{}, err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
	if err != nil {
		return model.GenerateResult{}, err
	}
	req.Header.Set("Content-Type", "application/json")
	if c.cfg.APIKey != "" {
		req.Header.Set("Authorization", "Bearer "+c.cfg.APIKey)
	}
	if stream {
		req.Header.Set("Accept", "text/event-stream")
	}

	if c.logger != nil {
		c.logger.Info(
			"model responses request",
			"endpoint", endpoint,
			"model", modelName,
			"num_ctx", c.cfg.ContextSize,
			"stream", stream,
		)
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		if stream && isStreamUnsupportedError(err) {
			if input.OnProgress != nil {
				input.OnProgress(model.GenerateProgress{
					Mode:          "stream_unsupported",
					Reason:        err.Error(),
					LastEventTime: time.Now().UTC(),
				})
			}
			return c.executeGenerate(ctx, input, endpoint, modelName, false)
		}
		return model.GenerateResult{}, err
	}
	defer resp.Body.Close()

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		raw, _ := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
		errRes := responseError(resp.StatusCode, raw)
		if stream && isStreamUnsupportedError(errRes) {
			if input.OnProgress != nil {
				input.OnProgress(model.GenerateProgress{
					Mode:          "stream_unsupported",
					Reason:        errRes.Error(),
					LastEventTime: time.Now().UTC(),
				})
			}
			return c.executeGenerate(ctx, input, endpoint, modelName, false)
		}
		return model.GenerateResult{}, errRes
	}

	if stream {
		reader := bufio.NewReader(resp.Body)
		var finalResponse responsesResponse
		var textBuilder strings.Builder
		var currentEvent string
		for {
			line, err := reader.ReadString('\n')
			if err != nil {
				if err == io.EOF {
					break
				}
				return model.GenerateResult{}, err
			}
			line = strings.TrimSpace(line)
			if line == "" {
				continue
			}
			if strings.HasPrefix(line, "event: ") {
				currentEvent = strings.TrimPrefix(line, "event: ")
				continue
			}
			if !strings.HasPrefix(line, "data: ") {
				continue
			}
			dataStr := strings.TrimPrefix(line, "data: ")
			if dataStr == "[DONE]" {
				break
			}

			var ev responsesStreamEvent
			errEv := json.Unmarshal([]byte(dataStr), &ev)
			if errEv == nil && (ev.Type != "" || ev.Error != nil || ev.Response != nil) {
				eventType := ev.Type
				if eventType == "" {
					eventType = currentEvent
				}

				switch eventType {
				case "response.output_text.delta":
					textBuilder.WriteString(ev.Delta)
					if input.OnProgress != nil {
						input.OnProgress(model.GenerateProgress{
							Mode:          "streaming",
							Reason:        "receiving stream chunks",
							LastEventTime: time.Now().UTC(),
						})
					}
				case "response.completed":
					if ev.Response != nil {
						finalResponse.ID = ev.Response.ID
						finalResponse.Model = ev.Response.Model
						if ev.Response.Usage != nil {
							finalResponse.Usage = ev.Response.Usage
						}
					}
				case "error":
					if ev.Error != nil {
						return model.GenerateResult{}, fmt.Errorf("responses API stream error: %s", ev.Error.Message)
					}
					return model.GenerateResult{}, fmt.Errorf("responses API stream error: unknown error")
				}
			} else {
				var chunk responsesResponse
				errChunk := json.Unmarshal([]byte(dataStr), &chunk)
				if errChunk == nil {
					if chunk.ID != "" {
						finalResponse.ID = chunk.ID
					}
					if chunk.Model != "" {
						finalResponse.Model = chunk.Model
					}
					if chunk.Usage != nil {
						if finalResponse.Usage == nil {
							finalResponse.Usage = &responsesUsage{}
						}
						if chunk.Usage.InputTokens != 0 {
							finalResponse.Usage.InputTokens = chunk.Usage.InputTokens
						}
						if chunk.Usage.OutputTokens != 0 {
							finalResponse.Usage.OutputTokens = chunk.Usage.OutputTokens
						}
						if chunk.Usage.TotalTokens != 0 {
							finalResponse.Usage.TotalTokens = chunk.Usage.TotalTokens
						}
						if chunk.Usage.PromptTokens != 0 {
							finalResponse.Usage.PromptTokens = chunk.Usage.PromptTokens
						}
						if chunk.Usage.CompletionTokens != 0 {
							finalResponse.Usage.CompletionTokens = chunk.Usage.CompletionTokens
						}
					}
					chunkText := chunk.text()
					if chunkText != "" {
						textBuilder.WriteString(chunkText)
					}
					if input.OnProgress != nil {
						input.OnProgress(model.GenerateProgress{
							Mode:          "streaming",
							Reason:        "receiving stream chunks",
							LastEventTime: time.Now().UTC(),
						})
					}
				} else {
					return model.GenerateResult{}, fmt.Errorf("failed to parse SSE data: %s (errEv: %v, errChunk: %v)", dataStr, errEv, errChunk)
				}
			}
			currentEvent = ""
		}

		finalResponse.OutputText = textBuilder.String()
		raw, _ := json.Marshal(finalResponse)

		usage := model.Usage{}
		if finalResponse.Usage != nil {
			usage.InputTokens = firstNonZero(finalResponse.Usage.InputTokens, finalResponse.Usage.PromptTokens)
			usage.OutputTokens = firstNonZero(finalResponse.Usage.OutputTokens, finalResponse.Usage.CompletionTokens)
			usage.TotalTokens = finalResponse.Usage.TotalTokens
			if usage.TotalTokens == 0 {
				usage.TotalTokens = usage.InputTokens + usage.OutputTokens
			}
		}

		return model.GenerateResult{
			ID:    finalResponse.ID,
			Model: firstNonEmpty(finalResponse.Model, modelName),
			Text:  finalResponse.text(),
			Usage: usage,
			Raw:   json.RawMessage(raw),
		}, nil
	}

	if input.OnProgress != nil {
		input.OnProgress(model.GenerateProgress{
			Mode:          "non_streaming",
			Reason:        "non-streaming mode",
			LastEventTime: time.Now().UTC(),
		})
	}

	raw, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20))
	if err != nil {
		return model.GenerateResult{}, err
	}

	var parsed responsesResponse
	if err := json.Unmarshal(raw, &parsed); err != nil {
		return model.GenerateResult{}, err
	}

	usage := model.Usage{}
	if parsed.Usage != nil {
		usage.InputTokens = firstNonZero(parsed.Usage.InputTokens, parsed.Usage.PromptTokens)
		usage.OutputTokens = firstNonZero(parsed.Usage.OutputTokens, parsed.Usage.CompletionTokens)
		usage.TotalTokens = parsed.Usage.TotalTokens
		if usage.TotalTokens == 0 {
			usage.TotalTokens = usage.InputTokens + usage.OutputTokens
		}
	}

	return model.GenerateResult{
		ID:    parsed.ID,
		Model: firstNonEmpty(parsed.Model, modelName),
		Text:  parsed.text(),
		Usage: usage,
		Raw:   json.RawMessage(raw),
	}, nil
}

type responsesRequest struct {
	Model           string            `json:"model"`
	Input           string            `json:"input"`
	Instructions    string            `json:"instructions,omitempty"`
	Metadata        map[string]any    `json:"metadata,omitempty"`
	Stream          bool              `json:"stream"`
	Temperature     *float64          `json:"temperature,omitempty"`
	TopP            *float64          `json:"top_p,omitempty"`
	MaxOutputTokens int               `json:"max_output_tokens,omitempty"`
	Options         *responsesOptions `json:"options,omitempty"`
}

type responsesOptions struct {
	NumCtx int `json:"num_ctx,omitempty"`
}

type responsesResponse struct {
	ID         string            `json:"id"`
	Model      string            `json:"model"`
	OutputText string            `json:"output_text"`
	Output     []responsesOutput `json:"output"`
	Usage      *responsesUsage   `json:"usage"`
}

type responsesOutput struct {
	Type    string             `json:"type"`
	Text    string             `json:"text"`
	Content []responsesContent `json:"content"`
}

type responsesContent struct {
	Type string `json:"type"`
	Text string `json:"text"`
}

type responsesUsage struct {
	InputTokens      int `json:"input_tokens"`
	OutputTokens     int `json:"output_tokens"`
	TotalTokens      int `json:"total_tokens"`
	PromptTokens     int `json:"prompt_tokens"`
	CompletionTokens int `json:"completion_tokens"`
}

func (r responsesResponse) text() string {
	if r.OutputText != "" {
		return r.OutputText
	}

	var b strings.Builder
	for _, output := range r.Output {
		if output.Text != "" {
			b.WriteString(output.Text)
		}
		for _, content := range output.Content {
			if content.Text != "" {
				b.WriteString(content.Text)
			}
		}
	}
	return b.String()
}

type errorResponse struct {
	Error struct {
		Message string `json:"message"`
		Type    string `json:"type"`
		Code    string `json:"code"`
	} `json:"error"`
}

func responseError(status int, raw []byte) error {
	var parsed errorResponse
	if err := json.Unmarshal(raw, &parsed); err == nil && parsed.Error.Message != "" {
		return fmt.Errorf("responses API request failed: status %d: %s", status, parsed.Error.Message)
	}
	msg := strings.TrimSpace(string(raw))
	if msg == "" {
		msg = http.StatusText(status)
	}
	return fmt.Errorf("responses API request failed: status %d: %s", status, msg)
}

func responsesURL(base string) (string, error) {
	base = strings.TrimSpace(base)
	if base == "" {
		return "", fmt.Errorf("model base URL is required")
	}

	parsed, err := url.Parse(base)
	if err != nil {
		return "", err
	}
	if parsed.Scheme == "" || parsed.Host == "" {
		return "", fmt.Errorf("model base URL must include scheme and host")
	}

	cleanPath := strings.TrimRight(parsed.Path, "/")
	switch {
	case strings.HasSuffix(cleanPath, "/v1/responses"):
		parsed.Path = cleanPath
	case strings.HasSuffix(cleanPath, "/v1"):
		parsed.Path = path.Join(cleanPath, "responses")
	default:
		parsed.Path = path.Join(cleanPath, responsesPath)
	}
	return parsed.String(), nil
}

// buildRequestMetadata merges flat string metadata with typed WorkspaceMetadata.
// WorkspaceMetadata wins on key collision ("workspace" key).
func buildRequestMetadata(input model.GenerateInput) map[string]any {
	if len(input.Metadata) == 0 && input.WorkspaceMetadata == nil {
		return nil
	}
	merged := make(map[string]any, len(input.Metadata)+1)
	for k, v := range input.Metadata {
		merged[k] = v
	}
	if input.WorkspaceMetadata != nil {
		if path := strings.TrimSpace(input.WorkspaceMetadata.Path); path != "" {
			merged["workspace"] = path
		}
	}
	return merged
}

func firstNonZero(values ...int) int {
	for _, value := range values {
		if value != 0 {
			return value
		}
	}
	return 0
}

func firstNonEmpty(values ...string) string {
	for _, value := range values {
		if value != "" {
			return value
		}
	}
	return ""
}

type responsesStreamEvent struct {
	Type     string `json:"type"`
	Delta    string `json:"delta,omitempty"`
	Response *struct {
		ID    string          `json:"id,omitempty"`
		Model string          `json:"model,omitempty"`
		Usage *responsesUsage `json:"usage,omitempty"`
	} `json:"response,omitempty"`
	Error *struct {
		Message string `json:"message,omitempty"`
		Type    string `json:"type,omitempty"`
		Code    string `json:"code,omitempty"`
	} `json:"error,omitempty"`
}

func isStreamUnsupportedError(err error) bool {
	if err == nil {
		return false
	}
	errStr := strings.ToLower(err.Error())
	signals := []string{
		"stream",
		"streaming",
		"unsupported",
		"not supported",
		"unsupported_parameter",
	}
	for _, sig := range signals {
		if strings.Contains(errStr, sig) {
			return true
		}
	}
	return false
}
