import {
  sankey,
  sankeyLinkHorizontal,
  type SankeyGraph,
  type SankeyLink,
  type SankeyNode,
} from "d3-sankey";
import { useId, type CSSProperties } from "react";
import type { SankeyEndpoint, SankeyFlowItem, SankeyFlowProps } from "./schema";

const SANKEY_WIDTH = 600;
const SANKEY_HEIGHT = 320;

type SankeyNodeDatum = {
  key: string;
  id: string;
  label: string;
  side: "source" | "target";
  originalIndex: number;
};

type SankeyLinkDatum = SankeyFlowItem & {
  source: string;
  target: string;
  originalIndex: number;
};

export type SankeyGeometryNode = SankeyNodeDatum & {
  x0: number;
  x1: number;
  y0: number;
  y1: number;
  total: number;
};

export type SankeyGeometryLink = {
  sourceId: string;
  targetId: string;
  value: number;
  note?: string;
  originalIndex: number;
  sourceIndex: number;
  targetIndex: number;
  sourceY: number;
  targetY: number;
  width: number;
  path: string;
};

export type SankeyGeometry = {
  width: number;
  height: number;
  nodes: SankeyGeometryNode[];
  links: SankeyGeometryLink[];
};

export function endpointTotals(
  sources: readonly SankeyEndpoint[],
  targets: readonly SankeyEndpoint[],
  flows: readonly SankeyFlowItem[],
) {
  const sourceTotals = Object.fromEntries(
    sources.map((source) => [source.id, 0]),
  ) as Record<string, number>;
  const targetTotals = Object.fromEntries(
    targets.map((target) => [target.id, 0]),
  ) as Record<string, number>;
  let total = 0;
  for (const flow of flows) {
    sourceTotals[flow.sourceId] =
      (sourceTotals[flow.sourceId] ?? 0) + flow.value;
    targetTotals[flow.targetId] =
      (targetTotals[flow.targetId] ?? 0) + flow.value;
    total += flow.value;
  }
  return { total, sources: sourceTotals, targets: targetTotals };
}

export function flowSharePct(value: number, total: number) {
  return total === 0 ? 0 : (value / total) * 100;
}

export function rankedFlows<T extends SankeyFlowItem>(flows: readonly T[]) {
  return flows
    .map((flow, originalIndex) => ({ ...flow, originalIndex }))
    .sort((a, b) => b.value - a.value || a.originalIndex - b.originalIndex);
}

export function sankeyGeometry(
  sources: readonly SankeyEndpoint[],
  targets: readonly SankeyEndpoint[],
  flows: readonly SankeyFlowItem[],
): SankeyGeometry {
  const nodes: SankeyNodeDatum[] = [
    ...sources.map((source, originalIndex) => ({
      ...source,
      key: `source:${source.id}`,
      side: "source" as const,
      originalIndex,
    })),
    ...targets.map((target, originalIndex) => ({
      ...target,
      key: `target:${target.id}`,
      side: "target" as const,
      originalIndex,
    })),
  ];
  const links: SankeyLinkDatum[] = flows.map((flow, originalIndex) => ({
    ...flow,
    source: `source:${flow.sourceId}`,
    target: `target:${flow.targetId}`,
    originalIndex,
  }));
  const layout = sankey<SankeyNodeDatum, SankeyLinkDatum>()
    .nodeId((node) => node.key)
    .nodeWidth(10)
    .nodePadding(18)
    .nodeSort((a, b) => a.originalIndex - b.originalIndex)
    .extent([
      [0, 4],
      [SANKEY_WIDTH, SANKEY_HEIGHT - 4],
    ])
    .iterations(24);
  const graph = layout({
    nodes: nodes.map((node) => ({ ...node })),
    links: links.map((link) => ({ ...link })),
  } as SankeyGraph<SankeyNodeDatum, SankeyLinkDatum>);
  const pathFor = sankeyLinkHorizontal<SankeyNodeDatum, SankeyLinkDatum>();

  return {
    width: SANKEY_WIDTH,
    height: SANKEY_HEIGHT,
    nodes: graph.nodes.map((node) => ({
      key: node.key,
      id: node.id,
      label: node.label,
      side: node.side,
      originalIndex: node.originalIndex,
      x0: node.x0 as number,
      x1: node.x1 as number,
      y0: node.y0 as number,
      y1: node.y1 as number,
      total: node.value as number,
    })),
    links: graph.links.map((link) => {
      const source = link.source as SankeyNode<
        SankeyNodeDatum,
        SankeyLinkDatum
      >;
      const target = link.target as SankeyNode<
        SankeyNodeDatum,
        SankeyLinkDatum
      >;
      return {
        sourceId: link.sourceId,
        targetId: link.targetId,
        value: link.value,
        note: link.note,
        originalIndex: link.originalIndex,
        sourceIndex: source.originalIndex,
        targetIndex: target.originalIndex,
        sourceY: link.y0 as number,
        targetY: link.y1 as number,
        width: link.width as number,
        path:
          pathFor(link as SankeyLink<SankeyNodeDatum, SankeyLinkDatum>) ?? "",
      };
    }),
  };
}

const formatValue = (value: number) =>
  new Intl.NumberFormat("ja-JP", { maximumFractionDigits: 1 }).format(value);
const stagger = (index: number) => `calc(var(--stagger-step) * ${index})`;
// 起点と終点をそのまま `-` で繋ぐと単射にならない —— ("a-b","c") と ("a","b-c") が
// どちらも "a-b-c" になり、React の key と、卓上・携帯で共有するデータ identity が
// 別々の流路で一致してしまう。区切りに使う文字を各辺で逃がしてから繋ぐ。
const escapeSegment = (value: string) => value.replace(/[_-]/g, (c) => `_${c.charCodeAt(0).toString(16)}_`);
const flowKey = (flow: Pick<SankeyFlowItem, "sourceId" | "targetId">) =>
  `${escapeSegment(flow.sourceId)}-${escapeSegment(flow.targetId)}`;

const sankeyInteractionCss = (geometry: SankeyGeometry) =>
  geometry.links
    .map(
      (link) => `
[data-sankey-map]:has([data-sankey-link-index="${link.originalIndex}"]:is(:hover, :focus, :active)) [data-sankey-visible-index="${link.originalIndex}"] {
  stroke: var(--color-primary);
  stroke-opacity: 1;
}
[data-sankey-map]:has([data-sankey-link-index="${link.originalIndex}"]:is(:hover, :focus, :active)) [data-sankey-side="source"][data-sankey-node-index="${link.sourceIndex}"],
[data-sankey-map]:has([data-sankey-link-index="${link.originalIndex}"]:is(:hover, :focus, :active)) [data-sankey-side="target"][data-sankey-node-index="${link.targetIndex}"] {
  color: var(--color-tertiary);
}
[data-sankey-map]:has([data-sankey-link-index="${link.originalIndex}"]:is(:hover, :focus, :active)) .sankey-endpoint-rail[data-sankey-side="source"][data-sankey-node-index="${link.sourceIndex}"],
[data-sankey-map]:has([data-sankey-link-index="${link.originalIndex}"]:is(:hover, :focus, :active)) .sankey-endpoint-rail[data-sankey-side="target"][data-sankey-node-index="${link.targetIndex}"] {
  fill: var(--color-primary);
  fill-opacity: 1;
}
`,
    )
    .join("");

function DesktopFlowMap({
  geometry,
  totals,
  unit,
  gradientId,
  priorityIndex,
  sourceById,
  targetById,
}: {
  geometry: SankeyGeometry;
  totals: ReturnType<typeof endpointTotals>;
  unit: string;
  gradientId: string;
  priorityIndex: number;
  sourceById: ReadonlyMap<string, SankeyEndpoint>;
  targetById: ReadonlyMap<string, SankeyEndpoint>;
}) {
  return (
    <div
      data-sankey-desktop="true"
      data-sankey-map="true"
      className="relative hidden h-80 sm:block"
    >
      <div className="absolute inset-y-0 left-44 right-44">
        <svg
          viewBox={`0 0 ${geometry.width} ${geometry.height}`}
          preserveAspectRatio="none"
          className="h-full w-full overflow-visible"
          role="img"
          aria-label="起点から終点への流量図"
        >
          <defs>
            <linearGradient id={gradientId} x1="0" x2="1">
              <stop offset="0%" stopColor="var(--color-tertiary)" />
              <stop offset="100%" stopColor="var(--color-accent-violet)" />
            </linearGradient>
          </defs>
          {geometry.links.map((link) => {
            const isPriority = link.originalIndex === priorityIndex;
            const accessibleLabel = `${sourceById.get(link.sourceId)?.label} → ${targetById.get(link.targetId)?.label}: ${formatValue(link.value)} ${unit}${link.note ? `。${link.note}` : ""}`;
            return (
              <g
                key={flowKey(link)}
                data-sankey-flow={flowKey(link)}
                data-sankey-source={link.sourceId}
                data-sankey-target={link.targetId}
              >
                <title>{accessibleLabel}</title>
                <path
                  data-sankey-link-index={link.originalIndex}
                  tabIndex={0}
                  role="img"
                  aria-label={accessibleLabel}
                  d={link.path}
                  fill="none"
                  stroke="transparent"
                  strokeWidth={Math.max(link.width, 18)}
                  pointerEvents="stroke"
                  className="cursor-pointer outline-none"
                />
                <path
                  data-sankey-visible-index={link.originalIndex}
                  data-sankey-priority={isPriority ? "true" : undefined}
                  d={link.path}
                  fill="none"
                  stroke={
                    isPriority ? `url(#${gradientId})` : "var(--color-outline)"
                  }
                  strokeLinecap="butt"
                  strokeOpacity={isPriority ? 0.96 : 0.58}
                  strokeWidth={Math.max(link.width, 1)}
                  pointerEvents="none"
                  className={`sankey-visible-flow ${
                    isPriority ? "sankey-priority-flow" : ""
                  }`}
                />
              </g>
            );
          })}
          {geometry.nodes.map((node) => (
            <rect
              key={node.key}
              data-sankey-endpoint-rail={node.id}
              data-sankey-side={node.side}
              data-sankey-node-index={node.originalIndex}
              className="sankey-endpoint-rail"
              x={node.x0}
              y={node.y0}
              width={Math.max(node.x1 - node.x0, 1)}
              height={Math.max(node.y1 - node.y0, 1)}
              rx={4}
              fill="var(--color-outline)"
              fillOpacity={0.82}
            />
          ))}
        </svg>
      </div>

      {geometry.nodes.map((node) => {
        const isSource = node.side === "source";
        const nodeTotal = isSource
          ? totals.sources[node.id]
          : totals.targets[node.id];
        const anchor = {
          top: `${((node.y0 + node.y1) / 2 / geometry.height) * 100}%`,
          animationDelay: stagger(
            node.originalIndex + (isSource ? 0 : geometry.nodes.length),
          ),
        } as CSSProperties;
        return (
          <div
            key={node.key}
            data-sankey-endpoint={node.id}
            data-sankey-side={node.side}
            data-sankey-node-index={node.originalIndex}
            className={`absolute z-10 w-40 -translate-y-1/2 transition-colors ${
              isSource ? "left-0 text-left" : "right-0 text-right"
            }`}
            style={anchor}
          >
            <div className="animate-rise">
              <span
                aria-hidden="true"
                className={`absolute top-1/2 w-4 border-t border-outline ${
                  isSource ? "-right-4" : "-left-4"
                }`}
              />
              <p className="font-mono text-label text-secondary">
                {isSource ? "起点" : "終点"}
              </p>
              <p className="mt-0.5 text-label">{node.label}</p>
              <p className="mt-1 font-mono text-h3">
                {formatValue(nodeTotal ?? 0)}
                <span className="ml-1 text-label">{unit}</span>
              </p>
            </div>
          </div>
        );
      })}
    </div>
  );
}

function MobileFlowLedger({
  sources,
  flows,
  totals,
  maxFlow,
  unit,
  targetById,
  priorityIndex,
}: {
  sources: readonly SankeyEndpoint[];
  flows: readonly SankeyFlowItem[];
  totals: ReturnType<typeof endpointTotals>;
  maxFlow: number;
  unit: string;
  targetById: ReadonlyMap<string, SankeyEndpoint>;
  priorityIndex: number;
}) {
  return (
    <div
      data-sankey-mobile-ledger="true"
      className="rounded-lg bg-surface-subtle/50 p-4 sm:hidden"
    >
      <p className="mb-4 text-body-sm text-secondary">
        起点ごとに分岐を整理。横railの長さは各流路の量を表します。
      </p>
      <div className="divide-y divide-outline">
        {sources.map((source) => {
          const sourceFlows = flows
            .map((flow, originalIndex) => ({ ...flow, originalIndex }))
            .filter((flow) => flow.sourceId === source.id);
          return (
            <section key={source.id} className="py-4 first:pt-0 last:pb-0">
              <header className="mb-2 flex items-baseline justify-between gap-3">
                <div className="min-w-0">
                  <p className="font-mono text-label text-secondary">起点</p>
                  <h2 className="break-words text-label">{source.label}</h2>
                </div>
                <p className="shrink-0 font-mono text-body-sm">
                  {formatValue(totals.sources[source.id] ?? 0)}
                  <span className="ml-1 text-label">{unit}</span>
                </p>
              </header>
              <div className="ml-1 space-y-1 border-l border-outline pl-4">
                {sourceFlows.map((flow) => {
                  const isPriority = flow.originalIndex === priorityIndex;
                  const width =
                    maxFlow === 0 ? 0 : (flow.value / maxFlow) * 100;
                  return (
                    <button
                      key={flowKey(flow)}
                      type="button"
                      data-sankey-mobile-flow={flowKey(flow)}
                      data-priority={isPriority ? "true" : undefined}
                      className="group/mobile-flow block w-full py-2 text-left transition-colors focus:text-tertiary focus:outline-none"
                    >
                      <span className="flex min-w-0 items-baseline justify-between gap-3">
                        <span className="min-w-0 break-words text-label">
                          → {targetById.get(flow.targetId)?.label}
                        </span>
                        <span className="shrink-0 font-mono text-body-sm">
                          {formatValue(flow.value)}
                          <span className="ml-1 text-label">{unit}</span>
                        </span>
                      </span>
                      <span className="mt-1.5 block h-1.5 overflow-hidden rounded-full bg-outline/35">
                        <span
                          className={`block h-full rounded-full ${
                            isPriority
                              ? "bg-gradient-to-r from-tertiary to-accent-violet"
                              : "bg-tertiary/55 group-focus/mobile-flow:bg-tertiary"
                          }`}
                          style={{ width: `${width}%` }}
                        />
                      </span>
                    </button>
                  );
                })}
              </div>
            </section>
          );
        })}
      </div>
    </div>
  );
}

export default function SankeyFlow({
  title,
  subtitle,
  unit = "",
  sources,
  targets,
  flows,
  instanceId,
}: SankeyFlowProps) {
  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)}_`,
    ) || "sankey";
  const gradientId = `${uid}-flow-gradient`;
  const totals = endpointTotals(sources, targets, flows);
  const ranked = rankedFlows(flows);
  const priority = ranked[0];
  const priorityIndex = priority?.originalIndex ?? -1;
  const sourceById = new Map(sources.map((source) => [source.id, source]));
  const targetById = new Map(targets.map((target) => [target.id, target]));
  const maxFlow = priority?.value ?? 0;
  const geometry = sankeyGeometry(sources, targets, flows);

  return (
    <section className="group/sankey mx-auto max-w-slide bg-surface p-6 font-sans text-on-surface sm:p-12">
      <style>{`
.sankey-visible-flow {
  transition: stroke 140ms ease-out, stroke-opacity 140ms ease-out;
}
.sankey-endpoint-rail {
  transition: fill 140ms ease-out, fill-opacity 140ms ease-out;
}
[data-sankey-map]:has([data-sankey-link-index]:is(:hover, :focus, :active)) .sankey-priority-flow {
  stroke-opacity: 0.28;
}
${sankeyInteractionCss(geometry)}
      `}</style>

      <input
        id={`${uid}-flow`}
        name={`${uid}-view`}
        type="radio"
        defaultChecked
        className="sankey-flow-view sr-only"
      />
      <input
        id={`${uid}-ranked`}
        name={`${uid}-view`}
        type="radio"
        className="sankey-ranked-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 shadow-card">
          <label
            htmlFor={`${uid}-flow`}
            className="cursor-pointer rounded-sm px-4 py-1.5 text-label text-secondary group-has-[.sankey-flow-view:checked]/sankey:bg-primary group-has-[.sankey-flow-view:checked]/sankey:text-on-primary"
          >
            流量図
          </label>
          <label
            htmlFor={`${uid}-ranked`}
            className="cursor-pointer rounded-sm px-4 py-1.5 text-label text-secondary group-has-[.sankey-ranked-view:checked]/sankey:bg-primary group-has-[.sankey-ranked-view:checked]/sankey:text-on-primary"
          >
            流路ランキング
          </label>
        </div>
      </header>

      <div
        data-testid="sankey-flow"
        className="block group-has-[.sankey-ranked-view:checked]/sankey:hidden"
      >
        <DesktopFlowMap
          geometry={geometry}
          totals={totals}
          unit={unit}
          gradientId={gradientId}
          priorityIndex={priorityIndex}
          sourceById={sourceById}
          targetById={targetById}
        />
        <MobileFlowLedger
          sources={sources}
          flows={flows}
          totals={totals}
          maxFlow={maxFlow}
          unit={unit}
          targetById={targetById}
          priorityIndex={priorityIndex}
        />
        {priority && (
          <div className="mt-4 grid grid-cols-[1fr_auto] items-center gap-x-4 gap-y-1 border-y border-outline py-3 sm:grid-cols-[auto_1fr_auto] sm:gap-y-0">
            <p className="font-mono text-label text-secondary">最大流路</p>
            <p
              data-sankey-summary-route="true"
              className="col-span-2 row-start-2 min-w-0 break-words text-label sm:col-span-1 sm:col-start-2 sm:row-start-1"
            >
              {sourceById.get(priority.sourceId)?.label}
              <span className="mx-2 text-secondary">→</span>
              {targetById.get(priority.targetId)?.label}
            </p>
            <p className="col-start-2 row-start-1 font-mono text-h3 sm:col-start-3">
              {formatValue(priority.value)}
              <span className="ml-1 text-label">{unit}</span>
            </p>
          </div>
        )}
      </div>

      <div
        data-testid="sankey-ranked"
        className="hidden group-has-[.sankey-ranked-view:checked]/sankey:block"
      >
        <div className="space-y-3">
          {ranked.map((flow, index) => {
            const isPriority = index === 0;
            const width = maxFlow === 0 ? 0 : (flow.value / maxFlow) * 100;
            return (
              <article
                key={flow.originalIndex}
                data-priority={isPriority ? "true" : "false"}
                className="animate-rise grid grid-cols-[auto_1fr_auto] items-center gap-4 rounded-lg border border-outline bg-surface p-4 shadow-card"
                style={{ animationDelay: stagger(index) }}
              >
                <span
                  className={`flex size-9 items-center justify-center rounded-full font-mono text-label ${
                    isPriority
                      ? "bg-gradient-to-br from-tertiary to-accent-violet text-on-primary"
                      : "bg-neutral text-secondary"
                  }`}
                >
                  {String(index + 1).padStart(2, "0")}
                </span>
                <div>
                  <p className="text-label">
                    {sourceById.get(flow.sourceId)?.label}
                    <span className="mx-2 text-secondary">→</span>
                    {targetById.get(flow.targetId)?.label}
                    {isPriority && (
                      <span className="ml-2 font-mono text-label text-tertiary">
                        最大流路
                      </span>
                    )}
                  </p>
                  <div className="mt-2 h-2 rounded-full bg-surface-subtle">
                    <div
                      className={
                        isPriority
                          ? "h-full rounded-full bg-gradient-to-r from-tertiary to-accent-violet"
                          : "h-full rounded-full bg-tertiary"
                      }
                      style={{ width: `${width}%` }}
                    />
                  </div>
                  {flow.note && (
                    <p className="mt-2 text-body-sm text-secondary">
                      {flow.note}
                    </p>
                  )}
                </div>
                <p className="font-mono text-h3">
                  {formatValue(flow.value)}
                  <span className="ml-1 text-label">{unit}</span>
                </p>
              </article>
            );
          })}
        </div>
      </div>
    </section>
  );
}
