/*
Copyright (C) 2025 QuantumNous

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.

For commercial licensing, please contact support@quantumnous.com
*/

import i18next from 'i18next';
import {
  resolveConsumeLogBillingRates,
  formatBillingUsdDisplay,
} from './billingFormula';
import { formatVideoResolutionDisplayLabel } from './videoResolutionLabel';
import { Modal, Tag, Typography, Avatar } from '@douyinfe/semi-ui';
import { copy, showSuccess } from './utils';
import { MOBILE_BREAKPOINT } from '../hooks/common/useIsMobile';
import { visit } from 'unist-util-visit';
import * as LobeIcons from '@lobehub/icons';
import {
  OpenAI,
  Claude,
  Gemini,
  Moonshot,
  Zhipu,
  Qwen,
  DeepSeek,
  Minimax,
  Wenxin,
  Spark,
  Midjourney,
  Hunyuan,
  Cohere,
  Cloudflare,
  Ai360,
  Yi,
  Jina,
  Mistral,
  XAI,
  Ollama,
  Doubao,
  Suno,
  Xinference,
  OpenRouter,
  Dify,
  Coze,
  SiliconCloud,
  FastGPT,
  Kling,
  Jimeng,
  Perplexity,
  Replicate,
} from '@lobehub/icons';

import {
  LayoutDashboard,
  TerminalSquare,
  MessageSquare,
  Key,
  BarChart3,
  Image as ImageIcon,
  CheckSquare,
  CreditCard,
  Layers,
  Gift,
  User,
  Settings,
  CircleUser,
  Package,
  Server,
  CalendarClock,
  Store,
  Share2,
  Handshake,
  UserPlus,
  ClipboardList,
  DollarSign,
  Home,
  BookOpen,
  Info,
  LayoutGrid,
} from 'lucide-react';
import {
  SiAtlassian,
  SiAuth0,
  SiAuthentik,
  SiBitbucket,
  SiDiscord,
  SiDropbox,
  SiFacebook,
  SiGitea,
  SiGithub,
  SiGitlab,
  SiGoogle,
  SiKeycloak,
  SiNextcloud,
  SiNotion,
  SiOkta,
  SiOpenid,
  SiReddit,
  SiSlack,
  SiTelegram,
  SiTwitch,
  SiWechat,
  SiX,
} from 'react-icons/si';

// 获取侧边栏Lucide图标组件
export function getLucideIcon(key, selected = false) {
  const size = 16;
  const strokeWidth = 2;
  const SELECTED_COLOR = 'var(--semi-color-primary)';
  const iconColor = selected ? SELECTED_COLOR : 'currentColor';
  const commonProps = {
    size,
    strokeWidth,
    className: `transition-colors duration-200 ${selected ? 'transition-transform duration-200 scale-105' : ''}`,
  };

  // 根据不同的key返回不同的图标
  switch (key) {
    case 'home':
      return <Home {...commonProps} color={iconColor} />;
    case 'pricing':
      return <LayoutGrid {...commonProps} color={iconColor} />;
    case 'docs':
      return <BookOpen {...commonProps} color={iconColor} />;
    case 'about':
      return <Info {...commonProps} color={iconColor} />;
    case 'detail':
      return <LayoutDashboard {...commonProps} color={iconColor} />;
    case 'playground':
      return <TerminalSquare {...commonProps} color={iconColor} />;
    case 'chat':
      return <MessageSquare {...commonProps} color={iconColor} />;
    case 'token':
      return <Key {...commonProps} color={iconColor} />;
    case 'log':
      return <BarChart3 {...commonProps} color={iconColor} />;
    case 'midjourney':
      return <ImageIcon {...commonProps} color={iconColor} />;
    case 'task':
      return <CheckSquare {...commonProps} color={iconColor} />;
    case 'topup':
      return <CreditCard {...commonProps} color={iconColor} />;
    case 'channel':
      return <Layers {...commonProps} color={iconColor} />;
    case 'redemption':
      return <Gift {...commonProps} color={iconColor} />;
    case 'user':
    case 'personal':
      return <User {...commonProps} color={iconColor} />;
    case 'models':
      return <Package {...commonProps} color={iconColor} />;
    case 'deployment':
      return <Server {...commonProps} color={iconColor} />;
    case 'subscription':
      return <CalendarClock {...commonProps} color={iconColor} />;
    case 'setting':
      return <Settings {...commonProps} color={iconColor} />;
    case 'supplier':
      return <Store {...commonProps} color={iconColor} />;
    case 'distributor_center':
      return <Share2 {...commonProps} color={iconColor} />;
    case 'distributor':
      return <Handshake {...commonProps} color={iconColor} />;
    case 'supplier-apply':
      return <UserPlus {...commonProps} color={iconColor} />;
    case 'supplier-channel':
      return <Layers {...commonProps} color={iconColor} />;
    case 'supplier-management':
      return <Store {...commonProps} color={iconColor} />;
    case 'supplier-pricing-settings':
      return <DollarSign {...commonProps} color={iconColor} />;
    case 'supplier-application-approval':
      return <ClipboardList {...commonProps} color={iconColor} />;
    case 'supplier-list':
      return <Store {...commonProps} color={iconColor} />;
    default:
      return <CircleUser {...commonProps} color={iconColor} />;
  }
}

// 获取模型分类
export const getModelCategories = (() => {
  let categoriesCache = null;
  let lastLocale = null;

  return (t) => {
    const currentLocale = i18next.language;
    if (categoriesCache && lastLocale === currentLocale) {
      return categoriesCache;
    }

    categoriesCache = {
      all: {
        label: t('全部模型'),
        icon: null,
        filter: () => true,
      },
      openai: {
        label: 'OpenAI',
        icon: <OpenAI />,
        filter: (model) =>
          model.model_name.toLowerCase().includes('gpt') ||
          model.model_name.toLowerCase().includes('dall-e') ||
          model.model_name.toLowerCase().includes('whisper') ||
          model.model_name.toLowerCase().includes('tts-1') ||
          model.model_name.toLowerCase().includes('text-embedding-3') ||
          model.model_name.toLowerCase().includes('text-moderation') ||
          model.model_name.toLowerCase().includes('babbage') ||
          model.model_name.toLowerCase().includes('davinci') ||
          model.model_name.toLowerCase().includes('curie') ||
          model.model_name.toLowerCase().includes('ada') ||
          model.model_name.toLowerCase().includes('o1') ||
          model.model_name.toLowerCase().includes('o3') ||
          model.model_name.toLowerCase().includes('o4'),
      },
      anthropic: {
        label: 'Anthropic',
        icon: <Claude.Color />,
        filter: (model) => model.model_name.toLowerCase().includes('claude'),
      },
      gemini: {
        label: 'Gemini',
        icon: <Gemini.Color />,
        filter: (model) =>
          model.model_name.toLowerCase().includes('gemini') ||
          model.model_name.toLowerCase().includes('gemma') ||
          model.model_name.toLowerCase().includes('learnlm') ||
          model.model_name.toLowerCase().startsWith('embedding-') ||
          model.model_name.toLowerCase().includes('text-embedding-004') ||
          model.model_name.toLowerCase().includes('imagen-4') ||
          model.model_name.toLowerCase().includes('veo-') ||
          model.model_name.toLowerCase().includes('aqa'),
      },
      moonshot: {
        label: 'Moonshot',
        icon: <Moonshot />,
        filter: (model) =>
          model.model_name.toLowerCase().includes('moonshot') ||
          model.model_name.toLowerCase().includes('kimi'),
      },
      zhipu: {
        label: t('智谱'),
        icon: <Zhipu.Color />,
        filter: (model) =>
          model.model_name.toLowerCase().includes('chatglm') ||
          model.model_name.toLowerCase().includes('glm-') ||
          model.model_name.toLowerCase().includes('cogview') ||
          model.model_name.toLowerCase().includes('cogvideo'),
      },
      qwen: {
        label: t('通义千问'),
        icon: <Qwen.Color />,
        filter: (model) => model.model_name.toLowerCase().includes('qwen'),
      },
      deepseek: {
        label: 'DeepSeek',
        icon: <DeepSeek.Color />,
        filter: (model) => model.model_name.toLowerCase().includes('deepseek'),
      },
      minimax: {
        label: 'MiniMax',
        icon: <Minimax.Color />,
        filter: (model) =>
          model.model_name.toLowerCase().includes('abab') ||
          model.model_name.toLowerCase().includes('minimax'),
      },
      baidu: {
        label: t('文心一言'),
        icon: <Wenxin.Color />,
        filter: (model) => model.model_name.toLowerCase().includes('ernie'),
      },
      xunfei: {
        label: t('讯飞星火'),
        icon: <Spark.Color />,
        filter: (model) => model.model_name.toLowerCase().includes('spark'),
      },
      midjourney: {
        label: 'Midjourney',
        icon: <Midjourney />,
        filter: (model) => model.model_name.toLowerCase().includes('mj_'),
      },
      tencent: {
        label: t('腾讯混元'),
        icon: <Hunyuan.Color />,
        filter: (model) => model.model_name.toLowerCase().includes('hunyuan'),
      },
      cohere: {
        label: 'Cohere',
        icon: <Cohere.Color />,
        filter: (model) =>
          model.model_name.toLowerCase().includes('command') ||
          model.model_name.toLowerCase().includes('c4ai-') ||
          model.model_name.toLowerCase().includes('embed-'),
      },
      cloudflare: {
        label: 'Cloudflare',
        icon: <Cloudflare.Color />,
        filter: (model) => model.model_name.toLowerCase().includes('@cf/'),
      },
      ai360: {
        label: t('360智脑'),
        icon: <Ai360.Color />,
        filter: (model) => model.model_name.toLowerCase().includes('360'),
      },
      jina: {
        label: 'Jina',
        icon: <Jina />,
        filter: (model) => model.model_name.toLowerCase().includes('jina'),
      },
      mistral: {
        label: 'Mistral AI',
        icon: <Mistral.Color />,
        filter: (model) =>
          model.model_name.toLowerCase().includes('mistral') ||
          model.model_name.toLowerCase().includes('codestral') ||
          model.model_name.toLowerCase().includes('pixtral') ||
          model.model_name.toLowerCase().includes('voxtral') ||
          model.model_name.toLowerCase().includes('magistral'),
      },
      xai: {
        label: 'xAI',
        icon: <XAI />,
        filter: (model) => model.model_name.toLowerCase().includes('grok'),
      },
      llama: {
        label: 'Llama',
        icon: <Ollama />,
        filter: (model) => model.model_name.toLowerCase().includes('llama'),
      },
      doubao: {
        label: t('豆包'),
        icon: <Doubao.Color />,
        filter: (model) => model.model_name.toLowerCase().includes('doubao'),
      },
      yi: {
        label: t('零一万物'),
        icon: <Yi.Color />,
        filter: (model) => model.model_name.toLowerCase().includes('yi'),
      },
    };

    lastLocale = currentLocale;
    return categoriesCache;
  };
})();

/**
 * 根据渠道类型返回对应的厂商图标
 * @param {number} channelType - 渠道类型值
 * @returns {JSX.Element|null} - 对应的厂商图标组件
 */
export function getChannelIcon(channelType) {
  const iconSize = 14;

  switch (channelType) {
    case 1: // OpenAI
    case 3: // Azure OpenAI
    case 57: // Codex
      return <OpenAI size={iconSize} />;
    case 2: // Midjourney Proxy
    case 5: // Midjourney Proxy Plus
      return <Midjourney size={iconSize} />;
    case 36: // Suno API
      return <Suno size={iconSize} />;
    case 4: // Ollama
      return <Ollama size={iconSize} />;
    case 14: // Anthropic Claude
    case 33: // AWS Claude
      return <Claude.Color size={iconSize} />;
    case 41: // Vertex AI
      return <Gemini.Color size={iconSize} />;
    case 34: // Cohere
      return <Cohere.Color size={iconSize} />;
    case 39: // Cloudflare
      return <Cloudflare.Color size={iconSize} />;
    case 43: // DeepSeek
      return <DeepSeek.Color size={iconSize} />;
    case 15: // 百度文心千帆
    case 46: // 百度文心千帆V2
      return <Wenxin.Color size={iconSize} />;
    case 17: // 阿里通义千问
      return <Qwen.Color size={iconSize} />;
    case 18: // 讯飞星火认知
      return <Spark.Color size={iconSize} />;
    case 16: // 智谱 ChatGLM
    case 26: // 智谱 GLM-4V
      return <Zhipu.Color size={iconSize} />;
    case 24: // Google Gemini
    case 11: // Google PaLM2
      return <Gemini.Color size={iconSize} />;
    case 47: // Xinference
      return <Xinference.Color size={iconSize} />;
    case 25: // Moonshot
      return <Moonshot size={iconSize} />;
    case 27: // Perplexity
      return <Perplexity.Color size={iconSize} />;
    case 20: // OpenRouter
      return <OpenRouter size={iconSize} />;
    case 19: // 360 智脑
      return <Ai360.Color size={iconSize} />;
    case 23: // 腾讯混元
      return <Hunyuan.Color size={iconSize} />;
    case 31: // 零一万物
      return <Yi.Color size={iconSize} />;
    case 35: // MiniMax
      return <Minimax.Color size={iconSize} />;
    case 37: // Dify
      return <Dify.Color size={iconSize} />;
    case 38: // Jina
      return <Jina size={iconSize} />;
    case 40: // SiliconCloud
      return <SiliconCloud.Color size={iconSize} />;
    case 42: // Mistral AI
      return <Mistral.Color size={iconSize} />;
    case 45: // 字节火山方舟、豆包通用
      return <Doubao.Color size={iconSize} />;
    case 48: // xAI
      return <XAI size={iconSize} />;
    case 49: // Coze
      return <Coze size={iconSize} />;
    case 50: // 可灵 Kling
      return <Kling.Color size={iconSize} />;
    case 51: // 即梦 Jimeng
      return <Jimeng.Color size={iconSize} />;
    case 54: // 豆包视频 Doubao Video
      return <Doubao.Color size={iconSize} />;
    case 56: // Replicate
      return <Replicate size={iconSize} />;
    case 59: // TokenFactoryOpen
      return <Server size={iconSize} strokeWidth={2} />;
    case 8: // 自定义渠道
    case 22: // 知识库：FastGPT
      return <FastGPT.Color size={iconSize} />;
    case 21: // 知识库：AI Proxy
    case 44: // 嵌入模型：MokaAI M3E
    default:
      return null; // 未知类型或自定义渠道不显示图标
  }
}

/**
 * 根据图标名称动态获取 LobeHub 图标组件
 * 支持：
 * - 基础："OpenAI"、"OpenAI.Color" 等
 * - 额外属性（点号链式）："OpenAI.Avatar.type={'platform'}"、"OpenRouter.Avatar.shape={'square'}"
 * - 继续兼容第二参数 size；若字符串里有 size=，以字符串为准
 * @param {string} iconName - 图标名称/描述
 * @param {number} size - 图标大小，默认为 14
 * @returns {JSX.Element} - 对应的图标组件或 Avatar
 */
export function getLobeHubIcon(iconName, size = 14) {
  if (typeof iconName === 'string') iconName = iconName.trim();
  // 如果没有图标名称，返回 Avatar
  if (!iconName) {
    return <Avatar size='extra-extra-small'>?</Avatar>;
  }

  // 解析组件路径与点号链式属性
  const segments = String(iconName).split('.');
  const baseKey = segments[0];
  const BaseIcon = LobeIcons[baseKey];

  let IconComponent = undefined;
  let propStartIndex = 1;

  if (BaseIcon && segments.length > 1 && BaseIcon[segments[1]]) {
    IconComponent = BaseIcon[segments[1]];
    propStartIndex = 2;
  } else {
    IconComponent = LobeIcons[baseKey];
    propStartIndex = 1;
  }

  // 失败兜底
  if (
    !IconComponent ||
    (typeof IconComponent !== 'function' && typeof IconComponent !== 'object')
  ) {
    const firstLetter = String(iconName).charAt(0).toUpperCase();
    return <Avatar size='extra-extra-small'>{firstLetter}</Avatar>;
  }

  // 解析点号链式属性，形如：key={...}、key='...'、key="..."、key=123、key、key=true/false
  const props = {};

  const parseValue = (raw) => {
    if (raw == null) return true;
    let v = String(raw).trim();
    // 去除一层花括号包裹
    if (v.startsWith('{') && v.endsWith('}')) {
      v = v.slice(1, -1).trim();
    }
    // 去除引号
    if (
      (v.startsWith('"') && v.endsWith('"')) ||
      (v.startsWith("'") && v.endsWith("'"))
    ) {
      return v.slice(1, -1);
    }
    // 布尔
    if (v === 'true') return true;
    if (v === 'false') return false;
    // 数字
    if (/^-?\d+(?:\.\d+)?$/.test(v)) return Number(v);
    // 其他原样返回字符串
    return v;
  };

  for (let i = propStartIndex; i < segments.length; i++) {
    const seg = segments[i];
    if (!seg) continue;
    const eqIdx = seg.indexOf('=');
    if (eqIdx === -1) {
      const key = seg.trim();
      // Avoid leaking unresolved subcomponent names like "Color" to DOM props.
      if (/^[A-Z]/.test(key)) continue;
      props[key] = true;
      continue;
    }
    const key = seg.slice(0, eqIdx).trim();
    const valRaw = seg.slice(eqIdx + 1).trim();
    props[key] = parseValue(valRaw);
  }

  // 兼容第二参数 size，若字符串中未显式指定 size，则使用函数入参
  if (props.size == null && size != null) props.size = size;

  return <IconComponent {...props} />;
}

/** react-icons 已移除 SiLinkedin（Simple Icons 品牌策略），用同形 SVG 保持 OAuth 展示一致 */
function OAuthLinkedinIcon({ size = 24 }) {
  const s = Number(size) > 0 ? Number(size) : 24;
  return (
    <svg
      xmlns='http://www.w3.org/2000/svg'
      viewBox='0 0 24 24'
      width={s}
      height={s}
      fill='currentColor'
      aria-hidden
    >
      <path d='M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z' />
    </svg>
  );
}

const oauthProviderIconMap = {
  github: SiGithub,
  gitlab: SiGitlab,
  gitea: SiGitea,
  google: SiGoogle,
  discord: SiDiscord,
  facebook: SiFacebook,
  linkedin: OAuthLinkedinIcon,
  x: SiX,
  twitter: SiX,
  slack: SiSlack,
  telegram: SiTelegram,
  wechat: SiWechat,
  keycloak: SiKeycloak,
  nextcloud: SiNextcloud,
  authentik: SiAuthentik,
  openid: SiOpenid,
  okta: SiOkta,
  auth0: SiAuth0,
  atlassian: SiAtlassian,
  bitbucket: SiBitbucket,
  notion: SiNotion,
  twitch: SiTwitch,
  reddit: SiReddit,
  dropbox: SiDropbox,
};

function isHttpUrl(value) {
  return /^https?:\/\//i.test(value || '');
}

function isSimpleEmoji(value) {
  if (!value) return false;
  const trimmed = String(value).trim();
  return trimmed.length > 0 && trimmed.length <= 4 && !isHttpUrl(trimmed);
}

function normalizeOAuthIconKey(raw) {
  return raw
    .trim()
    .toLowerCase()
    .replace(/^ri:/, '')
    .replace(/^react-icons:/, '')
    .replace(/^si:/, '');
}

/**
 * Render custom OAuth provider icon with react-icons or URL/emoji fallback.
 * Supported formats:
 * - react-icons simple key: github / gitlab / google / keycloak
 * - prefixed key: ri:github / si:github
 * - full URL image: https://example.com/logo.png
 * - emoji: 🐱
 */
export function getOAuthProviderIcon(iconName, size = 20) {
  const raw = String(iconName || '').trim();
  const iconSize = Number(size) > 0 ? Number(size) : 20;

  if (!raw) {
    return <Layers size={iconSize} color='var(--semi-color-text-2)' />;
  }

  if (isHttpUrl(raw)) {
    return (
      <img
        src={raw}
        alt='provider icon'
        width={iconSize}
        height={iconSize}
        style={{ borderRadius: 4, objectFit: 'cover' }}
      />
    );
  }

  if (isSimpleEmoji(raw)) {
    return (
      <span
        style={{
          width: iconSize,
          height: iconSize,
          lineHeight: `${iconSize}px`,
          textAlign: 'center',
          display: 'inline-block',
          fontSize: Math.max(Math.floor(iconSize * 0.8), 14),
        }}
      >
        {raw}
      </span>
    );
  }

  const key = normalizeOAuthIconKey(raw);
  const IconComp = oauthProviderIconMap[key];
  if (IconComp) {
    return <IconComp size={iconSize} />;
  }

  return (
    <Avatar size='extra-extra-small'>{raw.charAt(0).toUpperCase()}</Avatar>
  );
}

// 颜色列表
const colors = [
  'amber',
  'blue',
  'cyan',
  'green',
  'grey',
  'indigo',
  'light-blue',
  'lime',
  'orange',
  'pink',
  'purple',
  'red',
  'teal',
  'violet',
  'yellow',
];

// 基础10色色板 (N ≤ 10)
const baseColors = [
  '#1664FF', // 主色
  '#1AC6FF',
  '#FF8A00',
  '#3CC780',
  '#7442D4',
  '#FFC400',
  '#304D77',
  '#B48DEB',
  '#009488',
  '#FF7DDA',
];

// 扩展20色色板 (10 < N ≤ 20)
const extendedColors = [
  '#1664FF',
  '#B2CFFF',
  '#1AC6FF',
  '#94EFFF',
  '#FF8A00',
  '#FFCE7A',
  '#3CC780',
  '#B9EDCD',
  '#7442D4',
  '#DDC5FA',
  '#FFC400',
  '#FAE878',
  '#304D77',
  '#8B959E',
  '#B48DEB',
  '#EFE3FF',
  '#009488',
  '#59BAA8',
  '#FF7DDA',
  '#FFCFEE',
];

// 模型颜色映射
export const modelColorMap = {
  'dall-e': 'rgb(147,112,219)', // 深紫色
  // 'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
  'dall-e-3': 'rgb(153,50,204)', // 介于紫罗兰和洋红之间的色调
  'gpt-3.5-turbo': 'rgb(184,227,167)', // 浅绿色
  // 'gpt-3.5-turbo-0301': 'rgb(131,220,131)', // 亮绿色
  'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿
  'gpt-3.5-turbo-1106': 'rgb(32,178,170)', // 浅海洋绿
  'gpt-3.5-turbo-16k': 'rgb(149,252,206)', // 淡橙色
  'gpt-3.5-turbo-16k-0613': 'rgb(119,255,214)', // 淡桃
  'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', // 粉蓝色
  'gpt-4': 'rgb(135,206,235)', // 天蓝色
  // 'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色
  'gpt-4-0613': 'rgb(100,149,237)', // 矢车菊蓝
  'gpt-4-1106-preview': 'rgb(30,144,255)', // 道奇蓝
  'gpt-4-0125-preview': 'rgb(2,177,236)', // 深天蓝
  'gpt-4-turbo-preview': 'rgb(2,177,255)', // 深天蓝
  'gpt-4-32k': 'rgb(104,111,238)', // 中紫色
  // 'gpt-4-32k-0314': 'rgb(90,105,205)', // 暗灰蓝色
  'gpt-4-32k-0613': 'rgb(61,71,139)', // 暗蓝灰色
  'gpt-4-all': 'rgb(65,105,225)', // 皇家蓝
  'gpt-4-gizmo-*': 'rgb(0,0,255)', // 纯蓝色
  'gpt-4-vision-preview': 'rgb(25,25,112)', // 午夜蓝
  'text-ada-001': 'rgb(255,192,203)', // 粉红色
  'text-babbage-001': 'rgb(255,160,122)', // 浅珊瑚色
  'text-curie-001': 'rgb(219,112,147)', // 苍紫罗兰色
  // 'text-davinci-002': 'rgb(199,21,133)', // 中紫罗兰红色
  'text-davinci-003': 'rgb(219,112,147)', // 苍紫罗兰色（与Curie相同，表示同一个系列）
  'text-davinci-edit-001': 'rgb(255,105,180)', // 热粉色
  'text-embedding-ada-002': 'rgb(255,182,193)', // 浅粉红
  'text-embedding-v1': 'rgb(255,174,185)', // 浅粉红色（略有区别）
  'text-moderation-latest': 'rgb(255,130,171)', // 强粉色
  'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色（与Babbage相同，表示同一类功能）
  'tts-1': 'rgb(255,140,0)', // 深橙色
  'tts-1-1106': 'rgb(255,165,0)', // 橙色
  'tts-1-hd': 'rgb(255,215,0)', // 金色
  'tts-1-hd-1106': 'rgb(255,223,0)', // 金黄色（略有区别）
  'whisper-1': 'rgb(245,245,220)', // 米色
  'claude-3-opus-20240229': 'rgb(255,132,31)', // 橙红色
  'claude-3-sonnet-20240229': 'rgb(253,135,93)', // 橙色
  'claude-3-haiku-20240307': 'rgb(255,175,146)', // 浅橙色
};

export function modelToColor(modelName) {
  // 1. 如果模型在预定义的 modelColorMap 中，使用预定义颜色
  if (modelColorMap[modelName]) {
    return modelColorMap[modelName];
  }

  // 2. 生成一个稳定的数字作为索引
  let hash = 0;
  for (let i = 0; i < modelName.length; i++) {
    hash = (hash << 5) - hash + modelName.charCodeAt(i);
    hash = hash & hash; // Convert to 32-bit integer
  }
  hash = Math.abs(hash);

  // 3. 根据模型名称长度选择不同的色板
  const colorPalette = modelName.length > 10 ? extendedColors : baseColors;

  // 4. 使用hash值选择颜色
  const index = hash % colorPalette.length;
  return colorPalette[index];
}

export function stringToColor(str) {
  let sum = 0;
  for (let i = 0; i < str.length; i++) {
    sum += str.charCodeAt(i);
  }
  let i = sum % colors.length;
  return colors[i];
}

// 渲染带有模型图标的标签
export function renderModelTag(modelName, options = {}) {
  const {
    color,
    size = 'default',
    shape = 'circle',
    onClick,
    suffixIcon,
  } = options;

  const categories = getModelCategories(i18next.t);
  let icon = null;

  for (const [key, category] of Object.entries(categories)) {
    if (key !== 'all' && category.filter({ model_name: modelName })) {
      icon = category.icon;
      break;
    }
  }

  return (
    <Tag
      color={color || stringToColor(modelName)}
      prefixIcon={icon}
      suffixIcon={suffixIcon}
      size={size}
      shape={shape}
      onClick={onClick}
    >
      {modelName}
    </Tag>
  );
}

export function renderText(text, limit) {
  if (text.length > limit) {
    return text.slice(0, limit - 3) + '...';
  }
  return text;
}

/**
 * Render group tags based on the input group string
 * @param {string} group - The input group string
 * @returns {JSX.Element} - The rendered group tags
 */
export function renderGroup(group) {
  if (group === '') {
    return (
      <Tag key='default' color='white' shape='circle'>
        {i18next.t('用户分组')}
      </Tag>
    );
  }

  const tagColors = {
    vip: 'yellow',
    pro: 'yellow',
    svip: 'red',
    premium: 'red',
  };

  const groups = group.split(',').sort();

  return (
    <span key={group}>
      {groups.map((group) => (
        <Tag
          color={tagColors[group] || stringToColor(group)}
          key={group}
          shape='circle'
          onClick={async (event) => {
            event.stopPropagation();
            if (await copy(group)) {
              showSuccess(i18next.t('已复制：') + group);
            } else {
              Modal.error({
                title: i18next.t('无法复制到剪贴板，请手动复制'),
                content: group,
              });
            }
          }}
        >
          {group}
        </Tag>
      ))}
    </span>
  );
}

export function renderRatio(ratio) {
  let color = 'green';
  if (ratio > 5) {
    color = 'red';
  } else if (ratio > 3) {
    color = 'orange';
  } else if (ratio > 1) {
    color = 'blue';
  }
  return (
    <Tag color={color}>
      {ratio}x {i18next.t('倍率')}
    </Tag>
  );
}

const measureTextWidth = (
  text,
  style = {
    fontSize: '14px',
    fontFamily:
      '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
  },
  containerWidth,
) => {
  const span = document.createElement('span');

  span.style.visibility = 'hidden';
  span.style.position = 'absolute';
  span.style.whiteSpace = 'nowrap';
  span.style.fontSize = style.fontSize;
  span.style.fontFamily = style.fontFamily;

  span.textContent = text;

  document.body.appendChild(span);
  const width = span.offsetWidth;

  document.body.removeChild(span);

  return width;
};

export function truncateText(text, maxWidth = 200) {
  const isMobileScreen = window.matchMedia(
    `(max-width: ${MOBILE_BREAKPOINT - 1}px)`,
  ).matches;
  if (!isMobileScreen) {
    return text;
  }
  if (!text) return text;

  try {
    // Handle percentage-based maxWidth
    let actualMaxWidth = maxWidth;
    if (typeof maxWidth === 'string' && maxWidth.endsWith('%')) {
      const percentage = parseFloat(maxWidth) / 100;
      // Use window width as fallback container width
      actualMaxWidth = window.innerWidth * percentage;
    }

    const width = measureTextWidth(text);
    if (width <= actualMaxWidth) return text;

    let left = 0;
    let right = text.length;
    let result = text;

    while (left <= right) {
      const mid = Math.floor((left + right) / 2);
      const truncated = text.slice(0, mid) + '...';
      const currentWidth = measureTextWidth(truncated);

      if (currentWidth <= actualMaxWidth) {
        result = truncated;
        left = mid + 1;
      } else {
        right = mid - 1;
      }
    }

    return result;
  } catch (error) {
    console.warn(
      'Text measurement failed, falling back to character count',
      error,
    );
    if (text.length > 20) {
      return text.slice(0, 17) + '...';
    }
    return text;
  }
}

export const renderGroupOption = (item) => {
  const {
    disabled,
    selected,
    label,
    value,
    focused,
    className,
    style,
    onMouseEnter,
    onClick,
    empty,
    emptyContent,
    ...rest
  } = item;

  const baseStyle = {
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: '8px 16px',
    cursor: disabled ? 'not-allowed' : 'pointer',
    backgroundColor: focused ? 'var(--semi-color-fill-0)' : 'transparent',
    opacity: disabled ? 0.5 : 1,
    ...(selected && {
      backgroundColor: 'var(--semi-color-primary-light-default)',
    }),
    '&:hover': {
      backgroundColor: !disabled && 'var(--semi-color-fill-1)',
    },
  };

  const handleClick = () => {
    if (!disabled && onClick) {
      onClick();
    }
  };

  const handleMouseEnter = (e) => {
    if (!disabled && onMouseEnter) {
      onMouseEnter(e);
    }
  };

  return (
    <div
      style={baseStyle}
      onClick={handleClick}
      onMouseEnter={handleMouseEnter}
    >
      <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
        <Typography.Text strong type={disabled ? 'tertiary' : undefined}>
          {value}
        </Typography.Text>
        <Typography.Text type='secondary' size='small'>
          {label}
        </Typography.Text>
      </div>
      {item.ratio && renderRatio(item.ratio)}
    </div>
  );
};

export function renderNumber(num) {
  if (num >= 1000000000) {
    return (num / 1000000000).toFixed(1) + 'B';
  } else if (num >= 1000000) {
    return (num / 1000000).toFixed(1) + 'M';
  } else if (num >= 10000) {
    return (num / 1000).toFixed(1) + 'k';
  } else {
    return num;
  }
}

export function renderQuotaNumberWithDigit(num, digits = 2) {
  if (typeof num !== 'number' || isNaN(num)) {
    return 0;
  }
  const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
  num = parseFloat(num.toFixed(digits));
  const trimmed = trimFixedDecimalDisplay(num, digits);
  if (quotaDisplayType === 'CNY') {
    return '¥' + trimmed;
  } else if (quotaDisplayType === 'USD') {
    return '$' + trimmed;
  } else if (quotaDisplayType === 'CUSTOM') {
    const statusStr = localStorage.getItem('status');
    let symbol = '¤';
    try {
      if (statusStr) {
        const s = JSON.parse(statusStr);
        symbol = s?.custom_currency_symbol || symbol;
      }
    } catch (e) {}
    return symbol + trimmed;
  } else {
    return trimmed;
  }
}

export function renderNumberWithPoint(num) {
  if (num === undefined) return '';
  num = parseFloat(num.toFixed(2));
  if (num >= 100000) {
    // Convert number to string to manipulate it
    let numStr = num.toString();
    // Find the position of the decimal point
    let decimalPointIndex = numStr.indexOf('.');

    let wholePart = numStr;
    let decimalPart = '';

    // If there is a decimal point, split the number into whole and decimal parts
    if (decimalPointIndex !== -1) {
      wholePart = numStr.slice(0, decimalPointIndex);
      decimalPart = numStr.slice(decimalPointIndex);
    }

    // Take the first two and last two digits of the whole number part
    let shortenedWholePart = wholePart.slice(0, 2) + '..' + wholePart.slice(-2);

    // Return the formatted number
    return shortenedWholePart + decimalPart;
  }

  // If the number is less than 100,000, return it unmodified
  return num;
}

export function getQuotaPerUnit() {
  let quotaPerUnit = localStorage.getItem('quota_per_unit');
  quotaPerUnit = parseFloat(quotaPerUnit);
  return quotaPerUnit;
}

/**
 * 内部额度（点数）→ 与「余额/划转」展示货币一致的数值，用于划转等输入框（非 TOKENS）。
 * TOKENS 模式下与额度点数一致，直接返回 quota。
 */
export function quotaToDisplayInputAmount(quota) {
  if (quota == null || Number.isNaN(Number(quota))) return 0;
  const q = Number(quota);
  let quotaPerUnit = localStorage.getItem('quota_per_unit');
  quotaPerUnit = parseFloat(quotaPerUnit || '500000');
  const displayType = localStorage.getItem('quota_display_type') || 'USD';
  if (displayType === 'TOKENS') {
    return q;
  }
  const resultUSD = q / quotaPerUnit;
  if (displayType === 'CNY') {
    const statusStr = localStorage.getItem('status');
    let usdRate = 7;
    try {
      if (statusStr) {
        const s = JSON.parse(statusStr);
        usdRate = s?.usd_exchange_rate || 7;
      }
    } catch (e) {}
    return resultUSD * usdRate;
  }
  if (displayType === 'CUSTOM') {
    const statusStr = localStorage.getItem('status');
    let rate = 1;
    try {
      if (statusStr) {
        const s = JSON.parse(statusStr);
        rate = s?.custom_currency_exchange_rate || 1;
      }
    } catch (e) {}
    return resultUSD * rate;
  }
  return resultUSD;
}

/**
 * 划转输入框中的标价数值 → 内部额度点数（与 renderQuota / 后端 aff_transfer 一致）。
 */
export function displayInputAmountToQuota(amount) {
  if (amount == null || Number.isNaN(Number(amount))) return 0;
  const a = Number(amount);
  let quotaPerUnit = localStorage.getItem('quota_per_unit');
  quotaPerUnit = parseFloat(quotaPerUnit || '500000');
  const displayType = localStorage.getItem('quota_display_type') || 'USD';
  if (displayType === 'TOKENS') {
    return Math.round(a);
  }
  let resultUSD = a;
  if (displayType === 'CNY') {
    const statusStr = localStorage.getItem('status');
    let usdRate = 7;
    try {
      if (statusStr) {
        const s = JSON.parse(statusStr);
        usdRate = s?.usd_exchange_rate || 7;
      }
    } catch (e) {}
    resultUSD = a / usdRate;
  } else if (displayType === 'CUSTOM') {
    const statusStr = localStorage.getItem('status');
    let rate = 1;
    try {
      if (statusStr) {
        const s = JSON.parse(statusStr);
        rate = s?.custom_currency_exchange_rate || 1;
      }
    } catch (e) {}
    resultUSD = a / rate;
  }
  return Math.round(resultUSD * quotaPerUnit);
}

export function renderUnitWithQuota(quota) {
  let quotaPerUnit = localStorage.getItem('quota_per_unit');
  quotaPerUnit = parseFloat(quotaPerUnit);
  quota = parseFloat(quota);
  return quotaPerUnit * quota;
}

export function getQuotaWithUnit(quota, digits = 6) {
  let quotaPerUnit = localStorage.getItem('quota_per_unit');
  quotaPerUnit = parseFloat(quotaPerUnit);
  return (quota / quotaPerUnit).toFixed(digits);
}

export function renderQuotaWithAmount(amount) {
  const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
  if (quotaDisplayType === 'TOKENS') {
    return renderNumber(renderUnitWithQuota(amount));
  }

  const numericAmount = Number(amount);
  const formattedAmount = Number.isFinite(numericAmount)
    ? trimFixedDecimalDisplay(parseFloat(numericAmount.toFixed(2)), 2)
    : amount;

  if (quotaDisplayType === 'CNY') {
    return '¥' + formattedAmount;
  } else if (quotaDisplayType === 'CUSTOM') {
    const statusStr = localStorage.getItem('status');
    let symbol = '¤';
    try {
      if (statusStr) {
        const s = JSON.parse(statusStr);
        symbol = s?.custom_currency_symbol || symbol;
      }
    } catch (e) {}
    return symbol + formattedAmount;
  }
  return '$' + String(formattedAmount);
}

/**
 * 获取当前货币配置信息
 * @returns {Object} - { symbol, rate, type }
 */
export function getCurrencyConfig() {
  const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
  const statusStr = localStorage.getItem('status');

  let symbol = '$';
  let rate = 1;

  if (quotaDisplayType === 'CNY') {
    symbol = '¥';
    try {
      if (statusStr) {
        const s = JSON.parse(statusStr);
        rate = s?.usd_exchange_rate || 7;
      }
    } catch (e) {}
  } else if (quotaDisplayType === 'CUSTOM') {
    try {
      if (statusStr) {
        const s = JSON.parse(statusStr);
        symbol = s?.custom_currency_symbol || '¤';
        rate = s?.custom_currency_exchange_rate || 1;
      }
    } catch (e) {}
  }

  return { symbol, rate, type: quotaDisplayType };
}

/**
 * 将美元金额转换为当前选择的货币
 * @param {number} usdAmount - 美元金额
 * @param {number} digits - 小数位数
 * @returns {string} - 格式化后的货币字符串
 */
export function convertUSDToCurrency(usdAmount, digits = 2) {
  const { symbol, rate } = getCurrencyConfig();
  const convertedAmount = usdAmount * rate;
  return symbol + parseFloat(convertedAmount.toFixed(digits));
}

/** 按展示金额选择小数位：常态 minDigits；若舍入为 0 但实际 >0 则增至 maxDigits。 */
export function pickQuotaDisplayFractionDigits(
  displayValue,
  minDigits = 2,
  maxDigits = 6,
) {
  const v = Number(displayValue);
  if (!Number.isFinite(v) || v === 0) {
    return minDigits;
  }
  if (parseFloat(v.toFixed(minDigits)) !== 0) {
    return minDigits;
  }
  for (let d = minDigits + 1; d <= maxDigits; d++) {
    if (parseFloat(v.toFixed(d)) !== 0) {
      return d;
    }
  }
  return maxDigits;
}

/** 将内部额度转为当前展示货币下的数值与符号（TOKENS 模式返回 null，由调用方走 renderNumber）。 */
export function quotaToDisplayCurrencyParts(quota) {
  let quotaPerUnit = localStorage.getItem('quota_per_unit');
  const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
  quotaPerUnit = parseFloat(quotaPerUnit);
  if (quotaDisplayType === 'TOKENS') {
    return null;
  }
  const resultUSD = Number(quota) / quotaPerUnit;
  let symbol = '$';
  let value = resultUSD;
  if (quotaDisplayType === 'CNY') {
    const statusStr = localStorage.getItem('status');
    let usdRate = 1;
    try {
      if (statusStr) {
        const s = JSON.parse(statusStr);
        usdRate = s?.usd_exchange_rate || 1;
      }
    } catch (e) {}
    value = resultUSD * usdRate;
    symbol = '¥';
  } else if (quotaDisplayType === 'CUSTOM') {
    const statusStr = localStorage.getItem('status');
    let symbolCustom = '¤';
    let rate = 1;
    try {
      if (statusStr) {
        const s = JSON.parse(statusStr);
        symbolCustom = s?.custom_currency_symbol || symbolCustom;
        rate = s?.custom_currency_exchange_rate || rate;
      }
    } catch (e) {}
    value = resultUSD * rate;
    symbol = symbolCustom;
  }
  return { symbol, value };
}

export function renderQuota(quota, digits = 2) {
  const parts = quotaToDisplayCurrencyParts(quota);
  if (parts === null) {
    return renderNumber(quota);
  }
  const { symbol, value } = parts;
  const fixedResult = parseFloat(value.toFixed(digits));
  if (fixedResult === 0 && quota > 0 && value > 0) {
    const minValue = Math.pow(10, -digits);
    return symbol + trimFixedDecimalDisplay(minValue, digits);
  }
  return symbol + trimFixedDecimalDisplay(fixedResult, digits);
}

/**
 * 额度展示：默认保留 minDigits 位小数；极低分成在舍入为 0 时自动增至 maxDigits 位（不伪造最小展示值）。
 */
export function renderQuotaFlexible(quota, minDigits = 2, maxDigits = 6) {
  const parts = quotaToDisplayCurrencyParts(quota);
  if (parts === null) {
    return renderNumber(quota);
  }
  const { symbol, value } = parts;
  const digits = pickQuotaDisplayFractionDigits(value, minDigits, maxDigits);
  const fixedResult = parseFloat(value.toFixed(digits));
  return symbol + trimFixedDecimalDisplay(fixedResult, digits);
}

function isValidGroupRatio(ratio) {
  return Number.isFinite(ratio) && ratio !== -1;
}

/**
 * 将消费日志中的渠道折扣百分数（100=无折扣）转为与实扣一致的金额乘数。
 * @param {number|string|undefined} percent 渠道折扣百分数
 * @returns {number} 如 60 -> 0.6；缺省或非法 -> 1
 */
function normalizeChannelDiscountMultiplier(percent) {
  const n = Number(percent);
  if (!Number.isFinite(n) || n <= 0) {
    return 1;
  }
  return n / 100;
}

/** 日志/计费过程单价展示（与首页卡片 formatPrice 一致） */
function formatBillingUnitPrice(usdAmount) {
  return formatBillingUsdDisplay(usdAmount);
}

/**
 * Helper function to get effective ratio and label
 * @param {number} groupRatio - The default group ratio
 * @param {number} user_group_ratio - The user-specific group ratio
 * @returns {Object} - Object containing { ratio, label, useUserGroupRatio }
 */
function getEffectiveRatio(groupRatio, user_group_ratio) {
  const useUserGroupRatio = isValidGroupRatio(user_group_ratio);
  const ratioLabel = useUserGroupRatio
    ? i18next.t('专属倍率')
    : i18next.t('分组倍率');
  const effectiveRatio = useUserGroupRatio ? user_group_ratio : groupRatio;

  return {
    ratio: effectiveRatio,
    label: ratioLabel,
    useUserGroupRatio: useUserGroupRatio,
  };
}

function getQuotaDisplayType() {
  return localStorage.getItem('quota_display_type') || 'USD';
}

/**
 * 是否为「显式按次计价」：仅当 model_price 为有限正数时表示每次调用有固定美元单价。
 * `0`、`-1`、空值均视为非按次（走按量/倍率），与 renderLogContent 中「0 不按次」一致，避免日志里出现误导性的「按次 ¥0」。
 * @param {number|string|undefined|null} modelPrice 模型按次美元价（-1 表示按量等）
 * @returns {boolean}
 */
function isExplicitPerCallModelPrice(modelPrice) {
  if (modelPrice === -1 || modelPrice === undefined || modelPrice === null) {
    return false;
  }
  const n = Number(modelPrice);
  return Number.isFinite(n) && n > 0;
}

function resolveBillingDisplayMode(displayMode, modelPrice = -1) {
  if (isExplicitPerCallModelPrice(modelPrice)) {
    return 'price';
  }
  if (getQuotaDisplayType() === 'TOKENS') {
    return 'ratio';
  }
  return displayMode === 'ratio' ? 'ratio' : 'price';
}

function isPriceDisplayMode(displayMode, modelPrice = -1) {
  return resolveBillingDisplayMode(displayMode, modelPrice) === 'price';
}

/**
 * 在「额度显示为 TOKENS」偏好下，是否仅用倍率摘要展示计费过程（非显式按次时与 -1/0 等按量一致）。
 * @param {number|string|undefined|null} modelPrice
 * @returns {boolean}
 */
function shouldUseRatioBillingProcess(modelPrice = -1) {
  return (
    !isExplicitPerCallModelPrice(modelPrice) &&
    getQuotaDisplayType() === 'TOKENS'
  );
}

/** 固定小数位后去掉末尾多余的 0（如 1.50→1.5，2.00→2），用于金额等展示 */
export function trimFixedDecimalDisplay(num, fractionDigits = 2) {
  const n = Number(num);
  if (!Number.isFinite(n)) {
    return String(num ?? '');
  }
  let s = n.toFixed(fractionDigits);
  if (s.includes('.')) {
    s = s.replace(/\.?0+$/, '');
  }
  return s;
}

function appendPricePart(parts, condition, key, vars) {
  if (!condition) {
    return;
  }
  parts.push(i18next.t(key, vars));
}

function joinBillingSummary(parts) {
  return parts.filter(Boolean).join('，');
}

function getGroupRatioText(groupRatio, user_group_ratio) {
  const { ratio, label } = getEffectiveRatio(groupRatio, user_group_ratio);
  const ratioDisplay = Number.isFinite(Number(ratio))
    ? trimFixedDecimalDisplay(Number(ratio), 6)
    : ratio;
  return i18next.t('{{ratioType}} {{ratio}}x', {
    ratioType: label,
    ratio: ratioDisplay,
  });
}

function formatRatioValue(value, digits = 2) {
  const num = Number(value);
  if (!Number.isFinite(num)) {
    return 0;
  }
  return parseFloat(num.toFixed(digits));
}

function renderDisplayAmountFromUsd(usdAmount, digits = 2) {
  return renderQuotaWithAmount(Number(Number(usdAmount || 0).toFixed(digits)));
}

function formatBillingDisplayPrice(usdAmount, rate, digits = 2) {
  return parseFloat((usdAmount * rate).toFixed(digits));
}

function buildBillingText(key, vars) {
  return i18next.t(key, vars);
}

function buildBillingPriceText(
  key,
  { symbol, usdAmount, rate, amountKey = 'price', digits = 2, ...vars },
) {
  return buildBillingText(key, {
    symbol,
    [amountKey]: formatBillingDisplayPrice(usdAmount, rate, digits),
    ...vars,
  });
}

function renderBillingArticle(lines, { showReferenceNote = true } = {}) {
  const articleLines = lines.filter(Boolean);

  if (showReferenceNote) {
    articleLines.push(buildBillingText('仅供参考，以实际扣费为准'));
  }

  return (
    <article>
      {articleLines.map((line, index) => (
        <p key={index}>{line}</p>
      ))}
    </article>
  );
}

function resolveImagePerImageBillingMeta(billingMeta, modelPrice, channelDiscountMult) {
  const meta =
    billingMeta && typeof billingMeta === 'object' ? billingMeta : {};
  let usdPerImage = Number(meta?.image_usd_per_image);
  if (!Number.isFinite(usdPerImage) || usdPerImage <= 0) {
    const mp = Number(modelPrice);
    if (Number.isFinite(mp) && mp > 0) {
      usdPerImage = mp;
    } else {
      usdPerImage = 0;
    }
  }
  return {
    count: Math.max(1, Number(meta?.image_count) || 1),
    usdPerImage,
    resolution: String(meta?.image_resolution || '').trim(),
  };
}

/** 每张展示价 = (基础每张价 × costDisc + globalMp × markupRate) × 有效分组/专属倍率（与实扣一致） */
function effectiveImagePerImageUsd(
  usdPerImage,
  groupRatio,
  user_group_ratio,
  channelPriceDiscountPercent = 100,
  billingMeta = null,
) {
  const base = Number(usdPerImage);
  if (!Number.isFinite(base) || base <= 0) {
    return 0;
  }
  const { ratio: effectiveGroupRatio } = getEffectiveRatio(
    groupRatio,
    user_group_ratio,
  );
  const { effModelPrice } = resolveConsumeLogBillingRates({
    modelPrice: base,
    channelPriceDiscountPercent,
    billingMeta,
  });
  return effModelPrice * (Number(effectiveGroupRatio) || 1);
}

function resolveImagePerImageBillingDisplay(
  billingMeta,
  {
    modelPrice = -1,
    groupRatio,
    user_group_ratio,
    channelPriceDiscountPercent = 100,
  } = {},
) {
  const { symbol, rate } = getCurrencyConfig();
  const { costDisc } = resolveConsumeLogBillingRates({
    modelPrice: modelPrice > 0 ? modelPrice : 0,
    channelPriceDiscountPercent,
    billingMeta,
  });
  const imageMeta = resolveImagePerImageBillingMeta(
    billingMeta,
    modelPrice,
    costDisc,
  );
  const perImageUsd = effectiveImagePerImageUsd(
    imageMeta.usdPerImage,
    groupRatio,
    user_group_ratio,
    channelPriceDiscountPercent,
    billingMeta,
  );
  const resolutionLabel =
    formatVideoResolutionDisplayLabel(imageMeta.resolution) ||
    imageMeta.resolution;
  return {
    symbol,
    rate,
    imageMeta,
    perImageUsd,
    resolutionLabel,
    displayPrice: formatBillingDisplayPrice(perImageUsd, rate),
    totalDisplayPrice: formatBillingDisplayPrice(
      perImageUsd * imageMeta.count,
      rate,
    ),
  };
}

function formatImagePerImageBillingBrief(
  billingMeta,
  {
    modelPrice = -1,
    groupRatio,
    user_group_ratio,
    channelPriceDiscountPercent = 100,
    includeResolution = true,
    t = i18next.t.bind(i18next),
  } = {},
) {
  const tr = typeof t === 'function' ? t : i18next.t.bind(i18next);
  const { symbol, imageMeta, displayPrice, resolutionLabel } =
    resolveImagePerImageBillingDisplay(billingMeta, {
      modelPrice,
      groupRatio,
      user_group_ratio,
      channelPriceDiscountPercent,
    });
  let text = tr('每张 {{symbol}}{{price}}，数量{{count}}', {
    symbol,
    price: displayPrice,
    count: imageMeta.count,
  });
  if (includeResolution && resolutionLabel) {
    text = `${text}，${resolutionLabel}`;
  }
  return text;
}

function buildImagePerImageBillingTagItems(
  billingMeta,
  {
    modelPrice = -1,
    groupRatio,
    user_group_ratio,
    channelPriceDiscountPercent = 100,
    showTotal = false,
    t = i18next.t.bind(i18next),
  } = {},
) {
  const tr = typeof t === 'function' ? t : i18next.t.bind(i18next);
  const { symbol, imageMeta, displayPrice, totalDisplayPrice, resolutionLabel } =
    resolveImagePerImageBillingDisplay(billingMeta, {
      modelPrice,
      groupRatio,
      user_group_ratio,
      channelPriceDiscountPercent,
    });
  const items = [
    {
      key: 'brief',
      color: 'green',
      label: tr('每张 {{symbol}}{{price}}，数量{{count}}', {
        symbol,
        price: displayPrice,
        count: imageMeta.count,
      }),
    },
  ];
  if (resolutionLabel) {
    items.push({
      key: 'resolution',
      color: 'cyan',
      label: resolutionLabel,
    });
  }
  if (showTotal) {
    items.push({
      key: 'total',
      color: 'red',
      label: buildBillingText('合计 {{symbol}}{{price}}', {
        symbol,
        price: totalDisplayPrice,
      }),
    });
  }
  return items;
}

function renderImagePerImageBillingTags(
  billingMeta,
  options = {},
  { showTotal = false, showReferenceNote = false } = {},
) {
  const items = buildImagePerImageBillingTagItems(billingMeta, {
    ...options,
    showTotal,
  });
  return (
    <div>
      <div className='flex flex-wrap items-center' style={{ gap: 4 }}>
        {items.map((item) => (
          <Tag key={item.key} color={item.color} size='small'>
            {item.label}
          </Tag>
        ))}
      </div>
      {showReferenceNote ? (
        <Typography.Text
          type='tertiary'
          size='small'
          style={{ display: 'block', marginTop: 8 }}
        >
          {i18next.t('仅供参考，以实际扣费为准')}
        </Typography.Text>
      ) : null}
    </div>
  );
}

function buildImagePerImageLogSummarySegments(
  billingMeta,
  options = {},
) {
  return [
    {
      tone: 'secondary',
      text: formatImagePerImageBillingBrief(billingMeta, {
        ...options,
        includeResolution: false,
      }),
    },
  ];
}

// Shared core for simple price rendering (used by OpenAI-like and Claude-like variants)
function renderPriceSimpleCore({
  modelRatio,
  modelPrice = -1,
  groupRatio,
  user_group_ratio,
  cacheTokens = 0,
  cacheRatio = 1.0,
  cacheCreationTokens = 0,
  cacheCreationRatio = 1.0,
  cacheCreationTokens5m = 0,
  cacheCreationRatio5m = 1.0,
  cacheCreationTokens1h = 0,
  cacheCreationRatio1h = 1.0,
  image = false,
  imageRatio = 1.0,
  isSystemPromptOverride = false,
  displayMode = 'price',
  outputMode = 'text',
  // Video token-billing extras; see renderLogContent for full semantics.
  videoRatio = 0,
  videoCompletionRatio = 1.0,
  videoOutputTokens = 0,
  videoInputTextTokens = 0,
  // From log.other.billing_mode when backend stamps video_per_video / video_token.
  billingMode = '',
  /** 渠道价格折扣百分数（100=无折扣） */
  channelPriceDiscountPercent = 100,
  /** 消费日志 other（按张计费等扩展字段） */
  billingMeta = null,
}) {
  const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
    groupRatio,
    user_group_ratio,
  );
  const finalGroupRatio = effectiveGroupRatio;

  const { symbol, rate } = getCurrencyConfig();
  const rates = resolveConsumeLogBillingRates({
    modelRatio,
    completionRatio: 0,
    cacheRatio,
    cacheCreationRatio,
    cacheCreationRatio5m,
    cacheCreationRatio1h,
    modelPrice,
    channelPriceDiscountPercent,
    billingMeta,
  });
  const {
    inputRatioPrice,
    cacheRatioPrice,
    cacheCreationRatioPrice,
    cacheCreationRatioPrice5m,
    cacheCreationRatioPrice1h,
    mp,
    mr,
  } = rates;
  const hasSplitCacheCreation =
    cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;

  const shouldShowLegacyCacheCreation =
    !hasSplitCacheCreation && cacheCreationTokens !== 0;

  const shouldShowCache = cacheTokens !== 0;
  const shouldShowCacheCreation5m =
    hasSplitCacheCreation && cacheCreationTokens5m > 0;
  const shouldShowCacheCreation1h =
    hasSplitCacheCreation && cacheCreationTokens1h > 0;

  // Video token-billing detection: backend stamps video task logs with
  // video_output_tokens + video_ratio when the call is billed via the
  // duration*W*H*fps/1024 token formula. modelPrice is exactly 0 (not -1)
  // in this path, which would otherwise render misleadingly as "模型价格 $0".
  const isVideoTokenBilling =
    videoOutputTokens > 0 &&
    videoRatio > 0 &&
    (modelPrice === 0 || modelPrice === -1);

  const isVideoPerVideoFlatBilling =
    billingMode === 'video_per_video' &&
    (modelPrice === 0 || modelPrice === -1);
  const isVideoPerSecondFlatBilling =
    billingMode === 'video_per_second' &&
    (modelPrice === 0 || modelPrice === -1);

  if (outputMode === 'segments') {
    // 使用日志表「详情」等紧凑多行：不展示单独一行「分组/专属倍率」；各单价 = 原单价×有效分组倍率（与日志详情的折叠展示一致）。
    const groupMult =
      Number.isFinite(Number(finalGroupRatio)) && Number(finalGroupRatio) > 0
        ? Number(finalGroupRatio)
        : 1;
    const segments = [];

    if (isVideoPerSecondFlatBilling) {
      segments.push({
        tone: 'secondary',
        text: i18next.t('分辨率阶梯计费'),
      });
      if (isSystemPromptOverride) {
        segments.push({ tone: 'primary', text: i18next.t('系统提示覆盖') });
      }
      return segments;
    }

    if (isVideoPerVideoFlatBilling) {
      segments.push({
        tone: 'secondary',
        text: i18next.t('按视频数量计费'),
      });
      if (isSystemPromptOverride) {
        segments.push({ tone: 'primary', text: i18next.t('系统提示覆盖') });
      }
      return segments;
    }

    if (billingMode === 'image_per_image') {
      const segments = buildImagePerImageLogSummarySegments(billingMeta, {
        modelPrice,
        groupRatio,
        user_group_ratio,
        channelPriceDiscountPercent,
      });
      if (isSystemPromptOverride) {
        segments.push({ tone: 'primary', text: i18next.t('系统提示覆盖') });
      }
      return segments;
    }

    if (isVideoTokenBilling) {
      // Per the video token formula:
      //   quota = (inputTextTokens
      //            + outputVideoTokens * videoRatio * videoCompletionRatio
      //           ) * modelRatio * groupRatio
      // Effective $/1M-token unit prices (already folded with groupMult):
      const videoUnitPrice =
        inputRatioPrice * (videoRatio || 1) * (videoCompletionRatio || 1) * groupMult;
      segments.push({
        tone: 'secondary',
        text: i18next.t('视频输出 {{price}} / 1M tokens', {
          price: formatBillingUnitPrice(videoUnitPrice),
        }),
      });
      segments.push({
        tone: 'secondary',
        text: i18next.t('估算 tokens：{{count}}', {
          count: videoOutputTokens,
        }),
      });
      if (isSystemPromptOverride) {
        segments.push({ tone: 'primary', text: i18next.t('系统提示覆盖') });
      }
      return segments;
    }

    // Treat `modelPrice === 0` as "no per-call price configured" rather than
    // "explicitly $0 per call" — see the matching guard in renderLogContent
    // for the full rationale (legacy task logs may stamp literal 0).
    if (modelPrice !== -1 && modelPrice !== 0) {
      segments.push({
        tone: 'secondary',
        text: isPriceDisplayMode(displayMode, modelPrice)
          ? i18next.t('模型价格 {{price}}', {
              price: formatBillingUnitPrice(mp * groupMult),
            })
          : i18next.t('按次'),
      });
    } else if (isPriceDisplayMode(displayMode, modelPrice)) {
      segments.push({
        tone: 'secondary',
        text: i18next.t('输入 {{price}} / 1M tokens', {
          price: formatBillingUnitPrice(inputRatioPrice * groupMult),
        }),
      });

      if (shouldShowCache) {
        segments.push({
          tone: 'secondary',
          text: i18next.t('缓存读 {{price}} / 1M tokens', {
            price: formatBillingUnitPrice(cacheRatioPrice * groupMult),
          }),
        });
      }

      if (hasSplitCacheCreation && shouldShowCacheCreation5m) {
        segments.push({
          tone: 'secondary',
          text: i18next.t('5m缓存创建 {{price}} / 1M tokens', {
            price: formatBillingUnitPrice(
              cacheCreationRatioPrice5m * groupMult,
            ),
          }),
        });
      }
      if (hasSplitCacheCreation && shouldShowCacheCreation1h) {
        segments.push({
          tone: 'secondary',
          text: i18next.t('1h缓存创建 {{price}} / 1M tokens', {
            price: formatBillingUnitPrice(
              cacheCreationRatioPrice1h * groupMult,
            ),
          }),
        });
      }
      if (!hasSplitCacheCreation && shouldShowLegacyCacheCreation) {
        segments.push({
          tone: 'secondary',
          text: i18next.t('缓存创建 {{price}} / 1M tokens', {
            price: formatBillingUnitPrice(
              cacheCreationRatioPrice * groupMult,
            ),
          }),
        });
      }

      if (image) {
        segments.push({
          tone: 'secondary',
          text: i18next.t('图片输入 {{price}} / 1M tokens', {
            price: formatBillingUnitPrice(
              inputRatioPrice * imageRatio * groupMult,
            ),
          }),
        });
      }
    } else {
      segments.push({
        tone: 'secondary',
        text: i18next.t('模型: {{ratio}}', {
          ratio: modelRatio,
        }),
      });

      if (shouldShowCache) {
        segments.push({
          tone: 'secondary',
          text: i18next.t('缓存: {{cacheRatio}}', {
            cacheRatio: cacheRatio,
          }),
        });
      }

      if (hasSplitCacheCreation) {
        if (shouldShowCacheCreation5m && shouldShowCacheCreation1h) {
          segments.push({
            tone: 'secondary',
            text: i18next.t(
              '缓存创建: 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}',
              {
                cacheCreationRatio5m: cacheCreationRatio5m,
                cacheCreationRatio1h: cacheCreationRatio1h,
              },
            ),
          });
        } else if (shouldShowCacheCreation5m) {
          segments.push({
            tone: 'secondary',
            text: i18next.t('缓存创建: 5m {{cacheCreationRatio5m}}', {
              cacheCreationRatio5m: cacheCreationRatio5m,
            }),
          });
        } else if (shouldShowCacheCreation1h) {
          segments.push({
            tone: 'secondary',
            text: i18next.t('缓存创建: 1h {{cacheCreationRatio1h}}', {
              cacheCreationRatio1h: cacheCreationRatio1h,
            }),
          });
        }
      } else if (shouldShowLegacyCacheCreation) {
        segments.push({
          tone: 'secondary',
          text: i18next.t('缓存创建: {{cacheCreationRatio}}', {
            cacheCreationRatio: cacheCreationRatio,
          }),
        });
      }

      if (image) {
        segments.push({
          tone: 'secondary',
          text: i18next.t('图片输入: {{imageRatio}}', {
            imageRatio: imageRatio,
          }),
        });
      }
    }

    if (isSystemPromptOverride) {
      segments.push({
        tone: 'primary',
        text: i18next.t('系统提示覆盖'),
      });
    }

    return segments;
  }

  if (isVideoPerVideoFlatBilling) {
    return joinBillingSummary([
      i18next.t('按视频数量计费'),
      getGroupRatioText(groupRatio, user_group_ratio),
    ]);
  }

  // Video token-billing branch for the non-segments (text) output mode —
  // mirrors the segments-mode branch above so callers reading the textual
  // summary (e.g. tooltips, copy-to-clipboard) get the correct video pricing.
  if (isVideoTokenBilling) {
    const videoUnitPrice =
      inputRatioPrice * (videoRatio || 1) * (videoCompletionRatio || 1) * rate;
    return joinBillingSummary([
      i18next.t('视频输出 {{symbol}}{{price}} / 1M tokens', {
        symbol,
        price: parseFloat(videoUnitPrice.toFixed(2)),
      }),
      i18next.t('估算视频 tokens：{{count}}', { count: videoOutputTokens }),
      getGroupRatioText(groupRatio, user_group_ratio),
    ]);
  }

  // Treat modelPrice === 0 as "unset" rather than "$0/per-call" — same
  // rationale as in renderLogContent.
  if (modelPrice !== -1 && modelPrice !== 0) {
    if (isPriceDisplayMode(displayMode, modelPrice)) {
      return joinBillingSummary([
        i18next.t('模型价格：{{symbol}}{{price}}', {
          symbol: symbol,
          price: parseFloat((mp * rate).toFixed(2)),
        }),
        getGroupRatioText(groupRatio, user_group_ratio),
      ]);
    }
    const displayPrice = parseFloat((mp * rate).toFixed(2));
    return i18next.t('价格：{{symbol}}{{price}} * {{ratioType}}：{{ratio}}', {
      symbol: symbol,
      price: displayPrice,
      ratioType: ratioLabel,
      ratio: finalGroupRatio,
    });
  }

  if (isPriceDisplayMode(displayMode, modelPrice)) {
    const parts = [];
    if (modelPrice !== -1 && modelPrice !== 0) {
      parts.push(
        i18next.t('模型价格 {{price}}', {
          price: formatBillingUnitPrice(mp),
        }),
      );
      parts.push(getGroupRatioText(groupRatio, user_group_ratio));
      return joinBillingSummary(parts);
    }

    parts.push(
      i18next.t('输入 {{price}} / 1M tokens', {
        price: formatBillingUnitPrice(inputRatioPrice),
      }),
    );

    if (shouldShowCache) {
      parts.push(
        i18next.t('缓存读 {{price}} / 1M tokens', {
          price: formatBillingUnitPrice(cacheRatioPrice),
        }),
      );
    }

    if (hasSplitCacheCreation && shouldShowCacheCreation5m) {
      parts.push(
        i18next.t('5m缓存创建 {{price}} / 1M tokens', {
          price: formatBillingUnitPrice(cacheCreationRatioPrice5m),
        }),
      );
    }
    if (hasSplitCacheCreation && shouldShowCacheCreation1h) {
      parts.push(
        i18next.t('1h缓存创建 {{price}} / 1M tokens', {
          price: formatBillingUnitPrice(cacheCreationRatioPrice1h),
        }),
      );
    }
    if (!hasSplitCacheCreation && shouldShowLegacyCacheCreation) {
      parts.push(
        i18next.t('缓存创建 {{price}} / 1M tokens', {
          price: formatBillingUnitPrice(cacheCreationRatioPrice),
        }),
      );
    }

    if (image) {
      parts.push(
        i18next.t('图片输入 {{price}} / 1M tokens', {
          price: formatBillingUnitPrice(inputRatioPrice * imageRatio),
        }),
      );
    }

    parts.push(getGroupRatioText(groupRatio, user_group_ratio));

    let result = joinBillingSummary(parts);
    if (isSystemPromptOverride) {
      result += '\n\r' + i18next.t('系统提示覆盖');
    }
    return result;
  }

  const parts = [];
  // base: model ratio
  parts.push(i18next.t('模型: {{ratio}}'));

  // cache part (label differs when with image)
  if (shouldShowCache) {
    parts.push(i18next.t('缓存: {{cacheRatio}}'));
  }

  if (hasSplitCacheCreation) {
    if (shouldShowCacheCreation5m && shouldShowCacheCreation1h) {
      parts.push(
        i18next.t(
          '缓存创建: 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}',
        ),
      );
    } else if (shouldShowCacheCreation5m) {
      parts.push(i18next.t('缓存创建: 5m {{cacheCreationRatio5m}}'));
    } else if (shouldShowCacheCreation1h) {
      parts.push(i18next.t('缓存创建: 1h {{cacheCreationRatio1h}}'));
    }
  } else if (shouldShowLegacyCacheCreation) {
    parts.push(i18next.t('缓存创建: {{cacheCreationRatio}}'));
  }

  // image part
  if (image) {
    parts.push(i18next.t('图片输入: {{imageRatio}}'));
  }

  parts.push(`{{ratioType}}: {{groupRatio}}`);

  let result = i18next.t(parts.join(' * '), {
    ratio: modelRatio,
    ratioType: ratioLabel,
    groupRatio: finalGroupRatio,
    cacheRatio: cacheRatio,
    cacheCreationRatio: cacheCreationRatio,
    cacheCreationRatio5m: cacheCreationRatio5m,
    cacheCreationRatio1h: cacheCreationRatio1h,
    imageRatio: imageRatio,
  });

  if (isSystemPromptOverride) {
    result += '\n\r' + i18next.t('系统提示覆盖');
  }

  return result;
}

export function renderModelPrice(
  inputTokens,
  completionTokens,
  modelRatio,
  modelPrice = -1,
  completionRatio,
  groupRatio,
  user_group_ratio,
  cacheTokens = 0,
  cacheRatio = 1.0,
  image = false,
  imageRatio = 1.0,
  imageOutputTokens = 0,
  webSearch = false,
  webSearchCallCount = 0,
  webSearchPrice = 0,
  fileSearch = false,
  fileSearchCallCount = 0,
  fileSearchPrice = 0,
  audioInputSeperatePrice = false,
  audioInputTokens = 0,
  audioInputPrice = 0,
  imageGenerationCall = false,
  imageGenerationCallPrice = 0,
  displayMode = 'price',
  /** 渠道价格折扣百分数（100=无折扣），与消费日志 other.channel_price_discount_percent 一致 */
  channelPriceDiscountPercent = 100,
  /**
   * 为 true 时（消费日志「计费过程」）在 modelPrice=-1 时仍展示「按量」美元单价与括号内算式，
   * 不受本地「额度显示为 TOKENS」偏好影响。
   */
  forcePriceVolumeParametricDetail = false,
  /**
   * 仅消费日志「计费过程」：表头与括号内展示的 ¥/1M 等已乘有效分组/专属倍率，算式不再追加「* 分组倍率」。
   * @type {boolean}
   */
  billingProcessFoldGroupMultiplier = false,
  /** 消费日志 other（含 global_*、markup_discount_rate） */
  billingMeta = null,
) {
  const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
    groupRatio,
    user_group_ratio,
  );
  groupRatio = effectiveGroupRatio;

  const consumeFoldGroup =
    billingProcessFoldGroupMultiplier &&
    Number.isFinite(Number(groupRatio)) &&
    Number(groupRatio) > 0;
  const gDisp = consumeFoldGroup ? Number(groupRatio) : 1;

  const { symbol, rate } = getCurrencyConfig();
  const rates = resolveConsumeLogBillingRates({
    modelRatio,
    completionRatio,
    cacheRatio,
    modelPrice,
    channelPriceDiscountPercent,
    billingMeta,
  });
  const {
    costDisc,
    inputRatioPrice,
    completionRatioPrice,
    cacheRatioPrice,
    mp,
  } = rates;
  const imageInputUnitPrice = inputRatioPrice * imageRatio;
  const wsp = Number(webSearchPrice || 0) * costDisc;
  const fsp = Number(fileSearchPrice || 0) * costDisc;
  const aip = Number(audioInputPrice || 0) * costDisc;
  const igp = Number(imageGenerationCallPrice || 0) * costDisc;

  const useRatioOnlySummary =
    shouldUseRatioBillingProcess(modelPrice) &&
    !forcePriceVolumeParametricDetail;

  if (!useRatioOnlySummary) {
    if (isExplicitPerCallModelPrice(modelPrice)) {
      return renderBillingArticle([
        buildBillingPriceText('按次：{{symbol}}{{price}}', {
          symbol,
          usdAmount: mp,
          rate,
        }),
        buildBillingPriceText(
          '按次 {{symbol}}{{price}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}',
          {
            symbol,
            usdAmount: mp,
            rate,
            ratioType: ratioLabel,
            ratio: groupRatio,
            amountKey: 'price',
            total: formatBillingDisplayPrice(mp * groupRatio, rate),
          },
        ),
      ]);
    }

    if (completionRatio === undefined) {
      completionRatio = 0;
    }
    let effectiveInputTokens =
      inputTokens - cacheTokens + cacheTokens * cacheRatio;
    if (image && imageOutputTokens > 0) {
      effectiveInputTokens =
        inputTokens - imageOutputTokens + imageOutputTokens * imageRatio;
    }
    if (audioInputTokens > 0) {
      effectiveInputTokens -= audioInputTokens;
    }
    const price =
      (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
      (audioInputTokens / 1000000) * aip * groupRatio +
      (completionTokens / 1000000) * completionRatioPrice * groupRatio +
      (webSearchCallCount / 1000) * wsp * groupRatio +
      (fileSearchCallCount / 1000) * fsp * groupRatio +
      igp * groupRatio;

    let inputDesc = '';
    if (image && imageOutputTokens > 0) {
      inputDesc = buildBillingPriceText(
        '(输入 {{nonImageInput}} tokens + 图片输入 {{imageInput}} tokens / 1M tokens * {{symbol}}{{price}}',
        {
          nonImageInput: inputTokens - imageOutputTokens,
          imageInput: imageOutputTokens,
          symbol,
          usdAmount: inputRatioPrice * gDisp,
          rate,
        },
      );
    } else if (cacheTokens > 0) {
      inputDesc = buildBillingText(
        '(输入 {{nonCacheInput}} tokens / 1M tokens * {{symbol}}{{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * {{symbol}}{{cachePrice}}',
        {
          nonCacheInput: inputTokens - cacheTokens,
          cacheInput: cacheTokens,
          symbol,
          price: formatBillingDisplayPrice(inputRatioPrice * gDisp, rate),
          cachePrice: formatBillingDisplayPrice(cacheRatioPrice * gDisp, rate),
        },
      );
    } else if (audioInputSeperatePrice && audioInputTokens > 0) {
      inputDesc = buildBillingText(
        '(输入 {{nonAudioInput}} tokens / 1M tokens * {{symbol}}{{price}} + 音频输入 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioPrice}}',
        {
          nonAudioInput: inputTokens - audioInputTokens,
          audioInput: audioInputTokens,
          symbol,
          price: formatBillingDisplayPrice(inputRatioPrice * gDisp, rate),
          audioPrice: formatBillingDisplayPrice(aip * gDisp, rate),
        },
      );
    } else {
      inputDesc = buildBillingPriceText(
        '(输入 {{input}} tokens / 1M tokens * {{symbol}}{{price}}',
        {
          input: inputTokens,
          symbol,
          usdAmount: inputRatioPrice * gDisp,
          rate,
        },
      );
    }

    const outputDesc = billingProcessFoldGroupMultiplier
      ? buildBillingText(
          '输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}})',
          {
            completion: completionTokens,
            symbol,
            compPrice: formatBillingDisplayPrice(
              completionRatioPrice * gDisp,
              rate,
            ),
          },
        )
      : buildBillingText(
          '输出 {{completion}} tokens / 1M tokens * {{symbol}}{{compPrice}}) * {{ratioType}} {{ratio}}',
          {
            completion: completionTokens,
            symbol,
            compPrice: formatBillingDisplayPrice(completionRatioPrice, rate),
            ratio: groupRatio,
            ratioType: ratioLabel,
          },
        );

    const extraServices = [
      webSearch && webSearchCallCount > 0
        ? buildBillingPriceText(
            billingProcessFoldGroupMultiplier
              ? ' + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}}'
              : ' + Web搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}',
            billingProcessFoldGroupMultiplier
              ? {
                  count: webSearchCallCount,
                  symbol,
                  usdAmount: wsp * gDisp,
                  rate,
                }
              : {
                  count: webSearchCallCount,
                  symbol,
                  usdAmount: wsp,
                  rate,
                  ratio: groupRatio,
                  ratioType: ratioLabel,
                },
          )
        : '',
      fileSearch && fileSearchCallCount > 0
        ? buildBillingPriceText(
            billingProcessFoldGroupMultiplier
              ? ' + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}}'
              : ' + 文件搜索 {{count}}次 / 1K 次 * {{symbol}}{{price}} * {{ratioType}} {{ratio}}',
            billingProcessFoldGroupMultiplier
              ? {
                  count: fileSearchCallCount,
                  symbol,
                  usdAmount: fsp * gDisp,
                  rate,
                }
              : {
                  count: fileSearchCallCount,
                  symbol,
                  usdAmount: fsp,
                  rate,
                  ratio: groupRatio,
                  ratioType: ratioLabel,
                },
          )
        : '',
      imageGenerationCall && imageGenerationCallPrice > 0
        ? buildBillingPriceText(
            billingProcessFoldGroupMultiplier
              ? ' + 图片生成调用 {{symbol}}{{price}} / 1次'
              : ' + 图片生成调用 {{symbol}}{{price}} / 1次 * {{ratioType}} {{ratio}}',
            billingProcessFoldGroupMultiplier
              ? {
                  symbol,
                  usdAmount: igp * gDisp,
                  rate,
                }
              : {
                  symbol,
                  usdAmount: igp,
                  rate,
                  ratio: groupRatio,
                  ratioType: ratioLabel,
                },
          )
        : '',
    ].join('');

    const billingLines = [
      buildBillingPriceText(
        '输入价格：{{symbol}}{{price}} / 1M tokens{{audioPrice}}',
        {
          symbol,
          usdAmount: inputRatioPrice * gDisp,
          rate,
          audioPrice: audioInputSeperatePrice
            ? `，${i18next.t('音频输入价格')} ${symbol}${formatBillingDisplayPrice(aip * gDisp, rate)} / 1M tokens`
            : '',
        },
      ),
      buildBillingPriceText('输出价格：{{symbol}}{{total}} / 1M tokens', {
        symbol,
        usdAmount: completionRatioPrice * gDisp,
        rate,
        amountKey: 'total',
      }),
      cacheTokens > 0
        ? buildBillingPriceText(
            '缓存读取价格：{{symbol}}{{total}} / 1M tokens',
            {
              symbol,
              usdAmount: cacheRatioPrice * gDisp,
              rate,
              amountKey: 'total',
            },
          )
        : null,
      image && imageOutputTokens > 0
        ? buildBillingPriceText(
            '图片输入价格：{{symbol}}{{total}} / 1M tokens',
            {
              symbol,
              usdAmount: imageInputUnitPrice * gDisp,
              rate,
              amountKey: 'total',
            },
          )
        : null,
      webSearch && webSearchCallCount > 0
        ? buildBillingPriceText('Web搜索价格：{{symbol}}{{price}} / 1K 次', {
            symbol,
            usdAmount: wsp * gDisp,
            rate,
          })
        : null,
      fileSearch && fileSearchCallCount > 0
        ? buildBillingPriceText('文件搜索价格：{{symbol}}{{price}} / 1K 次', {
            symbol,
            usdAmount: fsp * gDisp,
            rate,
          })
        : null,
      imageGenerationCall && imageGenerationCallPrice > 0
        ? buildBillingPriceText('图片生成调用：{{symbol}}{{price}} / 1次', {
            symbol,
            usdAmount: igp * gDisp,
            rate,
          })
        : null,
      buildBillingText(
        '{{inputDesc}} + {{outputDesc}}{{extraServices}} = {{symbol}}{{total}}',
        {
          inputDesc,
          outputDesc,
          extraServices,
          symbol,
          total: formatBillingDisplayPrice(price, rate),
        },
      ),
    ];

    return renderBillingArticle(billingLines);
  }

  if (isExplicitPerCallModelPrice(modelPrice)) {
    const displayPrice = parseFloat((mp * rate).toFixed(2));
    const displayTotal = parseFloat((mp * groupRatio * rate).toFixed(2));
    return i18next.t(
      '按次：{{symbol}}{{price}} * {{ratioType}}：{{ratio}} = {{symbol}}{{total}}',
      {
        symbol: symbol,
        price: displayPrice,
        ratio: groupRatio,
        total: displayTotal,
        ratioType: ratioLabel,
      },
    );
  }

  if (completionRatio === undefined) {
    completionRatio = 0;
  }

  const modelRatioValue = formatRatioValue(modelRatio);
  const completionRatioValue = formatRatioValue(completionRatio);
  const cacheRatioValue = formatRatioValue(cacheRatio);
  const imageRatioValue = formatRatioValue(imageRatio);
  const audioRatioValue =
    audioInputSeperatePrice && aip > 0
      ? formatRatioValue(aip / inputRatioPrice)
      : null;

  const textInputTokens = Math.max(
    inputTokens - cacheTokens - audioInputTokens,
    0,
  );
  const imageInputTokens =
    image && imageOutputTokens > 0 ? imageOutputTokens : 0;
  const cacheInputTokens = cacheTokens;

  const textInputAmount =
    (textInputTokens / 1000000) * inputRatioPrice * groupRatio;
  const cacheInputAmount =
    (cacheInputTokens / 1000000) * cacheRatioPrice * groupRatio;
  const imageInputAmount =
    (imageInputTokens / 1000000) * imageInputUnitPrice * groupRatio;
  const audioInputAmount = (audioInputTokens / 1000000) * aip * groupRatio;
  const completionAmount =
    (completionTokens / 1000000) * completionRatioPrice * groupRatio;
  const webSearchAmount = (webSearchCallCount / 1000) * wsp * groupRatio;
  const fileSearchAmount = (fileSearchCallCount / 1000) * fsp * groupRatio;
  const imageGenerationAmount = igp * groupRatio;

  const totalAmount =
    textInputAmount +
    cacheInputAmount +
    imageInputAmount +
    audioInputAmount +
    completionAmount +
    webSearchAmount +
    fileSearchAmount +
    imageGenerationAmount;

  return renderBillingArticle([
    [
      buildBillingText('模型倍率 {{modelRatio}}', {
        modelRatio: modelRatioValue,
      }),
      buildBillingText('输出倍率 {{completionRatio}}', {
        completionRatio: completionRatioValue,
      }),
      cacheInputTokens > 0
        ? buildBillingText('缓存倍率 {{cacheRatio}}', {
            cacheRatio: cacheRatioValue,
          })
        : null,
      imageInputTokens > 0
        ? buildBillingText('图片倍率 {{imageRatio}}', {
            imageRatio: imageRatioValue,
          })
        : null,
      audioRatioValue !== null
        ? buildBillingText('音频倍率 {{audioRatio}}', {
            audioRatio: audioRatioValue,
          })
        : null,
      buildBillingText('{{ratioType}} {{ratio}}', {
        ratioType: ratioLabel,
        ratio: groupRatio,
      }),
    ]
      .filter(Boolean)
      .join('，'),
    textInputTokens > 0
      ? buildBillingText(
          '普通输入：{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
          {
            tokens: textInputTokens,
            modelRatio: modelRatioValue,
            ratioType: ratioLabel,
            ratio: groupRatio,
            amount: renderDisplayAmountFromUsd(textInputAmount),
          },
        )
      : null,
    cacheInputTokens > 0
      ? buildBillingText(
          '缓存输入：{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
          {
            tokens: cacheInputTokens,
            modelRatio: modelRatioValue,
            cacheRatio: cacheRatioValue,
            ratioType: ratioLabel,
            ratio: groupRatio,
            amount: renderDisplayAmountFromUsd(cacheInputAmount),
          },
        )
      : null,
    imageInputTokens > 0
      ? buildBillingText(
          '图片输入：{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 图片倍率 {{imageRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
          {
            tokens: imageInputTokens,
            modelRatio: modelRatioValue,
            imageRatio: imageRatioValue,
            ratioType: ratioLabel,
            ratio: groupRatio,
            amount: renderDisplayAmountFromUsd(imageInputAmount),
          },
        )
      : null,
    audioInputTokens > 0 && audioRatioValue !== null
      ? buildBillingText(
          '音频输入：{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
          {
            tokens: audioInputTokens,
            modelRatio: modelRatioValue,
            audioRatio: audioRatioValue,
            ratioType: ratioLabel,
            ratio: groupRatio,
            amount: renderDisplayAmountFromUsd(audioInputAmount),
          },
        )
      : null,
    buildBillingText(
      '输出：{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
      {
        tokens: completionTokens,
        modelRatio: modelRatioValue,
        completionRatio: completionRatioValue,
        ratioType: ratioLabel,
        ratio: groupRatio,
        amount: renderDisplayAmountFromUsd(completionAmount),
      },
    ),
    webSearch && webSearchCallCount > 0
      ? buildBillingText(
          'Web 搜索：{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}',
          {
            count: webSearchCallCount,
            price: renderDisplayAmountFromUsd(wsp),
            ratioType: ratioLabel,
            ratio: groupRatio,
            amount: renderDisplayAmountFromUsd(webSearchAmount),
          },
        )
      : null,
    fileSearch && fileSearchCallCount > 0
      ? buildBillingText(
          '文件搜索：{{count}} / 1K * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}',
          {
            count: fileSearchCallCount,
            price: renderDisplayAmountFromUsd(fsp),
            ratioType: ratioLabel,
            ratio: groupRatio,
            amount: renderDisplayAmountFromUsd(fileSearchAmount),
          },
        )
      : null,
    imageGenerationCall && imageGenerationCallPrice > 0
      ? buildBillingText(
          '图片生成：1 次 * 单价 {{price}} * {{ratioType}} {{ratio}} = {{amount}}',
          {
            price: renderDisplayAmountFromUsd(igp),
            ratioType: ratioLabel,
            ratio: groupRatio,
            amount: renderDisplayAmountFromUsd(imageGenerationAmount),
          },
        )
      : null,
    buildBillingText('合计：{{total}}', {
      total: renderDisplayAmountFromUsd(totalAmount),
    }),
  ]);
}

export function renderLogContent(
  modelRatio,
  completionRatio,
  modelPrice = -1,
  groupRatio,
  user_group_ratio,
  cacheRatio = 1.0,
  image = false,
  imageRatio = 1.0,
  webSearch = false,
  webSearchCallCount = 0,
  fileSearch = false,
  fileSearchCallCount = 0,
  displayMode = 'price',
  hideGroupRatioInDetail = false,
  // Video token-billing extras (from log.other for video task channels):
  // - videoRatio / videoCompletionRatio: per-token video multipliers.
  // - videoOutputTokens: estimated tokens for the generated video
  //   (duration*W*H*fps/1024); 0 means this log is not a video token-billed call.
  // - videoInputTextTokens: rough prompt token count.
  videoRatio = 0,
  videoCompletionRatio = 1.0,
  videoOutputTokens = 0,
  videoInputTextTokens = 0,
  billingMode = '',
  billedQuota = 0,
  /** 渠道价格折扣百分数（100=无折扣），与消费日志 other.channel_price_discount_percent 一致 */
  channelPriceDiscountPercent = 100,
  videoBillingDetail = {},
) {
  const {
    ratio,
    label: ratioLabel,
    useUserGroupRatio: useUserGroupRatio,
  } = getEffectiveRatio(groupRatio, user_group_ratio);

  // 获取货币配置
  const { symbol, rate } = getCurrencyConfig();
  const billingMeta =
    videoBillingDetail && typeof videoBillingDetail === 'object'
      ? videoBillingDetail
      : null;
  const rates = resolveConsumeLogBillingRates({
    modelRatio,
    completionRatio,
    cacheRatio,
    modelPrice,
    channelPriceDiscountPercent,
    billingMeta,
  });
  const {
    inputRatioPrice,
    completionRatioPrice,
    cacheRatioPrice,
    mp,
    mr,
  } = rates;

  const isVideoPerVideoFlatBilling =
    billingMode === 'video_per_video' &&
    (modelPrice === 0 || modelPrice === -1);
  const isVideoPerSecondFlatBilling =
    billingMode === 'video_per_second' &&
    (modelPrice === 0 || modelPrice === -1);
  if (isVideoPerSecondFlatBilling) {
    const estimatedTokens =
      Number.isFinite(Number(billedQuota)) && Number(billedQuota) > 0
        ? Math.round(Number(billedQuota))
        : 0;
    const seconds = Number(videoBillingDetail?.video_seconds || 0);
    const pricePerSecond = Number(
      videoBillingDetail?.video_price_per_second || 0,
    );
    const quotaPerUnit = Number(videoBillingDetail?.video_quota_per_unit || 0);
    const groupRatioForVideo = Number(videoBillingDetail?.group_ratio || 1);
    const channelDiscount = Number(
      videoBillingDetail?.channel_price_discount || 100,
    );
    const resolution = videoBillingDetail?.video_resolution || '';
    const ruleWidth = Number(videoBillingDetail?.video_rule_width || 0);
    const ruleHeight = Number(videoBillingDetail?.video_rule_height || 0);
    const width = Number(videoBillingDetail?.video_width || 0);
    const height = Number(videoBillingDetail?.video_height || 0);
    const hasAudio = videoBillingDetail?.video_has_audio === true;
    const unifiedAudio = videoBillingDetail?.video_unified_audio_price === true;
    const priceLabel = unifiedAudio
      ? i18next.t('每秒价')
      : hasAudio
        ? i18next.t('有音轨价')
        : i18next.t('无音轨价');
    const audioLabel = hasAudio ? i18next.t('有音轨') : i18next.t('无音轨');
    const hasDetail =
      seconds > 0 &&
      pricePerSecond > 0 &&
      quotaPerUnit > 0 &&
      (resolution || (ruleWidth > 0 && ruleHeight > 0));
    const parts = [
      hasDetail
        ? i18next.t(
            '分辨率阶梯计费：{{seconds}}秒 × {{resolution}}({{ruleWidth}}×{{ruleHeight}}，实际 {{width}}×{{height}}，{{audio}}) {{priceLabel}} ${{price}}/秒 × QuotaPerUnit {{quotaPerUnit}} × 分组倍率 {{groupRatio}} × 渠道折扣 {{channelDiscount}}%',
            {
              seconds,
              resolution: resolution || `${ruleWidth}x${ruleHeight}`,
              ruleWidth,
              ruleHeight,
              width,
              height,
              audio: audioLabel,
              priceLabel,
              price: pricePerSecond,
              quotaPerUnit,
              groupRatio: groupRatioForVideo || 1,
              channelDiscount,
            },
          )
        : i18next.t('分辨率阶梯计费'),
    ];
    if (estimatedTokens > 0) {
      parts.push(
        i18next.t('本次实际结算 tokens：{{count}}', { count: estimatedTokens }),
      );
    }
    if (!hideGroupRatioInDetail) {
      parts.push(getGroupRatioText(groupRatio, user_group_ratio));
    }
    return joinBillingSummary(parts);
  }
  if (isVideoPerVideoFlatBilling) {
    const estimatedTokens =
      Number.isFinite(Number(billedQuota)) && Number(billedQuota) > 0
        ? Math.round(Number(billedQuota))
        : 0;
    const parts = [i18next.t('按视频数量计费')];
    if (estimatedTokens > 0) {
      parts.push(
        i18next.t('本次实际结算 tokens：{{count}}', { count: estimatedTokens }),
      );
    }
    if (!hideGroupRatioInDetail) {
      parts.push(getGroupRatioText(groupRatio, user_group_ratio));
    }
    return joinBillingSummary(parts);
  }

  if (billingMode === 'image_per_image') {
    return renderImagePerImageBillingTags(videoBillingDetail, {
      modelPrice,
      groupRatio,
      user_group_ratio,
      channelPriceDiscountPercent,
    });
  }

  // Video token-billing branch: shown when the backend stamped the log with
  // video_ratio + video_output_tokens metadata. Has priority over the generic
  // per-token / per-call paths because modelPrice for these calls is 0 (not -1)
  // and would otherwise render as the misleading "模型价格 $0 / 次".
  const isVideoTokenBilling =
    videoOutputTokens > 0 &&
    videoRatio > 0 &&
    (modelPrice === 0 || modelPrice === -1);
  if (isVideoTokenBilling) {
    const displayMultiplier = hideGroupRatioInDetail ? ratio || 1 : 1;
    // Per the video token formula:
    //   quota = (inputTextTokens
    //            + outputVideoTokens * videoRatio * videoCompletionRatio
    //           ) * modelRatio * groupRatio
    // Effective $/1M-token unit prices are therefore:
    //   inputUnit  = modelRatio * 2          (text token price, same as chat)
    //   videoOut   = modelRatio * 2 * videoRatio * videoCompletionRatio
    const inputUnitPrice = inputRatioPrice * displayMultiplier * rate;
    const videoUnitPrice =
      inputRatioPrice *
      (videoRatio || 1) *
      (videoCompletionRatio || 1) *
      displayMultiplier *
      rate;
    const parts = [
      i18next.t('视频输出价格 {{symbol}}{{price}} / 1M tokens', {
        symbol,
        price: parseFloat(videoUnitPrice.toFixed(2)),
      }),
      i18next.t('估算视频 tokens：{{count}}', {
        count: videoOutputTokens,
      }),
    ];
    if (videoInputTextTokens > 0) {
      parts.push(
        i18next.t('文本输入价格 {{symbol}}{{price}} / 1M tokens', {
          symbol,
          price: parseFloat(inputUnitPrice.toFixed(2)),
        }),
      );
    }
    if (!hideGroupRatioInDetail) {
      parts.push(getGroupRatioText(groupRatio, user_group_ratio));
    }
    return joinBillingSummary(parts);
  }

  if (isPriceDisplayMode(displayMode, modelPrice)) {
    const displayMultiplier = hideGroupRatioInDetail ? ratio || 1 : 1;
    // Treat `modelPrice === 0` as "no per-call price configured" rather than
    // "explicitly $0 per call". Rationale:
    // - The default sentinel in this codebase is -1 (meaning "unset"), but
    //   legacy task logs (notably pre-video-token-billing video task logs)
    //   were stamped with literal 0 in `other.model_price`.
    // - Those logs are actually billed by ratio (and quota is non-zero), so
    //   showing "模型价格 $0 / 次" is misleading.
    // - When modelPrice is exactly 0, we either have a free model (handled
    //   by the ratio branch via modelRatio == 0) or a ratio-billed call we
    //   should render as such. Either way, falling through to the ratio
    //   rendering path is safer than rendering the misleading "$0 / 次".
    if (modelPrice !== -1 && modelPrice !== 0) {
      const parts = [
        i18next.t('模型价格 {{symbol}}{{price}} / 次', {
          symbol,
          price: parseFloat((mp * displayMultiplier * rate).toFixed(2)),
        }),
      ];
      if (!hideGroupRatioInDetail) {
        parts.push(getGroupRatioText(groupRatio, user_group_ratio));
      }
      return joinBillingSummary(parts);
    }

    const parts = [
      i18next.t('输入价格 {{symbol}}{{price}} / 1M tokens', {
        symbol,
        price: parseFloat((inputRatioPrice * displayMultiplier * rate).toFixed(2)),
      }),
      i18next.t('输出价格 {{symbol}}{{price}} / 1M tokens', {
        symbol,
        price: parseFloat(
          (completionRatioPrice * displayMultiplier * rate).toFixed(2),
        ),
      }),
    ];
    appendPricePart(
      parts,
      cacheRatio !== 1.0,
      '缓存读取价格 {{symbol}}{{price}} / 1M tokens',
      {
        symbol,
        price: parseFloat(
          (cacheRatioPrice * displayMultiplier * rate).toFixed(2),
        ),
      },
    );
    appendPricePart(
      parts,
      image,
      '图片输入价格 {{symbol}}{{price}} / 1M tokens',
      {
        symbol,
        price: parseFloat(
          (inputRatioPrice * imageRatio * displayMultiplier * rate).toFixed(2),
        ),
      },
    );
    appendPricePart(
      parts,
      webSearch,
      'Web 搜索调用 {{webSearchCallCount}} 次',
      {
        webSearchCallCount,
      },
    );
    appendPricePart(
      parts,
      fileSearch,
      '文件搜索调用 {{fileSearchCallCount}} 次',
      {
        fileSearchCallCount,
      },
    );
    if (!hideGroupRatioInDetail) {
      parts.push(getGroupRatioText(groupRatio, user_group_ratio));
    }
    return joinBillingSummary(parts);
  }

  if (isExplicitPerCallModelPrice(modelPrice)) {
    return i18next.t('模型价格 {{symbol}}{{price}}，{{ratioType}} {{ratio}}', {
      symbol: symbol,
      price: parseFloat((mp * rate).toFixed(2)),
      ratioType: ratioLabel,
      ratio,
    });
  } else {
    if (image) {
      return i18next.t(
        '模型倍率 {{modelRatio}}，缓存倍率 {{cacheRatio}}，输出倍率 {{completionRatio}}，图片输入倍率 {{imageRatio}}，{{ratioType}} {{ratio}}',
        {
          modelRatio: modelRatio,
          cacheRatio: cacheRatio,
          completionRatio: completionRatio,
          imageRatio: imageRatio,
          ratioType: ratioLabel,
          ratio,
        },
      );
    } else if (webSearch) {
      return i18next.t(
        '模型倍率 {{modelRatio}}，缓存倍率 {{cacheRatio}}，输出倍率 {{completionRatio}}，{{ratioType}} {{ratio}}，Web 搜索调用 {{webSearchCallCount}} 次',
        {
          modelRatio: modelRatio,
          cacheRatio: cacheRatio,
          completionRatio: completionRatio,
          ratioType: ratioLabel,
          ratio,
          webSearchCallCount,
        },
      );
    } else {
      return i18next.t(
        '模型倍率 {{modelRatio}}，缓存倍率 {{cacheRatio}}，输出倍率 {{completionRatio}}，{{ratioType}} {{ratio}}',
        {
          modelRatio: modelRatio,
          cacheRatio: cacheRatio,
          completionRatio: completionRatio,
          ratioType: ratioLabel,
          ratio,
        },
      );
    }
  }
}

export function renderModelPriceSimple(
  modelRatio,
  modelPrice = -1,
  groupRatio,
  user_group_ratio,
  cacheTokens = 0,
  cacheRatio = 1.0,
  cacheCreationTokens = 0,
  cacheCreationRatio = 1.0,
  cacheCreationTokens5m = 0,
  cacheCreationRatio5m = 1.0,
  cacheCreationTokens1h = 0,
  cacheCreationRatio1h = 1.0,
  image = false,
  imageRatio = 1.0,
  isSystemPromptOverride = false,
  provider = 'openai',
  displayMode = 'price',
  outputMode = 'text',
  // Video token-billing extras pulled from log.other for video task channels.
  videoRatio = 0,
  videoCompletionRatio = 1.0,
  videoOutputTokens = 0,
  videoInputTextTokens = 0,
  billingMode = '',
  /** 渠道价格折扣百分数（100=无折扣） */
  channelPriceDiscountPercent = 100,
  billingMeta = null,
) {
  return renderPriceSimpleCore({
    modelRatio,
    modelPrice,
    groupRatio,
    user_group_ratio,
    cacheTokens,
    cacheRatio,
    cacheCreationTokens,
    cacheCreationRatio,
    cacheCreationTokens5m,
    cacheCreationRatio5m,
    cacheCreationTokens1h,
    cacheCreationRatio1h,
    image,
    imageRatio,
    isSystemPromptOverride,
    displayMode,
    outputMode,
    videoRatio,
    videoCompletionRatio,
    videoOutputTokens,
    videoInputTextTokens,
    billingMode,
    channelPriceDiscountPercent,
    billingMeta,
  });
}

export function renderAudioModelPrice(
  inputTokens,
  completionTokens,
  modelRatio,
  modelPrice = -1,
  completionRatio,
  audioInputTokens,
  audioCompletionTokens,
  audioRatio,
  audioCompletionRatio,
  groupRatio,
  user_group_ratio,
  cacheTokens = 0,
  cacheRatio = 1.0,
  displayMode = 'price',
) {
  const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
    groupRatio,
    user_group_ratio,
  );
  groupRatio = effectiveGroupRatio;

  // 获取货币配置
  const { symbol, rate } = getCurrencyConfig();

  if (!shouldUseRatioBillingProcess(modelPrice)) {
    if (isExplicitPerCallModelPrice(modelPrice)) {
      return renderBillingArticle([
        buildBillingPriceText('模型价格：{{symbol}}{{price}} / 次', {
          symbol,
          usdAmount: modelPrice,
          rate,
        }),
        buildBillingPriceText(
          '模型价格 {{symbol}}{{price}} / 次 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}',
          {
            symbol,
            usdAmount: modelPrice,
            rate,
            ratioType: ratioLabel,
            ratio: groupRatio,
            total: formatBillingDisplayPrice(modelPrice * groupRatio, rate),
          },
        ),
      ]);
    }

    if (completionRatio === undefined) {
      completionRatio = 0;
    }
    audioRatio = parseFloat(parseFloat(audioRatio).toFixed(2));
    const inputRatioPrice = modelRatio * 2.0;
    const completionRatioPrice = modelRatio * 2.0 * completionRatio;
    const textPrice =
      ((inputTokens - cacheTokens + cacheTokens * cacheRatio) / 1000000) *
        inputRatioPrice *
        groupRatio +
      (completionTokens / 1000000) * completionRatioPrice * groupRatio;
    const audioPrice =
      (audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio +
      (audioCompletionTokens / 1000000) *
        inputRatioPrice *
        audioRatio *
        audioCompletionRatio *
        groupRatio;
    const totalPrice = textPrice + audioPrice;

    return renderBillingArticle([
      buildBillingPriceText('输入价格：{{symbol}}{{price}} / 1M tokens', {
        symbol,
        usdAmount: inputRatioPrice,
        rate,
      }),
      buildBillingPriceText('输出价格：{{symbol}}{{price}} / 1M tokens', {
        symbol,
        usdAmount: completionRatioPrice,
        rate,
      }),
      cacheTokens > 0
        ? buildBillingPriceText(
            '缓存读取价格：{{symbol}}{{price}} / 1M tokens',
            {
              symbol,
              usdAmount: inputRatioPrice * cacheRatio,
              rate,
            },
          )
        : null,
      buildBillingPriceText('音频输入价格：{{symbol}}{{price}} / 1M tokens', {
        symbol,
        usdAmount: inputRatioPrice * audioRatio,
        rate,
      }),
      buildBillingPriceText('音频输出价格：{{symbol}}{{price}} / 1M tokens', {
        symbol,
        usdAmount: inputRatioPrice * audioRatio * audioCompletionRatio,
        rate,
      }),
      buildBillingText(
        '文字提示 {{input}} tokens / 1M tokens * {{symbol}}{{textInputPrice}} + 文字输出 {{completion}} tokens / 1M tokens * {{symbol}}{{textCompPrice}} + 音频提示 {{audioInput}} tokens / 1M tokens * {{symbol}}{{audioInputPrice}} + 音频输出 {{audioCompletion}} tokens / 1M tokens * {{symbol}}{{audioCompPrice}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}',
        {
          input: inputTokens,
          completion: completionTokens,
          audioInput: audioInputTokens,
          audioCompletion: audioCompletionTokens,
          textInputPrice: formatBillingDisplayPrice(inputRatioPrice, rate),
          textCompPrice: formatBillingDisplayPrice(completionRatioPrice, rate),
          audioInputPrice: formatBillingDisplayPrice(
            audioRatio * inputRatioPrice,
            rate,
          ),
          audioCompPrice: formatBillingDisplayPrice(
            audioRatio * audioCompletionRatio * inputRatioPrice,
            rate,
          ),
          ratioType: ratioLabel,
          ratio: groupRatio,
          symbol,
          total: formatBillingDisplayPrice(totalPrice, rate),
        },
      ),
    ]);
  }

  // 1 ratio = $0.002 / 1K tokens
  if (isExplicitPerCallModelPrice(modelPrice)) {
    return i18next.t(
      '模型价格：{{symbol}}{{price}} * {{ratioType}}：{{ratio}} = {{symbol}}{{total}}',
      {
        symbol: symbol,
        price: parseFloat((modelPrice * rate).toFixed(2)),
        ratio: groupRatio,
        total: parseFloat((modelPrice * groupRatio * rate).toFixed(2)),
        ratioType: ratioLabel,
      },
    );
  }

  if (completionRatio === undefined) {
    completionRatio = 0;
  }

  const modelRatioValue = formatRatioValue(modelRatio);
  const completionRatioValue = formatRatioValue(completionRatio);
  const cacheRatioValue = formatRatioValue(cacheRatio);
  const audioRatioValue = formatRatioValue(audioRatio);
  const audioCompletionRatioValue = formatRatioValue(audioCompletionRatio);

  const inputRatioPrice = modelRatio * 2.0;
  const completionRatioPrice = modelRatio * 2.0 * completionRatioValue;

  const effectiveInputTokens =
    inputTokens - cacheTokens + cacheTokens * cacheRatioValue;

  const textPrice =
    (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
    (completionTokens / 1000000) * completionRatioPrice * groupRatio;
  const audioPrice =
    (audioInputTokens / 1000000) *
      inputRatioPrice *
      audioRatioValue *
      groupRatio +
    (audioCompletionTokens / 1000000) *
      inputRatioPrice *
      audioRatioValue *
      audioCompletionRatioValue *
      groupRatio;
  const totalPrice = textPrice + audioPrice;

  return renderBillingArticle([
    buildBillingText(
      '模型倍率 {{modelRatio}}，输出倍率 {{completionRatio}}，音频倍率 {{audioRatio}}，音频输出倍率 {{audioCompletionRatio}}，{{cachePart}}{{ratioType}} {{ratio}}',
      {
        modelRatio: modelRatioValue,
        completionRatio: completionRatioValue,
        audioRatio: audioRatioValue,
        audioCompletionRatio: audioCompletionRatioValue,
        cachePart:
          cacheTokens > 0
            ? `${i18next.t('缓存倍率')} ${cacheRatioValue}，`
            : '',
        ratioType: ratioLabel,
        ratio: groupRatio,
      },
    ),
    buildBillingText(
      '普通输入：{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
      {
        tokens: Math.max(inputTokens - cacheTokens, 0),
        modelRatio: modelRatioValue,
        ratioType: ratioLabel,
        ratio: groupRatio,
        amount: renderDisplayAmountFromUsd(
          (Math.max(inputTokens - cacheTokens, 0) / 1000000) *
            inputRatioPrice *
            groupRatio,
        ),
      },
    ),
    cacheTokens > 0
      ? buildBillingText(
          '缓存输入：{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
          {
            tokens: cacheTokens,
            modelRatio: modelRatioValue,
            cacheRatio: cacheRatioValue,
            ratioType: ratioLabel,
            ratio: groupRatio,
            amount: renderDisplayAmountFromUsd(
              (cacheTokens / 1000000) *
                inputRatioPrice *
                cacheRatioValue *
                groupRatio,
            ),
          },
        )
      : null,
    buildBillingText(
      '文字输出：{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
      {
        tokens: completionTokens,
        modelRatio: modelRatioValue,
        completionRatio: completionRatioValue,
        ratioType: ratioLabel,
        ratio: groupRatio,
        amount: renderDisplayAmountFromUsd(
          (completionTokens / 1000000) *
            inputRatioPrice *
            completionRatioValue *
            groupRatio,
        ),
      },
    ),
    buildBillingText(
      '音频输入：{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
      {
        tokens: audioInputTokens,
        modelRatio: modelRatioValue,
        audioRatio: audioRatioValue,
        ratioType: ratioLabel,
        ratio: groupRatio,
        amount: renderDisplayAmountFromUsd(
          (audioInputTokens / 1000000) *
            inputRatioPrice *
            audioRatioValue *
            groupRatio,
        ),
      },
    ),
    buildBillingText(
      '音频输出：{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 音频倍率 {{audioRatio}} * 音频输出倍率 {{audioCompletionRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
      {
        tokens: audioCompletionTokens,
        modelRatio: modelRatioValue,
        audioRatio: audioRatioValue,
        audioCompletionRatio: audioCompletionRatioValue,
        ratioType: ratioLabel,
        ratio: groupRatio,
        amount: renderDisplayAmountFromUsd(
          (audioCompletionTokens / 1000000) *
            inputRatioPrice *
            audioRatioValue *
            audioCompletionRatioValue *
            groupRatio,
        ),
      },
    ),
    buildBillingText(
      '合计：文字部分 {{textTotal}} + 音频部分 {{audioTotal}} = {{total}}',
      {
        textTotal: renderDisplayAmountFromUsd(textPrice),
        audioTotal: renderDisplayAmountFromUsd(audioPrice),
        total: renderDisplayAmountFromUsd(totalPrice),
      },
    ),
  ]);
}

export function renderQuotaWithPrompt(quota, digits) {
  const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD';
  if (quotaDisplayType !== 'TOKENS') {
    return i18next.t('等价金额：') + renderQuota(quota, digits);
  }
  return '';
}

export function renderClaudeModelPrice(
  inputTokens,
  completionTokens,
  modelRatio,
  modelPrice = -1,
  completionRatio,
  groupRatio,
  user_group_ratio,
  cacheTokens = 0,
  cacheRatio = 1.0,
  cacheCreationTokens = 0,
  cacheCreationRatio = 1.0,
  cacheCreationTokens5m = 0,
  cacheCreationRatio5m = 1.0,
  cacheCreationTokens1h = 0,
  cacheCreationRatio1h = 1.0,
  displayMode = 'price',
  /** 渠道价格折扣百分数（100=无折扣），与消费日志 other.channel_price_discount_percent 一致 */
  channelPriceDiscountPercent = 100,
  /** 同 renderModelPrice 的 forcePriceVolumeParametricDetail */
  forcePriceVolumeParametricDetail = false,
  /** 同 renderModelPrice 的 billingProcessFoldGroupMultiplier */
  billingProcessFoldGroupMultiplier = false,
  /** 消费日志 other（含 global_*、markup_discount_rate） */
  billingMeta = null,
) {
  const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
    groupRatio,
    user_group_ratio,
  );
  groupRatio = effectiveGroupRatio;

  const consumeFoldGroup =
    billingProcessFoldGroupMultiplier &&
    Number.isFinite(Number(groupRatio)) &&
    Number(groupRatio) > 0;
  const gDisp = consumeFoldGroup ? Number(groupRatio) : 1;

  // 获取货币配置
  const { symbol, rate } = getCurrencyConfig();
  const rates = resolveConsumeLogBillingRates({
    modelRatio,
    completionRatio,
    cacheRatio,
    cacheCreationRatio,
    cacheCreationRatio5m,
    cacheCreationRatio1h,
    modelPrice,
    channelPriceDiscountPercent,
    billingMeta,
  });
  const {
    inputRatioPrice,
    completionRatioPrice,
    cacheRatioPrice,
    cacheCreationRatioPrice,
    cacheCreationRatioPrice5m,
    cacheCreationRatioPrice1h,
    mp,
  } = rates;

  const useRatioOnlySummary =
    shouldUseRatioBillingProcess(modelPrice) &&
    !forcePriceVolumeParametricDetail;

  if (!useRatioOnlySummary) {
    if (isExplicitPerCallModelPrice(modelPrice)) {
      return renderBillingArticle([
        buildBillingPriceText('模型价格：{{symbol}}{{price}} / 次', {
          symbol,
          usdAmount: mp,
          rate,
        }),
        buildBillingPriceText(
          '模型价格 {{symbol}}{{price}} / 次 * {{ratioType}} {{ratio}} = {{symbol}}{{total}}',
          {
            symbol,
            usdAmount: mp,
            rate,
            ratioType: ratioLabel,
            ratio: groupRatio,
            total: formatBillingDisplayPrice(mp * groupRatio, rate),
          },
        ),
      ]);
    }

    if (completionRatio === undefined) {
      completionRatio = 0;
    }

    const hasSplitCacheCreation =
      cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
    const legacyCacheCreationTokens = hasSplitCacheCreation
      ? 0
      : cacheCreationTokens;
    const effectiveInputTokens =
      inputTokens +
      cacheTokens * cacheRatio +
      legacyCacheCreationTokens * cacheCreationRatio +
      cacheCreationTokens5m * cacheCreationRatio5m +
      cacheCreationTokens1h * cacheCreationRatio1h;
    const price =
      (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
      (completionTokens / 1000000) * completionRatioPrice * groupRatio;
    const inputUnitPrice = inputRatioPrice * gDisp * rate;
    const completionUnitPrice = completionRatioPrice * gDisp * rate;
    const cacheUnitPrice = cacheRatioPrice * gDisp * rate;
    const cacheCreationUnitPrice = cacheCreationRatioPrice * gDisp * rate;
    const cacheCreationUnitPrice5m = cacheCreationRatioPrice5m * gDisp * rate;
    const cacheCreationUnitPrice1h = cacheCreationRatioPrice1h * gDisp * rate;
    const shouldShowCache = cacheTokens > 0;
    const shouldShowLegacyCacheCreation =
      !hasSplitCacheCreation && cacheCreationTokens > 0;
    const shouldShowCacheCreation5m =
      hasSplitCacheCreation && cacheCreationTokens5m > 0;
    const shouldShowCacheCreation1h =
      hasSplitCacheCreation && cacheCreationTokens1h > 0;

    const breakdownSegments = [
      i18next.t('提示 {{input}} tokens / 1M tokens * {{symbol}}{{price}}', {
        input: inputTokens,
        symbol,
        price: inputUnitPrice.toFixed(2),
      }),
    ];

    if (shouldShowCache) {
      breakdownSegments.push(
        i18next.t('缓存 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}', {
          tokens: cacheTokens,
          symbol,
          price: cacheUnitPrice.toFixed(2),
        }),
      );
    }

    if (shouldShowLegacyCacheCreation) {
      breakdownSegments.push(
        i18next.t(
          '缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}',
          {
            tokens: cacheCreationTokens,
            symbol,
            price: cacheCreationUnitPrice.toFixed(2),
          },
        ),
      );
    }

    if (shouldShowCacheCreation5m) {
      breakdownSegments.push(
        i18next.t(
          '5m缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}',
          {
            tokens: cacheCreationTokens5m,
            symbol,
            price: cacheCreationUnitPrice5m.toFixed(2),
          },
        ),
      );
    }

    if (shouldShowCacheCreation1h) {
      breakdownSegments.push(
        i18next.t(
          '1h缓存创建 {{tokens}} tokens / 1M tokens * {{symbol}}{{price}}',
          {
            tokens: cacheCreationTokens1h,
            symbol,
            price: cacheCreationUnitPrice1h.toFixed(2),
          },
        ),
      );
    }

    breakdownSegments.push(
      i18next.t(
        '输出 {{completion}} tokens / 1M tokens * {{symbol}}{{price}}',
        {
          completion: completionTokens,
          symbol,
          price: completionUnitPrice.toFixed(2),
        },
      ),
    );

    const breakdownText = breakdownSegments.join(' + ');

    return renderBillingArticle([
      buildBillingPriceText('输入价格：{{symbol}}{{price}} / 1M tokens', {
        symbol,
        usdAmount: inputRatioPrice * gDisp,
        rate,
      }),
      buildBillingPriceText('输出价格：{{symbol}}{{price}} / 1M tokens', {
        symbol,
        usdAmount: completionRatioPrice * gDisp,
        rate,
      }),
      cacheTokens > 0
        ? buildBillingPriceText(
            '缓存读取价格：{{symbol}}{{price}} / 1M tokens',
            {
              symbol,
              usdAmount: cacheRatioPrice * gDisp,
              rate,
            },
          )
        : null,
      !hasSplitCacheCreation && cacheCreationTokens > 0
        ? buildBillingPriceText(
            '缓存创建价格：{{symbol}}{{price}} / 1M tokens',
            {
              symbol,
              usdAmount: cacheCreationRatioPrice * gDisp,
              rate,
            },
          )
        : null,
      hasSplitCacheCreation && cacheCreationTokens5m > 0
        ? buildBillingPriceText(
            '5m缓存创建价格：{{symbol}}{{price}} / 1M tokens',
            {
              symbol,
              usdAmount: cacheCreationRatioPrice5m * gDisp,
              rate,
            },
          )
        : null,
      hasSplitCacheCreation && cacheCreationTokens1h > 0
        ? buildBillingPriceText(
            '1h缓存创建价格：{{symbol}}{{price}} / 1M tokens',
            {
              symbol,
              usdAmount: cacheCreationRatioPrice1h * gDisp,
              rate,
            },
          )
        : null,
      billingProcessFoldGroupMultiplier
        ? buildBillingText('{{breakdown}} = {{symbol}}{{total}}', {
            breakdown: breakdownText,
            symbol,
            total: formatBillingDisplayPrice(price, rate),
          })
        : buildBillingText(
            '{{breakdown}} * {{ratioType}} {{ratio}} = {{symbol}}{{total}}',
            {
              breakdown: breakdownText,
              ratioType: ratioLabel,
              ratio: groupRatio,
              symbol,
              total: formatBillingDisplayPrice(price, rate),
            },
          ),
    ]);
  }

  if (isExplicitPerCallModelPrice(modelPrice)) {
    return i18next.t(
      '模型价格：{{symbol}}{{price}} * {{ratioType}}：{{ratio}} = {{symbol}}{{total}}',
      {
        symbol: symbol,
        price: parseFloat((mp * rate).toFixed(2)),
        ratioType: ratioLabel,
        ratio: groupRatio,
        total: parseFloat((mp * groupRatio * rate).toFixed(2)),
      },
    );
  }

  if (completionRatio === undefined) {
    completionRatio = 0;
  }

  const modelRatioValue = formatRatioValue(modelRatio);
  const completionRatioValue = formatRatioValue(completionRatio);
  const cacheRatioValue = formatRatioValue(cacheRatio);
  const cacheCreationRatioValue = formatRatioValue(cacheCreationRatio);
  const cacheCreationRatio5mValue = formatRatioValue(cacheCreationRatio5m);
  const cacheCreationRatio1hValue = formatRatioValue(cacheCreationRatio1h);

  const hasSplitCacheCreation =
    cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
  const shouldShowCache = cacheTokens > 0;
  const shouldShowLegacyCacheCreation =
    !hasSplitCacheCreation && cacheCreationTokens > 0;
  const shouldShowCacheCreation5m =
    hasSplitCacheCreation && cacheCreationTokens5m > 0;
  const shouldShowCacheCreation1h =
    hasSplitCacheCreation && cacheCreationTokens1h > 0;

  const legacyCacheCreationTokens = hasSplitCacheCreation
    ? 0
    : cacheCreationTokens;
  const effectiveInputTokens =
    inputTokens +
    cacheTokens * cacheRatioValue +
    legacyCacheCreationTokens * cacheCreationRatioValue +
    cacheCreationTokens5m * cacheCreationRatio5mValue +
    cacheCreationTokens1h * cacheCreationRatio1hValue;

  const totalAmount =
    (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio +
    (completionTokens / 1000000) * completionRatioPrice * groupRatio;

  return renderBillingArticle([
    buildBillingText(
      '模型倍率 {{modelRatio}}，输出倍率 {{completionRatio}}，缓存倍率 {{cacheRatio}}，{{ratioType}} {{ratio}}',
      {
        modelRatio: modelRatioValue,
        completionRatio: completionRatioValue,
        cacheRatio: cacheRatioValue,
        ratioType: ratioLabel,
        ratio: groupRatio,
      },
    ),
    hasSplitCacheCreation
      ? buildBillingText(
          '缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}',
          {
            cacheCreationRatio5m: cacheCreationRatio5mValue,
            cacheCreationRatio1h: cacheCreationRatio1hValue,
          },
        )
      : buildBillingText('缓存创建倍率 {{cacheCreationRatio}}', {
          cacheCreationRatio: cacheCreationRatioValue,
        }),
    buildBillingText(
      '普通输入：{{tokens}} / 1M * 模型倍率 {{modelRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
      {
        tokens: inputTokens,
        modelRatio: modelRatioValue,
        ratioType: ratioLabel,
        ratio: groupRatio,
        amount: renderDisplayAmountFromUsd(
          (inputTokens / 1000000) * inputRatioPrice * groupRatio,
        ),
      },
    ),
    shouldShowCache
      ? buildBillingText(
          '缓存读取：{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存倍率 {{cacheRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
          {
            tokens: cacheTokens,
            modelRatio: modelRatioValue,
            cacheRatio: cacheRatioValue,
            ratioType: ratioLabel,
            ratio: groupRatio,
            amount: renderDisplayAmountFromUsd(
              (cacheTokens / 1000000) *
                inputRatioPrice *
                cacheRatioValue *
                groupRatio,
            ),
          },
        )
      : null,
    shouldShowLegacyCacheCreation
      ? buildBillingText(
          '缓存创建：{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 缓存创建倍率 {{cacheCreationRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
          {
            tokens: cacheCreationTokens,
            modelRatio: modelRatioValue,
            cacheCreationRatio: cacheCreationRatioValue,
            ratioType: ratioLabel,
            ratio: groupRatio,
            amount: renderDisplayAmountFromUsd(
              (cacheCreationTokens / 1000000) *
                inputRatioPrice *
                cacheCreationRatioValue *
                groupRatio,
            ),
          },
        )
      : null,
    shouldShowCacheCreation5m
      ? buildBillingText(
          '5m缓存创建：{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 5m缓存创建倍率 {{cacheCreationRatio5m}} * {{ratioType}} {{ratio}} = {{amount}}',
          {
            tokens: cacheCreationTokens5m,
            modelRatio: modelRatioValue,
            cacheCreationRatio5m: cacheCreationRatio5mValue,
            ratioType: ratioLabel,
            ratio: groupRatio,
            amount: renderDisplayAmountFromUsd(
              (cacheCreationTokens5m / 1000000) *
                inputRatioPrice *
                cacheCreationRatio5mValue *
                groupRatio,
            ),
          },
        )
      : null,
    shouldShowCacheCreation1h
      ? buildBillingText(
          '1h缓存创建：{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 1h缓存创建倍率 {{cacheCreationRatio1h}} * {{ratioType}} {{ratio}} = {{amount}}',
          {
            tokens: cacheCreationTokens1h,
            modelRatio: modelRatioValue,
            cacheCreationRatio1h: cacheCreationRatio1hValue,
            ratioType: ratioLabel,
            ratio: groupRatio,
            amount: renderDisplayAmountFromUsd(
              (cacheCreationTokens1h / 1000000) *
                inputRatioPrice *
                cacheCreationRatio1hValue *
                groupRatio,
            ),
          },
        )
      : null,
    buildBillingText(
      '输出 {{completion}} tokens * 输出倍率 {{completionRatio}}',
      {
        completion: completionTokens,
        completionRatio: completionRatioValue,
      },
    ),
    buildBillingText(
      '输出：{{tokens}} / 1M * 模型倍率 {{modelRatio}} * 输出倍率 {{completionRatio}} * {{ratioType}} {{ratio}} = {{amount}}',
      {
        tokens: completionTokens,
        modelRatio: modelRatioValue,
        completionRatio: completionRatioValue,
        ratioType: ratioLabel,
        ratio: groupRatio,
        amount: renderDisplayAmountFromUsd(
          (completionTokens / 1000000) *
            inputRatioPrice *
            completionRatioValue *
            groupRatio,
        ),
      },
    ),
    buildBillingText('合计：{{total}}', {
      total: renderDisplayAmountFromUsd(totalAmount),
    }),
  ]);
}

export function renderClaudeLogContent(
  modelRatio,
  completionRatio,
  modelPrice = -1,
  groupRatio,
  user_group_ratio,
  cacheRatio = 1.0,
  cacheCreationRatio = 1.0,
  cacheCreationTokens5m = 0,
  cacheCreationRatio5m = 1.0,
  cacheCreationTokens1h = 0,
  cacheCreationRatio1h = 1.0,
  displayMode = 'price',
  hideGroupRatioInDetail = false,
  /** 渠道价格折扣百分数（100=无折扣），与消费日志 other.channel_price_discount_percent 一致 */
  channelPriceDiscountPercent = 100,
  /** 消费日志 other（含 global_*、markup_discount_rate） */
  billingMeta = null,
) {
  const { ratio: effectiveGroupRatio, label: ratioLabel } = getEffectiveRatio(
    groupRatio,
    user_group_ratio,
  );
  groupRatio = effectiveGroupRatio;

  // 获取货币配置
  const { symbol, rate } = getCurrencyConfig();
  const rates = resolveConsumeLogBillingRates({
    modelRatio,
    completionRatio,
    cacheRatio,
    cacheCreationRatio,
    cacheCreationRatio5m,
    cacheCreationRatio1h,
    modelPrice,
    channelPriceDiscountPercent,
    billingMeta,
  });
  const {
    inputRatioPrice,
    completionRatioPrice,
    cacheRatioPrice,
    cacheCreationRatioPrice,
    cacheCreationRatioPrice5m,
    cacheCreationRatioPrice1h,
    mp,
  } = rates;

  if (isPriceDisplayMode(displayMode, modelPrice)) {
    const displayMultiplier = hideGroupRatioInDetail
      ? effectiveGroupRatio || 1
      : 1;
    if (isExplicitPerCallModelPrice(modelPrice)) {
      const parts = [
        i18next.t('模型价格 {{symbol}}{{price}} / 次', {
          symbol,
          price: parseFloat((mp * displayMultiplier * rate).toFixed(2)),
        }),
      ];
      if (!hideGroupRatioInDetail) {
        parts.push(getGroupRatioText(groupRatio, user_group_ratio));
      }
      return joinBillingSummary(parts);
    }

    const parts = [
      i18next.t('输入价格 {{symbol}}{{price}} / 1M tokens', {
        symbol,
        price: parseFloat((inputRatioPrice * displayMultiplier * rate).toFixed(2)),
      }),
      i18next.t('输出价格 {{symbol}}{{price}} / 1M tokens', {
        symbol,
        price: parseFloat(
          (completionRatioPrice * displayMultiplier * rate).toFixed(2),
        ),
      }),
      i18next.t('缓存读取价格 {{symbol}}{{price}} / 1M tokens', {
        symbol,
        price: parseFloat(
          (cacheRatioPrice * displayMultiplier * rate).toFixed(2),
        ),
      }),
    ];
    const hasSplitCacheCreation =
      cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
    appendPricePart(
      parts,
      hasSplitCacheCreation && cacheCreationTokens5m > 0,
      '5m缓存创建价格 {{symbol}}{{price}} / 1M tokens',
      {
        symbol,
        price: parseFloat(
          (cacheCreationRatioPrice5m * displayMultiplier * rate).toFixed(2),
        ),
      },
    );
    appendPricePart(
      parts,
      hasSplitCacheCreation && cacheCreationTokens1h > 0,
      '1h缓存创建价格 {{symbol}}{{price}} / 1M tokens',
      {
        symbol,
        price: parseFloat(
          (cacheCreationRatioPrice1h * displayMultiplier * rate).toFixed(2),
        ),
      },
    );
    appendPricePart(
      parts,
      !hasSplitCacheCreation,
      '缓存创建价格 {{symbol}}{{price}} / 1M tokens',
      {
        symbol,
        price: parseFloat(
          (cacheCreationRatioPrice * displayMultiplier * rate).toFixed(2),
        ),
      },
    );
    if (!hideGroupRatioInDetail) {
      parts.push(getGroupRatioText(groupRatio, user_group_ratio));
    }
    return joinBillingSummary(parts);
  }

  if (isExplicitPerCallModelPrice(modelPrice)) {
    return i18next.t('模型价格 {{symbol}}{{price}}，{{ratioType}} {{ratio}}', {
      symbol: symbol,
      price: parseFloat((mp * rate).toFixed(2)),
      ratioType: ratioLabel,
      ratio: groupRatio,
    });
  } else {
    const hasSplitCacheCreation =
      cacheCreationTokens5m > 0 || cacheCreationTokens1h > 0;
    const shouldShowCacheCreation5m =
      hasSplitCacheCreation && cacheCreationTokens5m > 0;
    const shouldShowCacheCreation1h =
      hasSplitCacheCreation && cacheCreationTokens1h > 0;

    let cacheCreationPart = null;
    if (hasSplitCacheCreation) {
      if (shouldShowCacheCreation5m && shouldShowCacheCreation1h) {
        cacheCreationPart = i18next.t(
          '缓存创建倍率 5m {{cacheCreationRatio5m}} / 1h {{cacheCreationRatio1h}}',
          {
            cacheCreationRatio5m,
            cacheCreationRatio1h,
          },
        );
      } else if (shouldShowCacheCreation5m) {
        cacheCreationPart = i18next.t(
          '缓存创建倍率 5m {{cacheCreationRatio5m}}',
          {
            cacheCreationRatio5m,
          },
        );
      } else if (shouldShowCacheCreation1h) {
        cacheCreationPart = i18next.t(
          '缓存创建倍率 1h {{cacheCreationRatio1h}}',
          {
            cacheCreationRatio1h,
          },
        );
      }
    }

    if (!cacheCreationPart) {
      cacheCreationPart = i18next.t('缓存创建倍率 {{cacheCreationRatio}}', {
        cacheCreationRatio,
      });
    }

    const parts = [
      i18next.t('模型倍率 {{modelRatio}}', { modelRatio }),
      i18next.t('输出倍率 {{completionRatio}}', { completionRatio }),
      i18next.t('缓存倍率 {{cacheRatio}}', { cacheRatio }),
      cacheCreationPart,
      i18next.t('{{ratioType}} {{ratio}}', {
        ratioType: ratioLabel,
        ratio: groupRatio,
      }),
    ];

    return parts.join('，');
  }
}

/**
 * 阶梯区间上界展示，0 表示无穷档。
 * @param {number|string} to
 * @returns {string}
 */
function formatRequestTierToBoundary(to) {
  const n = Number(to);
  if (!Number.isFinite(n) || n === 0) {
    return '∞';
  }
  return String(n);
}

/**
 * 消费日志「计费过程」：阶梯计费（request_tier_breakdown）说明。
 * @param {object} other 解析后的 log.other
 * @param {number} channelPriceDiscountPercent 渠道折扣百分数（100=无折扣）
 * @param {function} tr i18n 翻译函数
 * @returns {JSX.Element}
 */
function renderRequestTierConsumeArticle(
  other,
  channelPriceDiscountPercent,
  tr = i18next.t.bind(i18next),
) {
  const bd = other?.request_tier_breakdown || {};
  const lines = [];
  lines.push(`${tr('阶梯计费')}（progressive）`);
  const pct = Number(
    other?.channel_price_discount_percent ?? channelPriceDiscountPercent,
  );
  if (Number.isFinite(pct) && pct > 0 && pct < 100) {
    lines.push(`${tr('渠道价格折扣(%)')} ${pct}%`);
  }
  lines.push(`${tr('说明')}：${tr('阶梯计费价格明细')}`);
  /**
   * 追加「阶梯前原始值 → 阶梯折算后加权值」一行（字段来自后端 RequestTierPricingBreakdown）。
   * @param {string} label 行标题
   * @param {string} beforeKey breakdown 字段名（before）
   * @param {string} afterKey breakdown 字段名（after）
   */
  const pushBeforeAfter = (label, beforeKey, afterKey) => {
    const b = bd[beforeKey];
    const a = bd[afterKey];
    if (b == null && a == null) {
      return;
    }
    lines.push(`${label}：${b ?? '-'} → ${a ?? '-'}`);
  };
  pushBeforeAfter(`${tr('输入')} tokens`, 'input_before', 'input_after');
  pushBeforeAfter(`${tr('输出')} tokens`, 'output_before', 'output_after');
  pushBeforeAfter(
    `${tr('缓存读取价格')} (tokens)`,
    'cache_read_before',
    'cache_read_after',
  );
  pushBeforeAfter(
    `${tr('缓存创建')} (tokens)`,
    'cache_write_before',
    'cache_write_after',
  );

  const catLabels = {
    input: tr('输入'),
    output: tr('输出'),
    cache_read: tr('缓存读取价格'),
    cache_write: tr('缓存创建'),
  };
  const details =
    bd.details && typeof bd.details === 'object' ? bd.details : {};
  for (const cat of Object.keys(details)) {
    const items = details[cat];
    if (!Array.isArray(items) || !items.length) {
      continue;
    }
    lines.push(`— ${catLabels[cat] || cat} —`);
    for (const it of items) {
      lines.push(
        `  [${it.from}, ${formatRequestTierToBoundary(it.to)})  ${it.tokens} tokens × ${it.ratio} ⇒ ${it.result}`,
      );
    }
  }
  const mr = other?.model_ratio;
  const gr = other?.group_ratio;
  const ugr = other?.user_group_ratio;
  lines.push(
    `${tr('模型倍率')} ${mr} × ${isValidGroupRatio(ugr) ? `${tr('专属倍率')} ${ugr}` : `${tr('分组倍率')} ${gr}`} → ${tr('额度')}`,
  );
  return renderBillingArticle(lines);
}

/**
 * 使用日志消费记录「计费过程」：视频任务 / 阶梯 / 按次 / 按量（与系统计费方式一致）。
 * @param {object} opts
 * @param {object} opts.record 日志行
 * @param {object} opts.other 解析后的 other
 * @param {'price'|'ratio'} [opts.billingDisplayMode]
 * @param {number} [opts.channelPriceDiscountPercent]
 * @param {function} [opts.t] i18n `t`
 * @returns {JSX.Element}
 */
export function renderConsumeBillingProcess({
  record,
  other,
  billingDisplayMode = 'price',
  channelPriceDiscountPercent = 100,
  t = i18next.t.bind(i18next),
}) {
  const tr = typeof t === 'function' ? t : i18next.t.bind(i18next);
  const chPct = Number(
    other?.channel_price_discount_percent ?? channelPriceDiscountPercent ?? 100,
  );
  const bm = other?.billing_mode;
  if (bm === 'image_per_image') {
    return renderImagePerImageBillingTags(
      other,
      {
        modelPrice: other?.model_price,
        groupRatio: other?.group_ratio,
        user_group_ratio: other?.user_group_ratio,
        channelPriceDiscountPercent: chPct,
        t: tr,
      },
      { showTotal: true, showReferenceNote: true },
    );
  }
  if (
    bm === 'video_token' ||
    bm === 'video_per_second' ||
    bm === 'video_per_video'
  ) {
    const agg = Number(record?.quota) || 0;
    return other?.claude
      ? renderClaudeLogContent(
          other?.model_ratio,
          other?.completion_ratio,
          other?.model_price,
          other?.group_ratio,
          other?.user_group_ratio,
          other?.cache_ratio || 1.0,
          other?.cache_creation_ratio || 1.0,
          other?.cache_creation_tokens_5m || 0,
          other?.cache_creation_ratio_5m || other?.cache_creation_ratio || 1.0,
          other?.cache_creation_tokens_1h || 0,
          other?.cache_creation_ratio_1h || other?.cache_creation_ratio || 1.0,
          billingDisplayMode,
          true,
          chPct,
          other,
        )
      : renderLogContent(
          other?.model_ratio,
          other?.completion_ratio,
          other?.model_price,
          other?.group_ratio,
          other?.user_group_ratio,
          other?.cache_ratio || 1.0,
          other?.image || false,
          other?.image_ratio || 0,
          other?.web_search || false,
          other?.web_search_call_count || 0,
          other?.file_search || false,
          other?.file_search_call_count || 0,
          billingDisplayMode,
          true,
          other?.video_ratio || 0,
          other?.video_completion_ratio || 1.0,
          other?.video_output_tokens || 0,
          other?.video_input_text_tokens || 0,
          bm || '',
          agg,
          chPct,
          other,
        );
  }

  if (
    other?.request_tier_pricing &&
    other?.request_tier_breakdown &&
    typeof other.request_tier_breakdown === 'object'
  ) {
    return renderRequestTierConsumeArticle(other, chPct, tr);
  }

  // use_price：后端写入的按量（token 单价）标记；为 true 时不得再按 model_price 走「按次」展示（避免 Playground 等路径 ModelPrice 残留导致「按次 ¥0」）。
  const usePriceVolume =
    other?.use_price === true ||
    other?.use_price === 1 ||
    other?.use_price === 'true';
  const modelPriceConsume = usePriceVolume ? -1 : (other?.model_price ?? -1);
  const isPerCall =
    !usePriceVolume &&
    Number.isFinite(Number(other?.model_price)) &&
    Number(other?.model_price) > 0;

  if (other?.claude || other?.usage_semantic === 'anthropic') {
    return renderClaudeModelPrice(
      record.prompt_tokens,
      record.completion_tokens,
      other?.model_ratio,
      modelPriceConsume,
      other?.completion_ratio ?? 0,
      other?.group_ratio,
      other?.user_group_ratio,
      other?.cache_tokens || 0,
      other?.cache_ratio || 1.0,
      other?.cache_creation_tokens || 0,
      other?.cache_creation_ratio || 1.0,
      other?.cache_creation_tokens_5m || 0,
      other?.cache_creation_ratio_5m || other?.cache_creation_ratio || 1.0,
      other?.cache_creation_tokens_1h || 0,
      other?.cache_creation_ratio_1h || other?.cache_creation_ratio || 1.0,
      billingDisplayMode,
      chPct,
      !isPerCall,
      true,
      other,
    );
  }

  return renderModelPrice(
    record.prompt_tokens,
    record.completion_tokens,
    other?.model_ratio,
    modelPriceConsume,
    other?.completion_ratio ?? 0,
    other?.group_ratio,
    other?.user_group_ratio,
    other?.cache_tokens || 0,
    other?.cache_ratio || 1.0,
    other?.image || false,
    other?.image_ratio || 0,
    other?.image_output || 0,
    other?.web_search || false,
    other?.web_search_call_count || 0,
    other?.web_search_price || 0,
    other?.file_search || false,
    other?.file_search_call_count || 0,
    other?.file_search_price || 0,
    other?.audio_input_seperate_price || false,
    other?.audio_input_token_count || 0,
    other?.audio_input_price || 0,
    other?.image_generation_call || false,
    other?.image_generation_call_price || 0,
    billingDisplayMode,
    chPct,
    !isPerCall,
    true,
    other,
  );
}

// 已统一至 renderModelPriceSimple，若仍有遗留引用，请改为传入 provider='claude'

/**
 * rehype 插件：将段落等文本节点拆分为逐词 <span>，并添加淡入动画 class。
 * 仅在流式渲染阶段使用，避免已渲染文字重复动画。
 */
export function rehypeSplitWordsIntoSpans(options = {}) {
  const { previousContentLength = 0 } = options;

  return (tree) => {
    let currentCharCount = 0; // 当前已处理的字符数

    visit(tree, 'element', (node) => {
      if (
        ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'strong'].includes(
          node.tagName,
        ) &&
        node.children
      ) {
        const newChildren = [];
        node.children.forEach((child) => {
          if (child.type === 'text') {
            try {
              // 使用 Intl.Segmenter 精准拆分中英文及标点
              const segmenter = new Intl.Segmenter('zh', {
                granularity: 'word',
              });
              const segments = segmenter.segment(child.value);

              Array.from(segments)
                .map((seg) => seg.segment)
                .filter(Boolean)
                .forEach((word) => {
                  const wordStartPos = currentCharCount;
                  const wordEndPos = currentCharCount + word.length;

                  // 判断这个词是否是新增的（在 previousContentLength 之后）
                  const isNewContent = wordStartPos >= previousContentLength;

                  newChildren.push({
                    type: 'element',
                    tagName: 'span',
                    properties: {
                      className: isNewContent ? ['animate-fade-in'] : [],
                    },
                    children: [{ type: 'text', value: word }],
                  });

                  currentCharCount = wordEndPos;
                });
            } catch (_) {
              // Fallback：如果浏览器不支持 Segmenter
              const textStartPos = currentCharCount;
              const isNewContent = textStartPos >= previousContentLength;

              if (isNewContent) {
                // 新内容，添加动画
                newChildren.push({
                  type: 'element',
                  tagName: 'span',
                  properties: {
                    className: ['animate-fade-in'],
                  },
                  children: [{ type: 'text', value: child.value }],
                });
              } else {
                // 旧内容，不添加动画
                newChildren.push(child);
              }

              currentCharCount += child.value.length;
            }
          } else {
            newChildren.push(child);
          }
        });
        node.children = newChildren;
      }
    });
  };
}
