import { hierarchy, tree as d3Tree } from "d3-hierarchy";
import { line, linkHorizontal } from "d3-shape";
import { useId } from "react";
import type { CSSProperties } from "react";
import type { LogicNode, LogicTreeProps } from "./schema";

// 2 つの識別子を `-` で繋ぐと単射にならない —— ("a-b","c") と ("a","b-c") が
// 同じ鍵になり、React の key が重複する。区切りに使う文字を各辺で逃がしてから繋ぐ。
const joinIds = (...parts: (string | number)[]) =>
  parts.map((p) => String(p).replace(/[_-]/g, (c) => `_${c.charCodeAt(0).toString(16)}_`)).join("-");

export type TreeNode = LogicNode & {
  originalIndex: number;
  children: TreeNode[];
};

export type TreeGeometryNode = {
  id: string;
  parentId?: string;
  label: string;
  note?: string;
  originalIndex: number;
  depth: number;
  aggregate: number;
  share: number;
  centerY: number;
  x: number;
  recordWidth: number;
  relatedTargetIndices: number[];
  childIds: string[];
};

export type TreeGeometryEdge = {
  parentId: string;
  childId: string;
  share: number;
  strokeWidth: number;
  isPriorityPath: boolean;
  relatedTargetIndices: number[];
};

export type TreeGeometry = {
  total: number;
  maxDepth: number;
  priorityId?: string;
  priorityPathIds: string[];
  nodes: TreeGeometryNode[];
  edges: TreeGeometryEdge[];
};

export function buildTree(nodes: readonly LogicNode[]): TreeNode {
  const built = new Map(
    nodes.map((node, originalIndex) => [
      node.id,
      { ...node, originalIndex, children: [] as TreeNode[] },
    ]),
  );
  let root: TreeNode | undefined;
  for (const node of nodes) {
    const current = built.get(node.id) as TreeNode;
    if (node.parentId) built.get(node.parentId)?.children.push(current);
    else root = current;
  }
  if (!root) throw new Error("logic-tree root is missing");
  return root;
}

export function aggregateValue(node: TreeNode): number {
  if (node.children.length === 0) return node.value ?? 0;
  return node.children.reduce((sum, child) => sum + aggregateValue(child), 0);
}

function flattenTree(node: TreeNode): TreeNode[] {
  return [node, ...node.children.flatMap(flattenTree)];
}

export function rankedLeaves(nodes: readonly LogicNode[]) {
  return flattenTree(buildTree(nodes))
    .filter((node) => node.children.length === 0)
    .sort(
      (a, b) =>
        (b.value ?? 0) - (a.value ?? 0) || a.originalIndex - b.originalIndex,
    );
}

const pathToRoot = (
  node: TreeNode,
  nodeById: ReadonlyMap<string, TreeNode>,
) => {
  const path: string[] = [];
  let current: TreeNode | undefined = node;
  while (current) {
    path.unshift(current.id);
    current = current.parentId ? nodeById.get(current.parentId) : undefined;
  }
  return path;
};

const descendantIds = (node: TreeNode): string[] => [
  node.id,
  ...node.children.flatMap(descendantIds),
];

export function treeGeometry(nodes: readonly LogicNode[]): TreeGeometry {
  const root = buildTree(nodes);
  const flat = flattenTree(root);
  const nodeById = new Map(flat.map((node) => [node.id, node]));
  const total = aggregateValue(root);
  const priority = rankedLeaves(nodes)[0];
  const priorityPathIds = priority ? pathToRoot(priority, nodeById) : [];
  const depthById = new Map<string, number>();

  const assignDepth = (node: TreeNode, depth: number) => {
    depthById.set(node.id, depth);
    node.children.forEach((child) => assignDepth(child, depth + 1));
  };
  assignDepth(root, 0);

  const maxDepth = Math.max(...depthById.values());
  const columnWidth = 100 / (maxDepth + 1);
  const laidOut = d3Tree<TreeNode>()
    .size([84, Math.max(maxDepth, 1)])
    .separation((a, b) => (a.parent === b.parent ? 1 : 1.3))(
    hierarchy(root, (node) => node.children),
  );
  const centerById = new Map(
    laidOut.descendants().map((node) => [node.data.id, 8 + node.x]),
  );

  const relatedByTargetId = new Map<string, Set<string>>();
  for (const target of flat) {
    const related = new Set(pathToRoot(target, nodeById));
    if (target.children.length > 0) {
      descendantIds(target).forEach((id) => related.add(id));
    }
    relatedByTargetId.set(target.id, related);
  }

  const relatedTargetIndices = (elementId: string) =>
    flat
      .filter((target) => relatedByTargetId.get(target.id)?.has(elementId))
      .map((target) => target.originalIndex);

  const geometryNodes = flat.map<TreeGeometryNode>((node) => {
    const depth = depthById.get(node.id) ?? 0;
    const x = depth * columnWidth + 2;
    return {
      id: node.id,
      parentId: node.parentId,
      label: node.label,
      note: node.note,
      originalIndex: node.originalIndex,
      depth,
      aggregate: aggregateValue(node),
      share: total === 0 ? 0 : aggregateValue(node) / total,
      centerY: centerById.get(node.id) ?? 50,
      x,
      recordWidth: depth === maxDepth ? 100 - x - 2 : columnWidth * 0.56,
      relatedTargetIndices: relatedTargetIndices(node.id),
      childIds: node.children.map((child) => child.id),
    };
  });

  const edges = geometryNodes
    .filter(
      (node): node is TreeGeometryNode & { parentId: string } =>
        node.parentId !== undefined,
    )
    .map<TreeGeometryEdge>((node) => ({
      parentId: node.parentId,
      childId: node.id,
      share: node.share,
      strokeWidth: Math.min(4.25, 1.25 + Math.max(0, node.share) * 6),
      isPriorityPath: priorityPathIds.includes(node.id) && node.id !== root.id,
      relatedTargetIndices: relatedTargetIndices(node.id),
    }));

  return {
    total,
    maxDepth,
    priorityId: priority?.id,
    priorityPathIds,
    nodes: geometryNodes,
    edges,
  };
}

const formatValue = (value: number) =>
  new Intl.NumberFormat("ja-JP", { maximumFractionDigits: 1 }).format(value);
const stagger = (index: number) => `calc(var(--stagger-step) * ${index})`;
const relationTokens = (indices: readonly number[]) => indices.join(" ");
const nodeRole = (node: TreeGeometryNode) =>
  node.depth === 0 ? "ROOT" : node.childIds.length > 0 ? "BRANCH" : "DRIVER";
type LinkPoint = [number, number];
type LinkDatum = { source: LinkPoint; target: LinkPoint };
const curvedLink = linkHorizontal<LinkDatum, LinkPoint>();
const straightLink = line<LinkPoint>();
const readablePath = (path: string | null) =>
  (path ?? "")
    .replace(/([A-Za-z])/g, " $1 ")
    .replaceAll(",", " ")
    .replace(/\s+/g, " ")
    .trim();

const interactionCss = (geometry: TreeGeometry) =>
  geometry.nodes
    .map(
      (target) => `
[data-logic-tree]:has([data-logic-hotspot-index="${target.originalIndex}"]:is(:hover, :focus, :active)) [data-logic-record][data-logic-related~="${target.originalIndex}"] {
  color: var(--color-tertiary);
  opacity: 1;
}
[data-logic-tree]:has([data-logic-hotspot-index="${target.originalIndex}"]:is(:hover, :focus, :active)) .logic-weighted-edge[data-logic-related~="${target.originalIndex}"] {
  stroke: var(--color-primary);
  stroke-opacity: 1;
}
[data-logic-tree]:has([data-logic-hotspot-index="${target.originalIndex}"]:is(:hover, :focus, :active)) .logic-node-port[data-logic-related~="${target.originalIndex}"] {
  fill: var(--color-primary);
  stroke: var(--color-primary);
  opacity: 1;
}
[data-logic-tree]:has([data-logic-hotspot-index="${target.originalIndex}"]:is(:hover, :focus, :active)) .logic-mobile-rail[data-logic-related~="${target.originalIndex}"] {
  background: var(--color-tertiary);
}
`,
    )
    .join("");

function DesktopTree({
  geometry,
  unit,
  gradientId,
}: {
  geometry: TreeGeometry;
  unit: string;
  gradientId: string;
}) {
  const byId = new Map(geometry.nodes.map((node) => [node.id, node]));
  return (
    <div
      data-logic-tree="desktop"
      data-logic-layout-engine="d3-tree"
      className="relative hidden h-[38rem] overflow-hidden rounded-lg bg-surface-subtle/50 sm:block"
    >
      <svg
        aria-hidden="true"
        className="absolute inset-0 h-full w-full overflow-visible"
        viewBox="0 0 100 100"
        preserveAspectRatio="none"
      >
        <defs>
          <linearGradient id={gradientId} x1="0" y1="0" x2="1" y2="1">
            <stop offset="0" stopColor="var(--color-tertiary)" />
            <stop offset="1" stopColor="var(--color-accent-violet)" />
          </linearGradient>
        </defs>
        {geometry.edges.map((edge) => {
          const parent = byId.get(edge.parentId) as TreeGeometryNode;
          const child = byId.get(edge.childId) as TreeGeometryNode;
          const startX = parent.x + 1 + parent.recordWidth;
          const midpoint = startX + (child.x - startX) / 2;
          const source: LinkPoint = [startX, parent.centerY];
          const target: LinkPoint = [child.x, child.centerY];
          return (
            <g
              key={joinIds(edge.parentId, edge.childId)}
              data-logic-edge-child={edge.childId}
              data-logic-priority-path={
                edge.isPriorityPath ? "true" : undefined
              }
              data-logic-related={relationTokens(edge.relatedTargetIndices)}
              className={`logic-weighted-edge ${
                edge.isPriorityPath ? "logic-priority-edge" : ""
              }`}
              fill="none"
              stroke={
                edge.isPriorityPath
                  ? `url(#${gradientId})`
                  : "var(--color-outline)"
              }
              strokeLinecap="round"
              strokeOpacity={edge.isPriorityPath ? 0.96 : 0.68}
              strokeWidth={edge.strokeWidth}
              vectorEffect="non-scaling-stroke"
            >
              <path
                data-logic-branch-style="curved"
                className="logic-curved-edge"
                d={readablePath(curvedLink({ source, target }))}
                vectorEffect="non-scaling-stroke"
              />
              <path
                data-logic-branch-style="straight"
                className="logic-straight-edge"
                d={readablePath(straightLink([source, target]))}
                vectorEffect="non-scaling-stroke"
              />
              <path
                data-logic-branch-style="orthogonal"
                className="logic-orthogonal-edge"
                d={`M ${startX} ${parent.centerY} H ${midpoint} V ${child.centerY} H ${child.x}`}
                strokeLinejoin="miter"
                vectorEffect="non-scaling-stroke"
              />
            </g>
          );
        })}
        {geometry.nodes.map((node) => (
          <circle
            key={node.id}
            data-logic-node-port={node.id}
            data-logic-priority-port={
              node.id === geometry.priorityId ? "true" : undefined
            }
            data-logic-related={relationTokens(node.relatedTargetIndices)}
            className={`logic-node-port ${
              node.id === geometry.priorityId ? "logic-priority-port" : ""
            }`}
            cx={node.x}
            cy={node.centerY}
            r={node.id === geometry.priorityId ? 0.55 : 0.42}
            fill={
              node.id === geometry.priorityId
                ? `url(#${gradientId})`
                : "var(--color-surface)"
            }
            stroke={
              node.id === geometry.priorityId
                ? "var(--color-tertiary)"
                : "var(--color-outline)"
            }
            vectorEffect="non-scaling-stroke"
          />
        ))}
      </svg>

      {geometry.nodes.map((node) => {
        const isPriority = node.id === geometry.priorityId;
        const anchorPosition = {
          left: `${node.x + 1}%`,
          top: `${node.centerY}%`,
          width: `${node.recordWidth}%`,
        } as CSSProperties;
        const motion = {
          animationDelay: stagger(node.originalIndex),
        } as CSSProperties;
        return (
          <div
            key={node.id}
            data-logic-record={node.id}
            data-logic-related={relationTokens(node.relatedTargetIndices)}
            data-priority={isPriority ? "true" : undefined}
            className="logic-node-anchor absolute z-10 -translate-y-1/2 text-on-surface transition-[color,opacity]"
            style={anchorPosition}
          >
            <button
              type="button"
              data-logic-hotspot={node.id}
              data-logic-hotspot-index={node.originalIndex}
              className="logic-node-record w-full animate-rise cursor-pointer py-1.5 pr-2 text-left text-inherit focus:outline-none"
              style={motion}
            >
              <span className="block font-mono text-label text-secondary">
                {nodeRole(node)}
              </span>
              <span className="mt-0.5 block text-label">{node.label}</span>
              <span className="mt-1 flex flex-wrap items-baseline gap-x-2">
                <span className="font-mono text-h3">
                  {formatValue(node.aggregate)}
                  <span className="ml-1 text-label">{unit}</span>
                </span>
                {isPriority && (
                  <span className="font-mono text-label text-tertiary">
                    最大寄与
                  </span>
                )}
              </span>
            </button>
          </div>
        );
      })}
    </div>
  );
}

function MobileTreeNode({
  node,
  byId,
  geometry,
  unit,
}: {
  node: TreeGeometryNode;
  byId: ReadonlyMap<string, TreeGeometryNode>;
  geometry: TreeGeometry;
  unit: string;
}) {
  const isPriority = node.id === geometry.priorityId;
  return (
    <li className="relative">
      <button
        type="button"
        data-logic-record={node.id}
        data-logic-hotspot={node.id}
        data-logic-hotspot-index={node.originalIndex}
        data-logic-related={relationTokens(node.relatedTargetIndices)}
        data-priority={isPriority ? "true" : undefined}
        className="logic-node-record block w-full cursor-pointer text-left text-on-surface transition-colors focus:outline-none"
      >
        <span className="flex min-w-0 flex-wrap items-baseline gap-2">
          <span className="shrink-0 font-mono text-label text-secondary">
            {nodeRole(node)}
          </span>
          <span className="min-w-0 flex-1 break-words text-label">{node.label}</span>
          <span className="ml-auto flex max-w-full min-w-0 flex-wrap justify-end gap-x-1 font-mono text-body-sm">
            {formatValue(node.aggregate)}
            <span className="break-all text-label">{unit}</span>
          </span>
        </span>
        <span className="mt-2 block h-1.5 w-full overflow-hidden rounded-full bg-neutral">
          <span
            data-logic-mobile-rail={node.id}
            data-logic-related={relationTokens(node.relatedTargetIndices)}
            className={`logic-mobile-rail block h-full rounded-full transition-colors ${
              isPriority
                ? "bg-gradient-to-r from-tertiary to-accent-violet"
                : node.depth === 0
                  ? "bg-primary"
                  : "bg-secondary/40"
            }`}
            style={{ width: `${node.share * 100}%` }}
          />
        </span>
        {isPriority && (
          <span className="mt-1 block text-right font-mono text-label text-tertiary">
            最大寄与
          </span>
        )}
      </button>
      {node.childIds.length > 0 && (
        <ul className="ml-3 mt-4 space-y-4 border-l border-outline pl-5">
          {node.childIds.map((childId) => {
            const child = byId.get(childId) as TreeGeometryNode;
            return (
              <MobileTreeNode
                key={childId}
                node={child}
                byId={byId}
                geometry={geometry}
                unit={unit}
              />
            );
          })}
        </ul>
      )}
    </li>
  );
}

function MobileTree({
  geometry,
  unit,
}: {
  geometry: TreeGeometry;
  unit: string;
}) {
  const byId = new Map(geometry.nodes.map((node) => [node.id, node]));
  const root = geometry.nodes.find(
    (node) => node.depth === 0,
  ) as TreeGeometryNode;
  return (
    <div
      data-logic-tree="mobile"
      className="rounded-lg bg-surface-subtle/50 p-4 sm:hidden"
    >
      <p className="mb-5 text-body-sm text-secondary">
        幹から枝、葉へ分解。railの長さは全体への寄与を表します。
      </p>
      <ul>
        <MobileTreeNode
          node={root}
          byId={byId}
          geometry={geometry}
          unit={unit}
        />
      </ul>
    </div>
  );
}

export default function LogicTree({
  title,
  subtitle,
  unit = "",
  nodes,
  instanceId,
}: LogicTreeProps) {
  const autoId = useId();
  const uid =
    (instanceId ?? autoId).replace(
      // 捨てる操作は単射でないので、schema を通る別々の値が同じ id になる
      // （`sales:a` と `sales/a` がどちらも `salesa`）。同じ文書へ 2 つ置くと
      // id と radio の name が衝突して、片方の操作がもう片方に効く。
      // 逃がし文字（`_`）自身も逃がすので、この変換は単射。
      /[^a-zA-Z0-9-]/g,
      (c) => `_${c.charCodeAt(0).toString(16)}_`,
    ) || "logic-tree";
  const geometry = treeGeometry(nodes);
  const tree = buildTree(nodes);
  const flat = flattenTree(tree);
  const leaves = rankedLeaves(nodes);
  const notes = flat.filter((node) => node.note);
  const gradientId = `${uid}-logic-priority`;

  return (
    <section
      data-logic-root="true"
      className="group/logic mx-auto max-w-slide bg-surface p-6 font-sans text-on-surface sm:p-12"
    >
      <style>{`
.logic-weighted-edge {
  transition: stroke 140ms ease-out, stroke-opacity 140ms ease-out;
}
.logic-node-port {
  transition: fill 140ms ease-out, stroke 140ms ease-out, opacity 140ms ease-out;
}
[data-logic-tree]:has([data-logic-hotspot]:is(:hover, :focus, :active)) .logic-priority-edge {
  stroke-opacity: 0.28;
}
[data-logic-tree]:has([data-logic-hotspot]:is(:hover, :focus, :active)) .logic-priority-port {
  opacity: 0.28;
}
[data-logic-tree]:has([data-logic-hotspot]:is(:hover, :focus, :active)) [data-logic-record][data-priority="true"] {
  opacity: 0.56;
}
.logic-straight-edge,
.logic-orthogonal-edge {
  display: none;
}
[data-logic-root]:has(.logic-straight-branch-view:checked) .logic-curved-edge {
  display: none;
}
[data-logic-root]:has(.logic-straight-branch-view:checked) .logic-straight-edge {
  display: inline;
}
[data-logic-root]:has(.logic-orthogonal-branch-view:checked) .logic-curved-edge {
  display: none;
}
[data-logic-root]:has(.logic-orthogonal-branch-view:checked) .logic-orthogonal-edge {
  display: inline;
}
.logic-node-record,
.logic-node-record:focus-visible {
  outline: none;
}
${interactionCss(geometry)}
      `}</style>
      <input
        id={`${uid}-tree`}
        name={`${uid}-view`}
        type="radio"
        defaultChecked
        className="logic-tree-view sr-only"
      />
      <input
        id={`${uid}-ranked`}
        name={`${uid}-view`}
        type="radio"
        className="logic-ranked-view sr-only"
      />
      <input
        id={`${uid}-branch-curved`}
        name={`${uid}-branch-style`}
        type="radio"
        defaultChecked
        className="logic-curved-branch-view sr-only"
      />
      <input
        id={`${uid}-branch-straight`}
        name={`${uid}-branch-style`}
        type="radio"
        className="logic-straight-branch-view sr-only"
      />
      <input
        id={`${uid}-branch-orthogonal`}
        name={`${uid}-branch-style`}
        type="radio"
        className="logic-orthogonal-branch-view sr-only"
      />
      <header className="mb-7 flex flex-wrap items-end justify-between gap-4">
        <div className="max-w-3xl">
          <h1 className="text-h1">{title}</h1>
          {subtitle && (
            <p className="mt-2 text-body-sm text-secondary">{subtitle}</p>
          )}
        </div>
        <div className="flex rounded-sm border border-outline bg-surface-subtle p-1">
          <label
            htmlFor={`${uid}-tree`}
            className="cursor-pointer rounded-sm px-4 py-1.5 text-label text-secondary group-has-[.logic-tree-view:checked]/logic:bg-primary group-has-[.logic-tree-view:checked]/logic:text-on-primary"
          >
            構造
          </label>
          <label
            htmlFor={`${uid}-ranked`}
            className="cursor-pointer rounded-sm px-4 py-1.5 text-label text-secondary group-has-[.logic-ranked-view:checked]/logic:bg-primary group-has-[.logic-ranked-view:checked]/logic:text-on-primary"
          >
            寄与度
          </label>
        </div>
      </header>

      <div
        data-testid="logic-tree"
        className="block group-has-[.logic-ranked-view:checked]/logic:hidden"
      >
        <div className="mb-3 hidden items-center justify-between gap-4 sm:flex">
          <span className="font-mono text-label text-secondary">
            ROOT → BRANCH → DRIVER
          </span>
          <div className="flex items-center gap-4">
            <span className="font-mono text-label text-secondary">
              線幅 = 寄与度
            </span>
            <fieldset
              data-logic-branch-controls="true"
              className="flex items-center gap-1 rounded-sm border border-outline bg-surface-subtle p-1"
            >
              <legend className="sr-only">枝の形</legend>
              <span
                aria-hidden="true"
                className="px-2 font-mono text-label text-secondary"
              >
                枝の形
              </span>
              <label
                htmlFor={`${uid}-branch-curved`}
                className="flex cursor-pointer items-center gap-1.5 rounded-sm px-2.5 py-1 text-label text-secondary transition-colors group-has-[.logic-curved-branch-view:checked]/logic:bg-surface group-has-[.logic-curved-branch-view:checked]/logic:text-on-surface"
              >
                <svg
                  aria-hidden="true"
                  className="h-3.5 w-5"
                  viewBox="0 0 20 14"
                  fill="none"
                >
                  <path
                    d="M1 12 C8 12 8 2 19 2"
                    stroke="currentColor"
                    strokeLinecap="round"
                  />
                </svg>
                曲線
              </label>
              <label
                htmlFor={`${uid}-branch-straight`}
                className="flex cursor-pointer items-center gap-1.5 rounded-sm px-2.5 py-1 text-label text-secondary transition-colors group-has-[.logic-straight-branch-view:checked]/logic:bg-surface group-has-[.logic-straight-branch-view:checked]/logic:text-on-surface"
              >
                <svg
                  aria-hidden="true"
                  className="h-3.5 w-5"
                  viewBox="0 0 20 14"
                  fill="none"
                >
                  <path
                    d="M1 12 L19 2"
                    stroke="currentColor"
                    strokeLinecap="round"
                  />
                </svg>
                直線
              </label>
              <label
                htmlFor={`${uid}-branch-orthogonal`}
                className="flex cursor-pointer items-center gap-1.5 rounded-sm px-2.5 py-1 text-label text-secondary transition-colors group-has-[.logic-orthogonal-branch-view:checked]/logic:bg-surface group-has-[.logic-orthogonal-branch-view:checked]/logic:text-on-surface"
              >
                <svg
                  aria-hidden="true"
                  className="h-3.5 w-5"
                  viewBox="0 0 20 14"
                  fill="none"
                >
                  <path
                    d="M1 12 H10 V2 H19"
                    stroke="currentColor"
                    strokeLinejoin="miter"
                  />
                </svg>
                直角
              </label>
            </fieldset>
          </div>
        </div>
        <DesktopTree geometry={geometry} unit={unit} gradientId={gradientId} />
        <MobileTree geometry={geometry} unit={unit} />
        <p className="mt-4 text-right text-label text-secondary">
          親の値は葉から自動集計
        </p>
      </div>

      <div
        data-testid="logic-ranked"
        className="hidden group-has-[.logic-ranked-view:checked]/logic:block"
      >
        <div className="mb-4 flex flex-wrap items-baseline justify-between gap-3">
          <h2 className="text-h2">葉ドライバーの寄与度</h2>
          <p className="font-mono text-label text-secondary">
            合計 {formatValue(geometry.total)} {unit}
          </p>
        </div>
        <div className="divide-y divide-outline border-y border-outline">
          {leaves.map((leaf, index) => {
            const value = leaf.value ?? 0;
            const pct =
              geometry.total === 0 ? 0 : (value / geometry.total) * 100;
            const isPriority = leaf.id === geometry.priorityId;
            return (
              <article
                key={leaf.id}
                data-priority={isPriority ? "true" : "false"}
                className="grid animate-rise grid-cols-[2.5rem_1fr_auto] items-center gap-3 py-4 sm:gap-4"
                style={{ animationDelay: stagger(index) }}
              >
                <span className="font-mono text-label text-secondary">
                  {String(index + 1).padStart(2, "0")}
                </span>
                <div>
                  <p className="text-label">
                    {leaf.label}
                    {isPriority && (
                      <span className="ml-2 font-mono text-label text-tertiary">
                        最大寄与
                      </span>
                    )}
                  </p>
                  <div className="mt-2 h-1.5 rounded-full bg-neutral">
                    <div
                      className={
                        isPriority
                          ? "h-full rounded-full bg-gradient-to-r from-tertiary to-accent-violet"
                          : "h-full rounded-full bg-primary/55"
                      }
                      style={{ width: `${pct}%` }}
                    />
                  </div>
                </div>
                <p className="font-mono text-body-sm sm:text-h3">
                  {formatValue(value)}
                  <span className="ml-1 text-label">{unit}</span>
                </p>
              </article>
            );
          })}
        </div>
      </div>

      {notes.length > 0 && (
        <details
          data-inline-disclosure="true"
          className="group mt-6 rounded-lg border border-outline bg-surface-subtle"
        >
          <summary className="flex cursor-pointer list-none items-center px-4 py-3 text-label text-secondary focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tertiary">
            <span>補足を確認</span>
            <span className="ml-auto font-mono">{notes.length}件</span>
            <span
              aria-hidden="true"
              className="ml-2 text-tertiary transition-transform group-open:rotate-45"
            >
              ＋
            </span>
          </summary>
          <div className="grid gap-3 border-t border-outline p-4 sm:grid-cols-2">
            {notes.map((node) => (
              <article
                key={node.id}
                data-note-item="true"
                className="rounded-md bg-surface p-4"
              >
                <h3 className="text-label">{node.label}</h3>
                <p className="mt-1 font-mono text-label text-secondary">
                  {node.children.length === 0
                    ? "DRIVER"
                    : node.parentId
                      ? "BRANCH"
                      : "ROOT"}{" "}
                  ・ {formatValue(aggregateValue(node))}
                  {unit}
                </p>
                <p className="mt-2 text-body-sm">{node.note}</p>
              </article>
            ))}
          </div>
        </details>
      )}
    </section>
  );
}
