/**
 * SmartSankeyChart — d3-sankey 布局 + React/SVG（仅基础样式）。
 * 节点支持 `node.colorToken`，连线支持 `link.colorToken/link.opacity`；
 * 未传时回落到 `config.dataColorToken` 与 `config.linkOpacity(默认 0.2)`。
 */

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { sankey } from 'd3-sankey';
import { normalizeDataColorToken } from './chart-common';
import { tokenNameToCssVar } from '../utils/token-resolver.js';
import { FloatingTooltip } from './native';

/** @param {string} tokenName 如 `data-0` */
function dataSlotCssVar(tokenName) {
  const n = normalizeDataColorToken(tokenName ?? 'data-0');
  return `var(${tokenNameToCssVar(n)})`;
}

/**
 * @param {number} n
 */
function clamp01(n) {
  if (!Number.isFinite(n)) return 0;
  return Math.min(1, Math.max(0, n));
}

/**
 * @param {{ source: { x1: number }, target: { x0: number }, y0: number, y1: number, width: number }} d
 */
function sankeyRibbonPath(d) {
  const x0 = d.source.x1;
  const x1 = d.target.x0;
  const w = Math.max(1, d.width ?? 1);
  const half = w / 2;
  const c0x = x0 + (x1 - x0) / 3;
  const c1x = x1 - (x1 - x0) / 3;
  const t0 = d.y0 - half;
  const t1 = d.y1 - half;
  const b0 = d.y0 + half;
  const b1 = d.y1 + half;
  return [
    `M${x0},${t0}`,
    `C${c0x},${t0} ${c1x},${t1} ${x1},${t1}`,
    `L${x1},${b1}`,
    `C${c1x},${b1} ${c0x},${b0} ${x0},${b0}`,
    'Z',
  ].join('');
}

/** @param {import('./SmartSankeyChart.types').SmartSankeyChartSpec | undefined} spec */
function mergeSankeyConfig(spec) {
  const c = spec?.config ?? {};
  const m = c.margin ?? {};
  const rawLinkOpacity = Number(c.linkOpacity);
  return {
    height: Number.isFinite(+c.height) ? +c.height : 360,
    title: typeof c.title === 'string' ? c.title : '',
    showDataTitle: c.showDataTitle !== false,
    dataColorToken: normalizeDataColorToken(
      typeof c.dataColorToken === 'string' ? c.dataColorToken : 'data-0',
    ),
    margin: {
      top: Number.isFinite(+m.top) ? +m.top : 20,
      right: Number.isFinite(+m.right) ? +m.right : 56,
      bottom: Number.isFinite(+m.bottom) ? +m.bottom : 20,
      left: Number.isFinite(+m.left) ? +m.left : 20,
    },
    nodePadding: Number.isFinite(+c.nodePadding) ? +c.nodePadding : 18,
    nodeWidth: Number.isFinite(+c.nodeWidth) ? +c.nodeWidth : 5,
    linkOpacity: Number.isFinite(rawLinkOpacity) ? clamp01(rawLinkOpacity) : 0.2,
    formatValue:
      typeof c.formatValue === 'function'
        ? c.formatValue
        : (v) => (typeof v === 'number' && Number.isFinite(v) ? v.toLocaleString('zh-CN') : String(v)),
  };
}

/**
 * @param {{ spec: import('./SmartSankeyChart.types').SmartSankeyChartSpec, className?: string, style?: import('react').CSSProperties }} props
 */
export default function SmartSankeyChart({ spec, className, style }) {
  const wrapRef = useRef(null);
  const [width, setWidth] = useState(640);
  const [tooltipMouse, setTooltipMouse] = useState(/** @type {{clientX:number,clientY:number}|null} */ (null));
  const [hoverTarget, setHoverTarget] = useState(
    /** @type {{
      kind: 'node'|'link',
      id: string,
      label: string,
      value: number,
      colorVar?: string,
      baseOpacity?: number,
      nodeId?: string,
      sourceId?: string,
      targetId?: string,
    } | null} */ (null),
  );

  useEffect(() => {
    const el = wrapRef.current;
    if (!el) return undefined;
    const ro = new ResizeObserver(() => {
      setWidth(Math.max(200, el.clientWidth || 200));
    });
    ro.observe(el);
    setWidth(Math.max(200, el.clientWidth || 200));
    return () => ro.disconnect();
  }, []);

  const layout = useMemo(() => {
    const cfg = mergeSankeyConfig(spec);
    const inputNodes = (spec?.nodes ?? []).map((n) => ({ ...n }));
    const inputLinks = (spec?.links ?? []).map((l) => ({ ...l }));

    const sh = sankey()
      .nodeId((d) => d.id)
      .nodeWidth(cfg.nodeWidth)
      .nodePadding(cfg.nodePadding)
      .extent([
        [cfg.margin.left, cfg.margin.top],
        [width - cfg.margin.right, cfg.height - cfg.margin.bottom],
      ]);

    const { nodes: laidNodes, links: laidLinks } = sh({ nodes: inputNodes, links: inputLinks });

    const seriesFillVar = dataSlotCssVar(cfg.dataColorToken);

    return {
      nodes: laidNodes,
      links: laidLinks,
      seriesFillVar,
      cfg,
    };
  }, [spec, width]);

  const clearHover = useCallback(() => {
    setHoverTarget(null);
    setTooltipMouse(null);
  }, []);

  const onSvgPointerMove = useCallback((e) => {
    setTooltipMouse({ clientX: e.clientX, clientY: e.clientY });
  }, []);

  const { nodes, links, seriesFillVar, cfg } = layout;
  const resolveNodeColorVar = useCallback(
    (node) => dataSlotCssVar(node?.colorToken ?? cfg.dataColorToken),
    [cfg.dataColorToken],
  );
  const resolveLinkColorVar = useCallback(
    (link) => dataSlotCssVar(link?.colorToken ?? link?.source?.colorToken ?? cfg.dataColorToken),
    [cfg.dataColorToken],
  );
  const resolveLinkBaseOpacity = useCallback(
    (link) => {
      const linkOpacity = Number(link?.opacity);
      return Number.isFinite(linkOpacity) ? clamp01(linkOpacity) : cfg.linkOpacity;
    },
    [cfg.linkOpacity],
  );

  const titleBlock = cfg.title ? (
    <div className="smart-sankey-chart__chrome" key="chrome">
      <div className="smart-sankey-chart__chrome-title">{cfg.title}</div>
    </div>
  ) : null;

  const labelSide = (nx0, nx1) => {
    const cx = (nx0 + nx1) / 2;
    const mid = width / 2;
    if (cx < mid) {
      return { textAnchor: 'start', tx: nx1 + 10 };
    }
    return { textAnchor: 'end', tx: nx0 - 10 };
  };

  return (
    <div
      ref={wrapRef}
      className={['smart-sankey-chart', 'smart-sankey-chart--basic', className].filter(Boolean).join(' ')}
      style={{
        width: '100%',
        position: 'relative',
        background: 'var(--color-surface, #FFFFFF)',
        ...style,
      }}
      onMouseLeave={clearHover}
    >
      {titleBlock}
      <svg
        width="100%"
        height={cfg.height}
        role="img"
        aria-label="桑基图"
        style={{ display: 'block' }}
        onPointerMove={onSvgPointerMove}
        onPointerLeave={clearHover}
      >
        <g className="smart-sankey-chart__links">
          {links.map((link) => {
            const srcId = link.source?.id;
            const tgtId = link.target?.id;
            const linkId = `link-${link.index ?? 0}-${srcId ?? ''}-${tgtId ?? ''}`;
            const linkColorVar = resolveLinkColorVar(link);
            const baseOpacity = resolveLinkBaseOpacity(link);
            const isLinkHover = hoverTarget?.kind === 'link' && hoverTarget.id === linkId;
            const isNodeHover =
              hoverTarget?.kind === 'node' && (hoverTarget.nodeId === srcId || hoverTarget.nodeId === tgtId);
            const fillOpacity =
              hoverTarget == null
                ? baseOpacity
                : isLinkHover
                  ? Math.min(1, baseOpacity + 0.22)
                  : isNodeHover
                    ? Math.min(1, baseOpacity + 0.14)
                    : Math.max(0.06, baseOpacity * 0.4);
            return (
              <path
                key={linkId}
                d={sankeyRibbonPath(link)}
                fill={linkColorVar}
                fillOpacity={fillOpacity}
                stroke="none"
                style={{ cursor: 'pointer' }}
                onPointerEnter={(e) => {
                  setTooltipMouse({ clientX: e.clientX, clientY: e.clientY });
                  setHoverTarget({
                    kind: 'link',
                    id: linkId,
                    label: `${link.source?.label ?? link.source?.id ?? ''} → ${link.target?.label ?? link.target?.id ?? ''}`,
                    value: Number(link.value) || 0,
                    colorVar: linkColorVar,
                    baseOpacity,
                    sourceId: srcId ?? '',
                    targetId: tgtId ?? '',
                  });
                }}
                data-sankey-link-id={linkId}
                data-sankey-label={`${link.source?.label ?? link.source?.id ?? ''} → ${link.target?.label ?? link.target?.id ?? ''}`}
                data-sankey-value={String(Number(link.value) || 0)}
                data-sankey-source={srcId ?? ''}
                data-sankey-target={tgtId ?? ''}
                data-sankey-color={linkColorVar}
                data-sankey-opacity={String(baseOpacity)}
              />
            );
          })}
        </g>

        <g className="smart-sankey-chart__nodes">
          {nodes.map((node) => {
            const nh = Math.max((node.y1 ?? 0) - (node.y0 ?? 0), 1);
            const nx0 = node.x0 ?? 0;
            const nx1 = node.x1 ?? 0;
            const ny0 = node.y0 ?? 0;
            const midY = ny0 + nh / 2;
            const { textAnchor, tx } = labelSide(nx0, nx1);
            const showPct = node.labelPercent != null && Number.isFinite(+node.labelPercent);
            const nodeId = `node-${node.id}`;
            const nodeColorVar = resolveNodeColorVar(node);
            const isNodeHover = hoverTarget?.kind === 'node' && hoverTarget.id === nodeId;
            const isLinkedToHover =
              hoverTarget?.kind === 'link' && (hoverTarget.sourceId === node.id || hoverTarget.targetId === node.id);
            const nodeOpacity =
              hoverTarget == null
                ? 1
                : isNodeHover
                  ? 1
                  : isLinkedToHover
                    ? 0.96
                    : 0.62;

            return (
              <g key={node.id} className="smart-sankey-chart__node">
                <rect
                  className="smart-sankey-chart__bar-rect"
                  x={nx0}
                  y={ny0}
                  width={Math.max(nx1 - nx0, 1)}
                  height={nh}
                  fill={nodeColorVar}
                  fillOpacity={nodeOpacity}
                  style={{ cursor: 'pointer' }}
                  onPointerEnter={(e) => {
                    setTooltipMouse({ clientX: e.clientX, clientY: e.clientY });
                    setHoverTarget({
                      kind: 'node',
                      id: nodeId,
                      label: node.label ?? node.id ?? '',
                      value: Number(node.value) || 0,
                      colorVar: nodeColorVar,
                      nodeId: node.id,
                    });
                  }}
                  data-sankey-node-id={node.id}
                  data-sankey-label={node.label ?? node.id ?? ''}
                  data-sankey-value={String(Number(node.value) || 0)}
                  data-sankey-color={nodeColorVar}
                />
                {cfg.showDataTitle ? (
                  <text
                    textAnchor={textAnchor}
                    dominantBaseline="middle"
                    y={midY}
                    className="smart-sankey-chart__node-label"
                  >
                    <tspan x={tx} dy="-0.55em" className="smart-sankey-chart__node-label-title">
                      {node.label}
                    </tspan>
                    {showPct ? (
                      <tspan x={tx} dy="1.15em" className="smart-sankey-chart__node-label-value">
                        {`${(+node.labelPercent).toFixed(2)}%`}
                      </tspan>
                    ) : null}
                  </text>
                ) : null}
              </g>
            );
          })}
        </g>
      </svg>

      {hoverTarget && tooltipMouse ? (
        <FloatingTooltip mouse={tooltipMouse}>
          <div
            className="chart-tooltip chart-tooltip--chart smart-sankey-chart__tooltip-portal"
          >
            <div className="smart-sankey-chart__tooltip-head">
              <span
                className="smart-sankey-chart__tooltip-dot"
                style={{ background: hoverTarget.colorVar || seriesFillVar }}
                aria-hidden
              />
              <div className="chart-tooltip__label smart-sankey-chart__tooltip-flow-label">
                {hoverTarget.label}
              </div>
            </div>
            <div className="smart-sankey-chart__tooltip-value">
              {cfg.formatValue(hoverTarget.value)}
            </div>
          </div>
        </FloatingTooltip>
      ) : null}
    </div>
  );
}
