package gateway

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log/slog"
	"mime/multipart"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"
)

// Client talks to the Astra Gateway public API.
type Client struct {
	baseURL    string
	httpClient *http.Client
	debug      bool
}

func NewClient(baseURL string) *Client {
	return &Client{
		baseURL: baseURL,
		httpClient: &http.Client{
			Timeout: 30 * time.Second,
		},
	}
}

func (c *Client) SetDebug(on bool) { c.debug = on }

// logCurl prints a copy-pasteable curl command to stderr when debug mode is on.
func (c *Client) logCurl(method, fullURL, apiKey, contentType string, body []byte) {
	if !c.debug {
		return
	}
	var b strings.Builder
	b.WriteString("\n\033[36m───── DEBUG CURL ─────\033[0m\n")
	b.WriteString("curl -X ")
	b.WriteString(method)
	b.WriteString(" \\\n  '")
	b.WriteString(fullURL)
	b.WriteString("' \\\n  -H 'Authorization: Bearer ")
	b.WriteString(apiKey)
	b.WriteString("'")
	if contentType != "" {
		b.WriteString(" \\\n  -H 'Content-Type: ")
		b.WriteString(contentType)
		b.WriteString("'")
	}
	if len(body) > 0 {
		b.WriteString(" \\\n  -d '")
		b.Write(body)
		b.WriteString("'")
	}
	b.WriteString("\n\033[36m─────────────────────\033[0m\n")
	fmt.Fprint(os.Stderr, b.String())
}

// logResponse prints the HTTP status and response body to stderr when debug mode is on.
func (c *Client) logResponse(status int, body []byte) {
	if !c.debug {
		return
	}
	color := "\033[32m" // green
	if status >= 400 {
		color = "\033[31m" // red
	}
	truncated := string(body)
	if len(truncated) > 2000 {
		truncated = truncated[:2000] + "… (truncated)"
	}
	fmt.Fprintf(os.Stderr, "%s← %d%s %s\n", color, status, "\033[0m", truncated)
}

// ---- request helpers ----

func (c *Client) get(ctx context.Context, path string, query url.Values, apiKey string) (json.RawMessage, error) {
	u := fmt.Sprintf("%s%s", c.baseURL, path)
	if len(query) > 0 {
		u += "?" + query.Encode()
	}

	c.logCurl("GET", u, apiKey, "", nil)

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Accept", "application/json")

	slog.Debug("gateway request", "method", "GET", "url", u)

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("gateway request failed: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("read response: %w", err)
	}

	c.logResponse(resp.StatusCode, body)

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("gateway returned %d: %s", resp.StatusCode, string(body))
	}

	return json.RawMessage(body), nil
}

func (c *Client) post(ctx context.Context, path string, body interface{}, apiKey string) (json.RawMessage, error) {
	return c.doJSON(ctx, http.MethodPost, path, body, apiKey)
}

func (c *Client) put(ctx context.Context, path string, body interface{}, apiKey string) (json.RawMessage, error) {
	return c.doJSON(ctx, http.MethodPut, path, body, apiKey)
}

func (c *Client) delete(ctx context.Context, path string, apiKey string) (json.RawMessage, error) {
	u := fmt.Sprintf("%s%s", c.baseURL, path)

	c.logCurl("DELETE", u, apiKey, "", nil)

	req, err := http.NewRequestWithContext(ctx, http.MethodDelete, u, nil)
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Accept", "application/json")

	slog.Debug("gateway request", "method", "DELETE", "url", u)

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("gateway request failed: %w", err)
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("read response: %w", err)
	}

	c.logResponse(resp.StatusCode, respBody)

	switch resp.StatusCode {
	case http.StatusOK, http.StatusCreated, http.StatusNoContent:
		if resp.StatusCode == http.StatusNoContent || len(respBody) == 0 {
			return nil, nil
		}
		return json.RawMessage(respBody), nil
	default:
		return nil, fmt.Errorf("gateway returned %d: %s", resp.StatusCode, string(respBody))
	}
}

func (c *Client) head(ctx context.Context, path, apiKey string) (int, error) {
	u := fmt.Sprintf("%s%s", c.baseURL, path)

	c.logCurl("HEAD", u, apiKey, "", nil)

	req, err := http.NewRequestWithContext(ctx, http.MethodHead, u, nil)
	if err != nil {
		return 0, fmt.Errorf("create request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)

	slog.Debug("gateway request", "method", "HEAD", "url", u)

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return 0, fmt.Errorf("gateway request failed: %w", err)
	}
	defer resp.Body.Close()
	io.Copy(io.Discard, resp.Body)

	return resp.StatusCode, nil
}

func (c *Client) doJSON(ctx context.Context, method, path string, body interface{}, apiKey string) (json.RawMessage, error) {
	u := fmt.Sprintf("%s%s", c.baseURL, path)

	var bodyReader io.Reader
	var bodyBytes []byte
	if body != nil {
		var err error
		bodyBytes, err = json.Marshal(body)
		if err != nil {
			return nil, fmt.Errorf("marshal body: %w", err)
		}
		bodyReader = bytes.NewReader(bodyBytes)
	}

	c.logCurl(method, u, apiKey, "application/json", bodyBytes)

	req, err := http.NewRequestWithContext(ctx, method, u, bodyReader)
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")

	slog.Debug("gateway request", "method", method, "url", u)

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("gateway request failed: %w", err)
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("read response: %w", err)
	}

	c.logResponse(resp.StatusCode, respBody)

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("gateway returned %d: %s", resp.StatusCode, string(respBody))
	}

	return json.RawMessage(respBody), nil
}

func (c *Client) doJSONWithTimeout(ctx context.Context, timeout time.Duration, method, path string, body interface{}, apiKey string) (json.RawMessage, error) {
	u := fmt.Sprintf("%s%s", c.baseURL, path)

	var bodyReader io.Reader
	var bodyBytes []byte
	if body != nil {
		var err error
		bodyBytes, err = json.Marshal(body)
		if err != nil {
			return nil, fmt.Errorf("marshal body: %w", err)
		}
		bodyReader = bytes.NewReader(bodyBytes)
	}

	c.logCurl(method, u, apiKey, "application/json", bodyBytes)

	req, err := http.NewRequestWithContext(ctx, method, u, bodyReader)
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")

	slog.Debug("gateway request", "method", method, "url", u, "timeout", timeout)

	client := &http.Client{Timeout: timeout}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("gateway request failed: %w", err)
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("read response: %w", err)
	}

	c.logResponse(resp.StatusCode, respBody)

	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("gateway returned %d: %s", resp.StatusCode, string(respBody))
	}

	return json.RawMessage(respBody), nil
}

func (c *Client) postLong(ctx context.Context, path string, body interface{}, apiKey string) (json.RawMessage, error) {
	return c.doJSONWithTimeout(ctx, 5*time.Minute, http.MethodPost, path, body, apiKey)
}

// ---- Agents ----

type Agent struct {
	ID                string          `json:"id"`
	Name              string          `json:"name"`
	Icon              string          `json:"icon,omitempty"`
	Description       string          `json:"description"`
	Type              string          `json:"type"`
	TenantID          string          `json:"tenant_id,omitempty"`
	Status            string          `json:"status"`
	Instructions      string          `json:"instructions"`
	ChannelType       string          `json:"channel_type"`
	CommunicationMode string          `json:"communication_mode,omitempty"`
	IsEnabled         bool            `json:"is_enabled"`
	KnowledgeBaseID   string          `json:"knowledge_base_id,omitempty"`
	CreditConsumed    int64           `json:"credit_consumed,omitempty"`
	ConversationCount int64           `json:"conversation_count,omitempty"`
	CreatedAt         string          `json:"created_at"`
	UpdatedAt         string          `json:"updated_at"`
	Attributes        json.RawMessage `json:"attributes,omitempty"`
	HasDraftConfig    bool            `json:"has_draft_config,omitempty"`
	HasLiveConfig     bool            `json:"has_live_config,omitempty"`
}

type AgentListResponse struct {
	Agents []Agent `json:"agents"`
	Total  int     `json:"total"`
	Limit  int     `json:"limit"`
	Offset int     `json:"offset"`
}

// AgentSummary is a lightweight view for list responses — no instructions.
type AgentSummary struct {
	ID                string `json:"id"`
	Name              string `json:"name"`
	Description       string `json:"description"`
	Type              string `json:"type"`
	Status            string `json:"status"`
	ChannelType       string `json:"channel_type"`
	CommunicationMode string `json:"communication_mode,omitempty"`
	IsEnabled         bool   `json:"is_enabled"`
	KnowledgeBaseID   string `json:"knowledge_base_id,omitempty"`
	ConversationCount int64  `json:"conversation_count,omitempty"`
	CreatedAt         string `json:"created_at"`
	UpdatedAt         string `json:"updated_at"`
	HasDraftConfig    bool   `json:"has_draft_config,omitempty"`
	HasLiveConfig     bool   `json:"has_live_config,omitempty"`
}

type AgentSummaryResponse struct {
	Agents []AgentSummary `json:"agents"`
	Total  int            `json:"total"`
}

type AgentListOpts struct {
	Page    int
	Limit   int
	Status  string
	Type    string
	Channel string
}

func (c *Client) ListAgents(ctx context.Context, apiKey string, opts AgentListOpts) (*AgentSummaryResponse, error) {
	q := url.Values{}
	if opts.Page > 0 {
		q.Set("page", strconv.Itoa(opts.Page))
	}
	if opts.Limit > 0 {
		q.Set("limit", strconv.Itoa(opts.Limit))
	}
	if opts.Status != "" {
		q.Set("status", opts.Status)
	}
	if opts.Type != "" {
		q.Set("type", opts.Type)
	}
	if opts.Channel != "" {
		q.Set("channel", opts.Channel)
	}
	raw, err := c.get(ctx, "/v1/agents", q, apiKey)
	if err != nil {
		return nil, err
	}
	var full AgentListResponse
	if err := json.Unmarshal(raw, &full); err != nil {
		return nil, fmt.Errorf("decode agents list: %w", err)
	}
	resp := &AgentSummaryResponse{Total: full.Total}
	for _, a := range full.Agents {
		resp.Agents = append(resp.Agents, AgentSummary{
			ID:                a.ID,
			Name:              a.Name,
			Description:       a.Description,
			Type:              a.Type,
			Status:            a.Status,
			ChannelType:       a.ChannelType,
			CommunicationMode: a.CommunicationMode,
			IsEnabled:         a.IsEnabled,
			KnowledgeBaseID:   a.KnowledgeBaseID,
			ConversationCount: a.ConversationCount,
			CreatedAt:         a.CreatedAt,
			UpdatedAt:         a.UpdatedAt,
			HasDraftConfig:    a.HasDraftConfig,
			HasLiveConfig:     a.HasLiveConfig,
		})
	}
	return resp, nil
}

func (c *Client) GetAgent(ctx context.Context, apiKey, agentID string) (*Agent, error) {
	raw, err := c.get(ctx, fmt.Sprintf("/v1/agents/%s", agentID), nil, apiKey)
	if err != nil {
		return nil, err
	}
	var result Agent
	if err := json.Unmarshal(raw, &result); err != nil {
		return nil, fmt.Errorf("decode agent: %w", err)
	}
	return &result, nil
}

type AgentCreateRequest struct {
	Name              string `json:"name"`
	Description       string `json:"description,omitempty"`
	Type              string `json:"type,omitempty"`
	ChannelType       string `json:"channel_type,omitempty"`
	CommunicationMode string `json:"communication_mode,omitempty"`
	TriggerType       string `json:"trigger_type,omitempty"`
	Instructions      string `json:"instructions,omitempty"`
	SystemRules       string `json:"system_rules,omitempty"`
	KnowledgeBaseID   string `json:"knowledge_base_id,omitempty"`
	TemplateID        string `json:"template_id,omitempty"`
}

func (c *Client) CreateAgent(ctx context.Context, apiKey string, req AgentCreateRequest) (*Agent, error) {
	raw, err := c.post(ctx, "/v1/agents", req, apiKey)
	if err != nil {
		return nil, err
	}
	var result Agent
	if err := json.Unmarshal(raw, &result); err != nil {
		return nil, fmt.Errorf("decode agent: %w", err)
	}
	return &result, nil
}

type AgentUpdateRequest struct {
	Name         string `json:"name,omitempty"`
	Description  string `json:"description,omitempty"`
	Instructions string `json:"instructions,omitempty"`
	SystemRules  string `json:"system_rules,omitempty"`
	IsEnabled    *bool  `json:"is_enabled,omitempty"`
	TriggerType  string `json:"trigger_type,omitempty"`
}

func (c *Client) UpdateAgent(ctx context.Context, apiKey, agentID string, req AgentUpdateRequest) (*Agent, error) {
	raw, err := c.put(ctx, fmt.Sprintf("/v1/agents/%s", agentID), req, apiKey)
	if err != nil {
		return nil, err
	}
	var result Agent
	if err := json.Unmarshal(raw, &result); err != nil {
		return nil, fmt.Errorf("decode agent: %w", err)
	}
	return &result, nil
}

func (c *Client) DeleteAgent(ctx context.Context, apiKey, agentID string) error {
	_, err := c.delete(ctx, fmt.Sprintf("/v1/agents/%s", agentID), apiKey)
	return err
}

func (c *Client) PublishAgent(ctx context.Context, apiKey, agentID string) (*Agent, error) {
	raw, err := c.post(ctx, fmt.Sprintf("/v1/agents/%s/publish", agentID), nil, apiKey)
	if err != nil {
		return nil, err
	}
	var result Agent
	if err := json.Unmarshal(raw, &result); err != nil {
		return nil, fmt.Errorf("decode agent: %w", err)
	}
	return &result, nil
}

type CloneAgentRequest struct {
	Name string `json:"name,omitempty"`
}

func (c *Client) CloneAgent(ctx context.Context, apiKey, agentID string, req *CloneAgentRequest) (*Agent, error) {
	raw, err := c.put(ctx, fmt.Sprintf("/v1/agents/%s/clone", agentID), req, apiKey)
	if err != nil {
		return nil, err
	}
	var result Agent
	if err := json.Unmarshal(raw, &result); err != nil {
		return nil, fmt.Errorf("decode cloned agent: %w", err)
	}
	return &result, nil
}

type UpdateInstructionRequest struct {
	Instruction       string        `json:"instruction"`
	IntegratedActions []interface{} `json:"integrated_actions,omitempty"`
}

func (c *Client) UpdateInstruction(ctx context.Context, apiKey, agentID string, req UpdateInstructionRequest) (json.RawMessage, error) {
	return c.put(ctx, fmt.Sprintf("/v1/agents/%s/instruction", agentID), req, apiKey)
}

func (c *Client) GetRuntimeConfig(ctx context.Context, apiKey, agentID, status string) (json.RawMessage, error) {
	q := url.Values{}
	if status != "" {
		q.Set("status", status)
	}
	return c.get(ctx, fmt.Sprintf("/v1/agents/%s/runtime-config", agentID), q, apiKey)
}

func (c *Client) GetBrandkit(ctx context.Context, apiKey, agentID string) (json.RawMessage, error) {
	return c.get(ctx, fmt.Sprintf("/v1/agents/%s/brandkit", agentID), nil, apiKey)
}

type FetchBrandkitRequest struct {
	Identifier  string `json:"identifier,omitempty"`
	AgentID     string `json:"agent_id,omitempty"`
	Description string `json:"description,omitempty"`
}

func (c *Client) FetchBrandkit(ctx context.Context, apiKey string, req FetchBrandkitRequest) (json.RawMessage, error) {
	return c.post(ctx, "/v1/brandkit", req, apiKey)
}

type MinimizedButtonLabel struct {
	Enabled bool   `json:"enabled"`
	Label   string `json:"label"`
}

type BrandkitUpdate struct {
	AgentID              string                 `json:"agent_id,omitempty"`
	BrandName            string                 `json:"brand_name,omitempty"`
	Domain               string                 `json:"domain,omitempty"`
	BrandDescription     string                 `json:"brand_description,omitempty"`
	Logos                []map[string]string    `json:"logos,omitempty"`
	Colors               []map[string]string    `json:"colors,omitempty"`
	Fonts                []map[string]string    `json:"fonts,omitempty"`
	Persona              string                 `json:"persona,omitempty"`
	AppearanceConfig     map[string]interface{} `json:"appearance_config,omitempty"`
	SalesConfig          map[string]interface{} `json:"sales_config,omitempty"`
	ChatPlaceholder      string                 `json:"chat_placeholder,omitempty"`
	MinimizedButtonLabel *MinimizedButtonLabel  `json:"minimized_button_label,omitempty"`
}

func (c *Client) CreateBrandkit(ctx context.Context, apiKey, agentID string, req BrandkitUpdate) (json.RawMessage, error) {
	req.AgentID = agentID
	return c.post(ctx, fmt.Sprintf("/v1/agents/%s/brandkit", agentID), req, apiKey)
}

func (c *Client) UpdateBrandkit(ctx context.Context, apiKey, agentID string, req BrandkitUpdate) (json.RawMessage, error) {
	req.AgentID = agentID
	return c.put(ctx, fmt.Sprintf("/v1/agents/%s/brandkit", agentID), req, apiKey)
}

func (c *Client) DeleteBrandkit(ctx context.Context, apiKey, agentID string) (json.RawMessage, error) {
	return c.delete(ctx, fmt.Sprintf("/v1/agents/%s/brandkit", agentID), apiKey)
}

// ---- Chat ----

type ChatRequest struct {
	AgentID        string                 `json:"agent_id"`
	UserQuery      string                 `json:"user_query"`
	ConversationID string                 `json:"conversation_id,omitempty"`
	UserID         string                 `json:"user_id,omitempty"`
	Stream         bool                   `json:"stream,omitempty"`
	Metadata       map[string]interface{} `json:"metadata,omitempty"`
}

type ChatResponse struct {
	ConversationID string `json:"conversation_id"`
	Answer         string `json:"answer"`
}

func (c *Client) sendChat(ctx context.Context, path, apiKey string, req ChatRequest) (interface{}, error) {
	if req.Stream {
		return c.sendChatStream(ctx, path, apiKey, req)
	}
	return c.sendChatBlocking(ctx, path, apiKey, req)
}

func (c *Client) sendChatBlocking(ctx context.Context, path, apiKey string, req ChatRequest) (json.RawMessage, error) {
	return c.post(ctx, path, req, apiKey)
}

func (c *Client) sendChatStream(ctx context.Context, path, apiKey string, req ChatRequest) (*ChatResponse, error) {
	u := fmt.Sprintf("%s%s", c.baseURL, path)

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

	c.logCurl("POST", u, apiKey, "application/json", data)

	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, u, bytes.NewReader(data))
	if err != nil {
		return nil, fmt.Errorf("create chat request: %w", err)
	}
	httpReq.Header.Set("Authorization", "Bearer "+apiKey)
	httpReq.Header.Set("Content-Type", "application/json")
	httpReq.Header.Set("Accept", "text/event-stream")

	resp, err := c.httpClient.Do(httpReq)
	if err != nil {
		return nil, fmt.Errorf("chat request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		c.logResponse(resp.StatusCode, body)
		return nil, fmt.Errorf("chat returned %d: %s", resp.StatusCode, string(body))
	}
	c.logResponse(resp.StatusCode, []byte("(SSE stream)"))

	return c.consumeSSE(resp.Body)
}

func (c *Client) consumeSSE(body io.Reader) (*ChatResponse, error) {
	var answer string
	var conversationID string
	scanner := bufio.NewScanner(body)
	buf := make([]byte, 0, 64*1024)
	scanner.Buffer(buf, 1024*1024) // allow up to 1MB lines

	for scanner.Scan() {
		line := scanner.Text()
		// SSE spec: both "data: value" and "data:value" are valid
		var payload string
		if strings.HasPrefix(line, "data: ") {
			payload = strings.TrimPrefix(line, "data: ")
		} else if strings.HasPrefix(line, "data:") {
			payload = strings.TrimPrefix(line, "data:")
		} else {
			continue
		}
		if payload == "[DONE]" {
			break
		}

		var event struct {
			Type         string `json:"type"`
			ContentDelta string `json:"content_delta,omitempty"`
			Content      string `json:"content,omitempty"`
			Session      *struct {
				ConversationID string `json:"conversation_id"`
			} `json:"session,omitempty"`
		}
		if err := json.Unmarshal([]byte(payload), &event); err != nil {
			continue
		}

		switch event.Type {
		case "SESSION_START":
			if event.Session != nil {
				conversationID = event.Session.ConversationID
			}
		case "AGENT_MESSAGE":
			answer += event.ContentDelta
		case "AGENT_MESSAGE_END":
			if event.Content != "" {
				answer = event.Content
			}
		}
	}

	return &ChatResponse{
		ConversationID: conversationID,
		Answer:         answer,
	}, nil
}

func (c *Client) SendMessage(ctx context.Context, apiKey string, req ChatRequest) (interface{}, error) {
	return c.sendChat(ctx, "/v1/chat", apiKey, req)
}

func (c *Client) SendPreviewMessage(ctx context.Context, apiKey string, req ChatRequest) (interface{}, error) {
	return c.sendChat(ctx, "/v1/chat-preview", apiKey, req)
}

func (c *Client) SendTestMessage(ctx context.Context, apiKey string, req ChatRequest) (interface{}, error) {
	return c.sendChat(ctx, "/v1/chat-preview", apiKey, req)
}

// ---- Retrieve ----

type RetrieveRequest struct {
	AgentID        string  `json:"agent_id"`
	Query          string  `json:"query"`
	TopK           int     `json:"top_k,omitempty"`
	ScoreThreshold float64 `json:"score_threshold,omitempty"`
}

type RetrieveResponse struct {
	Query   string `json:"query"`
	Results string `json:"results"`
}

func (c *Client) RetrieveKnowledge(ctx context.Context, apiKey string, req RetrieveRequest) (*RetrieveResponse, error) {
	raw, err := c.post(ctx, "/v1/retrieve", req, apiKey)
	if err != nil {
		return nil, err
	}
	var result RetrieveResponse
	if err := json.Unmarshal(raw, &result); err != nil {
		return nil, fmt.Errorf("decode retrieve response: %w", err)
	}
	return &result, nil
}

// ---- Conversations ----

type ConversationListResponse struct {
	Data   []Conversation `json:"data"`
	Total  int            `json:"total"`
	Limit  int            `json:"limit"`
	Offset int            `json:"offset"`
}

type Conversation struct {
	ID           string          `json:"id"`
	TenantID     string          `json:"tenant_id,omitempty"`
	AgentID      string          `json:"agent_id"`
	ContactID    string          `json:"contact_id,omitempty"`
	ExternalID   string          `json:"external_id,omitempty"`
	Channel      string          `json:"channel,omitempty"`
	Status       string          `json:"status,omitempty"`
	Mode         string          `json:"mode,omitempty"`
	MessageCount int             `json:"message_count,omitempty"`
	LastMessage  string          `json:"last_message,omitempty"`
	Metadata     json.RawMessage `json:"metadata,omitempty"`
	StartedAt    string          `json:"started_at,omitempty"`
	CreatedAt    string          `json:"created_at"`
	UpdatedAt    string          `json:"updated_at"`
	FirstName    string          `json:"first_name,omitempty"`
	LastName     string          `json:"last_name,omitempty"`
	Intent       string          `json:"intent,omitempty"`
	Type         string          `json:"type,omitempty"`
}

type ConversationListOpts struct {
	Page      int
	Limit     int
	AgentID   string
	ContactID string
	Channel   string
	Status    string
	StartTime string
	EndTime   string
}

func (c *Client) ListConversations(ctx context.Context, apiKey string, opts ConversationListOpts) (*ConversationListResponse, error) {
	q := url.Values{}
	if opts.Page > 0 {
		q.Set("page", strconv.Itoa(opts.Page))
	}
	if opts.Limit > 0 {
		q.Set("limit", strconv.Itoa(opts.Limit))
	}
	if opts.AgentID != "" {
		q.Set("agent_id", opts.AgentID)
	}
	if opts.ContactID != "" {
		q.Set("contact_id", opts.ContactID)
	}
	if opts.Channel != "" {
		q.Set("channel", opts.Channel)
	}
	if opts.Status != "" {
		q.Set("status", opts.Status)
	}
	if opts.StartTime != "" {
		q.Set("start_time", opts.StartTime)
	}
	if opts.EndTime != "" {
		q.Set("end_time", opts.EndTime)
	}

	raw, err := c.get(ctx, "/v1/conversations", q, apiKey)
	if err != nil {
		return nil, err
	}

	var result ConversationListResponse
	if err := json.Unmarshal(raw, &result); err != nil {
		return nil, fmt.Errorf("decode conversations: %w", err)
	}
	return &result, nil
}

func (c *Client) GetConversation(ctx context.Context, apiKey, conversationID string) (json.RawMessage, error) {
	return c.get(ctx, fmt.Sprintf("/v1/conversations/%s", conversationID), nil, apiKey)
}

type ConversationMessagesOpts struct {
	Page  int
	Limit int
}

type MessagesResponse struct {
	Messages MessageData `json:"messages"`
	Source   string      `json:"source"`
}

type MessageData struct {
	Data    []Message `json:"data"`
	Limit   int       `json:"limit"`
	HasMore bool      `json:"has_more"`
}

type Message struct {
	ConversationID string `json:"conversation_id"`
	Query          string `json:"query"`
	Answer         string `json:"answer"`
	CreatedAt      int64  `json:"created_at"`
}

func (c *Client) GetConversationMessages(ctx context.Context, apiKey, conversationID string, opts ConversationMessagesOpts) (*MessagesResponse, error) {
	q := url.Values{}
	if opts.Page > 0 {
		q.Set("page", strconv.Itoa(opts.Page))
	}
	if opts.Limit > 0 {
		q.Set("limit", strconv.Itoa(opts.Limit))
	}

	raw, err := c.get(ctx, fmt.Sprintf("/v1/conversations/%s/messages", conversationID), q, apiKey)
	if err != nil {
		return nil, err
	}

	var result MessagesResponse
	if err := json.Unmarshal(raw, &result); err != nil {
		return nil, fmt.Errorf("decode messages: %w", err)
	}
	return &result, nil
}

func (c *Client) GetConversationContact(ctx context.Context, apiKey, conversationID string) (json.RawMessage, error) {
	return c.get(ctx, fmt.Sprintf("/v1/conversations/%s/contact", conversationID), nil, apiKey)
}

// BatchConversationResult holds the result of fetching a single conversation
// and its messages in a batch call.
type BatchConversationResult struct {
	ConversationID string           `json:"conversation_id"`
	Conversation   json.RawMessage  `json:"conversation,omitempty"`
	Messages       *MessagesResponse `json:"messages,omitempty"`
	Error          string           `json:"error,omitempty"`
}

// GetConversationsBatch fetches multiple conversations and their messages
// concurrently. Each conversation is fetched in its own goroutine.
func (c *Client) GetConversationsBatch(ctx context.Context, apiKey string, conversationIDs []string, includeMessages bool) []BatchConversationResult {
	results := make([]BatchConversationResult, len(conversationIDs))
	var wg sync.WaitGroup
	wg.Add(len(conversationIDs))

	for i, cid := range conversationIDs {
		go func(idx int, id string) {
			defer wg.Done()
			r := BatchConversationResult{ConversationID: id}

			conv, err := c.GetConversation(ctx, apiKey, id)
			if err != nil {
				r.Error = err.Error()
				results[idx] = r
				return
			}
			r.Conversation = conv

			if includeMessages {
				msgs, err := c.GetConversationMessages(ctx, apiKey, id, ConversationMessagesOpts{})
				if err != nil {
					r.Error = fmt.Sprintf("messages: %s", err.Error())
				} else {
					r.Messages = msgs
				}
			}

			results[idx] = r
		}(i, cid)
	}

	wg.Wait()
	return results
}

// ---- Contacts ----

type ContactsResponse struct {
	Data   []Contact `json:"data"`
	Total  int       `json:"total"`
	Limit  int       `json:"limit"`
	Offset int       `json:"offset"`
}

type Contact struct {
	ID          string          `json:"id"`
	TenantID    string          `json:"tenant_id"`
	AgentID     string          `json:"agent_id"`
	Source      string          `json:"source"`
	Email       string          `json:"email"`
	FirstName   string          `json:"first_name"`
	LastName    string          `json:"last_name"`
	Phone       string          `json:"phone"`
	Company     string          `json:"company"`
	Intent      string          `json:"intent"`
	LeadScore   int             `json:"lead_score"`
	LeadSummary string          `json:"lead_summary"`
	Insights    json.RawMessage `json:"insights,omitempty"`
	CreatedAt   string          `json:"created_at"`
	UpdatedAt   string          `json:"updated_at"`
}

type ContactListOpts struct {
	Page         int
	Limit        int
	AgentID      string
	LeadScoreMin int
	LeadScoreMax int
	Source       string
	Search       string
}

func (c *Client) ListContacts(ctx context.Context, apiKey string, opts ContactListOpts) (*ContactsResponse, error) {
	q := url.Values{}
	if opts.Page > 0 {
		q.Set("page", strconv.Itoa(opts.Page))
	}
	if opts.Limit > 0 {
		q.Set("limit", strconv.Itoa(opts.Limit))
	}
	if opts.AgentID != "" {
		q.Set("agent_id", opts.AgentID)
	}
	if opts.LeadScoreMin > 0 {
		q.Set("lead_score_min", strconv.Itoa(opts.LeadScoreMin))
	}
	if opts.LeadScoreMax > 0 {
		q.Set("lead_score_max", strconv.Itoa(opts.LeadScoreMax))
	}
	if opts.Source != "" {
		q.Set("source", opts.Source)
	}
	if opts.Search != "" {
		q.Set("search", opts.Search)
	}

	raw, err := c.get(ctx, "/v1/contacts", q, apiKey)
	if err != nil {
		return nil, err
	}

	var result ContactsResponse
	if err := json.Unmarshal(raw, &result); err != nil {
		return nil, fmt.Errorf("decode contacts: %w", err)
	}
	return &result, nil
}

func (c *Client) GetContact(ctx context.Context, apiKey, contactID string) (json.RawMessage, error) {
	return c.get(ctx, fmt.Sprintf("/v1/contacts/%s", contactID), nil, apiKey)
}

type ContactConversationsOpts struct {
	Page  int
	Limit int
}

func (c *Client) GetContactConversations(ctx context.Context, apiKey, contactID string, opts ContactConversationsOpts) (json.RawMessage, error) {
	q := url.Values{}
	if opts.Page > 0 {
		q.Set("page", strconv.Itoa(opts.Page))
	}
	if opts.Limit > 0 {
		q.Set("limit", strconv.Itoa(opts.Limit))
	}
	return c.get(ctx, fmt.Sprintf("/v1/contacts/%s/conversations", contactID), q, apiKey)
}

// ---- Knowledge Bases ----

type KnowledgeBaseCreateRequest struct {
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	Permission  string `json:"permission,omitempty"`
}

type KnowledgeBaseCreateResponse struct {
	ID                string `json:"id"`
	Name              string `json:"name"`
	Description       string `json:"description"`
	Permission        string `json:"permission"`
	IndexingTechnique string `json:"indexing_technique"`
	DocumentCount     int    `json:"document_count"`
}

func (c *Client) CreateKnowledgeBase(ctx context.Context, apiKey string, req KnowledgeBaseCreateRequest) (*KnowledgeBaseCreateResponse, error) {
	if req.Permission == "" {
		req.Permission = "all_team_members"
	}
	wrapper := map[string]interface{}{
		"kb_data": req,
	}
	raw, err := c.post(ctx, "/v1/knowledge-bases", wrapper, apiKey)
	if err != nil {
		return nil, err
	}
	var result KnowledgeBaseCreateResponse
	if err := json.Unmarshal(raw, &result); err != nil {
		return nil, fmt.Errorf("decode create kb response: %w", err)
	}
	return &result, nil
}

type DocumentListResponse struct {
	Data           []Document `json:"data"`
	Total          int        `json:"total"`
	Page           int        `json:"page"`
	Limit          int        `json:"limit"`
	HasMore        bool       `json:"has_more"`
	CompletedCount int        `json:"completed_count"`
	ErrorCount     int        `json:"error_count"`
}

type Document struct {
	ID             string          `json:"id"`
	Name           string          `json:"name"`
	DataSourceType string          `json:"data_source_type"`
	DataSourceInfo json.RawMessage `json:"data_source_info,omitempty"`
	IndexingStatus string          `json:"indexing_status"`
	Enabled        bool            `json:"enabled"`
	Tokens         int             `json:"tokens"`
	WordCount      int             `json:"word_count"`
	HitCount       int             `json:"hit_count"`
	DisplayStatus  string          `json:"display_status"`
	CreatedAt      int64           `json:"created_at"`
}

type DocListOpts struct {
	Page  int
	Limit int
}

func (c *Client) ListDocuments(ctx context.Context, apiKey, datasetID string, opts DocListOpts) (*DocumentListResponse, error) {
	q := url.Values{}
	if opts.Page > 0 {
		q.Set("page", strconv.Itoa(opts.Page))
	}
	if opts.Limit > 0 {
		q.Set("limit", strconv.Itoa(opts.Limit))
	}

	raw, err := c.get(ctx, fmt.Sprintf("/v1/knowledge-bases/%s/documents", datasetID), q, apiKey)
	if err != nil {
		return nil, err
	}

	var result DocumentListResponse
	if err := json.Unmarshal(raw, &result); err != nil {
		return nil, fmt.Errorf("decode documents: %w", err)
	}
	return &result, nil
}

type DocumentDeleteRequest struct {
	TenantID string   `json:"tenant_id"`
	URLs     []string `json:"urls"`
	Size     int      `json:"size"`
}

func (c *Client) DeleteDocument(ctx context.Context, apiKey, datasetID, documentID string, body *DocumentDeleteRequest) error {
	path := fmt.Sprintf("/v1/knowledge-bases/%s/documents/%s", datasetID, documentID)
	if body != nil {
		_, err := c.doJSON(ctx, http.MethodDelete, path, body, apiKey)
		return err
	}
	_, err := c.delete(ctx, path, apiKey)
	return err
}

type ImportWebRequest struct {
	Title             string   `json:"title"`
	URLs              []string `json:"urls"`
	IndexingTechnique string   `json:"indexing_technique,omitempty"`
}

type ImportWebsiteRequest struct {
	URL      string `json:"url"`
	MaxPages int    `json:"max_pages,omitempty"`
}

type ImportResponse struct {
	JobID   string `json:"job_id"`
	Message string `json:"message"`
}

func (c *Client) ImportKnowledgeFromURL(ctx context.Context, apiKey, datasetID, title string, urls []string) (*ImportResponse, error) {
	raw, err := c.post(ctx, fmt.Sprintf("/v1/knowledge-bases/%s/import/web", datasetID), ImportWebRequest{URLs: urls, Title: title}, apiKey)
	if err != nil {
		return nil, err
	}
	var result ImportResponse
	if err := json.Unmarshal(raw, &result); err != nil {
		return nil, fmt.Errorf("decode import response: %w", err)
	}
	return &result, nil
}

func (c *Client) ImportKnowledgeFromWebsite(ctx context.Context, apiKey, datasetID, webURL string, maxPages int) (*ImportResponse, error) {
	req := ImportWebsiteRequest{URL: webURL}
	if maxPages > 0 {
		req.MaxPages = maxPages
	}
	raw, err := c.post(ctx, fmt.Sprintf("/v1/knowledge-bases/%s/import/website", datasetID), req, apiKey)
	if err != nil {
		return nil, err
	}
	var result ImportResponse
	if err := json.Unmarshal(raw, &result); err != nil {
		return nil, fmt.Errorf("decode import response: %w", err)
	}
	return &result, nil
}

type ImportStatusResponse struct {
	JobID  string `json:"job_id"`
	Status string `json:"status"`
}

// GetImportStatus checks the status of a URL-based import job.
// The RAG backend requires the original import URL as a query parameter.
func (c *Client) GetImportStatus(ctx context.Context, apiKey, datasetID, jobID, importURL string) (*ImportStatusResponse, error) {
	q := url.Values{}
	if importURL != "" {
		q.Set("url", importURL)
	}
	raw, err := c.get(ctx, fmt.Sprintf("/v1/knowledge-bases/%s/import/%s/status", datasetID, jobID), q, apiKey)
	if err != nil {
		return nil, err
	}
	var result ImportStatusResponse
	if err := json.Unmarshal(raw, &result); err != nil {
		return nil, fmt.Errorf("decode import status: %w", err)
	}
	return &result, nil
}

type DocumentCreateRequest struct {
	Title             string `json:"title"`
	Content           string `json:"content"`
	SourceType        string `json:"source_type,omitempty"`
	IndexingTechnique string `json:"indexing_technique,omitempty"`
}

func (c *Client) CreateDocument(ctx context.Context, apiKey, datasetID string, req DocumentCreateRequest) (json.RawMessage, error) {
	return c.post(ctx, fmt.Sprintf("/v1/knowledge-bases/%s/documents", datasetID), req, apiKey)
}

// ---- Annotations ----

type Annotation struct {
	ID             string   `json:"id"`
	Title          string   `json:"title"`
	Questions      []string `json:"questions"`
	Answer         string   `json:"answer"`
	HitCount       int      `json:"hit_count"`
	IndexingStatus string   `json:"indexing_status"`
	CreatedAt      int64    `json:"created_at"`
}

type AnnotationListResponse struct {
	Data    []Annotation `json:"data"`
	Total   int          `json:"total"`
	Page    int          `json:"page"`
	Limit   int          `json:"limit"`
	HasMore bool         `json:"has_more"`
}

type AnnotationListOpts struct {
	Page    int
	Limit   int
	Keyword string
}

func (c *Client) ListAnnotations(ctx context.Context, apiKey, agentID string, opts AnnotationListOpts) (*AnnotationListResponse, error) {
	q := url.Values{}
	if opts.Page > 0 {
		q.Set("page", strconv.Itoa(opts.Page))
	}
	if opts.Limit > 0 {
		q.Set("limit", strconv.Itoa(opts.Limit))
	}
	if opts.Keyword != "" {
		q.Set("keyword", opts.Keyword)
	}

	raw, err := c.get(ctx, fmt.Sprintf("/v1/agents/%s/annotations", agentID), q, apiKey)
	if err != nil {
		return nil, err
	}

	var result AnnotationListResponse
	if err := json.Unmarshal(raw, &result); err != nil {
		return nil, fmt.Errorf("decode annotations: %w", err)
	}
	return &result, nil
}

type AnnotationCreateRequest struct {
	Title     string   `json:"title"`
	Questions []string `json:"questions"`
	Answer    string   `json:"answer"`
}

type AnnotationCreateResponse struct {
	ID        string `json:"id"`
	Title     string `json:"title"`
	Content   string `json:"content"`
	CreatedAt int64  `json:"created_at"`
}

func (c *Client) CreateAnnotation(ctx context.Context, apiKey, agentID string, req AnnotationCreateRequest) (*AnnotationCreateResponse, error) {
	raw, err := c.post(ctx, fmt.Sprintf("/v1/agents/%s/annotations", agentID), req, apiKey)
	if err != nil {
		return nil, err
	}

	var result AnnotationCreateResponse
	if err := json.Unmarshal(raw, &result); err != nil {
		return nil, fmt.Errorf("decode annotation: %w", err)
	}
	return &result, nil
}

func (c *Client) GetAnnotation(ctx context.Context, apiKey, agentID, annotationID string) (json.RawMessage, error) {
	return c.get(ctx, fmt.Sprintf("/v1/agents/%s/annotations/%s", agentID, annotationID), nil, apiKey)
}

type AnnotationUpdateRequest struct {
	Title     string   `json:"title,omitempty"`
	Questions []string `json:"questions,omitempty"`
	Answer    string   `json:"answer,omitempty"`
}

func (c *Client) UpdateAnnotation(ctx context.Context, apiKey, agentID, annotationID string, req AnnotationUpdateRequest) (json.RawMessage, error) {
	return c.put(ctx, fmt.Sprintf("/v1/agents/%s/annotations/%s", agentID, annotationID), req, apiKey)
}

func (c *Client) DeleteAnnotation(ctx context.Context, apiKey, agentID, annotationID string) error {
	_, err := c.delete(ctx, fmt.Sprintf("/v1/agents/%s/annotations/%s", agentID, annotationID), apiKey)
	return err
}

// ---- Actions ----

type ActionsResponse struct {
	TenantID string   `json:"tenant_id"`
	Actions  []Action `json:"actions"`
	Count    int      `json:"count"`
}

type Action struct {
	ID            string          `json:"id"`
	AtID          string          `json:"at_id"`
	AtName        string          `json:"at_name"`
	Name          string          `json:"name"`
	Description   string          `json:"description"`
	Type          string          `json:"type"`
	Platform      string          `json:"platform"`
	ConnectionID  string          `json:"connection_id"`
	Status        string          `json:"status"`
	ExecutionType string          `json:"execution_type"`
	Metadata      json.RawMessage `json:"metadata,omitempty"`
	CreatedAt     string          `json:"created_at"`
	UpdatedAt     string          `json:"updated_at"`
}

type ActionListOpts struct {
	Page     int
	Limit    int
	Type     string
	Platform string
}

func (c *Client) ListActions(ctx context.Context, apiKey string, opts ActionListOpts) (json.RawMessage, error) {
	q := url.Values{}
	if opts.Page > 0 {
		q.Set("page", strconv.Itoa(opts.Page))
	}
	if opts.Limit > 0 {
		q.Set("limit", strconv.Itoa(opts.Limit))
	}
	if opts.Type != "" {
		q.Set("type", opts.Type)
	}
	if opts.Platform != "" {
		q.Set("platform", opts.Platform)
	}
	return c.get(ctx, "/v1/actions", q, apiKey)
}

func (c *Client) GetAction(ctx context.Context, apiKey, actionID string) (json.RawMessage, error) {
	return c.get(ctx, fmt.Sprintf("/v1/actions/%s", actionID), nil, apiKey)
}

// ---- Analytics (V2) ----

type AnalyticsOpts struct {
	AgentID   string
	StartTime string
	EndTime   string
}

func (c *Client) GetAnalytics(ctx context.Context, apiKey string, opts AnalyticsOpts) (json.RawMessage, error) {
	q := url.Values{}
	if opts.AgentID != "" {
		q.Set("agent_id", opts.AgentID)
	}
	if opts.StartTime != "" {
		q.Set("start_time", opts.StartTime)
	}
	if opts.EndTime != "" {
		q.Set("end_time", opts.EndTime)
	}
	return c.get(ctx, "/v1/analytics", q, apiKey)
}

type DistributionOpts struct {
	AgentID   string
	StartTime string
	EndTime   string
	Timezone  string
	Channel   string
}

func (c *Client) GetDistribution(ctx context.Context, apiKey string, opts DistributionOpts) (json.RawMessage, error) {
	q := url.Values{}
	q.Set("agent_id", opts.AgentID)
	if opts.StartTime != "" {
		q.Set("start_time", opts.StartTime)
	}
	if opts.EndTime != "" {
		q.Set("end_time", opts.EndTime)
	}
	if opts.Timezone != "" {
		q.Set("timezone", opts.Timezone)
	}
	if opts.Channel != "" {
		q.Set("channel", opts.Channel)
	}
	return c.get(ctx, "/v1/distribution", q, apiKey)
}

// ---- Templates ----

type TemplateListOpts struct {
	Category string
	Type     string
	Limit    int
}

func (c *Client) ListTemplates(ctx context.Context, apiKey string, opts TemplateListOpts) (json.RawMessage, error) {
	q := url.Values{}
	if opts.Category != "" {
		q.Set("category", opts.Category)
	}
	if opts.Type != "" {
		q.Set("type", opts.Type)
	}
	if opts.Limit > 0 {
		q.Set("limit", strconv.Itoa(opts.Limit))
	}
	return c.get(ctx, "/v1/templates", q, apiKey)
}

func (c *Client) ListTemplateCategories(ctx context.Context, apiKey string) (json.RawMessage, error) {
	return c.get(ctx, "/v1/templates/categories", nil, apiKey)
}

func (c *Client) GetTemplate(ctx context.Context, apiKey, templateID string) (json.RawMessage, error) {
	return c.get(ctx, fmt.Sprintf("/v1/templates/%s", templateID), nil, apiKey)
}

// ---- Connections ----

type ConnectionsResponse struct {
	TenantID    string       `json:"tenant_id,omitempty"`
	Platform    string       `json:"platform,omitempty"`
	Connections []Connection `json:"connections"`
	TotalCount  int          `json:"total_count,omitempty"`
}

type Connection struct {
	ID             int    `json:"id"`
	TenantID       string `json:"tenant_id,omitempty"`
	Platform       string `json:"platform"`
	ConnectionID   string `json:"connection_id"`
	ConnectionName string `json:"connection_name"`
	Enabled        bool   `json:"enabled"`
	CreatedAt      string `json:"created_at"`
	UpdatedAt      string `json:"updated_at,omitempty"`
}

func (c *Client) ListConnections(ctx context.Context, apiKey string) (*ConnectionsResponse, error) {
	raw, err := c.get(ctx, "/v1/connections", nil, apiKey)
	if err != nil {
		return nil, err
	}
	var result ConnectionsResponse
	if err := json.Unmarshal(raw, &result); err != nil {
		return nil, fmt.Errorf("decode connections: %w", err)
	}
	return &result, nil
}

type ConnectionCreateRequest struct {
	Platform       string `json:"platform"`
	ConnectionID   string `json:"connection_id"`
	ConnectionName string `json:"connection_name"`
	TenantID       string `json:"tenant_id"`
}

func (c *Client) CreateConnection(ctx context.Context, apiKey string, req ConnectionCreateRequest) (json.RawMessage, error) {
	return c.post(ctx, "/v1/connections", req, apiKey)
}

type WatiSettings struct {
	WatiEndpoint    string `json:"wati_endpoint"`
	WatiUserToken   string `json:"wati_user_token"`
	WatiTenant      string `json:"wati_tenant"`
	HumanAgentEmail string `json:"human_agent_email"`
}

func (c *Client) GetWatiSettings(ctx context.Context, apiKey string) (json.RawMessage, error) {
	return c.get(ctx, "/v1/wati/settings", nil, apiKey)
}

// ---- Webhooks ----

type WebhookCreateRequest struct {
	URL    string   `json:"url"`
	Events []string `json:"events"`
}

type WebhookUpdateRequest struct {
	IsActive *bool    `json:"is_active,omitempty"`
	Events   []string `json:"events,omitempty"`
}

func (c *Client) ListWebhooks(ctx context.Context, apiKey string) (json.RawMessage, error) {
	return c.get(ctx, "/v1/webhooks", nil, apiKey)
}

func (c *Client) CreateWebhook(ctx context.Context, apiKey string, req WebhookCreateRequest) (json.RawMessage, error) {
	return c.post(ctx, "/v1/webhooks", req, apiKey)
}

func (c *Client) UpdateWebhook(ctx context.Context, apiKey, webhookID string, req WebhookUpdateRequest) (json.RawMessage, error) {
	return c.put(ctx, fmt.Sprintf("/v1/webhooks/%s", webhookID), req, apiKey)
}

func (c *Client) DeleteWebhook(ctx context.Context, apiKey, webhookID string) error {
	_, err := c.delete(ctx, fmt.Sprintf("/v1/webhooks/%s", webhookID), apiKey)
	return err
}

// ---- Testing ----

type TestCaseConfig struct {
	HappyCount      int `json:"happy_count,omitempty"`
	ConfusedCount   int `json:"confused_count,omitempty"`
	ImplicitCount   int `json:"implicit_count,omitempty"`
	ToolMisuseCount int `json:"tool_misuse_count,omitempty"`
	OosCount        int `json:"oos_count,omitempty"`
}

type PipelineRequest struct {
	AgentID             string          `json:"agent_id"`
	UserID              string          `json:"user_id"`
	Instruction         string          `json:"instruction,omitempty"`
	Config              *TestCaseConfig `json:"config,omitempty"`
	RunAnalyze          *bool           `json:"run_analyze,omitempty"`
	ExecuteConcurrency  int             `json:"execute_concurrency,omitempty"`
	EvaluateConcurrency int             `json:"evaluate_concurrency,omitempty"`
}

func (c *Client) StartTestPipeline(ctx context.Context, apiKey string, req PipelineRequest) (json.RawMessage, error) {
	return c.postLong(ctx, "/v1/testing/all", req, apiKey)
}

func (c *Client) ListPipelineJobs(ctx context.Context, apiKey, agentID string, limit int) (json.RawMessage, error) {
	q := url.Values{}
	q.Set("agent_id", agentID)
	if limit > 0 {
		q.Set("limit", strconv.Itoa(limit))
	}
	return c.get(ctx, "/v1/testing/all", q, apiKey)
}

func (c *Client) GetPipelineJob(ctx context.Context, apiKey, jobID string) (json.RawMessage, error) {
	return c.get(ctx, fmt.Sprintf("/v1/testing/all/%s", jobID), nil, apiKey)
}

type CreateTestSuiteRequest struct {
	AgentID     string          `json:"agent_id"`
	Instruction string          `json:"instruction,omitempty"`
	Name        string          `json:"name,omitempty"`
	Config      *TestCaseConfig `json:"config,omitempty"`
}

func (c *Client) CreateTestSuite(ctx context.Context, apiKey string, req CreateTestSuiteRequest) (json.RawMessage, error) {
	return c.postLong(ctx, "/v1/testing/test-suites", req, apiKey)
}

type TestSuiteListOpts struct {
	AgentID string
	Status  string
	Limit   int
}

func (c *Client) ListTestSuites(ctx context.Context, apiKey string, opts TestSuiteListOpts) (json.RawMessage, error) {
	q := url.Values{}
	if opts.AgentID != "" {
		q.Set("agent_id", opts.AgentID)
	}
	if opts.Status != "" {
		q.Set("status", opts.Status)
	}
	if opts.Limit > 0 {
		q.Set("limit", strconv.Itoa(opts.Limit))
	}
	return c.get(ctx, "/v1/testing/test-suites", q, apiKey)
}

func (c *Client) GetTestSuite(ctx context.Context, apiKey, generatorID string) (json.RawMessage, error) {
	return c.get(ctx, fmt.Sprintf("/v1/testing/test-suites/%s", generatorID), nil, apiKey)
}

func (c *Client) GetTestSuiteProgress(ctx context.Context, apiKey, generatorID string) (json.RawMessage, error) {
	return c.get(ctx, fmt.Sprintf("/v1/testing/test-suites/%s/progress", generatorID), nil, apiKey)
}

type AddTestCaseRequest struct {
	UserQuery      string `json:"user_query"`
	ExpectedAnswer string `json:"expected_answer,omitempty"`
	UseLLM         *bool  `json:"use_llm,omitempty"`
}

func (c *Client) AddTestCase(ctx context.Context, apiKey, generatorID string, req AddTestCaseRequest) (json.RawMessage, error) {
	return c.post(ctx, fmt.Sprintf("/v1/testing/test-suites/%s/test-cases", generatorID), req, apiKey)
}

type CreateRunRequest struct {
	UserID              string `json:"user_id"`
	NewInstruction      string `json:"new_instruction,omitempty"`
	RunAnalyze          *bool  `json:"run_analyze,omitempty"`
	ExecuteConcurrency  int    `json:"execute_concurrency,omitempty"`
	EvaluateConcurrency int    `json:"evaluate_concurrency,omitempty"`
}

func (c *Client) CreateTestRun(ctx context.Context, apiKey, generatorID string, req CreateRunRequest) (json.RawMessage, error) {
	return c.postLong(ctx, fmt.Sprintf("/v1/testing/test-suites/%s/runs", generatorID), req, apiKey)
}

func (c *Client) ListTestRuns(ctx context.Context, apiKey, agentID string, limit int) (json.RawMessage, error) {
	q := url.Values{}
	q.Set("agent_id", agentID)
	if limit > 0 {
		q.Set("limit", strconv.Itoa(limit))
	}
	return c.get(ctx, "/v1/testing/runs", q, apiKey)
}

func (c *Client) GetTestRun(ctx context.Context, apiKey, runID string) (json.RawMessage, error) {
	return c.get(ctx, fmt.Sprintf("/v1/testing/runs/%s", runID), nil, apiKey)
}

func (c *Client) CreateRunAnalysis(ctx context.Context, apiKey, runID string) (json.RawMessage, error) {
	return c.postLong(ctx, fmt.Sprintf("/v1/testing/runs/%s/analyses", runID), nil, apiKey)
}

func (c *Client) GetRunAnalysis(ctx context.Context, apiKey, runID string) (json.RawMessage, error) {
	return c.get(ctx, fmt.Sprintf("/v1/testing/runs/%s/analyses", runID), nil, apiKey)
}

func (c *Client) AnalyzeFailures(ctx context.Context, apiKey, runID string) (json.RawMessage, error) {
	return c.postLong(ctx, fmt.Sprintf("/v1/testing/runs/%s/analyze-failures", runID), nil, apiKey)
}

type ImprovementSuggestion struct {
	Priority   string `json:"priority,omitempty"`
	Suggestion string `json:"suggestion"`
}

type GenerateInstructionRequest struct {
	Instruction            string                  `json:"instruction"`
	AnalysisJSON           any                     `json:"analysis_json,omitempty"`
	ImprovementSuggestions []ImprovementSuggestion `json:"improvement_suggestions,omitempty"`
	AgentDescription       string                  `json:"agent_description,omitempty"`
}

func (c *Client) GenerateInstruction(ctx context.Context, apiKey string, req GenerateInstructionRequest) (json.RawMessage, error) {
	return c.post(ctx, "/v1/testing/generate-instruction", req, apiKey)
}

func (c *Client) GetAgentTestOverview(ctx context.Context, apiKey, agentID string) (json.RawMessage, error) {
	return c.get(ctx, fmt.Sprintf("/v1/testing/agents/%s/overview", agentID), nil, apiKey)
}

func (c *Client) GetAgentLatestRun(ctx context.Context, apiKey, agentID string, runType string) (json.RawMessage, error) {
	q := url.Values{}
	if runType != "" {
		q.Set("type", runType)
	}
	return c.get(ctx, fmt.Sprintf("/v1/testing/agents/%s/latest-run", agentID), q, apiKey)
}

func (c *Client) GetAgentTestCases(ctx context.Context, apiKey, agentID string) (json.RawMessage, error) {
	return c.get(ctx, fmt.Sprintf("/v1/testing/agents/%s/test-cases", agentID), nil, apiKey)
}

// ---- Tenant Resolution ----

// ResolveTenantID returns the tenant ID associated with the given API key.
// It calls a lightweight endpoint and caches nothing (caller may cache).
func (c *Client) ResolveTenantID(ctx context.Context, apiKey string) (string, error) {
	conn, err := c.ListConnections(ctx, apiKey)
	if err != nil {
		return "", fmt.Errorf("resolve tenant: %w", err)
	}
	if conn.TenantID == "" {
		return "", fmt.Errorf("resolve tenant: empty tenant_id in response")
	}
	return conn.TenantID, nil
}

// ---- MCP / Integrations (Composio) ----

func (c *Client) GetMCPInfo(ctx context.Context, apiKey string) (json.RawMessage, error) {
	return c.get(ctx, "/v1/mcp/info", nil, apiKey)
}

func (c *Client) ListMCPActionTemplates(ctx context.Context, apiKey string) (json.RawMessage, error) {
	return c.get(ctx, "/v1/mcp/action_templates", nil, apiKey)
}

func (c *Client) ListMCPActions(ctx context.Context, apiKey string) (json.RawMessage, error) {
	return c.get(ctx, "/v1/mcp/actions", nil, apiKey)
}

func (c *Client) GetMCPAction(ctx context.Context, apiKey, actionID string) (json.RawMessage, error) {
	return c.get(ctx, fmt.Sprintf("/v1/mcp/actions/%s", actionID), nil, apiKey)
}

type MCPActionCreateRequest struct {
	ActionName   string                 `json:"action_name"`
	ActionIntent string                 `json:"action_intent,omitempty"`
	AtID         string                 `json:"at_id"`
	ConnectionID string                 `json:"connection_id,omitempty"`
	Status       string                 `json:"status"`
	CreatedBy    string                 `json:"created_by,omitempty"`
	TenantID     string                 `json:"tenant_id"`
	Metadata     map[string]interface{} `json:"metadata,omitempty"`
	Platform     string                 `json:"platform,omitempty"`
}

func (c *Client) CreateMCPAction(ctx context.Context, apiKey string, req MCPActionCreateRequest) (json.RawMessage, error) {
	return c.post(ctx, "/v1/mcp/actions", req, apiKey)
}

func (c *Client) UpdateMCPAction(ctx context.Context, apiKey, actionID string, body map[string]interface{}) (json.RawMessage, error) {
	return c.doJSON(ctx, http.MethodPatch, fmt.Sprintf("/v1/mcp/actions/%s", actionID), body, apiKey)
}

func (c *Client) DeleteMCPAction(ctx context.Context, apiKey, actionID string) (json.RawMessage, error) {
	return c.delete(ctx, fmt.Sprintf("/v1/mcp/actions/%s", actionID), apiKey)
}

type MCPAuthRequest struct {
	Platform    string `json:"platform"`
	TenantID    string `json:"tenant_id"`
	BearerToken string `json:"bearer_token,omitempty"`
	APIEndpoint string `json:"api_endpoint,omitempty"`
}

func (c *Client) CreateMCPAuth(ctx context.Context, apiKey string, req MCPAuthRequest) (json.RawMessage, error) {
	return c.post(ctx, "/v1/mcp/auth", req, apiKey)
}

func (c *Client) GetMCPAuth(ctx context.Context, apiKey, connectionID string) (json.RawMessage, error) {
	return c.get(ctx, fmt.Sprintf("/v1/mcp/auth/%s", connectionID), nil, apiKey)
}

func (c *Client) DeleteMCPAuth(ctx context.Context, apiKey, connectionID string) (json.RawMessage, error) {
	return c.delete(ctx, fmt.Sprintf("/v1/mcp/auth/%s", connectionID), apiKey)
}

type MCPWaitAuthRequest struct {
	Timeout  int    `json:"timeout"`
	Platform string `json:"platform,omitempty"`
}

type MCPWaitAuthResponse struct {
	Success     bool   `json:"success"`
	AccountName string `json:"account_name,omitempty"`
}

func (c *Client) WaitMCPAuth(ctx context.Context, apiKey, connectionID string, req MCPWaitAuthRequest) (*MCPWaitAuthResponse, error) {
	raw, err := c.post(ctx, fmt.Sprintf("/v1/mcp/auth/%s/wait", connectionID), req, apiKey)
	if err != nil {
		return nil, err
	}
	var result MCPWaitAuthResponse
	if err := json.Unmarshal(raw, &result); err != nil {
		return nil, fmt.Errorf("decode wait auth response: %w", err)
	}
	return &result, nil
}

type MCPRegisterAction struct {
	ActionID string `json:"action_id"`
	AtID     string `json:"at_id"`
}

type MCPRegisterRequest struct {
	AgentID  string              `json:"agent_id"`
	Mode     string              `json:"mode"`
	Actions  []MCPRegisterAction `json:"actions"`
	TenantID string              `json:"tenant_id"`
	Modality string              `json:"modality,omitempty"`
}

func (c *Client) RegisterMCPTools(ctx context.Context, apiKey string, req MCPRegisterRequest) (json.RawMessage, error) {
	return c.post(ctx, "/v1/mcp/register", req, apiKey)
}

type MCPDeregisterRequest struct {
	AgentID  string `json:"agent_id"`
	TenantID string `json:"tenant_id"`
}

func (c *Client) DeregisterMCPTools(ctx context.Context, apiKey string, req MCPDeregisterRequest) (json.RawMessage, error) {
	return c.post(ctx, "/v1/mcp/deregister", req, apiKey)
}

// ListMCPAgentTools calls the MCP server JSON-RPC 2.0 endpoint to list registered tools for an agent.
func (c *Client) ListMCPAgentTools(ctx context.Context, apiKey, agentID, mode, modality string) (json.RawMessage, error) {
	if mode == "" {
		mode = "published"
	}
	if modality == "" {
		modality = "text"
	}
	path := fmt.Sprintf("/v1/mcp/server?agent_id=%s&mode=%s&modality=%s", agentID, mode, modality)
	rpcReq := map[string]interface{}{
		"jsonrpc": "2.0",
		"id":      1,
		"method":  "tools/list",
		"params":  map[string]interface{}{},
	}
	return c.post(ctx, path, rpcReq, apiKey)
}

// ---- Voice Agent Config ----

func (c *Client) ListVoiceAgents(ctx context.Context, apiKey string, includeDisabled bool) (json.RawMessage, error) {
	q := url.Values{}
	if includeDisabled {
		q.Set("include_disabled", "true")
	}
	return c.get(ctx, "/v1/voice/agents", q, apiKey)
}

func (c *Client) GetVoiceAgent(ctx context.Context, apiKey, agentID string) (json.RawMessage, error) {
	return c.get(ctx, fmt.Sprintf("/v1/voice/agents/%s", agentID), nil, apiKey)
}

func (c *Client) GetVoiceAgentByTextAgent(ctx context.Context, apiKey, textAgentID string) (json.RawMessage, error) {
	q := url.Values{}
	q.Set("text_agent_id", textAgentID)
	return c.get(ctx, "/v1/voice/agents/by-text-agent", q, apiKey)
}

func (c *Client) GetDefaultTenantVoiceAgents(ctx context.Context, apiKey string) (json.RawMessage, error) {
	return c.get(ctx, "/v1/voice/agents/default-tenant", nil, apiKey)
}

type CreateVoiceAgentRequest struct {
	VoiceTenantID string          `json:"voice_tenant_id"`
	AgentName     string          `json:"agent_name"`
	TextAgentID   string          `json:"text_agent_id,omitempty"`
	Instruction   string          `json:"instruction,omitempty"`
	AgentConfig   json.RawMessage `json:"agent_config,omitempty"`
}

func (c *Client) CreateVoiceAgent(ctx context.Context, apiKey string, req CreateVoiceAgentRequest) (json.RawMessage, error) {
	return c.post(ctx, "/v1/voice/agents", req, apiKey)
}

type QuickCreateVoiceAgentRequest struct {
	TextAgentID string `json:"text_agent_id"`
	TemplateID  string `json:"template_id"`
	TenantID    string `json:"tenant_id,omitempty"`
	Alias       string `json:"alias,omitempty"`
}

func (c *Client) QuickCreateVoiceAgent(ctx context.Context, apiKey string, req QuickCreateVoiceAgentRequest) (json.RawMessage, error) {
	return c.post(ctx, "/v1/voice/agents/quick-create", req, apiKey)
}

type UpdateVoiceAgentRequest struct {
	AgentName   string          `json:"agent_name,omitempty"`
	Instruction string          `json:"instruction,omitempty"`
	AgentConfig json.RawMessage `json:"agent_config,omitempty"`
	Disabled    *bool           `json:"disabled,omitempty"`
}

func (c *Client) UpdateVoiceAgent(ctx context.Context, apiKey, agentID string, req UpdateVoiceAgentRequest) (json.RawMessage, error) {
	return c.put(ctx, fmt.Sprintf("/v1/voice/agents/%s", agentID), req, apiKey)
}

func (c *Client) DeleteVoiceAgent(ctx context.Context, apiKey, agentID string) error {
	_, err := c.delete(ctx, fmt.Sprintf("/v1/voice/agents/%s", agentID), apiKey)
	return err
}

type PublishVoiceAgentRequest struct {
	AgentID string `json:"agent_id"`
	Comment string `json:"comment,omitempty"`
}

func (c *Client) PublishVoiceAgent(ctx context.Context, apiKey string, req PublishVoiceAgentRequest) (json.RawMessage, error) {
	return c.post(ctx, "/v1/voice/agents/publish", req, apiKey)
}

func (c *Client) GetVoiceAgentCount(ctx context.Context, apiKey string) (json.RawMessage, error) {
	return c.get(ctx, "/v1/voice/agents/count", nil, apiKey)
}

func (c *Client) CheckVoiceAgentExists(ctx context.Context, apiKey, agentID string) (bool, error) {
	status, err := c.head(ctx, fmt.Sprintf("/v1/voice/agents/%s", agentID), apiKey)
	if err != nil {
		return false, err
	}
	return status == http.StatusOK, nil
}

// ---- Voice Calls ----

type VoiceNewCallRequest struct {
	CallID string `json:"callId"`
	SDP    string `json:"sdp"`
}

func (c *Client) NewVoiceCall(ctx context.Context, apiKey string, req VoiceNewCallRequest) (json.RawMessage, error) {
	return c.post(ctx, "/v1/voice/calls/new", req, apiKey)
}

type VoiceWebNewCallRequest struct {
	CallID         string `json:"callId"`
	AgentID        string `json:"agentId"`
	SDP            string `json:"sdp"`
	BusinessNumber string `json:"businessNumber,omitempty"`
	ContactName    string `json:"contactName,omitempty"`
	From           string `json:"from,omitempty"`
	Language       string `json:"language,omitempty"`
	Accent         string `json:"accent,omitempty"`
	ModelProvider  string `json:"modelProvider,omitempty"`
}

func (c *Client) WebNewVoiceCall(ctx context.Context, apiKey string, req VoiceWebNewCallRequest) (json.RawMessage, error) {
	return c.post(ctx, "/v1/voice/calls/web-new", req, apiKey)
}

func (c *Client) TestNewVoiceCall(ctx context.Context, apiKey string, req VoiceWebNewCallRequest) (json.RawMessage, error) {
	return c.post(ctx, "/v1/voice/calls/test/new-call", req, apiKey)
}

type VoiceTerminateCallRequest struct {
	CallID string `json:"callId"`
}

func (c *Client) TerminateVoiceCall(ctx context.Context, apiKey string, req VoiceTerminateCallRequest) (json.RawMessage, error) {
	return c.post(ctx, "/v1/voice/calls/terminate", req, apiKey)
}

func (c *Client) TestTerminateVoiceCall(ctx context.Context, apiKey string, req VoiceTerminateCallRequest) (json.RawMessage, error) {
	return c.post(ctx, "/v1/voice/calls/test/terminate-call", req, apiKey)
}

// ---- Voice Outbound ----

type VoiceInitiateOutboundRequest struct {
	WAID               string `json:"waid"`
	ChannelPhoneNumber string `json:"channelPhoneNumber,omitempty"`
	AgentID            string `json:"agentId,omitempty"`
	VoiceLanguage      string `json:"voiceLanguage,omitempty"`
	Accent             string `json:"accent,omitempty"`
	TenantID           string `json:"tenantId,omitempty"`
}

func (c *Client) InitiateOutboundCall(ctx context.Context, apiKey string, req VoiceInitiateOutboundRequest) (json.RawMessage, error) {
	return c.post(ctx, "/v1/voice/calls/outbound/initiate", req, apiKey)
}

// ---- Voice WebRTC ----

func (c *Client) GetWebRTCConfig(ctx context.Context, apiKey string) (json.RawMessage, error) {
	return c.get(ctx, "/v1/voice/webrtc/config", nil, apiKey)
}

// ---- Voice Minimax Clone ----

type MinimaxVoiceListOpts struct {
	VoiceID   string
	Status    string
	VoiceName string
	Page      int
	PageSize  int
}

func (c *Client) ListMinimaxVoices(ctx context.Context, apiKey string, opts MinimaxVoiceListOpts) (json.RawMessage, error) {
	q := url.Values{}
	if opts.VoiceID != "" {
		q.Set("voice_id", opts.VoiceID)
	}
	if opts.Status != "" {
		q.Set("status", opts.Status)
	}
	if opts.VoiceName != "" {
		q.Set("voice_name", opts.VoiceName)
	}
	if opts.Page > 0 {
		q.Set("page", strconv.Itoa(opts.Page))
	}
	if opts.PageSize > 0 {
		q.Set("page_size", strconv.Itoa(opts.PageSize))
	}
	return c.get(ctx, "/v1/voice/minimax-voices", q, apiKey)
}

func (c *Client) GetMinimaxVoice(ctx context.Context, apiKey, id string) (json.RawMessage, error) {
	return c.get(ctx, fmt.Sprintf("/v1/voice/minimax-voices/%s", id), nil, apiKey)
}

type VoiceSubmitCloneRequest struct {
	ID                  string   `json:"id,omitempty"`
	TenantID            string   `json:"tenant_id"`
	VoiceName           string   `json:"voice_name"`
	GCSURL              string   `json:"gcs_url"`
	CloneModel          string   `json:"clone_model,omitempty"`
	Language            string   `json:"language,omitempty"`
	Accent              string   `json:"accent,omitempty"`
	Gender              string   `json:"gender,omitempty"`
	Age                 string   `json:"age,omitempty"`
	Description         string   `json:"description,omitempty"`
	CustomTextToPreview string   `json:"custom_text_to_preview,omitempty"`
	CategoryIDs         []string `json:"category_ids,omitempty"`
}

func (c *Client) SubmitVoiceClone(ctx context.Context, apiKey string, req VoiceSubmitCloneRequest) (json.RawMessage, error) {
	return c.post(ctx, "/v1/voice/minimax-voices/submit-clone", req, apiKey)
}

type VoiceLibrarySearchOpts struct {
	Keyword  string
	Page     int
	PageSize int
}

func (c *Client) SearchVoiceLibrary(ctx context.Context, apiKey string, opts VoiceLibrarySearchOpts) (json.RawMessage, error) {
	q := url.Values{}
	if opts.Keyword != "" {
		q.Set("keyword", opts.Keyword)
	}
	if opts.Page > 0 {
		q.Set("page", strconv.Itoa(opts.Page))
	}
	if opts.PageSize > 0 {
		q.Set("page_size", strconv.Itoa(opts.PageSize))
	}
	return c.get(ctx, "/v1/voice/minimax-voices/voice-library/search", q, apiKey)
}

type VoiceUpdateMinimaxRequest struct {
	VoiceName   string   `json:"voice_name,omitempty"`
	Language    string   `json:"language,omitempty"`
	Accent      string   `json:"accent,omitempty"`
	Gender      string   `json:"gender,omitempty"`
	Age         string   `json:"age,omitempty"`
	Description string   `json:"description,omitempty"`
	Status      string   `json:"status,omitempty"`
	CategoryIDs []string `json:"category_ids,omitempty"`
}

func (c *Client) UpdateMinimaxVoice(ctx context.Context, apiKey, id string, req VoiceUpdateMinimaxRequest) (json.RawMessage, error) {
	return c.put(ctx, fmt.Sprintf("/v1/voice/minimax-voices/%s", id), req, apiKey)
}

func (c *Client) SearchCollectedVoices(ctx context.Context, apiKey, tenantID string) (json.RawMessage, error) {
	q := url.Values{}
	if tenantID != "" {
		q.Set("tenant_id", tenantID)
	}
	return c.get(ctx, "/v1/voice/minimax-voices/collected", q, apiKey)
}

// ---- Voice ASR ----

type VoiceASRTranscribeRequest struct {
	URL      string `json:"url"`
	Language string `json:"language,omitempty"`
	Tenant   string `json:"tenant,omitempty"`
}

func (c *Client) ASRTranscribe(ctx context.Context, apiKey string, req VoiceASRTranscribeRequest) (json.RawMessage, error) {
	return c.post(ctx, "/v1/voice/asr/transcribe", req, apiKey)
}

// UploadUserVoice uploads a voice audio file via multipart/form-data and returns a GCS URL
// for use with submit-clone. POST /v1/voice/minimax-voices/upload-user-voice
func (c *Client) UploadUserVoice(ctx context.Context, apiKey string, audioContent []byte, filename string) (json.RawMessage, error) {
	if filename == "" {
		filename = "audio.opus"
	}
	body := &bytes.Buffer{}
	w := multipart.NewWriter(body)

	part, err := w.CreateFormFile("audio", filename)
	if err != nil {
		return nil, fmt.Errorf("create form file: %w", err)
	}
	if _, err := part.Write(audioContent); err != nil {
		return nil, fmt.Errorf("write file part: %w", err)
	}
	if err := w.Close(); err != nil {
		return nil, fmt.Errorf("close multipart: %w", err)
	}

	u := c.baseURL + "/v1/voice/minimax-voices/upload-user-voice"
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, body)
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", w.FormDataContentType())
	req.Header.Set("Accept", "application/json")

	c.logCurl(req.Method, u, apiKey, w.FormDataContentType(), nil)

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("read response: %w", err)
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("gateway returned %d: %s", resp.StatusCode, string(respBody))
	}
	return respBody, nil
}

// ASRTranscribeUpload sends the audio file via multipart/form-data to /v1/voice/asr/transcribe/upload.
// Use this when the audio is obtained by download (e.g. from Wati with Bearer token).
func (c *Client) ASRTranscribeUpload(ctx context.Context, apiKey, tenant string, audioContent []byte, filename string) (json.RawMessage, error) {
	if filename == "" {
		filename = "audio.opus"
	}
	body := &bytes.Buffer{}
	w := multipart.NewWriter(body)

	part, err := w.CreateFormFile("file", filename)
	if err != nil {
		return nil, fmt.Errorf("create form file: %w", err)
	}
	if _, err := part.Write(audioContent); err != nil {
		return nil, fmt.Errorf("write file part: %w", err)
	}
	if tenant != "" {
		_ = w.WriteField("tenant", tenant)
	}
	if err := w.Close(); err != nil {
		return nil, fmt.Errorf("close multipart: %w", err)
	}

	u := c.baseURL + "/v1/voice/asr/transcribe/upload"
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, body)
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)
	req.Header.Set("Content-Type", w.FormDataContentType())
	req.Header.Set("Accept", "application/json")

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("request failed: %w", err)
	}
	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("read response: %w", err)
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("gateway returned %d: %s", resp.StatusCode, string(respBody))
	}
	return respBody, nil
}
