package clients

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"math"
	"net/http"
	"regexp"
	"strconv"
	"strings"
	"time"
)

var volcesElementReferencePattern = regexp.MustCompile(`(?i)<<<[[:space:]]*element[_-]?([0-9]+)[[:space:]]*>>>|@element([0-9]+)`)

type VolcesClient struct {
	HTTPClient *http.Client
}

func (c VolcesClient) Run(ctx context.Context, request Request) (Response, error) {
	apiKey := credential(request.Candidate.Credentials, "apiKey", "api_key", "key", "token")
	if apiKey == "" {
		return Response{}, &ClientError{Code: "missing_credentials", Message: "volces api key is required", Retryable: false}
	}

	switch request.Kind {
	case "images.generations", "images.edits":
		return c.runImage(ctx, request, apiKey)
	case "videos.generations":
		return c.runVideo(ctx, request, apiKey)
	default:
		return Response{}, &ClientError{Code: "unsupported_kind", Message: "unsupported volces request kind", Retryable: false}
	}
}

func (c VolcesClient) runImage(ctx context.Context, request Request, apiKey string) (Response, error) {
	body := volcesImageBody(request)
	raw, _ := json.Marshal(body)
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, joinURL(request.Candidate.BaseURL, "/images/generations"), bytes.NewReader(raw))
	if err != nil {
		return Response{}, err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+apiKey)
	applyUpstreamIdempotency(req, request)

	responseStartedAt := time.Now()
	if err := notifySubmissionStarted(request); err != nil {
		return Response{}, err
	}
	resp, err := httpClient(request.HTTPClient, c.HTTPClient).Do(req)
	if err != nil {
		return Response{}, transportClientError(err)
	}
	if err := notifyResponseReceived(request); err != nil {
		return Response{}, err
	}
	requestID := requestIDFromHTTPResponse(resp)
	result, wire, err := decodeHTTPResponseForProtocol(resp, ProtocolOpenAIImages)
	responseFinishedAt := time.Now()
	if err != nil {
		return Response{}, annotateResponseError(err, requestID, responseStartedAt, responseFinishedAt)
	}
	if requestID == "" {
		requestID = requestIDFromResult(result)
	}
	return Response{
		Result:             result,
		RequestID:          requestID,
		Usage:              usageFromOpenAI(result),
		Progress:           providerProgress(request),
		ResponseStartedAt:  responseStartedAt,
		ResponseFinishedAt: responseFinishedAt,
		ResponseDurationMS: responseDurationMS(responseStartedAt, responseFinishedAt),
		UpstreamProtocol:   ProtocolOpenAIImages,
		Wire:               wire,
	}, nil
}

func (c VolcesClient) runVideo(ctx context.Context, request Request, apiKey string) (Response, error) {
	submitStartedAt := time.Now()
	submitRequestID := strings.TrimSpace(request.RemoteTaskID)
	upstreamTaskID := strings.TrimSpace(request.RemoteTaskID)
	taskPath := volcesVideoTaskPath(request)
	if upstreamTaskID == "" {
		body := volcesVideoBody(request)
		if err := validateVolcesVideoTaskBody(body); err != nil {
			return Response{}, err
		}
		submitResult, requestID, _, err := c.postJSON(ctx, request, request.Candidate.BaseURL, taskPath, apiKey, body)
		submitRequestID = requestID
		if err != nil {
			return Response{}, annotateResponseError(err, submitRequestID, submitStartedAt, time.Now())
		}
		upstreamTaskID = strings.TrimSpace(stringFromAny(submitResult["id"]))
		if upstreamTaskID == "" {
			return Response{}, &ClientError{Code: "invalid_response", Message: "volces video task id is missing", RequestID: submitRequestID, Retryable: false}
		}
		if request.OnRemoteTaskSubmitted != nil {
			if err := request.OnRemoteTaskSubmitted(upstreamTaskID, submitResult); err != nil {
				return Response{}, err
			}
		}
	}

	interval := volcesPollInterval(request)
	timeout := volcesPollTimeout(request)
	deadline := time.NewTimer(timeout)
	defer deadline.Stop()
	nextPoll := time.NewTimer(0)
	defer nextPoll.Stop()

	var lastResult map[string]any
	lastRequestID := firstNonEmpty(submitRequestID, upstreamTaskID)
	transientFailures := 0
	for {
		select {
		case <-ctx.Done():
			return Response{}, &ClientError{Code: "cancelled", Message: ctx.Err().Error(), RequestID: lastRequestID, Retryable: true}
		case <-deadline.C:
			return Response{}, &ClientError{
				Code:      "timeout",
				Message:   fmt.Sprintf("volces video task %s did not finish before timeout; last status: %s", upstreamTaskID, volcesTaskStatus(lastResult)),
				RequestID: lastRequestID,
				Retryable: true,
			}
		case <-nextPoll.C:
			pollStartedAt := time.Now()
			pollResult, pollRequestID, pollWire, err := c.getJSON(ctx, request, request.Candidate.BaseURL, taskPath+"/"+upstreamTaskID, apiKey)
			pollFinishedAt := time.Now()
			requestID := firstNonEmpty(pollRequestID, submitRequestID, upstreamTaskID)
			lastRequestID = requestID
			if err != nil {
				err = annotateResponseError(err, requestID, pollStartedAt, pollFinishedAt)
				if !IsRetryable(err) {
					return Response{}, err
				}
				transientFailures++
				resetVolcesPollTimer(nextPoll, volcesRetryPollInterval(request, interval, transientFailures))
				continue
			}
			transientFailures = 0
			lastResult = pollResult
			if request.OnRemoteTaskPolled != nil {
				if err := request.OnRemoteTaskPolled(upstreamTaskID, pollResult); err != nil {
					return Response{}, err
				}
			}

			switch volcesTaskStatus(pollResult) {
			case "succeeded":
				result := volcesVideoSuccessResult(request, upstreamTaskID, pollResult)
				return Response{
					Result:             result,
					RequestID:          requestID,
					Usage:              volcesVideoUsage(pollResult),
					Progress:           volcesVideoProgress(request, upstreamTaskID),
					ResponseStartedAt:  submitStartedAt,
					ResponseFinishedAt: pollFinishedAt,
					ResponseDurationMS: responseDurationMS(submitStartedAt, pollFinishedAt),
					UpstreamProtocol:   ProtocolVolcesContents,
					Wire:               pollWire,
				}, nil
			case "failed", "cancelled":
				return Response{}, &ClientError{
					Code:               volcesTaskErrorCode(pollResult),
					Message:            volcesTaskErrorMessage(pollResult),
					RequestID:          requestID,
					ResponseStartedAt:  submitStartedAt,
					ResponseFinishedAt: pollFinishedAt,
					ResponseDurationMS: responseDurationMS(submitStartedAt, pollFinishedAt),
					Retryable:          false,
				}
			}
			resetVolcesPollTimer(nextPoll, interval)
		}
	}
}

// DeleteVideoTask calls the official contents-generations cancellation endpoint.
// It is intentionally separate from Run so task cancellation can use the same
// provider credentials that submitted the remote task.
func (c VolcesClient) DeleteVideoTask(ctx context.Context, request Request) (map[string]any, string, error) {
	apiKey := credential(request.Candidate.Credentials, "apiKey", "api_key", "key", "token")
	if apiKey == "" {
		return nil, "", &ClientError{Code: "missing_credentials", Message: "volces api key is required", Retryable: false}
	}
	remoteTaskID := strings.TrimSpace(request.RemoteTaskID)
	if remoteTaskID == "" {
		return nil, "", &ClientError{Code: "invalid_parameter", Message: "volces remote task id is required", Retryable: false}
	}
	taskPath := volcesVideoTaskPath(request) + "/" + remoteTaskID
	req, err := http.NewRequestWithContext(ctx, http.MethodDelete, joinURL(request.Candidate.BaseURL, taskPath), nil)
	if err != nil {
		return nil, "", err
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)
	response, err := httpClient(request.HTTPClient, c.HTTPClient).Do(req)
	if err != nil {
		return nil, "", transportClientError(err)
	}
	requestID := requestIDFromHTTPResponse(response)
	result, err := decodeHTTPResponse(response)
	if err != nil {
		return result, requestID, annotateResponseError(err, requestID, time.Now(), time.Now())
	}
	result, envelopeRequestID, err := normalizeVolcesCompatibleResult(result)
	return result, firstNonEmpty(requestID, envelopeRequestID), err
}

func volcesVideoTaskPath(request Request) string {
	path := firstNonEmptyStringValue(
		request.Candidate.PlatformConfig,
		"volcesVideoTaskPath",
		"videoTaskPath",
	)
	if path == "" {
		return "/contents/generations/tasks"
	}
	if !strings.HasPrefix(path, "/") {
		path = "/" + path
	}
	return strings.TrimRight(path, "/")
}

func (c VolcesClient) postJSON(ctx context.Context, request Request, baseURL string, path string, apiKey string, body map[string]any) (map[string]any, string, *WireResponse, error) {
	raw, _ := json.Marshal(body)
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, joinURL(baseURL, path), bytes.NewReader(raw))
	if err != nil {
		return nil, "", nil, err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+apiKey)
	applyUpstreamIdempotency(req, request)
	if err := notifySubmissionStarted(request); err != nil {
		return nil, "", nil, err
	}
	resp, err := httpClient(request.HTTPClient, c.HTTPClient).Do(req)
	if err != nil {
		return nil, "", nil, transportClientError(err)
	}
	if err := notifyResponseReceived(request); err != nil {
		return nil, "", nil, err
	}
	requestID := requestIDFromHTTPResponse(resp)
	result, wire, err := decodeHTTPResponseForProtocol(resp, ProtocolVolcesContents)
	if err != nil {
		if notifyErr := notifyWireResponse(request, wire); notifyErr != nil {
			return result, requestID, wire, notifyErr
		}
		return result, requestID, wire, err
	}
	original := result
	result, envelopeRequestID, err := normalizeVolcesCompatibleResult(result)
	if wire != nil {
		wire.Converted = volcesCompatibleResultConverted(original)
	}
	if notifyErr := notifyWireResponse(request, wire); notifyErr != nil {
		return result, firstNonEmpty(requestID, envelopeRequestID), wire, notifyErr
	}
	return result, firstNonEmpty(requestID, envelopeRequestID), wire, err
}

func (c VolcesClient) getJSON(ctx context.Context, request Request, baseURL string, path string, apiKey string) (map[string]any, string, *WireResponse, error) {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, joinURL(baseURL, path), nil)
	if err != nil {
		return nil, "", nil, err
	}
	req.Header.Set("Authorization", "Bearer "+apiKey)
	resp, err := httpClient(request.HTTPClient, c.HTTPClient).Do(req)
	if err != nil {
		return nil, "", nil, transportClientError(err)
	}
	requestID := requestIDFromHTTPResponse(resp)
	result, wire, err := decodeHTTPResponseForProtocol(resp, ProtocolVolcesContents)
	if err != nil {
		return result, requestID, wire, err
	}
	original := result
	result, envelopeRequestID, err := normalizeVolcesCompatibleResult(result)
	if wire != nil {
		wire.Converted = volcesCompatibleResultConverted(original)
	}
	return result, firstNonEmpty(requestID, envelopeRequestID), wire, err
}

func volcesCompatibleResultConverted(result map[string]any) bool {
	if _, ok := result["error"].(map[string]any); ok {
		return true
	}
	_, hasCode := result["code"]
	_, hasData := result["data"].(map[string]any)
	return hasCode && hasData
}

func normalizeVolcesCompatibleResult(result map[string]any) (map[string]any, string, error) {
	requestID := firstNonEmpty(
		stringFromAny(result["request_id"]),
		stringFromAny(result["requestId"]),
	)
	if errorObject, ok := result["error"].(map[string]any); ok {
		code := firstNonEmpty(
			stringFromAny(errorObject["code"]),
			stringFromAny(errorObject["type"]),
			"volces_compatible_error",
		)
		message := strings.TrimSpace(stringFromAny(errorObject["message"]))
		if message == "" {
			message = "volces compatible request failed"
		}
		return result, requestID, &ClientError{
			Code:      code,
			Message:   message,
			RequestID: requestID,
			Retryable: false,
		}
	}
	rawCode, hasCode := result["code"]
	if !hasCode {
		return result, requestID, nil
	}
	code, validCode := volcesIntFromAny(rawCode)
	if !validCode {
		return result, requestID, nil
	}
	if code != 0 {
		message := strings.TrimSpace(stringFromAny(result["message"]))
		if message == "" {
			message = fmt.Sprintf("volces compatible request failed with code %d", code)
		}
		return result, requestID, &ClientError{
			Code:      fmt.Sprintf("volces_%d", code),
			Message:   message,
			RequestID: requestID,
			Retryable: false,
		}
	}
	data, ok := result["data"].(map[string]any)
	if !ok {
		return result, requestID, nil
	}
	normalized := cloneBody(data)
	if requestID != "" && requestIDFromResult(normalized) == "" {
		normalized["request_id"] = requestID
	}
	return normalized, requestID, nil
}

func volcesImageBody(request Request) map[string]any {
	body := cleanProviderBody(request.Body)
	body["model"] = upstreamModelName(request.Candidate)
	if _, ok := body["watermark"]; !ok {
		body["watermark"] = false
	}
	if request.Kind == "images.generations" {
		if _, ok := body["seed"]; !ok {
			body["seed"] = -1
		}
	}
	resolution := normalizedProviderImageResolution(body["resolution"])
	size := widthHeightSize(body)
	if volcesImageUsesResolutionSize(request) && resolution != "" {
		body["size"] = resolution
	} else if size != "" {
		body["size"] = size
	} else if resolution != "" {
		body["size"] = resolution
	}
	for _, key := range []string{"aspect_ratio", "aspectRatio", "ratio", "resolution", "width", "height"} {
		delete(body, key)
	}
	normalizeVolcesSequentialImageGeneration(body, request)
	return body
}

func volcesImageUsesResolutionSize(request Request) bool {
	modelType := firstNonEmpty(request.ModelType, request.Candidate.ModelType)
	if capability, ok := request.Candidate.Capabilities[modelType].(map[string]any); ok {
		if strings.EqualFold(strings.TrimSpace(stringFromAny(capability["size_param_format"])), "resolution") {
			return true
		}
	}
	model := strings.ToLower(strings.TrimSpace(upstreamModelName(request.Candidate)))
	return strings.HasPrefix(model, "doubao-seedream-5-0")
}

func volcesVideoBody(request Request) map[string]any {
	body := cleanProviderBody(request.Body)
	body["model"] = upstreamModelName(request.Candidate)
	content := contentItems(body["content"])
	if len(content) == 0 {
		content = buildVolcesContentFromBody(body)
	}
	appendMultiShotTimeline(&content)
	convertVolcesElementsToImageReferences(&content)
	normalizeVolcesContentRoles(content)
	return volcesVideoTaskBody(body, content)
}

func cleanProviderBody(body map[string]any) map[string]any {
	out := cloneBody(body)
	for _, key := range []string{
		"runMode",
		"mode",
		"simulation",
		"testMode",
		"simulationProfile",
		"testProfile",
		"pollIntervalMs",
		"poll_interval_ms",
		"pollTimeoutSeconds",
		"poll_timeout_seconds",
		"_gateway_compatibility",
		"_gateway_target_protocol",
		"_compat_provider",
		"_kling_compat_version",
		"platform_id",
		"platformId",
		"platform_model_id",
		"platformModelId",
		"modelType",
		"model_type",
	} {
		delete(out, key)
	}
	return out
}

func buildVolcesContentFromBody(body map[string]any) []map[string]any {
	content := make([]map[string]any, 0)
	if prompt := firstNonEmptyStringValue(body, "prompt", "input"); prompt != "" {
		content = append(content, map[string]any{"type": "text", "text": prompt})
	}
	appendURLContent := func(kind string, role string, url string) {
		if strings.TrimSpace(url) == "" {
			return
		}
		switch kind {
		case "image_url":
			content = append(content, map[string]any{"type": kind, "role": role, "image_url": map[string]any{"url": strings.TrimSpace(url)}})
		case "video_url":
			content = append(content, map[string]any{"type": kind, "role": role, "video_url": map[string]any{"url": strings.TrimSpace(url)}})
		case "audio_url":
			content = append(content, map[string]any{"type": kind, "role": role, "audio_url": map[string]any{"url": strings.TrimSpace(url)}})
		}
	}

	firstFrame := firstNonEmptyStringValue(body, "first_frame", "firstFrame")
	appendURLContent("image_url", "first_frame", firstFrame)
	appendURLContent("image_url", "last_frame", firstNonEmptyStringValue(body, "last_frame", "lastFrame"))
	imageURLs := firstNonEmptyStringListFromAny(body["image"], body["images"], body["image_url"], body["imageUrl"], body["image_urls"], body["imageUrls"])
	if firstFrame == "" && len(imageURLs) > 0 {
		appendURLContent("image_url", "first_frame", imageURLs[0])
		imageURLs = imageURLs[1:]
	}
	for _, url := range imageURLs {
		appendURLContent("image_url", "reference_image", url)
	}
	for _, url := range firstNonEmptyStringListFromAny(body["reference_image"], body["referenceImage"]) {
		appendURLContent("image_url", "reference_image", url)
	}
	for _, url := range firstNonEmptyStringListFromAny(body["video"], body["video_url"], body["videoUrl"], body["reference_video"], body["referenceVideo"]) {
		appendURLContent("video_url", "reference_video", url)
	}
	for _, url := range firstNonEmptyStringListFromAny(body["audio_url"], body["audioUrl"], body["reference_audio"], body["referenceAudio"]) {
		appendURLContent("audio_url", "reference_audio", url)
	}
	if len(content) == 0 {
		content = append(content, map[string]any{"type": "text", "text": ""})
	}
	return content
}

func volcesVideoTaskBody(body map[string]any, content []map[string]any) map[string]any {
	out := map[string]any{
		"model":   body["model"],
		"content": sanitizeVolcesVideoContent(content),
	}
	addVolcesVideoTaskParams(out, body)
	return out
}

func validateVolcesVideoTaskBody(body map[string]any) error {
	firstFrameCount := 0
	lastFrameCount := 0
	for _, item := range contentItems(body["content"]) {
		if stringFromAny(item["type"]) != "image_url" {
			continue
		}
		switch stringFromAny(item["role"]) {
		case "first_frame":
			firstFrameCount++
		case "last_frame":
			lastFrameCount++
		}
	}
	if firstFrameCount > 1 {
		return &ClientError{
			Code:       "invalid_parameter",
			Message:    fmt.Sprintf("content contains %d first_frame image items; expected at most one first frame image content", firstFrameCount),
			StatusCode: 400,
			Retryable:  false,
		}
	}
	if lastFrameCount > 1 {
		return &ClientError{
			Code:       "invalid_parameter",
			Message:    fmt.Sprintf("content contains %d last_frame image items; expected at most one last frame image content", lastFrameCount),
			StatusCode: 400,
			Retryable:  false,
		}
	}
	return nil
}

func addVolcesVideoTaskParams(out map[string]any, body map[string]any) {
	copyVolcesStringParam(out, "callback_url", body, "callback_url", "callbackUrl")
	copyVolcesBoolParam(out, "return_last_frame", body, "return_last_frame", "returnLastFrame")
	copyVolcesIntParam(out, "execution_expires_after", body, "execution_expires_after", "executionExpiresAfter")
	copyVolcesBoolParam(out, "generate_audio", body, "generate_audio", "generateAudio", "audio")
	if tools, ok := body["tools"]; ok {
		out["tools"] = tools
	} else {
		for _, key := range []string{"enable_web_search", "enableWebSearch"} {
			if value, ok := volcesBoolFromAny(body[key]); ok && value {
				out["tools"] = []any{map[string]any{"type": "web_search"}}
				break
			}
		}
	}
	copyVolcesBoolParam(out, "draft", body, "draft")
	copyVolcesStringParam(out, "resolution", body, "resolution", "size")
	copyVolcesStringParam(out, "ratio", body, "ratio", "aspect_ratio", "aspectRatio")
	if copyVolcesIntParam(out, "frames", body, "frames") {
		delete(out, "duration")
	} else {
		copyVolcesIntParam(out, "duration", body, "duration", "duration_seconds", "durationSeconds", "dur")
	}
	copyVolcesIntParam(out, "seed", body, "seed")
	copyVolcesBoolParam(out, "camera_fixed", body, "camera_fixed", "cameraFixed", "camerafixed", "cf")
	copyVolcesBoolParam(out, "watermark", body, "watermark")
}

func copyVolcesStringParam(out map[string]any, target string, body map[string]any, keys ...string) bool {
	for _, key := range keys {
		if value := strings.TrimSpace(stringFromAny(body[key])); value != "" {
			out[target] = value
			return true
		}
	}
	return false
}

func copyVolcesIntParam(out map[string]any, target string, body map[string]any, keys ...string) bool {
	for _, key := range keys {
		if value, ok := volcesIntFromAny(body[key]); ok {
			out[target] = value
			return true
		}
	}
	return false
}

func copyVolcesBoolParam(out map[string]any, target string, body map[string]any, keys ...string) bool {
	for _, key := range keys {
		if value, ok := volcesBoolFromAny(body[key]); ok {
			out[target] = value
			return true
		}
	}
	return false
}

func volcesIntFromAny(value any) (int, bool) {
	switch typed := value.(type) {
	case nil:
		return 0, false
	case int:
		return typed, true
	case int64:
		return int(typed), true
	case float64:
		return int(math.Round(typed)), true
	case string:
		text := strings.TrimSpace(typed)
		if text == "" {
			return 0, false
		}
		if parsed, err := strconv.ParseFloat(text, 64); err == nil {
			return int(math.Round(parsed)), true
		}
		return 0, false
	default:
		return 0, false
	}
}

func volcesBoolFromAny(value any) (bool, bool) {
	switch typed := value.(type) {
	case nil:
		return false, false
	case bool:
		return typed, true
	case int:
		if typed == 1 {
			return true, true
		}
		if typed == 0 {
			return false, true
		}
	case int64:
		if typed == 1 {
			return true, true
		}
		if typed == 0 {
			return false, true
		}
	case float64:
		if typed == 1 {
			return true, true
		}
		if typed == 0 {
			return false, true
		}
	case string:
		normalized := strings.ToLower(strings.TrimSpace(typed))
		if normalized == "true" || normalized == "1" {
			return true, true
		}
		if normalized == "false" || normalized == "0" {
			return false, true
		}
	}
	return false, false
}

func sanitizeVolcesVideoContent(content []map[string]any) []map[string]any {
	out := make([]map[string]any, 0, len(content))
	for _, item := range content {
		switch stringFromAny(item["type"]) {
		case "text":
			out = append(out, map[string]any{
				"type": "text",
				"text": strings.TrimSpace(stringFromAny(item["text"])),
			})
		case "image_url":
			url := volcesNestedURL(item, "image_url")
			if url == "" {
				continue
			}
			out = append(out, map[string]any{
				"type":      "image_url",
				"role":      volcesImageRole(item),
				"image_url": map[string]any{"url": url},
			})
		case "video_url":
			url := volcesNestedURL(item, "video_url")
			if url == "" {
				continue
			}
			videoURL := map[string]any{"url": url}
			if value := strings.TrimSpace(stringFromAny(mapFromAny(item["video_url"])["refer_type"])); value != "" {
				videoURL["refer_type"] = value
			}
			if value := strings.TrimSpace(stringFromAny(mapFromAny(item["video_url"])["keep_original_sound"])); value != "" {
				videoURL["keep_original_sound"] = value
			}
			out = append(out, map[string]any{
				"type":      "video_url",
				"role":      "reference_video",
				"video_url": videoURL,
			})
		case "audio_url":
			url := volcesNestedURL(item, "audio_url")
			if url == "" {
				continue
			}
			out = append(out, map[string]any{
				"type":      "audio_url",
				"role":      "reference_audio",
				"audio_url": map[string]any{"url": url},
			})
		}
	}
	if len(out) == 0 {
		return []map[string]any{{"type": "text", "text": ""}}
	}
	return out
}

func volcesImageRole(item map[string]any) string {
	switch strings.TrimSpace(stringFromAny(item["role"])) {
	case "first_frame":
		return "first_frame"
	case "last_frame":
		return "last_frame"
	default:
		return "reference_image"
	}
}

func volcesNestedURL(item map[string]any, key string) string {
	nested := mapFromAny(item[key])
	return strings.TrimSpace(stringFromAny(nested["url"]))
}

func mapFromAny(value any) map[string]any {
	if object, ok := value.(map[string]any); ok {
		return object
	}
	return nil
}

func contentItems(value any) []map[string]any {
	switch typed := value.(type) {
	case []any:
		out := make([]map[string]any, 0, len(typed))
		for _, raw := range typed {
			item, ok := raw.(map[string]any)
			if !ok {
				continue
			}
			copied := map[string]any{}
			for key, value := range item {
				copied[key] = value
			}
			out = append(out, copied)
		}
		return out
	case []map[string]any:
		out := make([]map[string]any, 0, len(typed))
		for _, item := range typed {
			copied := map[string]any{}
			for key, value := range item {
				copied[key] = value
			}
			out = append(out, copied)
		}
		return out
	default:
		return nil
	}
}

func normalizeVolcesContentRoles(content []map[string]any) {
	for _, item := range content {
		itemType := strings.TrimSpace(stringFromAny(item["type"]))
		role := strings.TrimSpace(stringFromAny(item["role"]))
		switch itemType {
		case "image_url":
			if role != "first_frame" && role != "last_frame" {
				item["role"] = "reference_image"
			}
		case "video_url":
			item["role"] = "reference_video"
		case "audio_url":
			item["role"] = "reference_audio"
		}
	}
}

func convertVolcesElementsToImageReferences(content *[]map[string]any) {
	referenced := referencedVolcesElementIndexes(*content)
	out := make([]map[string]any, 0, len(*content))
	elementIndex := 0
	for _, item := range *content {
		if stringFromAny(item["type"]) != "element" {
			out = append(out, item)
			continue
		}
		elementIndex++
		if !referenced[elementIndex] {
			continue
		}
		url := volcesElementFrontalImageURL(item)
		if url == "" {
			continue
		}
		role := stringFromAny(item["role"])
		if role != "first_frame" && role != "last_frame" {
			role = "reference_image"
		}
		out = append(out, map[string]any{
			"type":      "image_url",
			"role":      role,
			"image_url": map[string]any{"url": url},
		})
	}
	*content = out
}

func referencedVolcesElementIndexes(content []map[string]any) map[int]bool {
	out := map[int]bool{}
	for _, item := range content {
		if stringFromAny(item["type"]) != "text" {
			continue
		}
		text := stringFromAny(item["text"])
		if strings.TrimSpace(text) == "" {
			continue
		}
		for _, match := range volcesElementReferencePattern.FindAllStringSubmatch(text, -1) {
			raw := ""
			if len(match) > 1 && match[1] != "" {
				raw = match[1]
			} else if len(match) > 2 {
				raw = match[2]
			}
			index, err := strconv.Atoi(raw)
			if err == nil && index > 0 {
				out[index] = true
			}
		}
	}
	return out
}

func volcesElementFrontalImageURL(item map[string]any) string {
	element := mapFromAny(item["element"])
	if element == nil {
		return ""
	}
	inline := mapFromAny(element["inline_element"])
	for _, value := range []any{
		inline["frontal_image_url"],
		element["frontal_image_url"],
		element["front_image_url"],
		element["image_url"],
	} {
		if url := strings.TrimSpace(stringFromAny(value)); url != "" {
			return url
		}
	}
	return volcesReferImageURL(firstPresent(inline["refer_images"], element["refer_images"]))
}

func volcesReferImageURL(value any) string {
	images := mapListFromAny(value)
	firstURL := ""
	for _, image := range images {
		url := strings.TrimSpace(stringFromAny(image["url"]))
		if url == "" {
			continue
		}
		if firstURL == "" {
			firstURL = url
		}
		slot := strings.ToLower(strings.TrimSpace(stringFromAny(image["slot_key"])))
		if slot == "frontal" || slot == "front" {
			return url
		}
	}
	return firstURL
}

func mapListFromAny(value any) []map[string]any {
	switch typed := value.(type) {
	case []any:
		out := make([]map[string]any, 0, len(typed))
		for _, item := range typed {
			if object := mapFromAny(item); object != nil {
				out = append(out, object)
			}
		}
		return out
	case []map[string]any:
		return typed
	default:
		return nil
	}
}

func appendMultiShotTimeline(content *[]map[string]any) {
	type shot struct {
		index    int
		text     string
		duration float64
	}
	shots := make([]shot, 0)
	items := *content
	remaining := items[:0]
	for index, item := range items {
		if stringFromAny(item["type"]) != "text" {
			remaining = append(remaining, item)
			continue
		}
		role := stringFromAny(item["role"])
		if role != "shot_prompt" && item["shot_index"] == nil {
			remaining = append(remaining, item)
			continue
		}
		text := strings.TrimSpace(stringFromAny(item["text"]))
		if text == "" {
			continue
		}
		shotIndex := numericValue(item["shot_index"], float64(index))
		shots = append(shots, shot{index: int(math.Floor(shotIndex)), text: text, duration: numericValue(item["duration"], 5)})
	}
	if len(shots) == 0 {
		return
	}
	*content = remaining

	for i := 0; i < len(shots)-1; i++ {
		for j := i + 1; j < len(shots); j++ {
			if shots[j].index < shots[i].index {
				shots[i], shots[j] = shots[j], shots[i]
			}
		}
	}
	cursor := 0.0
	lines := make([]string, 0, len(shots))
	for idx, shot := range shots {
		start := cursor
		duration := shot.duration
		if duration <= 0 {
			duration = 5
		}
		end := start + duration
		cursor = end
		shotNumber := shot.index + 1
		if shotNumber <= 0 {
			shotNumber = idx + 1
		}
		lines = append(lines, fmt.Sprintf("Shot %d, %gs~%gs: %s", shotNumber, start, end, shot.text))
	}

	textItem := ensureTextContent(content)
	current := stringFromAny(textItem["text"])
	const prefix = "Additional shot timeline (auto-generated):"
	if strings.Contains(current, prefix) {
		return
	}
	separator := ""
	if strings.TrimSpace(current) != "" {
		separator = "\n\n"
	}
	textItem["text"] = current + separator + prefix + "\n" + strings.Join(lines, "\n")
}

func ensureTextContent(content *[]map[string]any) map[string]any {
	for _, item := range *content {
		if stringFromAny(item["type"]) == "text" && item["shot_index"] == nil && stringFromAny(item["role"]) != "shot_prompt" {
			return item
		}
	}
	item := map[string]any{"type": "text", "text": ""}
	*content = append([]map[string]any{item}, (*content)...)
	return item
}

func supportsMultipleOutputs(request Request, capabilityName string) bool {
	for _, key := range []string{capabilityName, request.ModelType, "image_generate", "image_edit"} {
		if key == "" {
			continue
		}
		capability, _ := request.Candidate.Capabilities[key].(map[string]any)
		if boolFromAny(capability["output_multiple_images"]) {
			return true
		}
	}
	return false
}

func normalizeVolcesSequentialImageGeneration(body map[string]any, request Request) {
	if !supportsMultipleOutputs(request, request.ModelType) {
		delete(body, "sequential_image_generation")
		delete(body, "sequential_image_generation_options")
		if numericValue(body["n"], 1) > 1 {
			body["n"] = 1
		}
		if numericValue(body["batch_size"], 1) > 1 {
			body["batch_size"] = 1
		}
		return
	}
	options, hasOptions := volcesSequentialImageGenerationOptions(body)
	normalizeVolcesSequentialMaxImages(options, request)
	if _, explicit := body["sequential_image_generation"]; explicit {
		return
	}
	count := requestedVolcesSequentialImageCount(body, options)
	if count <= 1 {
		return
	}
	body["sequential_image_generation"] = "auto"
	if !hasOptions {
		options = map[string]any{}
		body["sequential_image_generation_options"] = options
	}
	if _, ok := options["max_images"]; !ok {
		options["max_images"] = count
		normalizeVolcesSequentialMaxImages(options, request)
	}
}

func volcesSequentialImageGenerationOptions(body map[string]any) (map[string]any, bool) {
	options, ok := body["sequential_image_generation_options"].(map[string]any)
	if !ok {
		return nil, false
	}
	copied := map[string]any{}
	for key, value := range options {
		copied[key] = value
	}
	body["sequential_image_generation_options"] = copied
	return copied, true
}

func normalizeVolcesSequentialMaxImages(options map[string]any, request Request) {
	if options == nil {
		return
	}
	raw, exists := options["max_images"]
	if !exists {
		return
	}
	current := int(math.Round(numericValue(raw, 0)))
	minCount, maxCount := volcesSequentialMaxImagesRange(request)
	adjusted := current
	if minCount > 0 && adjusted < minCount {
		adjusted = minCount
	}
	if maxCount > 0 && adjusted > maxCount {
		adjusted = maxCount
	}
	if adjusted != current {
		options["max_images"] = adjusted
	}
}

func requestedVolcesSequentialImageCount(body map[string]any, options map[string]any) int {
	for _, value := range []any{
		valueFromMap(options, "max_images"),
		body["count"],
		body["n"],
		body["batch_size"],
	} {
		count := int(math.Round(numericValue(value, 0)))
		if count > 0 {
			return count
		}
	}
	return 1
}

func valueFromMap(values map[string]any, key string) any {
	if values == nil {
		return nil
	}
	return values[key]
}

func volcesSequentialMaxImagesRange(request Request) (int, int) {
	minCount := 1
	maxCount := 0
	for _, key := range []string{request.ModelType, "image_generate", "image_edit"} {
		if key == "" {
			continue
		}
		capability, _ := request.Candidate.Capabilities[key].(map[string]any)
		if capability == nil {
			continue
		}
		if value := firstPositiveInt(capability, "output_min_images_count", "outputMinImagesCount", "min_output_images", "minOutputImages", "min_images", "minImages"); value > 0 {
			minCount = value
		}
		if value := firstPositiveInt(capability, "output_max_images_count", "outputMaxImagesCount", "max_output_images", "maxOutputImages", "max_images", "maxImages"); value > 0 {
			maxCount = value
		}
		return minCount, maxCount
	}
	return minCount, maxCount
}

func firstPositiveInt(values map[string]any, keys ...string) int {
	for _, key := range keys {
		value := int(math.Round(numericValue(values[key], 0)))
		if value > 0 {
			return value
		}
	}
	return 0
}

func widthHeightSize(body map[string]any) string {
	width := numericValue(body["width"], 0)
	height := numericValue(body["height"], 0)
	if width <= 0 || height <= 0 {
		return ""
	}
	return fmt.Sprintf("%dx%d", int(math.Round(width)), int(math.Round(height)))
}

func volcesTaskStatus(result map[string]any) string {
	return strings.ToLower(strings.TrimSpace(stringFromAny(result["status"])))
}

func volcesTaskErrorCode(result map[string]any) string {
	errorObj, _ := result["error"].(map[string]any)
	if code := strings.TrimSpace(stringFromAny(errorObj["code"])); code != "" {
		return code
	}
	status := volcesTaskStatus(result)
	if status == "cancelled" {
		return "volces_task_cancelled"
	}
	if status != "" {
		return status
	}
	return "volces_task_failed"
}

func volcesTaskErrorMessage(result map[string]any) string {
	errorObj, _ := result["error"].(map[string]any)
	if message := strings.TrimSpace(stringFromAny(errorObj["message"])); message != "" {
		return message
	}
	if status := volcesTaskStatus(result); status != "" {
		return "volces video task " + status
	}
	return "volces video task failed"
}

func volcesVideoSuccessResult(request Request, upstreamTaskID string, raw map[string]any) map[string]any {
	result := map[string]any{}
	for _, key := range []string{"seed", "resolution", "ratio", "duration", "frames", "framespersecond"} {
		if raw[key] != nil {
			result[key] = raw[key]
		}
	}
	content, _ := raw["content"].(map[string]any)
	videoURL := strings.TrimSpace(stringFromAny(content["video_url"]))
	created := intFromAny(raw["created_at"])
	if created == 0 {
		created = int(nowUnix())
	}
	data := []any{}
	if videoURL != "" {
		item := map[string]any{"url": videoURL, "type": "video"}
		if lastFrameURL := strings.TrimSpace(stringFromAny(content["last_frame_url"])); lastFrameURL != "" {
			item["last_frame_url"] = lastFrameURL
		}
		data = append(data, item)
	}
	result["id"] = firstNonEmpty(stringFromAny(raw["id"]), upstreamTaskID)
	result["model"] = firstNonEmpty(stringFromAny(raw["model"]), upstreamModelName(request.Candidate))
	result["status"] = "succeeded"
	result["object"] = "video.generation"
	result["created"] = created
	result["data"] = data
	return result
}

func volcesVideoUsage(raw map[string]any) Usage {
	usage, _ := raw["usage"].(map[string]any)
	output := intFromAny(usage["completion_tokens"])
	total := intFromAny(usage["total_tokens"])
	if total == 0 {
		total = output
	}
	return Usage{OutputTokens: output, TotalTokens: total}
}

func volcesVideoProgress(request Request, upstreamTaskID string) []Progress {
	progress := providerProgress(request)
	progress = append(progress, Progress{
		Phase:    "polling_result",
		Progress: 0.9,
		Message:  "volces video task completed",
		Payload:  map[string]any{"upstreamTaskId": upstreamTaskID},
	})
	return progress
}

func volcesPollInterval(request Request) time.Duration {
	ms := numericValue(firstPresent(request.Candidate.PlatformConfig["volcesPollIntervalMs"], request.Body["pollIntervalMs"], request.Body["poll_interval_ms"]), 5000)
	if ms < 100 {
		ms = 100
	}
	return time.Duration(ms) * time.Millisecond
}

func volcesPollTimeout(request Request) time.Duration {
	fallbackSeconds := ProviderRequestTimeout(request.Kind).Seconds()
	seconds := numericValue(firstPresent(request.Candidate.PlatformConfig["volcesPollTimeoutSeconds"], request.Body["pollTimeoutSeconds"], request.Body["poll_timeout_seconds"]), fallbackSeconds)
	if seconds < 1 {
		seconds = fallbackSeconds
	}
	return time.Duration(seconds) * time.Second
}

func volcesRetryPollInterval(request Request, normal time.Duration, failures int) time.Duration {
	if failures < 1 {
		return normal
	}
	max := time.Duration(numericValue(firstPresent(request.Candidate.PlatformConfig["volcesPollRetryMaxMs"], request.Body["pollRetryMaxMs"], request.Body["poll_retry_max_ms"]), 30000)) * time.Millisecond
	if max < normal {
		max = normal
	}
	delay := normal
	for attempt := 1; attempt < failures && delay < max; attempt++ {
		delay *= 2
	}
	if delay > max {
		return max
	}
	return delay
}

func resetVolcesPollTimer(timer *time.Timer, delay time.Duration) {
	if delay < 100*time.Millisecond {
		delay = 100 * time.Millisecond
	}
	if !timer.Stop() {
		select {
		case <-timer.C:
		default:
		}
	}
	timer.Reset(delay)
}

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

func firstNonEmptyStringValue(body map[string]any, keys ...string) string {
	for _, key := range keys {
		if value := strings.TrimSpace(stringFromAny(body[key])); value != "" {
			return value
		}
	}
	return ""
}

func stringListFromAny(value any) []string {
	switch typed := value.(type) {
	case string:
		if strings.TrimSpace(typed) == "" {
			return nil
		}
		return []string{typed}
	case []any:
		out := make([]string, 0, len(typed))
		for _, item := range typed {
			if value := strings.TrimSpace(stringFromAny(item)); value != "" {
				out = append(out, value)
			}
		}
		return out
	case []string:
		out := make([]string, 0, len(typed))
		for _, item := range typed {
			if value := strings.TrimSpace(item); value != "" {
				out = append(out, value)
			}
		}
		return out
	default:
		return nil
	}
}

func firstNonEmptyStringListFromAny(values ...any) []string {
	for _, value := range values {
		items := stringListFromAny(value)
		if len(items) > 0 {
			return items
		}
	}
	return nil
}

func numericValue(value any, fallback float64) float64 {
	switch typed := value.(type) {
	case int:
		return float64(typed)
	case int64:
		return float64(typed)
	case float64:
		return typed
	case string:
		var parsed float64
		if _, err := fmt.Sscanf(strings.TrimSpace(typed), "%f", &parsed); err == nil {
			return parsed
		}
		return fallback
	default:
		return fallback
	}
}

func boolFromAny(value any) bool {
	switch typed := value.(type) {
	case bool:
		return typed
	case string:
		normalized := strings.ToLower(strings.TrimSpace(typed))
		return normalized == "true" || normalized == "1"
	case float64:
		return typed == 1
	case int:
		return typed == 1
	default:
		return false
	}
}
