package clients

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"net/textproto"
	"net/url"
	"strings"
	"time"

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)

type OpenAIClient struct {
	HTTPClient  *http.Client
	Corrections *ParameterCorrectionCache
}

func (c OpenAIClient) Run(ctx context.Context, request Request) (Response, error) {
	apiKey := credential(request.Candidate.Credentials, "apiKey", "api_key", "key", "token")
	if apiKey == "" {
		return Response{}, &ClientError{Code: "missing_credentials", Message: "openai api key is required", Retryable: false}
	}
	protocol := request.UpstreamProtocol
	if protocol == "" && request.Kind == "responses" {
		protocol = ProtocolOpenAIResponses
	}
	endpointKind := request.Kind
	if request.Kind == "responses" && protocol == ProtocolOpenAIChatCompletions {
		endpointKind = "chat.completions"
	}
	endpoint := openAIEndpoint(endpointKind)
	if endpoint == "" {
		return Response{}, &ClientError{Code: "unsupported_kind", Message: "unsupported openai request kind", Retryable: false}
	}
	body := cloneBody(request.Body)
	if request.Kind == "responses" && protocol == ProtocolOpenAIChatCompletions {
		var convertErr error
		body, convertErr = ResponsesRequestToChat(request.Body, request.PreviousResponseTurns)
		if convertErr != nil {
			return Response{}, convertErr
		}
	}
	if endpointKind == "chat.completions" {
		body = NormalizeChatCompletionRequestBody(body)
		normalizedBody, normalizeErr := normalizeOpenAIChatAudioParts(body, request.Candidate)
		if normalizeErr != nil {
			return Response{}, normalizeErr
		}
		body = normalizedBody
		if err := applyOpenAIChatReasoningParamsWithSource(body, request.Candidate, openAIAdaptationSource(request, endpointKind)); err != nil {
			return Response{}, err
		}
		body = FilterOpenAIChatRequestBody(body)
	} else if request.Kind == "responses" {
		if err := applyOpenAIResponsesReasoningParamsWithSource(body, request.Candidate, request.OriginalBody); err != nil {
			return Response{}, err
		}
		body = FilterOpenAIResponsesRequestBody(body)
		if _, hasInput := body["input"]; !hasInput {
			if messages, hasMessages := request.Body["messages"]; hasMessages {
				body["input"] = messages
			}
		}
		delete(body, "messages")
		if request.UpstreamPreviousResponseID != "" {
			body["previous_response_id"] = request.UpstreamPreviousResponseID
		} else {
			delete(body, "previous_response_id")
		}
	}
	body["model"] = upstreamModelName(request.Candidate)
	normalizeOpenAIImageRequestBody(endpointKind, body, request.OriginalBody)
	stream := openAIEndpointSupportsStream(endpointKind) && (request.Stream || boolValue(body, "stream"))
	ensureOpenAIStreamUsage(body, endpointKind, stream)
	upstreamEndpoint := joinURL(openAIBaseURL(endpointKind, request.Candidate), endpoint)
	responseStartedAt := time.Now()
	correctionScope := newParameterCorrectionScope(request, endpointKind)
	correctionEnabled := endpointKind == "chat.completions" || endpointKind == "responses" ||
		endpointKind == "images.generations" || endpointKind == "images.edits"
	protectedCorrections := callerProtectedCorrectionParameters(request, endpointKind)
	if correctionEnabled {
		c.Corrections.apply(correctionScope, body, protectedCorrections)
	}
	provisionalRules := make([]parameterCorrectionRule, 0, 2)
	seenCorrectionErrors := make(map[string]struct{})
	var resp *http.Response
	requestClient := httpClient(request.HTTPClient, c.HTTPClient)
	for correctionAttempt := 0; ; correctionAttempt++ {
		raw, contentType, payloadErr := openAIRequestPayload(ctx, endpointKind, body, request.Candidate)
		if payloadErr != nil {
			return Response{}, payloadErr
		}
		req, requestErr := http.NewRequestWithContext(ctx, http.MethodPost, upstreamEndpoint, bytes.NewReader(raw))
		if requestErr != nil {
			return Response{}, requestErr
		}
		req.Header.Set("Content-Type", contentType)
		req.Header.Set("Authorization", "Bearer "+apiKey)
		applyUpstreamIdempotency(req, request)
		if err := notifySubmissionStarted(request); err != nil {
			return Response{}, err
		}
		resp, requestErr = requestClient.Do(req)
		if requestErr != nil {
			return Response{}, transportClientError(requestErr)
		}
		if err := notifyResponseReceived(request); err != nil {
			_ = resp.Body.Close()
			return Response{}, err
		}
		if !correctionEnabled || (resp.StatusCode != http.StatusBadRequest && resp.StatusCode != http.StatusUnprocessableEntity) {
			break
		}
		_, _, upstreamErr := decodeHTTPResponseForProtocol(resp, openAIWireProtocol(endpointKind))
		if upstreamErr == nil {
			break
		}
		fingerprint := fmt.Sprintf("%d:%s", resp.StatusCode, strings.ToLower(upstreamErr.Error()))
		if _, repeated := seenCorrectionErrors[fingerprint]; repeated || correctionAttempt >= 2 {
			return Response{}, annotateResponseError(upstreamErr, requestIDFromParameterError(upstreamErr), responseStartedAt, time.Now())
		}
		seenCorrectionErrors[fingerprint] = struct{}{}
		rule, safe := deriveParameterCorrection(upstreamErr, body, request.Candidate, protectedCorrections)
		if !safe || !applyParameterCorrectionRule(body, rule) {
			return Response{}, annotateResponseError(upstreamErr, requestIDFromParameterError(upstreamErr), responseStartedAt, time.Now())
		}
		c.Corrections.invalidate(correctionScope, rule.Param)
		provisionalRules = append(provisionalRules, rule)
	}
	if len(provisionalRules) > 0 && resp.StatusCode >= 200 && resp.StatusCode < 300 {
		c.Corrections.commit(correctionScope, provisionalRules)
	}
	requestID := requestIDFromHTTPResponse(resp)
	var result map[string]any
	var wire *WireResponse
	var err error
	upstreamResponseID := ""
	nativeStreamDelta := openAIWireStreamDelta(request.StreamDelta, openAIWireProtocol(endpointKind), resp)
	if request.Kind == "responses" && protocol == ProtocolOpenAIResponses && stream {
		result, upstreamResponseID, err = decodeNativeResponsesStream(resp, nativeStreamDelta)
		wire = &WireResponse{Protocol: openAIWireProtocol(endpointKind), StatusCode: resp.StatusCode, Headers: compatibleResponseHeaders(resp.Header)}
	} else {
		var streamDelta StreamDelta = nativeStreamDelta
		var adapter *chatResponsesStreamAdapter
		if request.Kind == "responses" && protocol == ProtocolOpenAIChatCompletions && stream {
			adapter = newChatResponsesStreamAdapter(request.PublicResponseID, request.Model, request.Body)
			streamDelta = func(event StreamDeltaEvent) error { return adapter.delta(event, request.StreamDelta) }
		}
		if stream {
			result, err = decodeOpenAIResponse(resp, true, streamDelta)
			wire = &WireResponse{Protocol: openAIWireProtocol(endpointKind), StatusCode: resp.StatusCode, Headers: compatibleResponseHeaders(resp.Header)}
		} else {
			result, wire, err = decodeHTTPResponseForProtocol(resp, openAIWireProtocol(endpointKind))
		}
		if err == nil && endpointKind == "chat.completions" {
			result = NormalizeChatCompletionResult(result)
		}
		if err == nil && request.Kind == "responses" && protocol == ProtocolOpenAIChatCompletions {
			chatResult := result
			upstreamResponseID = requestIDFromResult(chatResult)
			result = ChatResultToResponse(chatResult, request.PublicResponseID, request.Model, request.Body)
			if adapter != nil {
				err = adapter.done(result, request.StreamDelta)
			}
			if err == nil {
				return Response{
					Result: result, InternalResult: chatResult, RequestID: firstNonEmptyString(requestID, upstreamResponseID), Usage: usageFromOpenAI(chatResult),
					Progress: providerProgress(request), ResponseStartedAt: responseStartedAt, ResponseFinishedAt: time.Now(),
					UpstreamProtocol: protocol, UpstreamEndpoint: endpoint, UpstreamResponseID: upstreamResponseID,
					PublicResponseID: request.PublicResponseID, ResponseConverted: true,
					Wire: func() *WireResponse {
						if wire != nil {
							wire.Converted = true
						}
						return wire
					}(),
				}, nil
			}
		}
	}
	if err == nil && request.Kind == "chat.completions" {
		result = NormalizeChatCompletionResult(result)
	}
	responseFinishedAt := time.Now()
	if err != nil {
		return Response{}, annotateResponseError(err, requestID, responseStartedAt, responseFinishedAt)
	}
	if requestID == "" {
		requestID = requestIDFromResult(result)
	}
	if request.Kind == "responses" && protocol == ProtocolOpenAIResponses {
		if upstreamResponseID == "" {
			upstreamResponseID = requestIDFromResult(result)
		}
	}
	publicResponseID := request.PublicResponseID
	if request.Kind == "responses" && protocol == ProtocolOpenAIResponses {
		publicResponseID = upstreamResponseID
	}
	return Response{
		Result:             result,
		RequestID:          requestID,
		Usage:              usageFromOpenAI(result),
		Progress:           providerProgress(request),
		ResponseStartedAt:  responseStartedAt,
		ResponseFinishedAt: responseFinishedAt,
		ResponseDurationMS: responseDurationMS(responseStartedAt, responseFinishedAt),
		UpstreamProtocol:   protocol,
		UpstreamEndpoint:   endpoint,
		UpstreamResponseID: upstreamResponseID,
		PublicResponseID:   publicResponseID,
		Wire:               wire,
	}, nil
}

func openAIAdaptationSource(request Request, endpointKind string) map[string]any {
	if request.OriginalBody == nil || request.Kind != "responses" || endpointKind != "chat.completions" {
		return request.OriginalBody
	}
	out := cloneBody(request.OriginalBody)
	if reasoning, ok := request.OriginalBody["reasoning"].(map[string]any); ok {
		if effort, explicit := reasoning["effort"]; explicit {
			out["reasoning_effort"] = effort
		}
	}
	return out
}

func normalizeOpenAIImageRequestBody(endpointKind string, body map[string]any, originalBody map[string]any) {
	if endpointKind != "images.generations" && endpointKind != "images.edits" {
		return
	}
	normalizeOfficialOpenAIImageSize(body, originalBody)
	// OpenAI image endpoints express output geometry through size. The Gateway
	// keeps the generic fields for capability validation and billing, but they
	// are not valid OpenAI wire parameters.
	for _, key := range []string{
		"aspect_ratio",
		"aspectRatio",
		"resolution",
		"width",
		"height",
		"platform_id",
		"platformId",
		"platform_model_id",
		"platformModelId",
	} {
		delete(body, key)
	}
}

func openAIRequestPayload(ctx context.Context, endpointKind string, body map[string]any, candidate store.RuntimeModelCandidate) ([]byte, string, error) {
	if endpointKind != "images.edits" {
		raw, err := json.Marshal(body)
		return raw, "application/json", err
	}
	if OpenAIImageEditUsesJSONURL(candidate) {
		return openAIImageEditJSONPayload(body)
	}
	var payload bytes.Buffer
	writer := multipart.NewWriter(&payload)
	images := openAIImageEditValues(firstPresent(body["images"], body["image"]))
	if len(images) == 0 {
		return nil, "", &ClientError{
			Code:       "invalid_parameter",
			Message:    "image is required",
			Param:      "image",
			StatusCode: http.StatusBadRequest,
			Retryable:  false,
		}
	}
	imageFieldName := openAIImageEditFieldName(candidate, len(images))
	for index, value := range images {
		contentType, image, err := openAIImageEditPayload(ctx, value)
		if err != nil {
			return nil, "", err
		}
		if err := writeOpenAIImageEditFile(writer, imageFieldName, fmt.Sprintf("image-%d%s", index+1, openAIImageFileExtension(contentType)), contentType, image); err != nil {
			return nil, "", err
		}
	}
	if mask := firstPresent(body["mask"], body["mask_image"], body["maskImage"]); mask != nil {
		contentType, image, err := openAIImageEditPayload(ctx, mask)
		if err != nil {
			return nil, "", err
		}
		if err := writeOpenAIImageEditFile(writer, "mask", "mask"+openAIImageFileExtension(contentType), contentType, image); err != nil {
			return nil, "", err
		}
	}
	for key, value := range body {
		switch key {
		case "image", "images", "mask", "mask_image", "maskImage":
			continue
		}
		if strings.HasPrefix(key, "_") || value == nil {
			continue
		}
		fieldValue, err := openAIFormFieldValue(value)
		if err != nil {
			return nil, "", err
		}
		if fieldValue == "" {
			continue
		}
		if err := writer.WriteField(key, fieldValue); err != nil {
			return nil, "", err
		}
	}
	if err := writer.Close(); err != nil {
		return nil, "", err
	}
	return payload.Bytes(), writer.FormDataContentType(), nil
}

// OpenAIImageEditUsesJSONURL reports whether the platform explicitly opts in to
// JSON URL requests for image edits. Multipart remains the compatibility-safe
// default when the setting is absent or unrecognized.
func OpenAIImageEditUsesJSONURL(candidate store.RuntimeModelCandidate) bool {
	format := strings.ToLower(strings.TrimSpace(stringFromAny(candidate.PlatformConfig["imageEditRequestFormat"])))
	format = strings.ReplaceAll(format, "-", "_")
	format = strings.ReplaceAll(format, " ", "_")
	return format == "json" || format == "json_url"
}

func openAIImageEditJSONPayload(body map[string]any) ([]byte, string, error) {
	images := openAIImageEditValues(firstPresent(body["images"], body["image"]))
	if len(images) == 0 {
		return nil, "", &ClientError{
			Code:       "invalid_parameter",
			Message:    "image is required",
			Param:      "image",
			StatusCode: http.StatusBadRequest,
			Retryable:  false,
		}
	}
	imageURLs := make([]any, 0, len(images))
	for _, image := range images {
		imageURL, err := openAIImageEditURLValue(image, "image")
		if err != nil {
			return nil, "", err
		}
		imageURLs = append(imageURLs, imageURL)
	}
	payload := make(map[string]any, len(body))
	for key, value := range body {
		switch key {
		case "image", "images", "mask", "mask_image", "maskImage":
			continue
		}
		if strings.HasPrefix(key, "_") || value == nil {
			continue
		}
		payload[key] = value
	}
	if len(imageURLs) == 1 {
		payload["image"] = imageURLs[0]
	} else {
		payload["image"] = imageURLs
	}
	if mask := firstPresent(body["mask"], body["mask_image"], body["maskImage"]); mask != nil {
		maskURL, err := openAIImageEditURLValue(mask, "mask")
		if err != nil {
			return nil, "", err
		}
		payload["mask"] = maskURL
	}
	raw, err := json.Marshal(payload)
	return raw, "application/json", err
}

func openAIImageEditURLValue(value any, param string) (string, error) {
	switch typed := value.(type) {
	case map[string]any:
		for _, key := range []string{"url", "image_url", "imageUrl"} {
			if nested := typed[key]; nested != nil {
				return openAIImageEditURLValue(nested, param)
			}
		}
	case string:
		raw := strings.TrimSpace(typed)
		parsed, err := url.Parse(raw)
		if err == nil && parsed.Host != "" && (strings.EqualFold(parsed.Scheme, "http") || strings.EqualFold(parsed.Scheme, "https")) {
			return raw, nil
		}
	}
	return "", &ClientError{
		Code:       "invalid_parameter",
		Message:    "OpenAI image edit JSON mode requires " + param + " to be an HTTP(S) URL",
		Param:      param,
		StatusCode: http.StatusBadRequest,
		Retryable:  false,
	}
}

func openAIImageEditFieldName(candidate store.RuntimeModelCandidate, imageCount int) string {
	if configured := strings.TrimSpace(stringFromAny(candidate.PlatformConfig["imageEditMultipartFieldName"])); configured == "image" || configured == "image[]" {
		return configured
	}
	if imageCount > 1 {
		return "image[]"
	}
	parsed, err := url.Parse(strings.TrimSpace(candidate.BaseURL))
	if err == nil && strings.EqualFold(parsed.Hostname(), "api.openai.com") {
		return "image[]"
	}
	return "image"
}

func openAIImageEditValues(value any) []any {
	switch typed := value.(type) {
	case []any:
		return typed
	case []string:
		out := make([]any, 0, len(typed))
		for _, item := range typed {
			out = append(out, item)
		}
		return out
	case nil:
		return nil
	default:
		return []any{typed}
	}
}

func openAIImageEditPayload(ctx context.Context, value any) (string, []byte, error) {
	switch typed := value.(type) {
	case map[string]any:
		for _, key := range []string{"data", "b64_json", "base64", "url"} {
			if nested := typed[key]; nested != nil {
				return openAIImageEditPayload(ctx, nested)
			}
		}
	case string:
		raw := strings.TrimSpace(typed)
		if raw == "" {
			break
		}
		contentType := ""
		encoded := raw
		if strings.HasPrefix(strings.ToLower(raw), "data:") {
			prefix, payload, ok := strings.Cut(raw, ",")
			if !ok || !strings.Contains(strings.ToLower(prefix), ";base64") {
				break
			}
			contentType = strings.TrimSpace(strings.Split(strings.TrimPrefix(prefix, "data:"), ";")[0])
			encoded = payload
		} else if parsed, parseErr := url.Parse(raw); parseErr == nil && parsed.Host != "" && (strings.EqualFold(parsed.Scheme, "http") || strings.EqualFold(parsed.Scheme, "https")) {
			return fetchRemoteMediaInputPayload(ctx, raw, 256<<20)
		}
		image, err := decodeOpenAIImageEditBase64(encoded)
		if err != nil {
			break
		}
		if contentType == "" {
			contentType = strings.TrimSpace(strings.Split(http.DetectContentType(image), ";")[0])
		}
		return contentType, image, nil
	case []byte:
		if len(typed) > 0 {
			return strings.TrimSpace(strings.Split(http.DetectContentType(typed), ";")[0]), typed, nil
		}
	}
	return "", nil, &ClientError{
		Code:       "invalid_parameter",
		Message:    "image must be a base64 or data URL payload",
		Param:      "image",
		StatusCode: http.StatusBadRequest,
		Retryable:  false,
	}
}

func fetchRemoteMediaInputPayload(ctx context.Context, sourceURL string, maxBytes int64) (string, []byte, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, sourceURL, nil)
	if err != nil {
		return "", nil, &ClientError{Code: "request_asset_fetch_failed", Message: err.Error(), Retryable: false}
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", nil, &ClientError{Code: "request_asset_fetch_failed", Message: err.Error(), Retryable: true}
	}
	defer resp.Body.Close()
	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
		return "", nil, &ClientError{
			Code:       "request_asset_fetch_failed",
			Message:    resp.Status,
			StatusCode: resp.StatusCode,
			Retryable:  HTTPRetryable(resp.StatusCode),
		}
	}
	if maxBytes <= 0 {
		maxBytes = 256 << 20
	}
	payload, err := io.ReadAll(io.LimitReader(resp.Body, maxBytes+1))
	if err != nil {
		return "", nil, &ClientError{Code: "request_asset_fetch_failed", Message: err.Error(), Retryable: true}
	}
	if int64(len(payload)) > maxBytes {
		return "", nil, &ClientError{
			Code:       "invalid_parameter",
			Message:    "remote media input exceeds the download limit",
			Param:      "image",
			StatusCode: http.StatusBadRequest,
			Retryable:  false,
		}
	}
	contentType := strings.TrimSpace(strings.Split(resp.Header.Get("Content-Type"), ";")[0])
	if contentType == "" || contentType == "application/octet-stream" {
		contentType = strings.TrimSpace(strings.Split(http.DetectContentType(payload), ";")[0])
	}
	return contentType, payload, nil
}

func decodeOpenAIImageEditBase64(value string) ([]byte, error) {
	normalized := strings.Map(func(char rune) rune {
		switch char {
		case '\n', '\r', '\t', ' ':
			return -1
		default:
			return char
		}
	}, value)
	var lastErr error
	for _, encoding := range []*base64.Encoding{
		base64.StdEncoding,
		base64.RawStdEncoding,
		base64.URLEncoding,
		base64.RawURLEncoding,
	} {
		payload, err := encoding.DecodeString(normalized)
		if err == nil && len(payload) > 0 {
			return payload, nil
		}
		lastErr = err
	}
	return nil, lastErr
}

func writeOpenAIImageEditFile(writer *multipart.Writer, fieldName string, fileName string, contentType string, payload []byte) error {
	header := make(textproto.MIMEHeader)
	header.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, fieldName, escapeMultipartFilename(fileName)))
	if contentType != "" {
		header.Set("Content-Type", contentType)
	}
	part, err := writer.CreatePart(header)
	if err != nil {
		return err
	}
	_, err = io.Copy(part, bytes.NewReader(payload))
	return err
}

func openAIImageFileExtension(contentType string) string {
	switch strings.ToLower(strings.TrimSpace(strings.Split(contentType, ";")[0])) {
	case "image/jpeg":
		return ".jpg"
	case "image/webp":
		return ".webp"
	case "image/gif":
		return ".gif"
	default:
		return ".png"
	}
}

func openAIFormFieldValue(value any) (string, error) {
	switch typed := value.(type) {
	case string:
		return typed, nil
	case bool:
		return fmt.Sprintf("%t", typed), nil
	case float64:
		return fmt.Sprintf("%v", typed), nil
	case float32:
		return fmt.Sprintf("%v", typed), nil
	case int:
		return fmt.Sprintf("%d", typed), nil
	case int64:
		return fmt.Sprintf("%d", typed), nil
	case json.Number:
		return typed.String(), nil
	default:
		encoded, err := json.Marshal(value)
		return string(encoded), err
	}
}

func openAIWireStreamDelta(next StreamDelta, protocol string, response *http.Response) StreamDelta {
	if next == nil {
		return nil
	}
	return func(event StreamDeltaEvent) error {
		event.WireProtocol = protocol
		if response != nil {
			event.WireStatusCode = response.StatusCode
			event.WireHeaders = compatibleResponseHeaders(response.Header)
		}
		return next(event)
	}
}

func openAIWireProtocol(kind string) string {
	switch kind {
	case "chat.completions":
		return ProtocolOpenAIChatCompletions
	case "responses":
		return ProtocolOpenAIResponses
	case "embeddings":
		return ProtocolOpenAIEmbeddings
	case "images.generations", "images.edits":
		return ProtocolOpenAIImages
	default:
		return "openai_" + strings.ReplaceAll(kind, ".", "_")
	}
}

func decodeOpenAIResponse(resp *http.Response, stream bool, onDelta StreamDelta) (map[string]any, error) {
	if stream {
		result, err := decodeOpenAIStreamResponse(resp, onDelta)
		if err == nil {
			return result, nil
		}
		return nil, err
	}
	return decodeHTTPResponse(resp)
}

func openAIEndpoint(kind string) string {
	switch kind {
	case "chat.completions":
		return "/chat/completions"
	case "responses":
		return "/responses"
	case "embeddings":
		return "/embeddings"
	case "reranks":
		return "/reranks"
	case "images.generations":
		return "/images/generations"
	case "images.edits":
		return "/images/edits"
	default:
		return ""
	}
}

func openAIEndpointSupportsStream(kind string) bool {
	return kind == "chat.completions" || kind == "responses"
}

func openAIBaseURL(kind string, candidate store.RuntimeModelCandidate) string {
	base := strings.TrimSpace(candidate.BaseURL)
	if kind != "reranks" {
		return base
	}
	if strings.Contains(base, "/compatible-mode/") && (strings.EqualFold(candidate.Provider, "aliyun-bailian-openai") || strings.Contains(base, "dashscope")) {
		return strings.Replace(base, "/compatible-mode/", "/compatible-api/", 1)
	}
	if base == "" && strings.EqualFold(candidate.Provider, "aliyun-bailian-openai") {
		return "https://dashscope.aliyuncs.com/compatible-api/v1"
	}
	return base
}

func cloneBody(body map[string]any) map[string]any {
	out := map[string]any{}
	for key, value := range body {
		out[key] = value
	}
	return out
}

func ensureOpenAIStreamUsage(body map[string]any, kind string, stream bool) {
	if !stream || kind != "chat.completions" {
		return
	}
	streamOptions := map[string]any{}
	if raw, explicit := body["stream_options"]; explicit {
		existing, ok := raw.(map[string]any)
		if !ok {
			return
		}
		for key, value := range existing {
			streamOptions[key] = value
		}
	}
	if _, explicit := streamOptions["include_usage"]; !explicit {
		streamOptions["include_usage"] = true
	}
	body["stream_options"] = streamOptions
}

func joinURL(base string, path string) string {
	base = strings.TrimRight(strings.TrimSpace(base), "/")
	if base == "" {
		base = "https://api.openai.com/v1"
	}
	return base + path
}

func httpClient(clients ...*http.Client) *http.Client {
	for _, client := range clients {
		if client != nil {
			return client
		}
	}
	return http.DefaultClient
}

func providerProgress(request Request) []Progress {
	return []Progress{
		{Phase: "submitting", Progress: 0.35, Message: "provider request submitted", Payload: map[string]any{"clientId": request.Candidate.ClientID}},
		{Phase: "fetching_result", Progress: 0.8, Message: "provider response received", Payload: map[string]any{"provider": request.Candidate.Provider}},
	}
}
