import { useCallback, useMemo, useState } from 'react';
import { Legend, FloatingTooltip } from './native';
import {
  CHART_DATA_COLOR_SLOTS,
  normalizeDataColorToken,
} from './chart-common';
import ChartTooltip from './ChartTooltip';
import { tokenNameToCssVar } from '../utils/token-resolver.js';

export { CHART_DATA_COLOR_SLOTS } from './chart-common';

const FALLBACK_SLICE = '#98A2B3';

/** 外环 / 环图内环（px），与稿一致；当容器较小时按比例缩放 */
const PIE_OUTER_RADIUS_PX = 110;
const PIE_DONUT_INNER_RADIUS_PX = 70;
const PIE_MIN_OUTER_RADIUS_PX = 48;

/** 扇区缝隙：与铺底同色的 stroke，缝在径向上约 1px 等宽 */
const PIE_SECTOR_GAP_STROKE_WIDTH = 1;
const PIE_SECTOR_GAP_FALLBACK = 'var(--color-bg-page, #F9FAFB)';
const PIE_SECTOR_CORNER_RADIUS_PX = 4;

/** 图例与饼图主体之间的净距（px） */
const PIE_LEGEND_GAP_PX = 12;
const PIE_LABEL_MIN_GAP_PX = 10;
const PIE_LABEL_BLOCK_HEIGHT_PX = 34;
const PIE_LABEL_COMPACT_HEIGHT_PX = 18;
const PIE_LABEL_VERTICAL_INSET_PX = 8;
const PIE_LABEL_BEND_OFFSET_PX = 18;
const PIE_LABEL_END_OFFSET_PX = 30;
const PIE_LABEL_TEXT_OFFSET_PX = 8;
const PIE_SMALL_SLICE_THRESHOLD = 8;

const DEFAULT_DATA = [
  { name: '分类 1', value: 320, colorToken: 'data-0' },
  { name: '分类 2', value: 260, colorToken: 'data-4' },
  { name: '分类 3', value: 280, colorToken: 'data-2' },
  { name: '分类 4', value: 300, colorToken: 'data-5' },
];

/** @typedef {'top'|'bottom'|'left'|'right'} LegendPosition */

/** 解析颜色 token 为 CSS 变量字符串 */
function resolveSliceFill(colorToken) {
  if (/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(colorToken)) return colorToken;
  const v = tokenNameToCssVar(colorToken);
  if (v && v.startsWith('--')) return `var(${v}, ${FALLBACK_SLICE})`;
  return FALLBACK_SLICE;
}

/**
 * 生成单个扇区 SVG path：内外两端均做 cornerRadius（与原 recharts cornerRadius 视觉接近）
 *
 * 坐标约定：a0/a1 弧度，0 = 12 点钟方向，顺时针为正
 *   x = cx + r * sin(a)
 *   y = cy - r * cos(a)
 *
 * 圆角实现：每端径向让出 safeCr，对应弧度 dAng = safeCr / r；用 Q（二次贝塞尔）
 * 让径向脚 → 弧上端点，控制点放在原始径向直角处。
 */
function buildSectorPath({ cx, cy, ir, or, a0, a1, cr }) {
  const span = Math.abs(a1 - a0);
  if (span <= 1e-6 || or <= 0) return '';
  const fullCircle = span >= Math.PI * 2 - 1e-6;
  const pt = (r, a) => [cx + r * Math.sin(a), cy - r * Math.cos(a)];

  if (fullCircle) {
    if (ir > 0) {
      const [x1, y1] = pt(or, 0);
      const [x2, y2] = pt(or, Math.PI);
      const [ix1, iy1] = pt(ir, 0);
      const [ix2, iy2] = pt(ir, Math.PI);
      return [
        `M${x1},${y1}`,
        `A${or},${or} 0 1 1 ${x2},${y2}`,
        `A${or},${or} 0 1 1 ${x1},${y1}`,
        `M${ix1},${iy1}`,
        `A${ir},${ir} 0 1 0 ${ix2},${iy2}`,
        `A${ir},${ir} 0 1 0 ${ix1},${iy1}`,
        'Z',
      ].join(' ');
    }
    return [
      `M${cx},${cy - or}`,
      `A${or},${or} 0 1 1 ${cx},${cy + or}`,
      `A${or},${or} 0 1 1 ${cx},${cy - or}`,
      'Z',
    ].join(' ');
  }

  const ringW = Math.max(0, or - ir);
  const halfChord = or * Math.sin(span / 2);
  const safeCr = Math.max(0, Math.min(cr, ringW / 2, halfChord));
  const largeArc = span > Math.PI ? 1 : 0;

  if (safeCr <= 0.5) {
    const [ox0, oy0] = pt(or, a0);
    const [ox1, oy1] = pt(or, a1);
    if (ir > 0) {
      const [ix0, iy0] = pt(ir, a0);
      const [ix1, iy1] = pt(ir, a1);
      return [
        `M${ix0},${iy0}`,
        `L${ox0},${oy0}`,
        `A${or},${or} 0 ${largeArc} 1 ${ox1},${oy1}`,
        `L${ix1},${iy1}`,
        `A${ir},${ir} 0 ${largeArc} 0 ${ix0},${iy0}`,
        'Z',
      ].join(' ');
    }
    return [
      `M${cx},${cy}`,
      `L${ox0},${oy0}`,
      `A${or},${or} 0 ${largeArc} 1 ${ox1},${oy1}`,
      'Z',
    ].join(' ');
  }

  const dAngOut = safeCr / Math.max(1, or);
  const dAngIn = ir > 0 ? safeCr / Math.max(1, ir) : 0;
  const oa0 = a0 + dAngOut;
  const oa1 = a1 - dAngOut;
  const ia0 = a0 + dAngIn;
  const ia1 = a1 - dAngIn;

  const [oA_x, oA_y] = pt(or, oa0);
  const [oB_x, oB_y] = pt(or, oa1);
  const [oAi_x, oAi_y] = pt(or - safeCr, a0);
  const [oBi_x, oBi_y] = pt(or - safeCr, a1);
  const [oA_ctrl_x, oA_ctrl_y] = pt(or, a0);
  const [oB_ctrl_x, oB_ctrl_y] = pt(or, a1);
  const oArcLarge = (oa1 - oa0) > Math.PI ? 1 : 0;

  if (ir > 0) {
    const [iA_x, iA_y] = pt(ir, ia0);
    const [iB_x, iB_y] = pt(ir, ia1);
    const [iAo_x, iAo_y] = pt(ir + safeCr, a0);
    const [iBo_x, iBo_y] = pt(ir + safeCr, a1);
    const [iA_ctrl_x, iA_ctrl_y] = pt(ir, a0);
    const [iB_ctrl_x, iB_ctrl_y] = pt(ir, a1);
    const iArcLarge = (ia1 - ia0) > Math.PI ? 1 : 0;
    return [
      `M${iAo_x},${iAo_y}`,
      `L${oAi_x},${oAi_y}`,
      `Q${oA_ctrl_x},${oA_ctrl_y} ${oA_x},${oA_y}`,
      `A${or},${or} 0 ${oArcLarge} 1 ${oB_x},${oB_y}`,
      `Q${oB_ctrl_x},${oB_ctrl_y} ${oBi_x},${oBi_y}`,
      `L${iBo_x},${iBo_y}`,
      `Q${iB_ctrl_x},${iB_ctrl_y} ${iB_x},${iB_y}`,
      `A${ir},${ir} 0 ${iArcLarge} 0 ${iA_x},${iA_y}`,
      `Q${iA_ctrl_x},${iA_ctrl_y} ${iAo_x},${iAo_y}`,
      'Z',
    ].join(' ');
  }

  return [
    `M${cx},${cy}`,
    `L${oAi_x},${oAi_y}`,
    `Q${oA_ctrl_x},${oA_ctrl_y} ${oA_x},${oA_y}`,
    `A${or},${or} 0 ${oArcLarge} 1 ${oB_x},${oB_y}`,
    `Q${oB_ctrl_x},${oB_ctrl_y} ${cx + (or - safeCr) * Math.sin(a1)},${cy - (or - safeCr) * Math.cos(a1)}`,
    'Z',
  ].join(' ');
}

function clamp(value, min, max) {
  if (Number.isNaN(value)) return min;
  return Math.max(min, Math.min(max, value));
}

function pickVisibleSideLabels(items, topBound, bottomBound, gap) {
  if (!items.length) return [];
  const availableHeight = Math.max(0, bottomBound - topBound);
  if (availableHeight <= 0) return [];

  const ranked = [...items].sort((a, b) => {
    if (b.priority !== a.priority) return b.priority - a.priority;
    if (b.pct !== a.pct) return b.pct - a.pct;
    return Math.abs(a.rawY - b.cy) - Math.abs(b.rawY - b.cy);
  });

  const keep = new Set();
  let usedHeight = 0;
  ranked.forEach((item) => {
    const extra = item.height + (keep.size > 0 ? gap : 0);
    if (usedHeight + extra <= availableHeight + 0.5) {
      keep.add(item.index);
      usedHeight += extra;
    }
  });

  return items.filter((item) => keep.has(item.index));
}

function distributeSideLabels(items, topBound, bottomBound, gap) {
  if (!items.length) return [];

  const ordered = [...items].sort((a, b) => a.rawY - b.rawY);
  const centers = [];

  ordered.forEach((item, idx) => {
    const minCenter = topBound + item.height / 2;
    const maxCenter = bottomBound - item.height / 2;
    let center = clamp(item.rawY, minCenter, maxCenter);
    if (idx > 0) {
      const prev = ordered[idx - 1];
      const required =
        centers[idx - 1] + prev.height / 2 + gap + item.height / 2;
      center = Math.max(center, required);
    }
    centers.push(center);
  });

  const last = ordered.length - 1;
  const overflow = centers[last] + ordered[last].height / 2 - bottomBound;
  if (overflow > 0) {
    centers[last] -= overflow;
    for (let idx = last - 1; idx >= 0; idx -= 1) {
      const next = ordered[idx + 1];
      const maxCenter =
        centers[idx + 1] - next.height / 2 - gap - ordered[idx].height / 2;
      centers[idx] = Math.min(centers[idx], maxCenter);
    }
  }

  const underflow = topBound - (centers[0] - ordered[0].height / 2);
  if (underflow > 0) {
    centers[0] += underflow;
    for (let idx = 1; idx < ordered.length; idx += 1) {
      const prev = ordered[idx - 1];
      const minCenter =
        centers[idx - 1] + prev.height / 2 + gap + ordered[idx].height / 2;
      centers[idx] = Math.max(centers[idx], minCenter);
    }
  }

  return ordered.map((item, idx) => ({
    ...item,
    labelY: centers[idx],
  }));
}

function mergeConfig(c) {
  const defaults = {
    showLegend: true,
    showLabel: false,
    labelLayoutMode: 'legacy',
    compactSmallSlices: true,
    hideOverflowLabels: true,
    labelSideOverrides: {},
    balanceLabelSides: false,
    labelBendOffset: PIE_LABEL_BEND_OFFSET_PX,
    labelEndOffset: PIE_LABEL_END_OFFSET_PX,
    labelMinGap: PIE_LABEL_MIN_GAP_PX,
    labelBandPadding: 22,
    legendPosition: 'bottom',
    height: 320,
    donut: false,
    margin: { top: 12, right: 12, bottom: 28, left: 12 },
  };
  if (!c || typeof c !== 'object') return defaults;
  const legacyDonut = Number(c.innerRadiusRatio) > 0;
  const donut = typeof c.donut === 'boolean' ? c.donut : legacyDonut;
  return {
    ...defaults,
    ...c,
    donut,
    height: Number(c.height ?? defaults.height) || defaults.height,
    margin: { ...defaults.margin, ...(c.margin || {}) },
  };
}

/**
 * 饼图 / 环图（native 实现，外塔 API 与原 SmartPieChart 一致）
 *
 * 全特性覆盖：
 * - 实心饼 / 环图（donut）
 * - 扇区缝隙 stroke + 外圆角 cornerRadius
 * - hover 扇区径向放大（bump）
 * - activeIndex / activeSliceName 受控高亮
 * - onSliceClick 透传 { index, name, value, payload, nativeEvent }
 * - showLabel 引线 + 百分比 + 名称
 * - 跟手 tooltip
 * - Legend top/bottom/left/right
 */
export default function SmartPieChart({ spec }) {
  const { data: rawData, config: rawConfig } = spec || {};
  const config = mergeConfig(rawConfig);

  const chartData = useMemo(() => {
    const list = Array.isArray(rawData) && rawData.length > 0 ? rawData : DEFAULT_DATA;
    return list.map((row, idx) => ({
      name: row.name ?? `Part ${idx + 1}`,
      value: Number(row.value) || 0,
      colorToken: /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(String(row.colorToken || '').trim())
        ? String(row.colorToken).trim()
        : normalizeDataColorToken(row.colorToken || `data-${idx % CHART_DATA_COLOR_SLOTS}`),
    }));
  }, [rawData]);

  const fills = useMemo(
    () => chartData.map((d) => resolveSliceFill(d.colorToken)),
    [chartData],
  );

  const {
    showLegend,
    showLabel,
    legendPosition,
    height,
    donut,
    margin: marginFromConfig,
    onSliceClick,
    activeIndex: activeIndexFromConfig,
    activeSliceName,
    legendItems: legendItemsFromConfig,
    labelLayoutMode,
    compactSmallSlices,
    hideOverflowLabels,
    labelSideOverrides,
    balanceLabelSides,
    labelBendOffset,
    labelEndOffset,
    labelMinGap,
    labelBandPadding,
  } = config;

  /** 自定义 legend items（与 chartData 解耦）；不传则按数据生成 */
  const legendItemsResolved = useMemo(() => {
    if (Array.isArray(legendItemsFromConfig) && legendItemsFromConfig.length) {
      return legendItemsFromConfig.map((it) => ({
        name: it.name,
        color: resolveSliceFill(it.colorToken || it.color || 'data-1'),
      }));
    }
    return chartData.map((d, i) => ({ name: d.name, color: fills[i] }));
  }, [legendItemsFromConfig, chartData, fills]);

  const activeIndexResolved = useMemo(() => {
    if (Number.isInteger(activeIndexFromConfig)) {
      const idx = Number(activeIndexFromConfig);
      if (idx >= 0 && idx < chartData.length) return idx;
    }
    if (typeof activeSliceName === 'string' && activeSliceName) {
      const idx = chartData.findIndex((it) => it.name === activeSliceName);
      if (idx >= 0) return idx;
    }
    return undefined;
  }, [activeIndexFromConfig, activeSliceName, chartData]);

  const [hoverIdx, setHoverIdx] = useState(null);
  const [mouse, setMouse] = useState(null); // {x, y} in container coords

  const handleSliceClick = useCallback(
    (entry, idx, evt) => {
      if (typeof onSliceClick !== 'function') return;
      onSliceClick({
        index: idx,
        name: entry.name,
        value: entry.value,
        payload: entry,
        nativeEvent: evt?.nativeEvent ?? evt,
      });
    },
    [onSliceClick],
  );

  // 计算扇区角度区间（弧度，顺时针，0 = 12 点）
  const total = useMemo(() => chartData.reduce((s, d) => s + (d.value || 0), 0), [chartData]);
  const angles = useMemo(() => {
    const out = [];
    let acc = 0;
    chartData.forEach((d) => {
      const ang = total > 0 ? (d.value / total) * Math.PI * 2 : 0;
      out.push({ a0: acc, a1: acc + ang });
      acc += ang;
    });
    return out;
  }, [chartData, total]);

  /** 在对应边增加净距，让饼与图例之间稳定留出 PIE_LEGEND_GAP_PX */
  const chartMargin = useMemo(() => {
    const m = { ...marginFromConfig };
    if (!showLegend) return m;
    const add = PIE_LEGEND_GAP_PX;
    if (legendPosition === 'bottom') m.bottom = (Number(m.bottom) || 0) + add;
    else if (legendPosition === 'top') m.top = (Number(m.top) || 0) + add;
    else if (legendPosition === 'left') m.left = (Number(m.left) || 0) + add;
    else if (legendPosition === 'right') m.right = (Number(m.right) || 0) + add;
    return m;
  }, [showLegend, legendPosition, marginFromConfig]);

  const innerR = donut ? PIE_DONUT_INNER_RADIUS_PX : 0;
  const outerR = PIE_OUTER_RADIUS_PX;
  // fillContainer 模式下完全跟随父容器高度；非 fillContainer 时若需要展示标签则保底 400
  const effectiveHeight = config.fillContainer
    ? height
    : (showLabel ? Math.max(height, 400) : height);
  // 饼心垂直居中，避免下方留空导致饼图被迫缩小
  const cyRatio = 0.5;

  const handleSvgMouseLeave = useCallback(() => {
    setHoverIdx(null);
    setMouse(null);
  }, []);

  return (
    <div
      className="smart-pie-chart"
      style={{
        position: 'relative',
        width: '100%',
        // fillContainer 模式：跟随父容器高度自适应（用于嵌入到 flex-1 的卡片中）
        height: config.fillContainer ? '100%' : effectiveHeight,
        minHeight: config.fillContainer ? PIE_MIN_OUTER_RADIUS_PX * 2 + 24 : undefined,
        outline: 'none',
        background: 'var(--color-surface, #FFFFFF)',
      }}
      tabIndex={-1}
    >
      <PieSvg
        height={effectiveHeight}
        fillContainer={!!config.fillContainer}
        margin={chartMargin}
        innerR={innerR}
        outerR={outerR}
        cyRatio={cyRatio}
        chartData={chartData}
        fills={fills}
        angles={angles}
        showLabel={showLabel}
        labelLayoutMode={labelLayoutMode}
        compactSmallSlices={compactSmallSlices}
        hideOverflowLabels={hideOverflowLabels}
        labelSideOverrides={labelSideOverrides}
        balanceLabelSides={balanceLabelSides}
        labelBendOffset={labelBendOffset}
        labelEndOffset={labelEndOffset}
        labelMinGap={labelMinGap}
        labelBandPadding={labelBandPadding}
        activeIndex={activeIndexResolved}
        hoverIdx={hoverIdx}
        setHoverIdx={setHoverIdx}
        setMouse={setMouse}
        onSliceClick={handleSliceClick}
        onLeave={handleSvgMouseLeave}
      />

      {/* Legend */}
      {showLegend && (
        <div
          style={{
            position: 'absolute',
            left: 0,
            right: 0,
            ...(legendPosition === 'top' ? { top: 0 } : {}),
            ...(legendPosition === 'bottom' ? { bottom: 0 } : {}),
            ...(legendPosition === 'left'
              ? { left: 0, top: '50%', transform: 'translateY(-50%)', right: 'auto', width: 96 }
              : {}),
            ...(legendPosition === 'right'
              ? { right: 0, top: '50%', transform: 'translateY(-50%)', left: 'auto', width: 96 }
              : {}),
            padding: '4px 8px',
          }}
        >
          <Legend
            items={legendItemsResolved}
            shape="dot"
            position={legendPosition}
          />
        </div>
      )}

      {/* Tooltip 跟手 */}
      {hoverIdx != null && mouse && chartData[hoverIdx] && (
        <FloatingTooltip mouse={{ clientX: mouse.clientX, clientY: mouse.clientY }}>
          <ChartTooltip
            variant="chart"
            label={chartData[hoverIdx].name}
            rows={[{
              name: chartData[hoverIdx].name,
              value: chartData[hoverIdx].value,
              color: fills[hoverIdx],
              dataKey: 'value',
            }]}
          />
        </FloatingTooltip>
      )}
    </div>
  );
}

/**
 * 内部组件：测量容器宽度后绘制饼图 SVG
 */
function PieSvg({
  height,
  fillContainer = false,
  margin,
  innerR,
  outerR,
  cyRatio,
  chartData,
  fills,
  angles,
  showLabel,
  labelLayoutMode = 'legacy',
  compactSmallSlices = true,
  hideOverflowLabels = true,
  labelSideOverrides = {},
  balanceLabelSides = false,
  labelBendOffset = PIE_LABEL_BEND_OFFSET_PX,
  labelEndOffset = PIE_LABEL_END_OFFSET_PX,
  labelMinGap = PIE_LABEL_MIN_GAP_PX,
  labelBandPadding = 22,
  activeIndex,
  hoverIdx,
  setHoverIdx,
  setMouse,
  onSliceClick,
  onLeave,
}) {
  // 用 ResizeObserver 思路：直接 ref + state width/height
  const [width, setWidth] = useState(0);
  const [measuredH, setMeasuredH] = useState(0);
  const containerRef = useCallback((node) => {
    if (!node) return;
    const ro = new ResizeObserver((entries) => {
      for (const e of entries) {
        setWidth(Math.floor(e.contentRect.width));
        setMeasuredH(Math.floor(e.contentRect.height));
      }
    });
    ro.observe(node);
    const rect = node.getBoundingClientRect();
    setWidth(Math.floor(rect.width));
    setMeasuredH(Math.floor(rect.height));
  }, []);

  // fillContainer 时高度跟随实际容器；否则使用 spec 传入的 height
  const effectiveHeight = fillContainer && measuredH > 0 ? measuredH : height;

  const cx = (margin.left + (width - margin.left - margin.right) / 2) || 0;
  const cy = margin.top + (effectiveHeight - margin.top - margin.bottom) * cyRatio;

  // 自适应缩放：根据可用宽高约束最大半径，避免容器变窄时被截断
  const availableW = Math.max(0, width - margin.left - margin.right);
  const availableH = Math.max(0, effectiveHeight - margin.top - margin.bottom);
  const maxBumpPx = Math.max(5, Math.min(16, outerR * 0.055));
  // 显示扇形标签时：保留白卡 chart slot 内边距（16px）的视觉空间，避免标签贴卡边
  // 水平方向需预留 引线(8) + 间隔(4) + 文本(~20) ≈ 32；垂直方向预留两行文本 ≈ 24
  const labelReserveH = showLabel ? 32 : 0;
  const labelReserveV = showLabel ? 24 : 0;
  // 顶部到圆心 / 圆心到底部 取较小者作为可用半径上限
  const verticalBudget = Math.min(
    availableH * cyRatio,
    availableH * (1 - cyRatio),
  );
  const radiusBudget = Math.max(
    0,
    Math.min(availableW / 2 - labelReserveH, verticalBudget - labelReserveV) - maxBumpPx,
  );
  const scale = outerR > 0 ? Math.min(1, radiusBudget / outerR) : 1;
  const fittedOuterR = scale < 1
    ? Math.max(PIE_MIN_OUTER_RADIUS_PX, outerR * scale)
    : outerR;
  const fittedInnerR = innerR > 0 ? innerR * (fittedOuterR / outerR) : 0;

  const handleMove = useCallback(
    (e) => {
      const rect = e.currentTarget.getBoundingClientRect();
      setMouse({
        x: e.clientX - rect.left,
        y: e.clientY - rect.top,
        clientX: e.clientX,
        clientY: e.clientY,
        containerW: rect.width,
      });
    },
    [setMouse],
  );

  const sectorGapStroke = PIE_SECTOR_GAP_FALLBACK;
  const totalValue = useMemo(
    () => chartData.reduce((sum, item) => sum + (item.value || 0), 0),
    [chartData],
  );
  const labelLayout = useMemo(() => {
    if (!showLabel || width <= 0 || totalValue <= 0) return [];

    const minGap = Math.max(0, Number(labelMinGap) || PIE_LABEL_MIN_GAP_PX);
    const bandPadding = Math.max(0, Number(labelBandPadding) || 0);
    const sideTop = Math.max(
      margin.top + PIE_LABEL_VERTICAL_INSET_PX,
      cy - fittedOuterR - bandPadding,
    );
    const sideBottom = Math.min(
      effectiveHeight - margin.bottom - PIE_LABEL_VERTICAL_INSET_PX,
      cy + fittedOuterR + bandPadding,
    );
    const bendOffset = Math.max(4, Number(labelBendOffset) || PIE_LABEL_BEND_OFFSET_PX);
    const endOffset = Math.max(bendOffset + 4, Number(labelEndOffset) || PIE_LABEL_END_OFFSET_PX);
    const rightBendX = cx + fittedOuterR + bendOffset;
    const leftBendX = cx - fittedOuterR - bendOffset;
    const rightEndX = cx + fittedOuterR + endOffset;
    const leftEndX = cx - fittedOuterR - endOffset;

    const rawItems = chartData.map((entry, i) => {
      const { a0, a1 } = angles[i];
      const mid = (a0 + a1) / 2;
      const dirX = Math.sin(mid);
      const dirY = -Math.cos(mid);
      const sx = cx + (fittedOuterR + 1) * dirX;
      const sy = cy + (fittedOuterR + 1) * dirY;
      const pct = totalValue > 0 ? (entry.value / totalValue) * 100 : 0;
      const compact = compactSmallSlices && pct < PIE_SMALL_SLICE_THRESHOLD;
      const sideOverride = labelSideOverrides?.[entry.name];
      const isRight = sideOverride ? sideOverride === 'right' : dirX >= 0;
      const rawY = cy + (fittedOuterR + 20) * dirY;

      return {
        index: i,
        name: entry.name,
        pct,
        compact,
        priority: compact ? 1 : 2,
        height: compact ? PIE_LABEL_COMPACT_HEIGHT_PX : PIE_LABEL_BLOCK_HEIGHT_PX,
        side: isRight ? 'right' : 'left',
        sideLocked: !!sideOverride,
        sx,
        sy,
        rawY,
        cy,
        bendX: isRight ? rightBendX : leftBendX,
        endX: isRight ? rightEndX : leftEndX,
        textX: isRight
          ? rightEndX + PIE_LABEL_TEXT_OFFSET_PX
          : leftEndX - PIE_LABEL_TEXT_OFFSET_PX,
        anchor: isRight ? 'start' : 'end',
      };
    });

    if (balanceLabelSides) {
      const movable = rawItems
        .filter((item) => !item.sideLocked)
        .sort((a, b) => a.pct - b.pct);
      let leftCount = rawItems.filter((item) => item.side === 'left').length;
      let rightCount = rawItems.length - leftCount;
      movable.forEach((item) => {
        if (Math.abs(leftCount - rightCount) <= 1) return;
        if (leftCount > rightCount && item.side === 'left') {
          item.side = 'right';
          item.bendX = rightBendX;
          item.endX = rightEndX;
          item.textX = rightEndX + PIE_LABEL_TEXT_OFFSET_PX;
          item.anchor = 'start';
          leftCount -= 1;
          rightCount += 1;
        } else if (rightCount > leftCount && item.side === 'right') {
          item.side = 'left';
          item.bendX = leftBendX;
          item.endX = leftEndX;
          item.textX = leftEndX - PIE_LABEL_TEXT_OFFSET_PX;
          item.anchor = 'end';
          rightCount -= 1;
          leftCount += 1;
        }
      });
    }

    const leftSource = rawItems.filter((item) => item.side === 'left');
    const rightSource = rawItems.filter((item) => item.side === 'right');
    const left = distributeSideLabels(
      hideOverflowLabels
        ? pickVisibleSideLabels(
          leftSource,
          sideTop,
          sideBottom,
          minGap,
        )
        : leftSource,
      sideTop,
      sideBottom,
      minGap,
    );
    const right = distributeSideLabels(
      hideOverflowLabels
        ? pickVisibleSideLabels(
          rightSource,
          sideTop,
          sideBottom,
          minGap,
        )
        : rightSource,
      sideTop,
      sideBottom,
      minGap,
    );

    return [...left, ...right].sort((a, b) => a.index - b.index);
  }, [
    angles,
    chartData,
    cx,
    cy,
    effectiveHeight,
    fittedOuterR,
    margin.bottom,
    margin.top,
    showLabel,
    compactSmallSlices,
    hideOverflowLabels,
    labelSideOverrides,
    balanceLabelSides,
    labelBendOffset,
    labelEndOffset,
    labelMinGap,
    labelBandPadding,
    totalValue,
    width,
  ]);

  return (
    <div
      ref={containerRef}
      style={{ position: 'absolute', inset: 0 }}
      onMouseLeave={onLeave}
    >
      {width > 0 && (
        <svg
          width={width}
          height={effectiveHeight}
          style={{ display: 'block', overflow: 'visible' }}
          onMouseMove={handleMove}
        >
          {chartData.map((entry, i) => {
            const { a0, a1 } = angles[i];
            const isActive = activeIndex === i || hoverIdx === i;
            const bump = isActive ? Math.max(5, Math.min(16, fittedOuterR * 0.055)) : 0;
            const d = buildSectorPath({
              cx,
              cy,
              ir: fittedInnerR,
              or: fittedOuterR + bump,
              a0,
              a1,
              cr: PIE_SECTOR_CORNER_RADIUS_PX,
            });
            return (
              <path
                key={`${entry.name}-${i}`}
                d={d}
                fill={fills[i]}
                stroke={sectorGapStroke}
                strokeWidth={PIE_SECTOR_GAP_STROKE_WIDTH}
                style={{ cursor: onSliceClick ? 'pointer' : 'default' }}
                onPointerEnter={() => setHoverIdx(i)}
                onPointerLeave={() => setHoverIdx((cur) => (cur === i ? null : cur))}
                onClick={(e) => onSliceClick(entry, i, e)}
              />
            );
          })}

          {/* 引线 + 标签 */}
          {showLabel && (labelLayoutMode === 'legacy'
            ? chartData.map((entry, i) => {
              const { a0, a1 } = angles[i];
              const mid = (a0 + a1) / 2;
              const cos = Math.sin(mid);
              const sin = -Math.cos(mid);
              const sx = cx + (fittedOuterR + 1) * cos;
              const sy = cy + (fittedOuterR + 1) * sin;
              const elbowMinDist = fittedOuterR + 12;
              const rawEx = cx + (fittedOuterR + 20) * cos;
              const rawEy = cy + (fittedOuterR + 20) * sin;
              const maxDy = fittedOuterR * 0.85;
              const my = Math.max(cy - maxDy, Math.min(cy + maxDy, rawEy));
              const dy = my - cy;
              const minDxSq = elbowMinDist * elbowMinDist - dy * dy;
              const minDx = minDxSq > 0 ? Math.sqrt(minDxSq) : 0;
              const sign = cos >= 0 ? 1 : -1;
              const rawDx = rawEx - cx;
              const dxAbs = Math.max(minDx, Math.abs(rawDx));
              const mx = cx + sign * dxAbs;
              const isRight = sign > 0;
              const ex = isRight ? cx + fittedOuterR + 24 : cx - fittedOuterR - 24;
              const ey = my;
              const textX = isRight ? ex + 6 : ex - 6;
              const anchor = isRight ? 'start' : 'end';
              const pct = totalValue > 0 ? (entry.value / totalValue) * 100 : 0;
              return (
                <g key={`lbl-${i}`} pointerEvents="none">
                  <path
                    d={`M${sx},${sy} L${mx},${my} L${ex},${ey}`}
                    stroke={fills[i]}
                    strokeWidth={1}
                    fill="none"
                  />
                  <text
                    x={textX}
                    y={ey - 4}
                    textAnchor={anchor}
                    fill="var(--semi-color-text-0, #1F2329)"
                    fontWeight={600}
                    fontSize={14}
                  >
                    {`${pct.toFixed(1)}%`}
                  </text>
                  <text
                    x={textX}
                    y={ey + 16}
                    textAnchor={anchor}
                    fill="var(--semi-color-text-2, #4E5969)"
                    fontSize={12}
                  >
                    {entry.name}
                  </text>
                </g>
              );
            })
            : labelLayout.map((label) => {
              const blockTop = label.labelY - label.height / 2;
              return (
                <g key={`lbl-${label.index}`} pointerEvents="none">
                  <path
                    d={`M${label.sx},${label.sy} L${label.bendX},${label.labelY} L${label.endX},${label.labelY}`}
                    stroke={fills[label.index]}
                    strokeWidth={1}
                    fill="none"
                  />
                  {label.compact ? (
                    <text
                      x={label.textX}
                      y={blockTop + 13}
                      textAnchor={label.anchor}
                      fill="var(--semi-color-text-2, #4E5969)"
                      fontSize={12}
                    >
                      <tspan fontWeight={600} fill="var(--semi-color-text-0, #1F2329)">
                        {`${label.pct.toFixed(1)}%`}
                      </tspan>
                      <tspan dx={4}>{label.name}</tspan>
                    </text>
                  ) : (
                    <text
                      x={label.textX}
                      y={blockTop + 14}
                      textAnchor={label.anchor}
                    >
                      <tspan
                        x={label.textX}
                        fill="var(--semi-color-text-0, #1F2329)"
                        fontWeight={600}
                        fontSize={14}
                      >
                        {`${label.pct.toFixed(1)}%`}
                      </tspan>
                      <tspan
                        x={label.textX}
                        dy={16}
                        fill="var(--semi-color-text-2, #4E5969)"
                        fontSize={12}
                      >
                        {label.name}
                      </tspan>
                    </text>
                  )}
                </g>
              );
            }))}
        </svg>
      )}
    </div>
  );
}
