package service

import (
	"context"
	"fmt"
	"math"
	"strings"

	"github.com/QuantumNous/new-api/common"
	"github.com/QuantumNous/new-api/constant"
	"github.com/QuantumNous/new-api/logger"
	"github.com/QuantumNous/new-api/model"
	relaycommon "github.com/QuantumNous/new-api/relay/common"
	"github.com/QuantumNous/new-api/setting/ratio_setting"
	"github.com/gin-gonic/gin"
)

// LogTaskConsumption 记录任务消费日志和统计信息（仅记录，不涉及实际扣费）。
// 实际扣费已由 BillingSession（PreConsumeBilling + SettleBilling）完成。
func LogTaskConsumption(c *gin.Context, info *relaycommon.RelayInfo) {
	tokenName := c.GetString("token_name")
	logContent := fmt.Sprintf("操作 %s", info.Action)

	// 视频按 token 计费分支：任务型视频渠道 + UsePrice + ModelPrice=0 + VideoOutputTokens>0。
	// 该分支下 quota 已由 outputVideoTokens × ratios × group 直接算出，
	// OtherRatios 的 seconds/size 不参与计费（已在 relay_task.go 步骤 5/6 跳过），
	// 因此 logContent 应展示真实公式而非 "计算参数：seconds, size"。
	isVideoTokenBilling := constant.IsVideoTaskChannel(info.ChannelType) &&
		info.PriceData.UsePrice &&
		info.PriceData.ModelPrice == 0 &&
		info.PriceData.VideoOutputTokens > 0

	// 视频按分辨率/条一口价（*_per_video）：ModelPriceHelperVideo 将 ModelRatio 置 0、
	// VideoOutputTokens 为 0，预扣已在 relay 中按条合并，不应再展示为「按次 $0」或 seconds 倍率文案。
	isVideoPerVideoFlatBilling := constant.IsVideoTaskChannel(info.ChannelType) &&
		info.PriceData.UsePrice &&
		info.PriceData.ModelPrice == 0 &&
		info.PriceData.VideoOutputTokens == 0 &&
		info.PriceData.ModelRatio == 0
	isVideoPerSecondBilling := isVideoPerVideoFlatBilling &&
		info.PriceData.OtherRatios != nil &&
		info.PriceData.OtherRatios["seconds"] > 0
	var videoPerSecondDetail *videoPerSecondBillingDetail
	if isVideoPerSecondBilling {
		videoPerSecondDetail = videoPerSecondBillingDetailFromSubmit(c, info)
	}

	switch {
	case common.StringsContains(constant.TaskPricePatches, info.OriginModelName):
		logContent = fmt.Sprintf("%s，按次计费", logContent)
	case isVideoTokenBilling:
		// 例：操作 generate, 视频 tokens：86400 (输入文本 13), 模型倍率 15.00, 视频倍率 1.00 × 1.00
		logContent = fmt.Sprintf(
			"%s, 视频 tokens：%d (输入文本 %d), 模型倍率 %.2f, 视频倍率 %.2f × %.2f",
			logContent,
			info.PriceData.VideoOutputTokens,
			info.PriceData.VideoInputTextTokens,
			info.PriceData.ModelRatio,
			info.PriceData.VideoRatio,
			info.PriceData.VideoCompletionRatio,
		)
	case isVideoPerSecondBilling:
		logContent = formatVideoPerSecondBillingDetail(logContent+"，视频按秒计费", videoPerSecondDetail, info.PriceData.Quota)
	case isVideoPerVideoFlatBilling:
		logContent = fmt.Sprintf("%s，按视频数量计费", logContent)
	default:
		if len(info.PriceData.OtherRatios) > 0 {
			var contents []string
			for key, ra := range info.PriceData.OtherRatios {
				if 1.0 != ra {
					contents = append(contents, fmt.Sprintf("%s: %.2f", key, ra))
				}
			}
			if len(contents) > 0 {
				logContent = fmt.Sprintf("%s, 计算参数：%s", logContent, strings.Join(contents, ", "))
			}
		}
	}

	other := make(map[string]interface{})
	other["request_path"] = c.Request.URL.Path
	if strings.TrimSpace(info.PublicTaskID) != "" {
		other["task_id"] = strings.TrimSpace(info.PublicTaskID)
	}
	other["model_price"] = info.PriceData.ModelPrice
	other["group_ratio"] = info.PriceData.GroupRatioInfo.GroupRatio
	if info.PriceData.GroupRatioInfo.HasSpecialRatio {
		other["user_group_ratio"] = info.PriceData.GroupRatioInfo.GroupSpecialRatio
	}
	if info.IsModelMapped {
		other["is_model_mapped"] = true
		other["upstream_model_name"] = info.UpstreamModelName
	}
	// 视频按 token 计费：写入完整计费元数据，供前端日志详情按 token 公式展示。
	if isVideoTokenBilling {
		other["billing_mode"] = "video_token"
		other["model_ratio"] = info.PriceData.ModelRatio
		other["video_ratio"] = info.PriceData.VideoRatio
		other["video_completion_ratio"] = info.PriceData.VideoCompletionRatio
		other["video_output_tokens"] = info.PriceData.VideoOutputTokens
		other["video_input_text_tokens"] = info.PriceData.VideoInputTextTokens
	}
	if isVideoPerSecondBilling {
		other["billing_mode"] = "video_per_second"
		other["model_ratio"] = info.PriceData.ModelRatio
		appendVideoPerSecondBillingDetailOther(other, videoPerSecondDetail, info.PriceData.Quota)
	} else if isVideoPerVideoFlatBilling {
		other["billing_mode"] = "video_per_video"
		other["model_ratio"] = info.PriceData.ModelRatio
		appendVideoPerVideoBillingDetailOther(c, other, info)
	}
	discPct := float64(100)
	if info.PriceData.ChannelPriceDiscount != nil {
		discPct = *info.PriceData.ChannelPriceDiscount
	} else {
		discPct = model.ResolveChannelPriceDiscountPercent(info.ChannelId)
	}
	other["channel_price_discount_percent"] = discPct
	model.RecordConsumeLog(c, info.UserId, model.RecordConsumeLogParams{
		ChannelId: info.ChannelId,
		ModelName: info.OriginModelName,
		TokenName: tokenName,
		Quota:     info.PriceData.Quota,
		Content:   logContent,
		TokenId:   info.TokenId,
		Group:     info.UsingGroup,
		Other:     other,
	})
	model.UpdateUserUsedQuotaAndRequestCount(info.UserId, info.PriceData.Quota)
	model.UpdateChannelUsedQuota(info.ChannelId, info.PriceData.Quota)
}

// ---------------------------------------------------------------------------
// 异步任务计费辅助函数
// ---------------------------------------------------------------------------

// resolveTokenKey 通过 TokenId 运行时获取令牌 Key（用于 Redis 缓存操作）。
// 如果令牌已被删除或查询失败，返回空字符串。
func resolveTokenKey(ctx context.Context, tokenId int, taskID string) string {
	token, err := model.GetTokenById(tokenId)
	if err != nil {
		logger.LogWarn(ctx, fmt.Sprintf("获取令牌 key 失败 (tokenId=%d, task=%s): %s", tokenId, taskID, err.Error()))
		return ""
	}
	return token.Key
}

// taskIsSubscription 判断任务是否通过订阅计费。
func taskIsSubscription(task *model.Task) bool {
	return task.PrivateData.BillingSource == BillingSourceSubscription && task.PrivateData.SubscriptionId > 0
}

// taskAdjustFunding 调整任务的资金来源（钱包或订阅），delta > 0 表示扣费，delta < 0 表示退还。
func taskAdjustFunding(task *model.Task, delta int) error {
	if taskIsSubscription(task) {
		return model.PostConsumeUserSubscriptionDelta(task.PrivateData.SubscriptionId, int64(delta))
	}
	if delta > 0 {
		return model.DecreaseUserQuota(task.UserId, delta)
	}
	return model.IncreaseUserQuota(task.UserId, -delta, false)
}

// taskAdjustTokenQuota 调整任务的令牌额度，delta > 0 表示扣费，delta < 0 表示退还。
// 需要通过 resolveTokenKey 运行时获取 key（不从 PrivateData 中读取）。
func taskAdjustTokenQuota(ctx context.Context, task *model.Task, delta int) {
	if task.PrivateData.TokenId <= 0 || delta == 0 {
		return
	}
	tokenKey := resolveTokenKey(ctx, task.PrivateData.TokenId, task.TaskID)
	if tokenKey == "" {
		return
	}
	var err error
	if delta > 0 {
		err = model.DecreaseTokenQuota(task.PrivateData.TokenId, tokenKey, delta)
	} else {
		err = model.IncreaseTokenQuota(task.PrivateData.TokenId, tokenKey, -delta)
	}
	if err != nil {
		logger.LogWarn(ctx, fmt.Sprintf("调整令牌额度失败 (delta=%d, task=%s): %s", delta, task.TaskID, err.Error()))
	}
}

// taskBillingOther 从 task 的 BillingContext 构建日志 Other 字段。
func taskBillingOther(task *model.Task) map[string]interface{} {
	other := make(map[string]interface{})
	if bc := task.PrivateData.BillingContext; bc != nil {
		other["model_price"] = bc.ModelPrice
		other["model_ratio"] = bc.ModelRatio
		other["group_ratio"] = bc.GroupRatio
		if len(bc.OtherRatios) > 0 {
			for k, v := range bc.OtherRatios {
				other[k] = v
			}
		}
		// 任务差额日志补全视频计费模式，避免前端误判为“上游返回”并渲染 NaN。
		if bc.ModelPrice == 0 && bc.ModelRatio == 0 {
			if secs, ok := bc.OtherRatios["seconds"]; ok && secs > 0 {
				other["billing_mode"] = "video_per_second"
			}
		}
	}
	props := task.Properties
	if props.UpstreamModelName != "" && props.UpstreamModelName != props.OriginModelName {
		other["is_model_mapped"] = true
		other["upstream_model_name"] = props.UpstreamModelName
	}
	discPct := float64(0)
	if bc := task.PrivateData.BillingContext; bc != nil && bc.ChannelPriceDiscountPercent > 0 {
		discPct = bc.ChannelPriceDiscountPercent
	}
	if discPct <= 0 && task.ChannelId > 0 {
		discPct = model.ResolveChannelPriceDiscountPercent(task.ChannelId)
	}
	if discPct <= 0 {
		discPct = 100
	}
	other["channel_price_discount_percent"] = discPct
	return other
}

func videoPerSecondBillingDetailFromSubmit(c *gin.Context, info *relaycommon.RelayInfo) *videoPerSecondBillingDetail {
	if c == nil || info == nil {
		return nil
	}
	req, err := relaycommon.GetTaskRequest(c)
	if err != nil {
		return nil
	}
	modelName := strings.TrimSpace(info.OriginModelName)
	if modelName == "" {
		return nil
	}
	rules, ok := ratio_setting.GetChannelVideoPricingRules(info.ChannelId, modelName)
	if !ok || !ratio_setting.HasUsableVideoPerSecondRules(rules) {
		var globalOK bool
		rules, globalOK = ratio_setting.GetVideoPricingRules(modelName)
		if !globalOK || !ratio_setting.HasUsableVideoPerSecondRules(rules) {
			return nil
		}
	}
	width, height := videoDimensionsFromTaskRequest(req)
	hasAudio := taskRequestHasAudio(req)
	mode := detectVideoBillingModeFromSubmitRequest(c)
	match, ok := matchPerSecondPriceDetail(rules, mode, width, height, hasAudio)
	if !ok || match.PricePerSecond <= 0 {
		return nil
	}
	seconds := videoDurationFromTaskRequest(req)
	if seconds <= 0 {
		seconds = int(info.PriceData.OtherRatios["seconds"])
	}
	if seconds <= 0 {
		return nil
	}
	groupRatio := info.PriceData.GroupRatioInfo.GroupRatio
	if groupRatio <= 0 {
		groupRatio = 1
	}
	return &videoPerSecondBillingDetail{
		Mode:                   mode,
		Seconds:                seconds,
		Width:                  width,
		Height:                 height,
		HasAudio:               hasAudio,
		Resolution:             match.Resolution,
		RuleWidth:              match.RuleWidth,
		RuleHeight:             match.RuleHeight,
		PricePerSecond:         match.PricePerSecond,
		GroupRatio:             groupRatio,
		QuotaPerUnit:           common.QuotaPerUnit,
		ChannelDiscountPercent: resolveVideoLogChannelDiscountPercent(info),
		UnifiedAudio:           match.UnifiedAudio,
	}
}

func resolveVideoLogChannelDiscountPercent(info *relaycommon.RelayInfo) float64 {
	if info != nil && info.PriceData.ChannelPriceDiscount != nil {
		return *info.PriceData.ChannelPriceDiscount
	}
	if info != nil {
		return model.ResolveChannelPriceDiscountPercent(info.ChannelId)
	}
	return 100
}

func videoDurationFromTaskRequest(req relaycommon.TaskSubmitReq) int {
	if req.Metadata != nil {
		if d := toInt(req.Metadata["duration"]); d > 0 {
			return d
		}
	}
	if strings.TrimSpace(req.Seconds) != "" {
		if f := toFloat64(req.Seconds); f > 0 {
			return int(math.Ceil(f))
		}
	}
	if req.Duration > 0 {
		return req.Duration
	}
	return 5
}

func videoDimensionsFromTaskRequest(req relaycommon.TaskSubmitReq) (int, int) {
	if size := strings.TrimSpace(req.Size); size != "" {
		parts := strings.Split(strings.ToLower(size), "x")
		if len(parts) == 2 {
			w := toInt(parts[0])
			h := toInt(parts[1])
			if w > 0 && h > 0 {
				return w, h
			}
		}
	}
	if req.Metadata != nil {
		w := toInt(req.Metadata["width"])
		h := toInt(req.Metadata["height"])
		if w > 0 && h > 0 {
			return w, h
		}
	}
	return 720, 1280
}

func taskRequestHasAudio(req relaycommon.TaskSubmitReq) bool {
	if req.Metadata == nil {
		return false
	}
	for _, key := range []string{"has_audio", "generate_audio"} {
		if v, ok := req.Metadata[key]; ok {
			switch x := v.(type) {
			case bool:
				return x
			case string:
				return strings.EqualFold(strings.TrimSpace(x), "true")
			}
		}
	}
	return false
}

func formatVideoPerSecondBillingDetail(prefix string, detail *videoPerSecondBillingDetail, quota int) string {
	if detail == nil {
		return fmt.Sprintf("%s（按最终成片时长向上取整 × 对应分辨率/音轨单价）", prefix)
	}
	priceLabel := "每秒价"
	if !detail.UnifiedAudio {
		if detail.HasAudio {
			priceLabel = "有音轨价"
		} else {
			priceLabel = "无音轨价"
		}
	}
	resolution := strings.TrimSpace(detail.Resolution)
	if resolution == "" {
		resolution = fmt.Sprintf("%dx%d", detail.RuleWidth, detail.RuleHeight)
	}
	pricePerSec := detail.PricePerSecond
	if detail.EffectivePricePerSecond > 0 {
		pricePerSec = detail.EffectivePricePerSecond
	}
	return fmt.Sprintf(
		"%s：%d秒 × %s(%dx%d，实际 %dx%d，%s) %s $%g/秒(渠道$%g+全局$%g×加价%.0f%%) × QuotaPerUnit %.0f × 分组倍率 %.4g × 渠道折扣 %.4g%% = %d tokens",
		prefix,
		detail.Seconds,
		resolution,
		detail.RuleWidth,
		detail.RuleHeight,
		detail.Width,
		detail.Height,
		audioLabel(detail.HasAudio),
		priceLabel,
		pricePerSec,
		detail.PricePerSecond,
		detail.GlobalPricePerSecond,
		detail.MarkupDiscountPercent,
		detail.QuotaPerUnit,
		detail.GroupRatio,
		videoChannelDiscountPercent(detail),
		quota,
	)
}

func videoChannelDiscountPercent(detail *videoPerSecondBillingDetail) float64 {
	if detail == nil || detail.ChannelDiscountPercent <= 0 {
		return 100
	}
	return detail.ChannelDiscountPercent
}

func appendVideoPerSecondBillingDetailOther(other map[string]interface{}, detail *videoPerSecondBillingDetail, quota int) {
	if other == nil || detail == nil {
		return
	}
	other["video_seconds"] = detail.Seconds
	other["video_width"] = detail.Width
	other["video_height"] = detail.Height
	other["video_has_audio"] = detail.HasAudio
	other["video_resolution"] = detail.Resolution
	other["video_rule_width"] = detail.RuleWidth
	other["video_rule_height"] = detail.RuleHeight
	other["video_price_per_second"] = detail.PricePerSecond
	if detail.GlobalPricePerSecond > 0 {
		other["global_video_price_per_second"] = detail.GlobalPricePerSecond
	}
	if detail.EffectivePricePerSecond > 0 {
		other["effective_video_price_per_second"] = detail.EffectivePricePerSecond
	}
	if detail.MarkupDiscountPercent > 0 {
		other["markup_discount_rate"] = detail.MarkupDiscountPercent
	}
	other["video_quota_per_unit"] = detail.QuotaPerUnit
	other["channel_price_discount"] = videoChannelDiscountPercent(detail)
	other["video_billed_quota"] = quota
	other["video_unified_audio_price"] = detail.UnifiedAudio
}

type videoPerVideoBillingDetail struct {
	Mode                   string
	Count                  int
	Width                  int
	Height                 int
	HasAudio               bool
	Resolution             string
	RuleWidth              int
	RuleHeight             int
	PricePerVideo          float64
	GroupRatio             float64
	QuotaPerUnit           float64
	ChannelDiscountPercent float64
	UnifiedAudio           bool
}

type videoPerVideoPriceMatch struct {
	Resolution    string
	RuleWidth     int
	RuleHeight    int
	PricePerVideo float64
	UnifiedAudio  bool
}

func videoPerVideoBillingDetailFromSubmit(c *gin.Context, info *relaycommon.RelayInfo, quota int) *videoPerVideoBillingDetail {
	if c == nil || info == nil {
		return nil
	}
	req, err := relaycommon.GetTaskRequest(c)
	if err != nil {
		return nil
	}
	modelName := strings.TrimSpace(info.OriginModelName)
	if modelName == "" {
		return nil
	}
	rules, ok := ratio_setting.GetChannelVideoPricingRules(info.ChannelId, modelName)
	if !ok || !ratio_setting.HasUsableVideoPerVideoRules(rules) {
		var globalOK bool
		rules, globalOK = ratio_setting.GetVideoPricingRules(modelName)
		if !globalOK || !ratio_setting.HasUsableVideoPerVideoRules(rules) {
			return nil
		}
	}
	width, height := videoDimensionsFromTaskRequest(req)
	hasAudio := taskRequestHasAudio(req)
	mode := detectVideoBillingModeFromSubmitRequest(c)
	match, ok := matchPerVideoPriceDetail(rules, mode, width, height, hasAudio)
	if !ok || match.PricePerVideo <= 0 {
		return nil
	}
	groupRatio := info.PriceData.GroupRatioInfo.GroupRatio
	if groupRatio <= 0 {
		groupRatio = 1
	}
	count := 1
	finalPricePerVideo := match.PricePerVideo * groupRatio * (resolveVideoLogChannelDiscountPercent(info) / 100)
	if common.QuotaPerUnit > 0 && quota > 0 {
		finalPricePerVideo = float64(quota) / common.QuotaPerUnit / float64(count)
	}
	return &videoPerVideoBillingDetail{
		Mode:                   mode,
		Count:                  count,
		Width:                  width,
		Height:                 height,
		HasAudio:               hasAudio,
		Resolution:             match.Resolution,
		RuleWidth:              match.RuleWidth,
		RuleHeight:             match.RuleHeight,
		PricePerVideo:          finalPricePerVideo,
		GroupRatio:             groupRatio,
		QuotaPerUnit:           common.QuotaPerUnit,
		ChannelDiscountPercent: resolveVideoLogChannelDiscountPercent(info),
		UnifiedAudio:           match.UnifiedAudio,
	}
}

func matchPerVideoPriceDetail(r ratio_setting.VideoPricingRules, mode string, width, height int, hasAudio bool) (*videoPerVideoPriceMatch, bool) {
	var rows []ratio_setting.VideoResolutionAudioPriceRule
	switch mode {
	case "image_to_video":
		rows = r.ImageToVideoPerItem
	case "video_to_video":
		rows = r.VideoToVideoPerItem
	default:
		rows = r.TextToVideoPerItem
	}
	if match, ok := matchPerSecondPriceDetail(ratio_setting.VideoPricingRules{
		TextToVideoPerSecond: rows,
	}, "text_to_video", width, height, hasAudio); ok {
		return &videoPerVideoPriceMatch{
			Resolution:    match.Resolution,
			RuleWidth:     match.RuleWidth,
			RuleHeight:    match.RuleHeight,
			PricePerVideo: match.PricePerSecond,
			UnifiedAudio:  match.UnifiedAudio,
		}, true
	}

	switch mode {
	case "image_to_video":
		return matchLegacyPerVideoRulesByPixelsDetail(width, height, r.ImageToVideoPerVideo)
	case "video_to_video":
		return matchLegacyVideoToVideoRulesByPixelsDetail(width, height, r.VideoToVideoInputPerVideo, r.VideoToVideoOutputPerVideo)
	default:
		return matchLegacyPerVideoRulesByPixelsDetail(width, height, r.TextToVideoPerVideo)
	}
}

func matchLegacyVideoToVideoRulesByPixelsDetail(width, height int, inputRows, outputRows []ratio_setting.VideoResolutionPerVideoRule) (*videoPerVideoPriceMatch, bool) {
	input, inputOK := matchLegacyPerVideoRulesByPixelsDetail(width, height, inputRows)
	output, outputOK := matchLegacyPerVideoRulesByPixelsDetail(width, height, outputRows)
	if !inputOK && !outputOK {
		return nil, false
	}
	if inputOK && outputOK {
		output.PricePerVideo += input.PricePerVideo
		return output, true
	}
	if outputOK {
		return output, true
	}
	return input, true
}

func matchLegacyPerVideoRulesByPixelsDetail(width, height int, rows []ratio_setting.VideoResolutionPerVideoRule) (*videoPerVideoPriceMatch, bool) {
	if len(rows) == 0 || width <= 0 || height <= 0 {
		return nil, false
	}
	targetPixels := width * height
	targetRatio := targetVideoResolutionRatio(width, height)
	best := -1
	minDiffRatio := math.MaxFloat64
	bestW, bestH := 0, 0
	for i, row := range rows {
		if row.VideoPrice <= 0 {
			continue
		}
		ruleW, ruleH, ok := parseVideoResolutionFlexibleForRatio(row.Resolution, targetRatio)
		if !ok || ruleW <= 0 || ruleH <= 0 {
			continue
		}
		rulePixels := ruleW * ruleH
		diffRatio := math.Abs(float64(targetPixels-rulePixels)) / float64(rulePixels)
		if diffRatio < minDiffRatio {
			minDiffRatio = diffRatio
			best = i
			bestW = ruleW
			bestH = ruleH
		}
	}
	if best < 0 {
		return nil, false
	}
	row := rows[best]
	return &videoPerVideoPriceMatch{
		Resolution:    row.Resolution,
		RuleWidth:     bestW,
		RuleHeight:    bestH,
		PricePerVideo: row.VideoPrice,
		UnifiedAudio:  true,
	}, true
}

func appendVideoPerVideoBillingDetailOther(c *gin.Context, other map[string]interface{}, info *relaycommon.RelayInfo) {
	if other == nil || info == nil {
		return
	}
	quota := info.PriceData.Quota
	if quota < 0 {
		quota = 0
	}
	videoCount := 1
	quotaPerUnit := common.QuotaPerUnit
	finalPricePerVideo := 0.0
	if quotaPerUnit > 0 && videoCount > 0 {
		finalPricePerVideo = float64(quota) / quotaPerUnit / float64(videoCount)
	}
	other["video_count"] = videoCount
	other["video_price_per_video"] = finalPricePerVideo
	other["video_quota_per_unit"] = quotaPerUnit
	other["channel_price_discount"] = resolveVideoLogChannelDiscountPercent(info)
	other["video_billed_quota"] = quota

	if detail := videoPerVideoBillingDetailFromSubmit(c, info, quota); detail != nil {
		other["video_count"] = detail.Count
		other["video_width"] = detail.Width
		other["video_height"] = detail.Height
		other["video_has_audio"] = detail.HasAudio
		other["video_resolution"] = detail.Resolution
		other["video_rule_width"] = detail.RuleWidth
		other["video_rule_height"] = detail.RuleHeight
		other["video_price_per_video"] = detail.PricePerVideo
		other["video_quota_per_unit"] = detail.QuotaPerUnit
		other["channel_price_discount"] = detail.ChannelDiscountPercent
		other["video_unified_audio_price"] = detail.UnifiedAudio
		return
	}

	if c == nil {
		return
	}
	req, err := relaycommon.GetTaskRequest(c)
	if err != nil {
		return
	}
	width, height := videoDimensionsFromTaskRequest(req)
	if width > 0 {
		other["video_width"] = width
	}
	if height > 0 {
		other["video_height"] = height
	}
	if duration := videoDurationFromTaskRequest(req); duration > 0 {
		other["video_seconds"] = duration
	}
	other["video_has_audio"] = taskRequestHasAudio(req)
}

func videoPerSecondBillingDetailOther(detail *videoPerSecondBillingDetail, quota int) map[string]interface{} {
	other := make(map[string]interface{})
	appendVideoPerSecondBillingDetailOther(other, detail, quota)
	return other
}

func audioLabel(hasAudio bool) string {
	if hasAudio {
		return "有音轨"
	}
	return "无音轨"
}

// taskModelName 从 BillingContext 或 Properties 中获取模型名称。
func taskModelName(task *model.Task) string {
	if bc := task.PrivateData.BillingContext; bc != nil && bc.OriginModelName != "" {
		return bc.OriginModelName
	}
	return task.Properties.OriginModelName
}

// RefundTaskQuota 统一的任务失败退款逻辑。
// 当异步任务失败时，将预扣的 quota 退还给用户（支持钱包和订阅），并退还令牌额度。
func RefundTaskQuota(ctx context.Context, task *model.Task, reason string) {
	quota := task.Quota
	if quota == 0 {
		return
	}

	// 1. 退还资金来源（钱包或订阅）
	if err := taskAdjustFunding(task, -quota); err != nil {
		logger.LogWarn(ctx, fmt.Sprintf("退还资金来源失败 task %s: %s", task.TaskID, err.Error()))
		return
	}

	// 2. 退还令牌额度
	taskAdjustTokenQuota(ctx, task, -quota)

	// 3. 记录日志
	other := taskBillingOther(task)
	other["task_id"] = task.TaskID
	other["reason"] = reason
	model.RecordTaskBillingLog(model.RecordTaskBillingLogParams{
		UserId:    task.UserId,
		LogType:   model.LogTypeRefund,
		Content:   "",
		ChannelId: task.ChannelId,
		ModelName: taskModelName(task),
		TokenName: task.PrivateData.TokenName,
		Quota:     quota,
		TokenId:   task.PrivateData.TokenId,
		Group:     task.Group,
		Other:     other,
	})
}

// RecalculateTaskQuota 通用的异步差额结算。
// actualQuota 是任务完成后的实际应扣额度，与预扣额度 (task.Quota) 做差额结算。
// reason 用于日志记录（例如 "token重算" 或 "adaptor调整"）。
func RecalculateTaskQuota(ctx context.Context, task *model.Task, actualQuota int, reason string, extraOther ...map[string]interface{}) {
	if actualQuota <= 0 {
		return
	}
	preConsumedQuota := task.Quota
	quotaDelta := actualQuota - preConsumedQuota

	if quotaDelta == 0 {
		logger.LogInfo(ctx, fmt.Sprintf("任务 %s 预扣费准确（%s，%s）",
			task.TaskID, logger.LogQuota(actualQuota), reason))
		return
	}

	logger.LogInfo(ctx, fmt.Sprintf("任务 %s 差额结算：delta=%s（实际：%s，预扣：%s，%s）",
		task.TaskID,
		logger.LogQuota(quotaDelta),
		logger.LogQuota(actualQuota),
		logger.LogQuota(preConsumedQuota),
		reason,
	))

	// 调整资金来源
	if err := taskAdjustFunding(task, quotaDelta); err != nil {
		logger.LogError(ctx, fmt.Sprintf("差额结算资金调整失败 task %s: %s", task.TaskID, err.Error()))
		return
	}

	// 调整令牌额度
	taskAdjustTokenQuota(ctx, task, quotaDelta)

	task.Quota = actualQuota
	if task.ID > 0 {
		if err := model.DB.Model(&model.Task{}).Where("id = ?", task.ID).Update("quota", actualQuota).Error; err != nil {
			logger.LogWarn(ctx, fmt.Sprintf("更新任务实际计费额度失败 task %s: %s", task.TaskID, err.Error()))
		}
	}

	var logType int
	var logQuota int
	if quotaDelta > 0 {
		logType = model.LogTypeConsume
		logQuota = quotaDelta
		model.UpdateUserUsedQuotaAndRequestCount(task.UserId, quotaDelta)
		model.UpdateChannelUsedQuota(task.ChannelId, quotaDelta)
	} else {
		logType = model.LogTypeRefund
		logQuota = -quotaDelta
	}
	other := taskBillingOther(task)
	other["task_id"] = task.TaskID
	//other["reason"] = reason
	other["pre_consumed_quota"] = preConsumedQuota
	other["actual_quota"] = actualQuota
	for _, extra := range extraOther {
		if extra == nil {
			continue
		}
		for k, v := range extra {
			if k == profitShareExtraTotalTokensKey {
				continue
			}
			other[k] = v
		}
	}
	model.RecordTaskBillingLog(model.RecordTaskBillingLogParams{
		UserId:    task.UserId,
		LogType:   logType,
		Content:   reason,
		ChannelId: task.ChannelId,
		ModelName: taskModelName(task),
		TokenName: task.PrivateData.TokenName,
		Quota:     logQuota,
		TokenId:   task.PrivateData.TokenId,
		Group:     task.Group,
		Other:     other,
	})
}

// RecalculateTaskQuotaByTokens 根据实际 token 消耗重新计费（异步差额结算）。
// 当任务成功且返回了 totalTokens 时，根据模型倍率和分组倍率重新计算实际扣费额度，
// 与预扣费的差额进行补扣或退还。支持钱包和订阅计费来源。
func RecalculateTaskQuotaByTokens(ctx context.Context, task *model.Task, totalTokens int) bool {
	if totalTokens <= 0 {
		return false
	}

	modelName := taskModelName(task)

	// 获取模型价格和倍率
	modelRatio, hasRatioSetting, _ := ratio_setting.GetModelRatio(modelName)
	// 只有配置了倍率(非固定价格)时才按 token 重新计费
	if !hasRatioSetting || modelRatio <= 0 {
		return false
	}

	// 获取用户和组的倍率信息
	group := task.Group
	if group == "" {
		user, err := model.GetUserById(task.UserId, false)
		if err == nil {
			group = user.Group
		}
	}
	if group == "" {
		return false
	}

	groupRatio := ratio_setting.GetGroupRatio(group)
	userGroupRatio, hasUserGroupRatio := ratio_setting.GetGroupGroupRatio(group, group)

	var finalGroupRatio float64
	if hasUserGroupRatio {
		finalGroupRatio = userGroupRatio
	} else {
		finalGroupRatio = groupRatio
	}

	costDisc := model.ResolveChannelPriceDiscountPercent(task.ChannelId)
	markupDisc := model.ResolveEffectiveMarkupDiscountPercentForInviteeBilling(task.UserId, task.ChannelId, modelName)
	globalMr, globalOK, _ := ratio_setting.GetModelRatio(modelName)
	if !globalOK {
		globalMr = 0
	}
	effRate := model.EffectiveInputRate(modelRatio, globalMr, costDisc, markupDisc)
	actualQuota := int(math.Round(float64(totalTokens) * effRate * finalGroupRatio))

	reason := fmt.Sprintf("token重算：tokens=%d, modelRatio=%.2f, groupRatio=%.2f, channelId=%d", totalTokens, modelRatio, finalGroupRatio, task.ChannelId)
	RecalculateTaskQuota(ctx, task, actualQuota, reason, map[string]interface{}{
		profitShareExtraTotalTokensKey: totalTokens,
	})
	return true
}
