package providers

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"
	"time"
)

const (
	antigravityDailyBaseURL   = "https://daily-cloudcode-pa.googleapis.com"
	antigravitySandboxBaseURL = "https://daily-cloudcode-pa.sandbox.googleapis.com"
	antigravityProdBaseURL    = "https://cloudcode-pa.googleapis.com"
)

type AntigravityProvider struct {
	base *HTTPProvider
}

func NewAntigravityProvider(providerName, apiKey, apiBase, defaultModel string, supportsResponsesCompact bool, authMode string, timeout time.Duration, oauth *oauthManager) *AntigravityProvider {
	normalizedBase := normalizeAPIBase(apiBase)
	if normalizedBase == "" {
		normalizedBase = antigravityDailyBaseURL
	}
	return &AntigravityProvider{
		base: NewHTTPProvider(providerName, apiKey, normalizedBase, defaultModel, supportsResponsesCompact, authMode, timeout, oauth),
	}
}

func (p *AntigravityProvider) GetDefaultModel() string {
	if p == nil || p.base == nil {
		return ""
	}
	return p.base.GetDefaultModel()
}

func (p *AntigravityProvider) CountTokens(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*UsageInfo, error) {
	if p == nil || p.base == nil {
		return nil, fmt.Errorf("provider not configured")
	}
	attempts, err := p.base.authAttempts(ctx)
	if err != nil {
		return nil, err
	}
	var lastBody []byte
	var lastStatus int
	var lastType string
	for _, attempt := range attempts {
		for _, baseURL := range p.baseURLs() {
			requestBody := p.buildRequestBody(messages, tools, model, options, attempt.session, false)
			delete(requestBody, "project")
			delete(requestBody, "model")
			request := mapFromAny(requestBody["request"])
			delete(request, "safetySettings")
			requestBody["request"] = request
			body, status, ctype, reqErr := p.performCountTokensAttempt(ctx, p.countTokensEndpoint(baseURL), requestBody, attempt)
			if reqErr != nil {
				if strings.Contains(strings.ToLower(reqErr.Error()), "context canceled") || strings.Contains(strings.ToLower(reqErr.Error()), "deadline exceeded") {
					return nil, reqErr
				}
				lastBody, lastStatus, lastType = nil, 0, ""
				continue
			}
			lastBody, lastStatus, lastType = body, status, ctype
			if status == http.StatusTooManyRequests {
				continue
			}
			reason, retry := classifyOAuthFailure(status, body)
			if retry {
				if attempt.kind == "oauth" && attempt.session != nil && p.base.oauth != nil {
					p.base.oauth.markExhausted(attempt.session, reason)
					recordProviderOAuthError(p.base.providerName, attempt.session, reason)
				}
				if attempt.kind == "api_key" {
					p.base.markAPIKeyFailure(reason)
				}
				break
			}
			if status != http.StatusOK {
				return nil, fmt.Errorf("API error (status %d, content-type %q): %s", status, ctype, previewResponseBody(body))
			}
			var payload struct {
				TotalTokens int `json:"totalTokens"`
			}
			if err := json.Unmarshal(body, &payload); err != nil {
				return nil, fmt.Errorf("invalid countTokens response: %w", err)
			}
			p.base.markAttemptSuccess(attempt)
			return &UsageInfo{PromptTokens: payload.TotalTokens, TotalTokens: payload.TotalTokens}, nil
		}
	}
	return nil, fmt.Errorf("API error (status %d, content-type %q): %s", lastStatus, lastType, previewResponseBody(lastBody))
}

func (p *AntigravityProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
	body, status, ctype, err := p.doRequest(ctx, messages, tools, model, options, false, nil)
	if err != nil {
		return nil, err
	}
	if status != http.StatusOK {
		return nil, fmt.Errorf("API error (status %d, content-type %q): %s", status, ctype, previewResponseBody(body))
	}
	if !json.Valid(body) {
		return nil, fmt.Errorf("API error (status %d, content-type %q): non-JSON response: %s", status, ctype, previewResponseBody(body))
	}
	return parseAntigravityResponse(body)
}

func (p *AntigravityProvider) ChatStream(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}, onDelta func(string)) (*LLMResponse, error) {
	body, status, ctype, err := p.doRequest(ctx, messages, tools, model, options, true, onDelta)
	if err != nil {
		return nil, err
	}
	if status != http.StatusOK {
		return nil, fmt.Errorf("API error (status %d, content-type %q): %s", status, ctype, previewResponseBody(body))
	}
	if !json.Valid(body) {
		return nil, fmt.Errorf("API error (status %d, content-type %q): non-JSON response: %s", status, ctype, previewResponseBody(body))
	}
	return parseAntigravityResponse(body)
}

func (p *AntigravityProvider) doRequest(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}, stream bool, onDelta func(string)) ([]byte, int, string, error) {
	if p == nil || p.base == nil {
		return nil, 0, "", fmt.Errorf("provider not configured")
	}
	attempts, err := p.base.authAttempts(ctx)
	if err != nil {
		return nil, 0, "", err
	}
	var lastBody []byte
	var lastStatus int
	var lastType string
	for _, attempt := range attempts {
		for _, baseURL := range p.baseURLs() {
			requestBody := p.buildRequestBody(messages, tools, model, options, attempt.session, stream)
			endpoint := p.endpoint(baseURL, stream)
			for retryAttempt := 0; retryAttempt < 3; retryAttempt++ {
				body, status, ctype, reqErr := p.performAttempt(ctx, endpoint, requestBody, attempt, stream, onDelta)
				if reqErr != nil {
					if strings.Contains(strings.ToLower(reqErr.Error()), "context canceled") || strings.Contains(strings.ToLower(reqErr.Error()), "deadline exceeded") {
						return nil, 0, "", reqErr
					}
					lastBody, lastStatus, lastType = nil, 0, ""
					break
				}
				lastBody, lastStatus, lastType = body, status, ctype
				if antigravityShouldRetryNoCapacity(status, body) && retryAttempt < 2 {
					if err := antigravityWait(ctx, antigravityNoCapacityRetryDelay(retryAttempt)); err != nil {
						return nil, 0, "", err
					}
					continue
				}
				if status == http.StatusTooManyRequests || status == http.StatusServiceUnavailable || status == http.StatusBadGateway {
					break
				}
				reason, retry := classifyOAuthFailure(status, body)
				if retry {
					if attempt.kind == "oauth" && attempt.session != nil && p.base.oauth != nil {
						p.base.oauth.markExhausted(attempt.session, reason)
						recordProviderOAuthError(p.base.providerName, attempt.session, reason)
					}
					if attempt.kind == "api_key" {
						p.base.markAPIKeyFailure(reason)
					}
					break
				}
				p.base.markAttemptSuccess(attempt)
				return body, status, ctype, nil
			}
		}
	}
	return lastBody, lastStatus, lastType, nil
}

func (p *AntigravityProvider) performAttempt(ctx context.Context, endpoint string, payload map[string]any, attempt authAttempt, stream bool, onDelta func(string)) ([]byte, int, string, error) {
	jsonData, err := json.Marshal(payload)
	if err != nil {
		return nil, 0, "", fmt.Errorf("failed to marshal request: %w", err)
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(jsonData))
	if err != nil {
		return nil, 0, "", fmt.Errorf("failed to create request: %w", err)
	}
	req.Close = true
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", defaultAntigravityAPIUserAgent)
	req.Header.Set("X-Goog-Api-Client", defaultAntigravityAPIClient)
	req.Header.Set("Client-Metadata", defaultAntigravityClientMeta)
	if stream {
		req.Header.Set("Accept", "text/event-stream")
	} else {
		req.Header.Set("Accept", "application/json")
	}
	applyAttemptAuth(req, attempt)
	client, err := p.base.httpClientForAttempt(attempt)
	if err != nil {
		return nil, 0, "", err
	}
	resp, err := client.Do(req)
	if err != nil {
		return nil, 0, "", fmt.Errorf("failed to send request: %w", err)
	}
	defer resp.Body.Close()
	ctype := strings.TrimSpace(resp.Header.Get("Content-Type"))
	if stream && strings.Contains(strings.ToLower(ctype), "text/event-stream") {
		return consumeAntigravityStream(resp, onDelta)
	}
	body, readErr := io.ReadAll(resp.Body)
	if readErr != nil {
		return nil, resp.StatusCode, ctype, fmt.Errorf("failed to read response: %w", readErr)
	}
	return body, resp.StatusCode, ctype, nil
}

func (p *AntigravityProvider) endpoint(baseURL string, stream bool) string {
	base := normalizeAPIBase(baseURL)
	if base == "" {
		base = antigravityDailyBaseURL
	}
	path := "/" + defaultAntigravityAPIVersion + ":generateContent"
	if stream {
		path = "/" + defaultAntigravityAPIVersion + ":streamGenerateContent?alt=sse"
	}
	return base + path
}

func (p *AntigravityProvider) countTokensEndpoint(baseURL string) string {
	base := normalizeAPIBase(baseURL)
	if base == "" {
		base = antigravityDailyBaseURL
	}
	return base + "/" + defaultAntigravityAPIVersion + ":countTokens"
}

func (p *AntigravityProvider) baseURLs() []string {
	if p == nil || p.base == nil {
		return []string{antigravityDailyBaseURL}
	}
	if custom := normalizeAPIBase(p.base.apiBase); custom != "" &&
		!strings.Contains(strings.ToLower(custom), "api.openai.com") &&
		custom != antigravityDailyBaseURL {
		return []string{custom}
	}
	return []string{antigravityDailyBaseURL, antigravitySandboxBaseURL, antigravityProdBaseURL, defaultAntigravityAPIEndpoint}
}

func (p *AntigravityProvider) buildRequestBody(messages []Message, tools []ToolDefinition, model string, options map[string]interface{}, session *oauthSession, stream bool) map[string]any {
	request := map[string]any{}
	baseModel := strings.TrimSpace(qwenBaseModel(model))
	systemParts := make([]map[string]any, 0)
	contents := make([]map[string]any, 0, len(messages))
	callNames := map[string]string{}
	for _, msg := range messages {
		role := strings.ToLower(strings.TrimSpace(msg.Role))
		switch role {
		case "system", "developer":
			if text := antigravityMessageText(msg); text != "" {
				systemParts = append(systemParts, map[string]any{"text": text})
			}
		case "user":
			if parts := antigravityTextParts(msg); len(parts) > 0 {
				contents = append(contents, map[string]any{"role": "user", "parts": parts})
			}
		case "assistant":
			parts := antigravityAssistantParts(msg)
			for _, tc := range msg.ToolCalls {
				name := strings.TrimSpace(tc.Name)
				if tc.Function != nil && strings.TrimSpace(tc.Function.Name) != "" {
					name = strings.TrimSpace(tc.Function.Name)
				}
				if name != "" && strings.TrimSpace(tc.ID) != "" {
					callNames[strings.TrimSpace(tc.ID)] = name
				}
			}
			if len(parts) > 0 {
				contents = append(contents, map[string]any{"role": "model", "parts": parts})
			}
		case "tool":
			if part := antigravityToolResponsePart(msg, callNames); part != nil {
				contents = append(contents, map[string]any{"role": "function", "parts": []map[string]any{part}})
			}
		default:
			if text := antigravityMessageText(msg); text != "" {
				contents = append(contents, map[string]any{"role": "user", "parts": []map[string]any{{"text": text}}})
			}
		}
	}
	if len(systemParts) > 0 {
		request["systemInstruction"] = map[string]any{"parts": systemParts}
	}
	if len(contents) > 0 {
		request["contents"] = contents
	}
	if gen := antigravityGenerationConfig(options); len(gen) > 0 {
		request["generationConfig"] = gen
	}
	if extra, ok := mapOption(options, "gemini_generation_config"); ok && len(extra) > 0 {
		gen := mapFromAny(request["generationConfig"])
		if gen == nil {
			gen = map[string]any{}
		}
		for k, v := range extra {
			gen[k] = v
		}
		request["generationConfig"] = gen
	}
	if toolDecls := antigravityToolDeclarations(tools); len(toolDecls) > 0 {
		request["tools"] = []map[string]any{{"function_declarations": toolDecls}}
		request["toolConfig"] = map[string]any{
			"functionCallingConfig": map[string]any{"mode": "AUTO"},
		}
	}
	projectID := ""
	if session != nil {
		projectID = firstNonEmpty(strings.TrimSpace(session.ProjectID), asString(session.Token["project_id"]), asString(session.Token["project-id"]), asString(session.Token["projectId"]), asString(session.Token["project"]))
	}
	if projectID == "" {
		projectID = "default-project"
	}
	applyAntigravityThinkingSuffix(request, model)
	requestType := "agent"
	if strings.Contains(strings.ToLower(baseModel), "image") {
		requestType = "image_gen"
	}
	return map[string]any{
		"project":     projectID,
		"model":       baseModel,
		"userAgent":   "antigravity",
		"requestType": requestType,
		"requestId":   "agent-" + randomSessionID(),
		"request":     request,
	}
}

func antigravityMessageText(msg Message) string {
	parts := antigravityTextParts(msg)
	if len(parts) == 0 {
		return strings.TrimSpace(msg.Content)
	}
	lines := make([]string, 0, len(parts))
	for _, part := range parts {
		text := strings.TrimSpace(asString(part["text"]))
		if text != "" {
			lines = append(lines, text)
		}
	}
	return strings.TrimSpace(strings.Join(lines, "\n"))
}

func antigravityTextParts(msg Message) []map[string]any {
	if len(msg.ContentParts) == 0 {
		if text := strings.TrimSpace(msg.Content); text != "" {
			return []map[string]any{{"text": text}}
		}
		return nil
	}
	parts := make([]map[string]any, 0, len(msg.ContentParts))
	for _, part := range msg.ContentParts {
		switch strings.ToLower(strings.TrimSpace(part.Type)) {
		case "", "text", "input_text":
			if text := strings.TrimSpace(part.Text); text != "" {
				parts = append(parts, map[string]any{"text": text})
			}
		}
	}
	if len(parts) == 0 && strings.TrimSpace(msg.Content) != "" {
		return []map[string]any{{"text": strings.TrimSpace(msg.Content)}}
	}
	return parts
}

func antigravityAssistantParts(msg Message) []map[string]any {
	parts := antigravityTextParts(msg)
	for _, tc := range msg.ToolCalls {
		name := strings.TrimSpace(tc.Name)
		args := map[string]any{}
		if tc.Function != nil {
			if strings.TrimSpace(tc.Function.Name) != "" {
				name = strings.TrimSpace(tc.Function.Name)
			}
			if strings.TrimSpace(tc.Function.Arguments) != "" {
				_ = json.Unmarshal([]byte(tc.Function.Arguments), &args)
			}
		}
		if len(args) == 0 && len(tc.Arguments) > 0 {
			args = tc.Arguments
		}
		if name == "" {
			continue
		}
		part := map[string]any{
			"functionCall": map[string]any{
				"name": name,
				"args": args,
			},
		}
		if strings.TrimSpace(tc.ID) != "" {
			part["functionCall"].(map[string]any)["id"] = strings.TrimSpace(tc.ID)
		}
		parts = append(parts, part)
	}
	return parts
}

func antigravityToolResponsePart(msg Message, callNames map[string]string) map[string]any {
	callID := strings.TrimSpace(msg.ToolCallID)
	if callID == "" {
		return nil
	}
	name := strings.TrimSpace(callNames[callID])
	if name == "" {
		name = "tool_result"
	}
	return map[string]any{
		"functionResponse": map[string]any{
			"name": name,
			"id":   callID,
			"response": map[string]any{
				"result": strings.TrimSpace(msg.Content),
			},
		},
	}
}

func antigravityToolDeclarations(tools []ToolDefinition) []map[string]any {
	out := make([]map[string]any, 0, len(tools))
	for _, tool := range tools {
		name := strings.TrimSpace(tool.Function.Name)
		if name == "" {
			name = strings.TrimSpace(tool.Name)
		}
		if name == "" {
			continue
		}
		params := tool.Function.Parameters
		if len(params) == 0 {
			params = tool.Parameters
		}
		entry := map[string]any{
			"name":                 name,
			"description":          strings.TrimSpace(firstNonEmpty(tool.Function.Description, tool.Description)),
			"parametersJsonSchema": params,
		}
		if len(params) == 0 {
			entry["parametersJsonSchema"] = map[string]any{"type": "object", "properties": map[string]any{}}
		}
		out = append(out, entry)
	}
	return out
}

func antigravityGenerationConfig(options map[string]any) map[string]any {
	cfg := map[string]any{}
	if maxTokens, ok := int64FromOption(options, "max_tokens"); ok {
		cfg["maxOutputTokens"] = maxTokens
	}
	if temperature, ok := float64FromOption(options, "temperature"); ok {
		cfg["temperature"] = temperature
	}
	return cfg
}

func applyAntigravityThinkingSuffix(request map[string]any, model string) {
	suffix := qwenModelSuffix(model)
	if suffix == "" {
		return
	}
	baseModel := strings.TrimSpace(qwenBaseModel(model))
	gen := mapFromAny(request["generationConfig"])
	if gen == nil {
		gen = map[string]any{}
	}
	thinkingConfig := mapFromAny(gen["thinkingConfig"])
	if thinkingConfig == nil {
		thinkingConfig = map[string]any{}
	}
	includeThoughts, userSetIncludeThoughts := geminiExistingIncludeThoughts(thinkingConfig)
	delete(thinkingConfig, "thinkingBudget")
	delete(thinkingConfig, "thinking_budget")
	delete(thinkingConfig, "thinkingLevel")
	delete(thinkingConfig, "thinking_level")
	delete(thinkingConfig, "include_thoughts")

	setIncludeThoughts := func(defaultValue bool, force bool) {
		if force || !userSetIncludeThoughts {
			includeThoughts = defaultValue
		}
		thinkingConfig["includeThoughts"] = includeThoughts
	}

	lower := strings.ToLower(strings.TrimSpace(suffix))
	switch {
	case lower == "auto" || lower == "-1":
		thinkingConfig["thinkingBudget"] = -1
		setIncludeThoughts(true, false)
	case lower == "none" || lower == "0":
		if geminiUsesThinkingLevels(baseModel) {
			thinkingConfig["thinkingLevel"] = "low"
		} else {
			thinkingConfig["thinkingBudget"] = 128
		}
		setIncludeThoughts(false, true)
	case isGeminiThinkingLevel(lower):
		if geminiUsesThinkingLevels(baseModel) {
			thinkingConfig["thinkingLevel"] = normalizeGeminiThinkingLevel(lower)
		} else {
			thinkingConfig["thinkingBudget"] = geminiThinkingBudgetForLevel(lower)
		}
		setIncludeThoughts(true, false)
	default:
		if budget, err := strconv.Atoi(lower); err == nil {
			switch {
			case budget < 0:
				thinkingConfig["thinkingBudget"] = -1
				setIncludeThoughts(true, false)
			case budget == 0:
				if geminiUsesThinkingLevels(baseModel) {
					thinkingConfig["thinkingLevel"] = "low"
				} else {
					thinkingConfig["thinkingBudget"] = 128
				}
				setIncludeThoughts(false, true)
			default:
				thinkingConfig["thinkingBudget"] = budget
				setIncludeThoughts(true, false)
			}
		}
	}
	if len(thinkingConfig) == 0 {
		return
	}
	gen["thinkingConfig"] = thinkingConfig
	request["generationConfig"] = gen
}

func consumeAntigravityStream(resp *http.Response, onDelta func(string)) ([]byte, int, string, error) {
	if onDelta == nil {
		onDelta = func(string) {}
	}
	scanner := bufio.NewScanner(resp.Body)
	scanner.Buffer(make([]byte, 0, 64*1024), 2*1024*1024)
	var dataLines []string
	state := &antigravityStreamState{}
	for scanner.Scan() {
		line := scanner.Text()
		if strings.TrimSpace(line) == "" {
			if len(dataLines) > 0 {
				payload := strings.Join(dataLines, "\n")
				dataLines = dataLines[:0]
				if strings.TrimSpace(payload) != "" && strings.TrimSpace(payload) != "[DONE]" {
					if delta := state.consume([]byte(payload)); delta != "" {
						onDelta(delta)
					}
				}
			}
			continue
		}
		if strings.HasPrefix(line, "data:") {
			dataLines = append(dataLines, strings.TrimSpace(strings.TrimPrefix(line, "data:")))
		}
	}
	if err := scanner.Err(); err != nil {
		return nil, resp.StatusCode, strings.TrimSpace(resp.Header.Get("Content-Type")), fmt.Errorf("failed to read stream: %w", err)
	}
	return state.finalBody(), resp.StatusCode, strings.TrimSpace(resp.Header.Get("Content-Type")), nil
}

func (p *AntigravityProvider) performCountTokensAttempt(ctx context.Context, endpoint string, payload map[string]any, attempt authAttempt) ([]byte, int, string, error) {
	jsonData, err := json.Marshal(payload)
	if err != nil {
		return nil, 0, "", fmt.Errorf("failed to marshal request: %w", err)
	}
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(jsonData))
	if err != nil {
		return nil, 0, "", fmt.Errorf("failed to create request: %w", err)
	}
	req.Close = true
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("User-Agent", defaultAntigravityAPIUserAgent)
	req.Header.Set("X-Goog-Api-Client", defaultAntigravityAPIClient)
	req.Header.Set("Client-Metadata", defaultAntigravityClientMeta)
	applyAttemptAuth(req, attempt)
	client, err := p.base.httpClientForAttempt(attempt)
	if err != nil {
		return nil, 0, "", err
	}
	resp, err := client.Do(req)
	if err != nil {
		return nil, 0, "", fmt.Errorf("failed to send request: %w", err)
	}
	defer resp.Body.Close()
	body, readErr := io.ReadAll(resp.Body)
	if readErr != nil {
		return nil, resp.StatusCode, strings.TrimSpace(resp.Header.Get("Content-Type")), fmt.Errorf("failed to read response: %w", readErr)
	}
	return body, resp.StatusCode, strings.TrimSpace(resp.Header.Get("Content-Type")), nil
}

func antigravityShouldRetryNoCapacity(statusCode int, body []byte) bool {
	if statusCode != http.StatusServiceUnavailable {
		return false
	}
	return strings.Contains(strings.ToLower(string(body)), "no capacity available")
}

func antigravityNoCapacityRetryDelay(attempt int) time.Duration {
	if attempt < 0 {
		attempt = 0
	}
	delay := time.Duration(attempt+1) * 250 * time.Millisecond
	if delay > 2*time.Second {
		delay = 2 * time.Second
	}
	return delay
}

func antigravityWait(ctx context.Context, wait time.Duration) error {
	if wait <= 0 {
		return nil
	}
	timer := time.NewTimer(wait)
	defer timer.Stop()
	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-timer.C:
		return nil
	}
}

type antigravityStreamState struct {
	Text         string
	ToolCalls    []ToolCall
	FinishReason string
	Usage        *UsageInfo
}

func (s *antigravityStreamState) consume(payload []byte) string {
	resp, err := parseAntigravityResponse(payload)
	if err != nil {
		return ""
	}
	delta := antigravityDeltaText(s.Text, resp.Content)
	if resp.Content != "" {
		if delta == resp.Content && strings.TrimSpace(s.Text) != "" && !strings.HasPrefix(resp.Content, s.Text) {
			s.Text += delta
		} else if resp.Content != s.Text {
			s.Text = resp.Content
		}
	}
	if len(resp.ToolCalls) > 0 {
		s.ToolCalls = resp.ToolCalls
	}
	if strings.TrimSpace(resp.FinishReason) != "" {
		s.FinishReason = resp.FinishReason
	}
	if resp.Usage != nil {
		s.Usage = resp.Usage
	}
	return delta
}

func (s *antigravityStreamState) finalBody() []byte {
	parts := make([]map[string]any, 0, 1+len(s.ToolCalls))
	if strings.TrimSpace(s.Text) != "" {
		parts = append(parts, map[string]any{"text": s.Text})
	}
	for _, tc := range s.ToolCalls {
		args := map[string]any{}
		if tc.Function != nil && strings.TrimSpace(tc.Function.Arguments) != "" {
			_ = json.Unmarshal([]byte(tc.Function.Arguments), &args)
		}
		if len(args) == 0 && len(tc.Arguments) > 0 {
			args = tc.Arguments
		}
		part := map[string]any{
			"functionCall": map[string]any{
				"name": tc.Name,
				"args": args,
			},
		}
		if strings.TrimSpace(tc.ID) != "" {
			part["functionCall"].(map[string]any)["id"] = tc.ID
		}
		parts = append(parts, part)
	}
	root := map[string]any{
		"response": map[string]any{
			"candidates": []map[string]any{{
				"content": map[string]any{"parts": parts},
			}},
		},
	}
	if strings.TrimSpace(s.FinishReason) != "" {
		root["response"].(map[string]any)["candidates"].([]map[string]any)[0]["finishReason"] = s.FinishReason
	}
	if s.Usage != nil {
		root["response"].(map[string]any)["usageMetadata"] = map[string]any{
			"promptTokenCount":     s.Usage.PromptTokens,
			"candidatesTokenCount": s.Usage.CompletionTokens,
			"totalTokenCount":      s.Usage.TotalTokens,
		}
	}
	raw, _ := json.Marshal(root)
	return raw
}

func antigravityDeltaText(previous, current string) string {
	if current == "" {
		return ""
	}
	if previous == "" {
		return current
	}
	if strings.HasPrefix(current, previous) {
		return current[len(previous):]
	}
	return current
}

func parseAntigravityResponse(body []byte) (*LLMResponse, error) {
	var payload map[string]any
	if err := json.Unmarshal(body, &payload); err != nil {
		return nil, fmt.Errorf("failed to unmarshal antigravity response: %w", err)
	}
	root := payload
	if responseMap := mapFromAny(payload["response"]); len(responseMap) > 0 {
		root = responseMap
	}
	candidatesRaw, _ := root["candidates"].([]any)
	if len(candidatesRaw) == 0 {
		return &LLMResponse{}, nil
	}
	first := mapFromAny(candidatesRaw[0])
	content := mapFromAny(first["content"])
	partsRaw, _ := content["parts"].([]any)
	texts := make([]string, 0, len(partsRaw))
	toolCalls := make([]ToolCall, 0)
	for _, item := range partsRaw {
		part := mapFromAny(item)
		if asString(part["text"]) != "" && !strings.EqualFold(asString(part["thought"]), "true") {
			texts = append(texts, asString(part["text"]))
		}
		functionCall := mapFromAny(part["functionCall"])
		if len(functionCall) == 0 {
			continue
		}
		args := map[string]any{}
		if rawArgs, ok := functionCall["args"]; ok {
			switch typed := rawArgs.(type) {
			case map[string]any:
				args = typed
			case string:
				_ = json.Unmarshal([]byte(typed), &args)
			}
		}
		id := strings.TrimSpace(firstNonEmpty(asString(functionCall["id"]), asString(functionCall["call_id"])))
		name := strings.TrimSpace(asString(functionCall["name"]))
		argJSON, _ := json.Marshal(args)
		toolCalls = append(toolCalls, ToolCall{
			ID:   id,
			Name: name,
			Function: &FunctionCall{
				Name:      name,
				Arguments: string(argJSON),
			},
			Arguments: args,
		})
	}
	finishReason := strings.TrimSpace(asString(first["finishReason"]))
	if finishReason == "" || strings.EqualFold(finishReason, "completed") {
		finishReason = "stop"
	}
	usageMeta := mapFromAny(root["usageMetadata"])
	var usage *UsageInfo
	if len(usageMeta) > 0 {
		usage = &UsageInfo{
			PromptTokens:     intValue(usageMeta["promptTokenCount"]),
			CompletionTokens: intValue(usageMeta["candidatesTokenCount"]),
			TotalTokens:      intValue(usageMeta["totalTokenCount"]),
		}
		if usage.PromptTokens == 0 && usage.CompletionTokens == 0 && usage.TotalTokens == 0 {
			usage = nil
		}
	}
	return &LLMResponse{
		Content:      strings.TrimSpace(strings.Join(texts, "\n")),
		ToolCalls:    toolCalls,
		FinishReason: finishReason,
		Usage:        usage,
	}, nil
}

func intValue(value any) int {
	switch typed := value.(type) {
	case int:
		return typed
	case int64:
		return int(typed)
	case float64:
		return int(typed)
	case json.Number:
		if v, err := typed.Int64(); err == nil {
			return int(v)
		}
	case string:
		var num int
		if _, err := fmt.Sscanf(strings.TrimSpace(typed), "%d", &num); err == nil {
			return num
		}
	}
	return 0
}
