package ai

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"
	"unicode/utf8"

	providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider"
	"github.com/1Panel-dev/1Panel/agent/global"
)

const (
	defaultTimeout        = 30 * time.Second
	defaultUserAgent      = "1panel-terminal-ai/1.0"
	defaultAnthropicToken = 1024
	defaultTerminalTokens = 8192
)

type Client interface {
	ChatCompletion(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error)
}

type ClientConfig struct {
	Provider   string
	BaseURL    string
	APIKey     string
	Model      string
	APIType    string
	AuthMode   string
	MaxTokens  int
	Timeout    time.Duration
	HTTPClient *http.Client
}

type GeneratorConfig struct {
	Provider  string
	BaseURL   string
	APIKey    string
	Model     string
	APIType   string
	AuthMode  string
	MaxTokens int
}

type ChatMessage struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

type ChatCompletionRequest struct {
	Messages    []ChatMessage `json:"messages"`
	MaxTokens   int           `json:"max_tokens,omitempty"`
	Temperature *float64      `json:"temperature,omitempty"`
	TopP        *float64      `json:"top_p,omitempty"`
}

type ThinkingMode struct {
	Type string `json:"type"`
}

type ChatCompletionResponse struct {
	ID      string        `json:"id"`
	Model   string        `json:"model"`
	Content string        `json:"content"`
	RawText string        `json:"rawText"`
	Usage   ResponseUsage `json:"usage"`
}

type ResponseUsage struct {
	PromptTokens     int `json:"promptTokens"`
	CompletionTokens int `json:"completionTokens"`
	TotalTokens      int `json:"totalTokens"`
}

type terminalAIClient struct {
	config     ClientConfig
	httpClient *http.Client
}

func NewClient(cfg ClientConfig) (Client, error) {
	providerKey := strings.ToLower(strings.TrimSpace(cfg.Provider))
	if providerKey == "" {
		providerKey = "custom"
	}
	if strings.TrimSpace(cfg.Model) == "" {
		return nil, fmt.Errorf("model is required")
	}
	if strings.TrimSpace(cfg.APIKey) == "" && providerKey != "ollama" {
		return nil, fmt.Errorf("api key is required")
	}
	baseURL := normalizeBaseURL(providerKey, cfg.BaseURL)
	if baseURL == "" {
		return nil, fmt.Errorf("base url is required")
	}
	cfg.Provider = providerKey
	cfg.BaseURL = baseURL
	cfg.APIType = normalizeClientAPIType(providerKey, cfg.APIType)
	authMode, err := providercatalog.ResolveAuthMode(providerKey, cfg.APIType, cfg.AuthMode)
	if err != nil {
		return nil, err
	}
	cfg.AuthMode = authMode
	if cfg.Timeout <= 0 {
		cfg.Timeout = defaultTimeout
	}
	client := cfg.HTTPClient
	if client == nil {
		client = &http.Client{Timeout: cfg.Timeout}
	}
	return &terminalAIClient{
		config:     cfg,
		httpClient: client,
	}, nil
}

func (c *terminalAIClient) ChatCompletion(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) {
	if len(req.Messages) == 0 {
		return nil, fmt.Errorf("messages are required")
	}
	switch c.config.APIType {
	case "anthropic-messages":
		return c.chatCompletionAnthropic(ctx, req)
	case "gemini-generate-content":
		return c.chatCompletionGemini(ctx, req)
	case "openai-responses":
		return c.chatCompletionOpenAIResponses(ctx, req)
	default:
		return c.chatCompletionOpenAI(ctx, req)
	}
}

func (c *terminalAIClient) chatCompletionOpenAI(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) {
	payload := openAIChatCompletionRequest{
		Model:       strings.TrimSpace(c.config.Model),
		Messages:    req.Messages,
		MaxTokens:   firstPositive(req.MaxTokens, c.config.MaxTokens),
		Temperature: req.Temperature,
		TopP:        req.TopP,
		Thinking:    thinkingModeForModel(c.config.Model),
	}
	body, err := json.Marshal(payload)
	if err != nil {
		return nil, fmt.Errorf("marshal request: %w", err)
	}
	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, buildChatCompletionsURL(c.config.BaseURL), bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}
	httpReq.Header.Set("Authorization", "Bearer "+strings.TrimSpace(c.config.APIKey))
	httpReq.Header.Set("Content-Type", "application/json")
	httpReq.Header.Set("Accept", "application/json")
	httpReq.Header.Set("User-Agent", defaultUserAgent)

	respBody, err := c.do(httpReq)
	if err != nil {
		return nil, err
	}

	var completionResp openAIChatCompletionResponse
	if err := json.Unmarshal(respBody, &completionResp); err != nil {
		return nil, fmt.Errorf("decode response: %w", err)
	}
	content := extractAssistantContent(completionResp)
	return &ChatCompletionResponse{
		ID:      completionResp.ID,
		Model:   firstNonEmptyString(c.config.Model, completionResp.Model),
		Content: content,
		RawText: strings.TrimSpace(string(respBody)),
		Usage: ResponseUsage{
			PromptTokens:     completionResp.Usage.PromptTokens,
			CompletionTokens: completionResp.Usage.CompletionTokens,
			TotalTokens:      completionResp.Usage.TotalTokens,
		},
	}, nil
}

func (c *terminalAIClient) chatCompletionOpenAIResponses(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) {
	payload := openAIResponsesRequest{
		Model:           strings.TrimSpace(c.config.Model),
		Input:           toResponsesInput(req.Messages),
		MaxOutputTokens: firstPositive(req.MaxTokens, c.config.MaxTokens),
		Temperature:     req.Temperature,
		TopP:            req.TopP,
	}
	body, err := json.Marshal(payload)
	if err != nil {
		return nil, fmt.Errorf("marshal request: %w", err)
	}
	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, buildResponsesURL(c.config.BaseURL), bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}
	if strings.TrimSpace(c.config.APIKey) != "" {
		httpReq.Header.Set("Authorization", "Bearer "+strings.TrimSpace(c.config.APIKey))
	}
	httpReq.Header.Set("Content-Type", "application/json")
	httpReq.Header.Set("Accept", "application/json")
	httpReq.Header.Set("User-Agent", defaultUserAgent)

	respBody, err := c.do(httpReq)
	if err != nil {
		return nil, err
	}

	var completionResp openAIResponsesResponse
	if err := json.Unmarshal(respBody, &completionResp); err != nil {
		return nil, fmt.Errorf("decode response: %w", err)
	}
	content := extractResponsesContent(completionResp)
	return &ChatCompletionResponse{
		ID:      completionResp.ID,
		Model:   firstNonEmptyString(c.config.Model, completionResp.Model),
		Content: content,
		RawText: strings.TrimSpace(string(respBody)),
		Usage: ResponseUsage{
			PromptTokens:     completionResp.Usage.InputTokens,
			CompletionTokens: completionResp.Usage.OutputTokens,
			TotalTokens:      completionResp.Usage.TotalTokens,
		},
	}, nil
}

func (c *terminalAIClient) chatCompletionAnthropic(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) {
	system, messages := toAnthropicMessages(req.Messages)
	payload := anthropicMessagesRequest{
		Model:       strings.TrimSpace(c.config.Model),
		System:      system,
		Messages:    messages,
		MaxTokens:   firstPositive(req.MaxTokens, c.config.MaxTokens, defaultAnthropicToken),
		Temperature: req.Temperature,
		TopP:        req.TopP,
	}
	body, err := json.Marshal(payload)
	if err != nil {
		return nil, fmt.Errorf("marshal request: %w", err)
	}
	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, buildAnthropicMessagesURL(c.config.BaseURL), bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}
	if c.config.AuthMode == providercatalog.AuthModeBearer {
		httpReq.Header.Set("Authorization", "Bearer "+strings.TrimSpace(c.config.APIKey))
	} else {
		httpReq.Header.Set("x-api-key", strings.TrimSpace(c.config.APIKey))
	}
	httpReq.Header.Set("anthropic-version", "2023-06-01")
	httpReq.Header.Set("Content-Type", "application/json")
	httpReq.Header.Set("Accept", "application/json")
	httpReq.Header.Set("User-Agent", defaultUserAgent)

	respBody, err := c.do(httpReq)
	if err != nil {
		return nil, err
	}

	var completionResp anthropicMessagesResponse
	if err := json.Unmarshal(respBody, &completionResp); err != nil {
		return nil, fmt.Errorf("decode response: %w", err)
	}
	content := extractAnthropicContent(completionResp)
	return &ChatCompletionResponse{
		ID:      completionResp.ID,
		Model:   firstNonEmptyString(c.config.Model, completionResp.Model),
		Content: content,
		RawText: strings.TrimSpace(string(respBody)),
		Usage: ResponseUsage{
			PromptTokens:     completionResp.Usage.InputTokens,
			CompletionTokens: completionResp.Usage.OutputTokens,
			TotalTokens:      completionResp.Usage.InputTokens + completionResp.Usage.OutputTokens,
		},
	}, nil
}

func (c *terminalAIClient) chatCompletionGemini(ctx context.Context, req ChatCompletionRequest) (*ChatCompletionResponse, error) {
	payload := geminiGenerateContentRequest{
		Contents: toGeminiContents(req.Messages),
		GenerationConfig: &geminiGenerationConfig{
			MaxOutputTokens: firstPositive(req.MaxTokens, c.config.MaxTokens),
			Temperature:     req.Temperature,
			TopP:            req.TopP,
		},
	}
	body, err := json.Marshal(payload)
	if err != nil {
		return nil, fmt.Errorf("marshal request: %w", err)
	}
	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, buildGeminiGenerateContentURL(c.config.BaseURL, strings.TrimSpace(c.config.Model)), bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}
	httpReq.Header.Set("x-goog-api-key", strings.TrimSpace(c.config.APIKey))
	httpReq.Header.Set("Content-Type", "application/json")
	httpReq.Header.Set("Accept", "application/json")
	httpReq.Header.Set("User-Agent", defaultUserAgent)

	respBody, err := c.do(httpReq)
	if err != nil {
		return nil, err
	}

	var completionResp geminiGenerateContentResponse
	if err := json.Unmarshal(respBody, &completionResp); err != nil {
		return nil, fmt.Errorf("decode response: %w", err)
	}
	content := extractGeminiContent(completionResp)
	return &ChatCompletionResponse{
		Model:   firstNonEmptyString(c.config.Model, completionResp.ModelVersion),
		Content: content,
		RawText: strings.TrimSpace(string(respBody)),
		Usage: ResponseUsage{
			PromptTokens:     completionResp.Usage.PromptTokenCount,
			CompletionTokens: completionResp.Usage.CandidatesTokenCount,
			TotalTokens:      completionResp.Usage.TotalTokenCount,
		},
	}, nil
}

func (c *terminalAIClient) do(httpReq *http.Request) ([]byte, error) {
	start := time.Now()
	resp, err := c.httpClient.Do(httpReq)
	if err != nil {
		c.logRequestCost(httpReq, 0, time.Since(start), err)
		return nil, fmt.Errorf("do request: %w", err)
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		c.logRequestCost(httpReq, resp.StatusCode, time.Since(start), err)
		return nil, fmt.Errorf("read response: %w", err)
	}
	if resp.StatusCode >= http.StatusBadRequest {
		providerErr := parseProviderError(resp.StatusCode, respBody)
		c.logRequestCost(httpReq, resp.StatusCode, time.Since(start), providerErr)
		return nil, providerErr
	}
	c.logRequestCost(httpReq, resp.StatusCode, time.Since(start), nil)
	return respBody, nil
}

func (c *terminalAIClient) logRequestCost(httpReq *http.Request, statusCode int, duration time.Duration, err error) {
	if global.LOG == nil || httpReq == nil {
		return
	}
	if err != nil {
		global.LOG.Warnf("[terminal-ai] request finished, provider=%s model=%s apiType=%s method=%s url=%s status=%d duration=%s err=%v",
			c.config.Provider,
			c.config.Model,
			c.config.APIType,
			httpReq.Method,
			httpReq.URL.String(),
			statusCode,
			duration,
			err,
		)
		return
	}
	global.LOG.Infof("[terminal-ai] request finished, provider=%s model=%s apiType=%s method=%s url=%s status=%d duration=%s",
		c.config.Provider,
		c.config.Model,
		c.config.APIType,
		httpReq.Method,
		httpReq.URL.String(),
		statusCode,
		duration,
	)
}

type openAIChatCompletionRequest struct {
	Model       string        `json:"model"`
	Messages    []ChatMessage `json:"messages"`
	MaxTokens   int           `json:"max_tokens,omitempty"`
	Temperature *float64      `json:"temperature,omitempty"`
	TopP        *float64      `json:"top_p,omitempty"`
	Thinking    *ThinkingMode `json:"thinking,omitempty"`
}

type openAIChatCompletionResponse struct {
	ID      string `json:"id"`
	Model   string `json:"model"`
	Choices []struct {
		Message ChatMessage `json:"message"`
		Text    string      `json:"text"`
	} `json:"choices"`
	Usage struct {
		PromptTokens     int `json:"prompt_tokens"`
		CompletionTokens int `json:"completion_tokens"`
		TotalTokens      int `json:"total_tokens"`
	} `json:"usage"`
}

type openAIResponsesRequest struct {
	Model           string                 `json:"model"`
	Input           []openAIResponsesInput `json:"input"`
	MaxOutputTokens int                    `json:"max_output_tokens,omitempty"`
	Temperature     *float64               `json:"temperature,omitempty"`
	TopP            *float64               `json:"top_p,omitempty"`
}

type openAIResponsesInput struct {
	Role    string                     `json:"role"`
	Content []openAIResponsesInputPart `json:"content"`
}

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

type openAIResponsesResponse struct {
	ID     string `json:"id"`
	Model  string `json:"model"`
	Output []struct {
		Content []struct {
			Type string `json:"type"`
			Text string `json:"text"`
		} `json:"content"`
	} `json:"output"`
	OutputText string `json:"output_text"`
	Usage      struct {
		InputTokens  int `json:"input_tokens"`
		OutputTokens int `json:"output_tokens"`
		TotalTokens  int `json:"total_tokens"`
	} `json:"usage"`
}

type anthropicMessagesRequest struct {
	Model       string             `json:"model"`
	System      string             `json:"system,omitempty"`
	Messages    []anthropicMessage `json:"messages"`
	MaxTokens   int                `json:"max_tokens"`
	Temperature *float64           `json:"temperature,omitempty"`
	TopP        *float64           `json:"top_p,omitempty"`
}

type anthropicMessage struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

type anthropicMessagesResponse struct {
	ID      string `json:"id"`
	Model   string `json:"model"`
	Content []struct {
		Type string `json:"type"`
		Text string `json:"text"`
	} `json:"content"`
	Usage struct {
		InputTokens  int `json:"input_tokens"`
		OutputTokens int `json:"output_tokens"`
	} `json:"usage"`
}

type geminiGenerateContentRequest struct {
	Contents         []geminiContent         `json:"contents"`
	GenerationConfig *geminiGenerationConfig `json:"generationConfig,omitempty"`
}

type geminiContent struct {
	Role  string              `json:"role,omitempty"`
	Parts []geminiContentPart `json:"parts"`
}

type geminiContentPart struct {
	Text string `json:"text"`
}

type geminiGenerationConfig struct {
	MaxOutputTokens int      `json:"maxOutputTokens,omitempty"`
	Temperature     *float64 `json:"temperature,omitempty"`
	TopP            *float64 `json:"topP,omitempty"`
}

type geminiGenerateContentResponse struct {
	Candidates []struct {
		Content geminiContent `json:"content"`
	} `json:"candidates"`
	ModelVersion string `json:"modelVersion"`
	Usage        struct {
		PromptTokenCount     int `json:"promptTokenCount"`
		CandidatesTokenCount int `json:"candidatesTokenCount"`
		TotalTokenCount      int `json:"totalTokenCount"`
	} `json:"usageMetadata"`
}

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

func normalizeBaseURL(provider, rawBaseURL string) string {
	baseURL := strings.TrimSpace(rawBaseURL)
	if baseURL == "" {
		defaultBaseURL, ok := providercatalog.DefaultBaseURL(provider)
		if ok {
			baseURL = defaultBaseURL
		}
	}
	return strings.TrimRight(baseURL, "/")
}

func buildChatCompletionsURL(baseURL string) string {
	baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
	if strings.HasSuffix(baseURL, "/chat/completions") {
		return baseURL
	}
	parsed, err := url.Parse(baseURL)
	if err != nil {
		return baseURL + "/chat/completions"
	}
	switch {
	case strings.HasSuffix(parsed.Path, "/v1"):
		parsed.Path += "/chat/completions"
	case strings.HasSuffix(parsed.Path, "/v1beta"):
		parsed.Path += "/chat/completions"
	default:
		parsed.Path = strings.TrimRight(parsed.Path, "/") + "/v1/chat/completions"
	}
	return strings.TrimRight(parsed.String(), "/")
}

func buildResponsesURL(baseURL string) string {
	baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
	if strings.HasSuffix(baseURL, "/responses") {
		return baseURL
	}
	parsed, err := url.Parse(baseURL)
	if err != nil {
		return baseURL + "/responses"
	}
	if strings.HasSuffix(parsed.Path, "/v1") {
		parsed.Path += "/responses"
	} else {
		parsed.Path = strings.TrimRight(parsed.Path, "/") + "/v1/responses"
	}
	return strings.TrimRight(parsed.String(), "/")
}

func buildAnthropicMessagesURL(baseURL string) string {
	baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
	if strings.HasSuffix(baseURL, "/messages") {
		return baseURL
	}
	parsed, err := url.Parse(baseURL)
	if err != nil {
		return baseURL + "/messages"
	}
	if strings.HasSuffix(parsed.Path, "/v1") {
		parsed.Path += "/messages"
	} else {
		parsed.Path = strings.TrimRight(parsed.Path, "/") + "/v1/messages"
	}
	return strings.TrimRight(parsed.String(), "/")
}

func buildGeminiGenerateContentURL(baseURL, model string) string {
	baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
	model = strings.TrimSpace(model)
	if model == "" {
		model = "gemini-3-flash-preview"
	}
	parsed, err := url.Parse(baseURL)
	if err != nil {
		return strings.TrimRight(baseURL, "/") + "/v1beta/models/" + model + ":generateContent"
	}
	if strings.Contains(parsed.Path, "/models/") && strings.HasSuffix(parsed.Path, ":generateContent") {
		return parsed.String()
	}
	if strings.Contains(parsed.Path, "/v1beta") {
		parsed.Path = strings.TrimRight(parsed.Path, "/") + "/models/" + model + ":generateContent"
	} else {
		parsed.Path = strings.TrimRight(parsed.Path, "/") + "/v1beta/models/" + model + ":generateContent"
	}
	return parsed.String()
}

func thinkingModeForModel(model string) *ThinkingMode {
	if strings.EqualFold(strings.TrimSpace(model), "kimi-k2.5") {
		return &ThinkingMode{Type: "disabled"}
	}
	return nil
}

func normalizeClientAPIType(provider, apiType string) string {
	if value := strings.ToLower(strings.TrimSpace(apiType)); value != "" {
		return value
	}
	return providercatalog.DefaultAPIType(strings.ToLower(strings.TrimSpace(provider)))
}

func extractAssistantContent(resp openAIChatCompletionResponse) string {
	if len(resp.Choices) == 0 {
		return ""
	}
	if content := strings.TrimSpace(resp.Choices[0].Message.Content); content != "" {
		return content
	}
	return strings.TrimSpace(resp.Choices[0].Text)
}

func extractResponsesContent(resp openAIResponsesResponse) string {
	if text := strings.TrimSpace(resp.OutputText); text != "" {
		return text
	}
	for _, item := range resp.Output {
		for _, content := range item.Content {
			if strings.TrimSpace(content.Text) != "" {
				return strings.TrimSpace(content.Text)
			}
		}
	}
	return ""
}

func extractAnthropicContent(resp anthropicMessagesResponse) string {
	var parts []string
	for _, item := range resp.Content {
		if strings.TrimSpace(item.Text) == "" {
			continue
		}
		parts = append(parts, strings.TrimSpace(item.Text))
	}
	return strings.TrimSpace(strings.Join(parts, "\n"))
}

func extractGeminiContent(resp geminiGenerateContentResponse) string {
	if len(resp.Candidates) == 0 {
		return ""
	}
	var parts []string
	for _, part := range resp.Candidates[0].Content.Parts {
		if strings.TrimSpace(part.Text) == "" {
			continue
		}
		parts = append(parts, strings.TrimSpace(part.Text))
	}
	return strings.TrimSpace(strings.Join(parts, "\n"))
}

func toResponsesInput(messages []ChatMessage) []openAIResponsesInput {
	result := make([]openAIResponsesInput, 0, len(messages))
	for _, message := range messages {
		if strings.TrimSpace(message.Content) == "" {
			continue
		}
		result = append(result, openAIResponsesInput{
			Role: normalizeRole(message.Role),
			Content: []openAIResponsesInputPart{{
				Type: "input_text",
				Text: message.Content,
			}},
		})
	}
	return result
}

func toAnthropicMessages(messages []ChatMessage) (string, []anthropicMessage) {
	var systemParts []string
	result := make([]anthropicMessage, 0, len(messages))
	for _, message := range messages {
		content := strings.TrimSpace(message.Content)
		if content == "" {
			continue
		}
		role := normalizeRole(message.Role)
		if role == "system" {
			systemParts = append(systemParts, content)
			continue
		}
		if role != "assistant" {
			role = "user"
		}
		result = append(result, anthropicMessage{
			Role:    role,
			Content: content,
		})
	}
	if len(result) == 0 && len(systemParts) > 0 {
		result = append(result, anthropicMessage{
			Role:    "user",
			Content: "Generate one shell command only.",
		})
	}
	return strings.Join(systemParts, "\n\n"), result
}

func toGeminiContents(messages []ChatMessage) []geminiContent {
	result := make([]geminiContent, 0, len(messages))
	for _, message := range messages {
		content := strings.TrimSpace(message.Content)
		if content == "" {
			continue
		}
		role := normalizeRole(message.Role)
		if role == "assistant" {
			role = "model"
		} else {
			role = "user"
		}
		result = append(result, geminiContent{
			Role: role,
			Parts: []geminiContentPart{{
				Text: content,
			}},
		})
	}
	return result
}

func normalizeRole(role string) string {
	switch strings.ToLower(strings.TrimSpace(role)) {
	case "assistant", "model":
		return "assistant"
	case "system":
		return "system"
	default:
		return "user"
	}
}

func parseProviderError(statusCode int, body []byte) error {
	var errResp openAIErrorResponse
	if err := json.Unmarshal(body, &errResp); err == nil && strings.TrimSpace(errResp.Error.Message) != "" {
		if strings.TrimSpace(errResp.Error.Code) != "" {
			return fmt.Errorf("provider returned %d: %s (%s)", statusCode, errResp.Error.Message, errResp.Error.Code)
		}
		return fmt.Errorf("provider returned %d: %s", statusCode, errResp.Error.Message)
	}

	var generic map[string]interface{}
	if err := json.Unmarshal(body, &generic); err == nil {
		if message := extractErrorMessage(generic); message != "" {
			return fmt.Errorf("provider returned %d: %s", statusCode, message)
		}
	}

	message := strings.TrimSpace(string(body))
	if message == "" {
		message = http.StatusText(statusCode)
	}
	if !utf8.ValidString(message) {
		message = http.StatusText(statusCode)
	}
	return fmt.Errorf("provider returned %d: %s", statusCode, message)
}

func extractErrorMessage(value map[string]interface{}) string {
	if errorValue, ok := value["error"]; ok {
		switch typed := errorValue.(type) {
		case string:
			return strings.TrimSpace(typed)
		case map[string]interface{}:
			if msg, ok := typed["message"].(string); ok {
				return strings.TrimSpace(msg)
			}
			if msg, ok := typed["status"].(string); ok {
				return strings.TrimSpace(msg)
			}
		}
	}
	if msg, ok := value["message"].(string); ok {
		return strings.TrimSpace(msg)
	}
	return ""
}

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

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