package openai

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

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

const (
	defaultAPIKey     = "ollama"
	defaultTimeoutSec = 300
	responsesPath     = "/v1/responses"
)

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

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

func NewClient(cfg Config, logger *slog.Logger) *Client {
	if cfg.APIKey == "" {
		cfg.APIKey = defaultAPIKey
	}
	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")
	}

	reqBody := responsesRequest{
		Model:           modelName,
		Input:           input.Input,
		Instructions:    input.Instructions,
		Metadata:        buildRequestMetadata(input),
		Stream:          false,
		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 c.logger != nil {
		c.logger.Info(
			"model responses request",
			"endpoint", endpoint,
			"model", modelName,
			"num_ctx", c.cfg.ContextSize,
		)
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return model.GenerateResult{}, err
	}
	defer resp.Body.Close()

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

	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 ""
}
