import { useId } from "react";
import type { DecisionOption, DecisionTreeProps } from "./schema";

const round = (value: number) => Number(value.toFixed(2));

// 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 function computeExpectedValues(options: DecisionOption[]) {
  const computed = options.map((option, index) => ({
    ...option,
    expectedValue: Number(
      option.branches
        .reduce(
          (sum, branch) =>
            sum + (branch.probability / 100) * branch.outcomeValue,
          0,
        )
        .toFixed(2),
    ),
    inputIndex: index,
  }));
  const best = [...computed].sort(
    (a, b) => b.expectedValue - a.expectedValue || a.inputIndex - b.inputIndex,
  )[0];
  return computed.map((option) => ({
    ...option,
    recommended: option.id === best?.id,
  }));
}

export function decisionTreeGeometry(options: DecisionOption[]) {
  const computed = computeExpectedValues(options);
  const maxExpected = Math.max(
    1,
    ...computed.map((option) => Math.abs(option.expectedValue)),
  );
  const maxOutcome = Math.max(
    1,
    ...computed.flatMap((option) =>
      option.branches.map((branch) => Math.abs(branch.outcomeValue)),
    ),
  );
  const optionSpan = computed.length === 1 ? 0 : 280 / (computed.length - 1);
  const optionNodes = computed.map((option, optionIndex) => ({
    ...option,
    x: 350,
    y: round(100 + optionIndex * optionSpan),
    size: round(24 + (Math.abs(option.expectedValue) / maxExpected) * 10),
  }));
  const outcomes = optionNodes.flatMap((option) => {
    const count = option.branches.length;
    const spread = count <= 1 ? 0 : Math.min(68, 96 / (count - 1));
    return option.branches.map((branch, branchIndex) => ({
      ...branch,
      id: joinIds(option.id, branch.id),
      optionId: option.id,
      optionLabel: option.label,
      x: 760,
      y: round(option.y + (branchIndex - (count - 1) / 2) * spread),
      radius: round(10 + Math.sqrt(Math.abs(branch.outcomeValue) / maxOutcome) * 18),
    }));
  });
  const rootEdges = optionNodes.map((option) => ({
    id: `root-${option.id}`,
    target: option,
    strokeWidth: round(1.4 + Math.max(0, option.expectedValue) / maxExpected),
  }));
  const branchEdges = outcomes.map((outcome) => ({
    id: joinIds(outcome.optionId, outcome.id),
    source: optionNodes.find((option) => option.id === outcome.optionId)!,
    target: outcome,
    strokeWidth: round(1 + outcome.probability * 0.05),
  }));
  return { options: optionNodes, outcomes, rootEdges, branchEdges };
}

export default function DecisionTree({
  title,
  subtitle,
  valueUnit,
  options,
  instanceId,
}: DecisionTreeProps) {
  // 既定を固定文字列にすると、id を指定せず 2 つ置いたときに radio の name と
  // SVG marker の id を共有してしまう。他の型と同じく useId() に寄せる。
  const autoId = useId();
  const uid = (instanceId ?? autoId).replace(
      // 逃がし文字（`_`）自身も逃がすので単射。捨てる形にすると `a:b` と `a/b` が
      // 同じ id になる。
      /[^a-zA-Z0-9-]/g,
      (c: string) => `_${c.charCodeAt(0).toString(16)}_`,
    ) || "decision-tree";
  const computed = computeExpectedValues(options);
  const best = computed.find((option) => option.recommended)!;
  const notes = options.flatMap((option) =>
    option.branches
      .filter((branch) => branch.note)
      .map((branch) => ({ option: option.label, ...branch })),
  );
  const geometry = decisionTreeGeometry(options);
  const markerId = `${uid}-probability-arrow`;

  return (
    <section
      className="asset group/decision overflow-hidden rounded-xl border border-outline bg-surface p-5 text-primary shadow-card sm:p-8"
      data-layout="decision-tree"
    >
      <header className="border-b border-outline pb-5">
        <p className="font-mono text-label text-tertiary">PROBABILITY SPINE</p>
        <h1 className="mt-2 text-h1 text-balance">{title}</h1>
        {subtitle && <p className="mt-2 text-body text-secondary">{subtitle}</p>}
      </header>

      <div className="mt-5 flex flex-wrap justify-between gap-3">
        <div className="inline-flex rounded-full border border-outline bg-surface-subtle p-1 text-label">
          <label className="rounded-full px-3 py-1.5 has-[:checked]:bg-primary has-[:checked]:text-on-primary">
            <input className="decision-view-tree sr-only" type="radio" name={`${uid}-view`} defaultChecked />分岐
          </label>
          <label className="rounded-full px-3 py-1.5 has-[:checked]:bg-primary has-[:checked]:text-on-primary">
            <input className="decision-view-ledger sr-only" type="radio" name={`${uid}-view`} />期待値
          </label>
        </div>
        <span className="font-mono text-label text-secondary">LINE=PROBABILITY · CIRCLE=OUTCOME · DIAMOND=OPTION</span>
      </div>

      <div
        className="mt-5 grid gap-2 border-l-4 border-tertiary bg-gradient-to-r from-tertiary/20 to-transparent px-4 py-3 sm:grid-cols-[auto_1fr_auto] sm:items-center"
        data-decision-default-priority="true"
      >
        <span className="font-mono text-label text-tertiary">RECOMMENDED</span>
        <strong>{best.label}</strong>
        <span className="font-mono text-h3">EV {best.expectedValue}{valueUnit}</span>
      </div>

      <div data-decision-view="tree" className="mt-6 group-has-[.decision-view-ledger:checked]/decision:hidden">
        <svg
          className="hidden h-96 w-full sm:block"
          viewBox="0 0 1000 460"
          role="img"
          aria-label="意思決定点から選択肢と確率別結果へ分岐する決定木"
          data-primary-geometry="decision-tree"
          data-geometry-channels="expected-value option-size probability-width outcome-radius outcome-sign"
        >
          <defs>
            <marker id={markerId} viewBox="0 0 10 10" refX="8" refY="5" markerWidth="9" markerHeight="9" markerUnits="userSpaceOnUse" orient="auto">
              <path d="M 0 0 L 10 5 L 0 10 z" className="fill-primary" />
            </marker>
          </defs>

          <line x1="70" x2="70" y1="42" y2="418" className="stroke-outline" vectorEffect="non-scaling-stroke" />
          <circle cx="70" cy="230" r="13" className="fill-primary" />
          <text x="70" y="28" textAnchor="middle" className="fill-secondary font-mono text-label">DECISION</text>
          <text x="350" y="28" textAnchor="middle" className="fill-secondary font-mono text-label">OPTIONS / EV</text>
          <text x="760" y="28" textAnchor="middle" className="fill-secondary font-mono text-label">OUTCOMES / P</text>

          {geometry.rootEdges.map((edge) => {
            const endX = edge.target.x - edge.target.size - 10;
            return (
              <path
                key={edge.id}
                d={`M 83 230 C 180 230, 220 ${edge.target.y}, ${endX} ${edge.target.y}`}
                fill="none"
                className={edge.target.recommended ? "stroke-tertiary" : "stroke-primary"}
                strokeWidth={edge.strokeWidth}
                vectorEffect="non-scaling-stroke"
                markerEnd={`url(#${markerId})`}
                data-primary-edge={edge.id}
              />
            );
          })}

          {geometry.branchEdges.map((edge) => {
            const startX = edge.source.x + edge.source.size + 8;
            const endX = edge.target.x - edge.target.radius - 10;
            const controlX = (startX + endX) / 2;
            return (
              <path
                key={edge.id}
                d={`M ${startX} ${edge.source.y} C ${controlX} ${edge.source.y}, ${controlX} ${edge.target.y}, ${endX} ${edge.target.y}`}
                fill="none"
                className={edge.source.recommended ? "stroke-tertiary" : "stroke-primary"}
                strokeWidth={edge.strokeWidth}
                vectorEffect="non-scaling-stroke"
                markerEnd={`url(#${markerId})`}
                data-primary-edge={edge.id}
              />
            );
          })}

          {geometry.options.map((option, index) => {
            const isPriority = option.recommended;
            const points = `${option.x},${option.y - option.size} ${option.x + option.size},${option.y} ${option.x},${option.y + option.size} ${option.x - option.size},${option.y}`;
            return (
              <g
                key={option.id}
                tabIndex={0}
                data-decision-option={option.id}
                data-primary-record={option.id}
                data-geometry-a={option.expectedValue}
                data-geometry-b={option.y}
                data-primary-emphasis={isPriority ? "true" : undefined}
              >
                {isPriority && <circle cx={option.x} cy={option.y} r={option.size + 13} className="fill-tertiary/15" />}
                <polygon points={points} className={isPriority ? "fill-tertiary" : "fill-primary"} />
                <text data-primary-label={option.id} x={option.x} y={option.y - option.size - 10} textAnchor="middle" className="fill-primary font-mono text-label font-semibold">{String(index + 1).padStart(2, "0")}</text>
              </g>
            );
          })}

          {geometry.outcomes.map((outcome) => {
            const positive = outcome.outcomeValue >= 0;
            return (
              <g
                key={outcome.id}
                tabIndex={0}
                data-primary-record={outcome.id}
                data-geometry-a={outcome.outcomeValue}
                data-geometry-b={outcome.probability}
                aria-label={`${outcome.label}: ${outcome.probability}% · ${outcome.outcomeValue}${valueUnit}`}
              >
                <circle cx={outcome.x} cy={outcome.y} r={outcome.radius} className={positive ? "fill-primary" : "fill-tertiary"} />
              </g>
            );
          })}
        </svg>

        <div className="decision-desktop-ledger mt-4 hidden divide-y divide-outline border-y border-outline sm:block">
          {computed.map((option, optionIndex) => (
            <article key={option.id} data-decision-desktop-option={option.id} data-decision-option={option.id} data-primary-record={option.id} className="grid grid-cols-[3rem_minmax(0,1fr)] gap-3 py-3">
              <span className="font-mono text-label text-tertiary">{String(optionIndex + 1).padStart(2, "0")}</span>
              <div className="min-w-0 break-words">
                <div className="flex flex-wrap justify-between gap-x-4"><strong data-primary-label={option.id}>{option.label}</strong><span className="font-mono text-label">EV {option.expectedValue}{valueUnit}{option.recommended ? " · 推奨" : ""}</span></div>
                <div className="mt-2 divide-y divide-outline/60">
                  {option.branches.map((branch, branchIndex) => (
                    <p key={branch.id} data-decision-desktop-outcome={branch.id} className="py-2 text-label text-secondary"><span className="mr-2 font-mono text-tertiary">O{String(geometry.outcomes.findIndex((outcome) => outcome.id === joinIds(option.id, branch.id)) + 1).padStart(2, "0")}</span><strong className="text-primary">{branch.label}</strong><span> · {branch.probability}% · {branch.outcomeValue > 0 ? "+" : ""}{branch.outcomeValue}{valueUnit}</span></p>
                  ))}
                </div>
              </div>
            </article>
          ))}
        </div>

        <div className="relative space-y-4 pl-7 sm:hidden">
          <span className="absolute bottom-2 left-2 top-2 w-px bg-outline" aria-hidden="true" />
          {computed.map((option, index) => (
            <article key={option.id} data-decision-option={option.id} data-decision-mobile-option={option.id} className={`relative py-3 pl-3 ${option.recommended ? "border-l-4 border-tertiary" : "border-b border-outline"}`}>
              <span className={`absolute -left-6 top-4 h-3 w-3 rotate-45 ${option.recommended ? "bg-tertiary ring-4 ring-tertiary/20" : "bg-primary"}`} aria-hidden="true" />
              <div className="flex justify-between gap-3"><strong>{String(index + 1).padStart(2, "0")} · {option.label}</strong><span className="font-mono text-label">EV {option.expectedValue}</span></div>
              <p className="mt-2 text-label text-secondary">{option.branches.map((branch) => `${branch.label} ${branch.probability}% / ${branch.outcomeValue}`).join(" · ")}</p>
            </article>
          ))}
        </div>
      </div>

      <div data-decision-view="evidence-ledger" className="mt-6 hidden group-has-[.decision-view-ledger:checked]/decision:block">
        <div className="divide-y divide-outline border-y border-outline">
          {[...computed].sort((a, b) => b.expectedValue - a.expectedValue).map((option, index) => (
            <article key={option.id} data-decision-option={option.id} className={`grid grid-cols-[3rem_1fr_auto] items-center gap-3 py-3 ${option.recommended ? "border-l-4 border-tertiary pl-3" : "pl-4"}`}>
              <span className="font-mono text-label text-secondary">{String(index + 1).padStart(2, "0")}</span>
              <div><strong>{option.label}</strong><p className="text-label text-secondary">{option.branches.map((branch) => `${branch.probability}%×${branch.outcomeValue}`).join(" + ")}</p></div>
              <span className="font-mono text-h3">{option.expectedValue}</span>
            </article>
          ))}
        </div>
      </div>

      {notes.length > 0 && (
        <details data-inline-disclosure className="mt-6 rounded-lg border border-outline bg-surface-subtle p-4">
          <summary className="cursor-pointer font-semibold">補足を確認（{notes.length}件）</summary>
          {notes.map((branch) => <p key={joinIds(branch.option, branch.id)} data-note-item="true" className="mt-3 text-body text-secondary"><strong className="text-primary">{branch.option} / {branch.label}:</strong> {branch.note}</p>)}
        </details>
      )}
    </section>
  );
}
