import { useId } from "react";
import type {
  LandscapeSystem,
  SystemInterface,
  SystemLandscapeMapProps,
} from "./schema";

type Axis = SystemLandscapeMapProps["domains"][number];

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

/**
 * 図の中に置くラベルの縁取り。`stroke-surface` と組で使う。
 *
 * 塗りの前に地の色で縁を引くので、連携線はラベルの手前で途切れて見え、線の上にラベルが
 * 来ても字が残る。背後に何も無いところでは地と同じ色なので見えない。
 *
 * 線を避けて置き場を解き直す道は取らない。この型の連携線はレイヤの間を縦横に走っていて、
 * 丸だけを避ける求解ですら 43 万本の総当たりを要した（45d736b）。そこへ線を足すと、解が
 * 無い入力が必ず残る。縁取りは入力に依らず効く。
 */
const HALO = { paintOrder: "stroke", strokeWidth: 4, strokeLinejoin: "round" } as const;

export function rankSystemRisks(
  systems: LandscapeSystem[],
  interfaces: SystemInterface[],
) {
  return systems
    .map((system, index) => {
      const connectionCount = interfaces.filter(
        (item) => item.sourceId === system.id || item.targetId === system.id,
      ).length;
      return {
        ...system,
        connectionCount,
        risk: system.criticality * (5 - system.health) + connectionCount,
        inputIndex: index,
      };
    })
    .sort((a, b) => b.risk - a.risk || a.inputIndex - b.inputIndex);
}

/**
 * 連携線のラベルをどこに置くか。
 *
 * 中点の真上（`textAnchor="middle"`）に置くと、縦向きの線ではラベルが行き先の丸と
 * 同じ x に来る。丸のラベルは枠に 1 つしか無いとき丸の真上か真下へ 2 行で置くので、
 * 線のラベルとぶつかる。レイヤ 3 段の行間は 118px で、両端の丸の半径と丸のラベル
 * 2 行がその大半を使うため、縦の線に残る隙間は元から 20px も無い。
 *
 * 実測（1280px・`svg text` の外接矩形）: 直す前は「顧客情報」と「CRM」が 0.7px、
 * 「経費・購買申請」と「ERP」が 0.1px しか離れておらず、上下が接して 1 つの塊に
 * 見えていた。丸のラベル 2 行の意図した行間が 4px なので、そこと区別が付かない。
 *
 * 縦に近い線ではラベルを線の脇へ逃がす。右寄りの線を右へ逃がすと軸の外へ出るので、
 * その場合は左へ倒す。横に近い線は従来どおり中点の上に置く —— 横の線では行き先の
 * 丸のラベルと x が揃わないので、ぶつかる理由が無い。
 */
/**
 * 線のラベルを線から持ち上げる量。
 *
 * 横へ逃がすだけでは足りなかった。逃がしたあとも「顧客情報」と「CRM」は横に 3.1px、
 * 「経費・購買申請」と「ERP」は 5.2px しか空いておらず、ラベル衝突の下限 3px を
 * かろうじて超えているだけだった（1200px で実測）。行き先の名前の幅は入力で変わるので、
 * 横の逃げは入力に依らない余裕を作れない。縦へ持ち上げるほうは作れる。
 *
 * 11 がこの図の天井で、12 にすると上のレイヤの健全性の輪へ字が乗る（`layout:browser` が
 * 「顧客情報」と「経費・購買申請」の 2 件で落ちる）。輪は丸より後に描かれるので縁取りが
 * 効かない。11 で最小の隙間は 4.03px —— 下限に対する余裕は 1px しかないが、これは図の
 * 骨格が決めている限界で、これ以上は横の逃げか丸の配置を解き直すしかない。
 */
const LIFT = 11;

export const edgeLabelPlacement = (
  middleX: number,
  middleY: number,
  dx: number,
  dy: number,
) => {
  // 縦横比 2:1 より急なら「縦の線」。真横に近い線を巻き込まない角度。
  if (Math.abs(dy) <= Math.abs(dx) * 2) {
    return { x: middleX, y: middleY - LIFT, anchor: "middle" as const };
  }
  // 逃がす量は丸のラベル 1 行目の半幅を超えている必要がある。10 では足りず
  // （実測で横の重なりが 23px → 3px に減るだけだった）、システム名の半幅は
  // サンプル最長の「顧客ポータル」で 33 なので 20 を取る。縦は動かさない ——
  // 下げると丸のラベルへ寄ってしまう。
  // 描画領域は viewBox の 0-1000。右端に寄った線は左へ逃がす。
  const toLeft = middleX > 700;
  return {
    x: round(middleX + (toLeft ? -20 : 20)),
    y: round(middleY - LIFT),
    anchor: toLeft ? ("end" as const) : ("start" as const),
  };
};

export function systemLandscapeGeometry(
  domains: Axis[],
  layers: Axis[],
  systems: LandscapeSystem[],
  interfaces: SystemInterface[],
) {
  const maxUsers = Math.max(...systems.map((system) => system.userCount));
  const nodes = systems.map((system) => {
    const domainIndex = domains.findIndex((item) => item.id === system.domainId);
    const layerIndex = layers.findIndex((item) => item.id === system.layerId);
    const peers = systems.filter(
      (item) =>
        item.domainId === system.domainId && item.layerId === system.layerId,
    );
    const peerIndex = peers.findIndex((item) => item.id === system.id);
    const peerOffset = peerIndex - (peers.length - 1) / 2;
    return {
      ...system,
      x: round(
        120 + ((domainIndex + 0.5) / domains.length) * 850 + peerOffset * 62,
      ),
      y: round(
        66 + ((layerIndex + 0.5) / layers.length) * 354 + peerOffset * 18,
      ),
      radius: round(18 + Math.sqrt(system.userCount / maxUsers) * 22),
      domainIndex,
      peerCount: peers.length,
      labelSide:
        peers.length > 1 ? (peerIndex % 2 === 0 ? "left" : "right") : "right",
    };
  });
  const positions = new Map(nodes.map((node) => [node.id, node]));
  const edges = interfaces.map((item, index) => ({
    ...item,
    id: `${item.sourceId}-${item.targetId}-${index}`,
    source: positions.get(item.sourceId)!,
    target: positions.get(item.targetId)!,
    strokeWidth: round(1 + item.criticality * 0.65),
  }));
  return { nodes, edges };
}

export default function SystemLandscapeMap({
  title,
  subtitle,
  domains,
  layers,
  systems,
  interfaces,
  instanceId,
}: SystemLandscapeMapProps) {
  // 既定を固定文字列にすると、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)}_`,
    ) || "system-landscape";
  const ranked = rankSystemRisks(systems, interfaces);
  const priority = ranked[0];
  const notes = systems.filter((item) => item.note);
  const geometry = systemLandscapeGeometry(
    domains,
    layers,
    systems,
    interfaces,
  );
  const markerId = `${uid}-interface-arrow`;
  const drawnEdges = geometry.edges.map((edge) => {
    const dx = edge.target.x - edge.source.x;
    const dy = edge.target.y - edge.source.y;
    // この値は middleX を通ってラベルの置き場の分岐に入るので、実装依存の近似である
    // Math.hypot ではなく、正しい丸めが保証される Math.sqrt を使う。
    const distance = Math.sqrt(dx ** 2 + dy ** 2) || 1;
    const ux = dx / distance;
    const uy = dy / distance;
    const startX = edge.source.x + ux * (edge.source.radius + 4);
    const startY = edge.source.y + uy * (edge.source.radius + 4);
    const endX = edge.target.x - ux * (edge.target.radius + 10);
    const endY = edge.target.y - uy * (edge.target.radius + 10);
    const middleX = (startX + endX) / 2;
    const middleY = (startY + endY) / 2;
    return {
      edge,
      path: `M ${startX} ${startY} Q ${middleX} ${startY} ${endX} ${endY}`,
      label: edgeLabelPlacement(middleX, middleY, dx, dy),
      touchesPriority:
        edge.sourceId === priority.id || edge.targetId === priority.id,
    };
  });

  return (
    <section
      className="asset group/system overflow-hidden rounded-xl border border-outline bg-surface p-5 text-primary shadow-card sm:p-8"
      data-layout="system-landscape-map"
    >
      <header className="border-b border-outline pb-5">
        <p className="font-mono text-label text-tertiary">APPLICATION TOPOLOGY</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="system-view-map 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="system-view-risk sr-only" type="radio" name={`${uid}-view`} />刷新優先
          </label>
        </div>
        <span className="font-mono text-label text-secondary">RADIUS=USERS · RING=HEALTH · LINE=CRITICALITY</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-system-default-priority="true"
      >
        <span className="font-mono text-label text-tertiary">MODERNIZE FIRST</span>
        <strong>{priority.name}</strong>
        <span className="font-mono text-h3">RISK {priority.risk}</span>
      </div>

      <div data-system-view="landscape" className="mt-6 group-has-[.system-view-risk:checked]/system:hidden">
        <svg
          className="hidden h-96 w-full sm:block"
          viewBox="0 0 1000 460"
          role="img"
          aria-label="業務ドメインとシステム層に配置したアプリケーション連携図"
          data-primary-geometry="system-landscape-map"
          data-geometry-channels="domain layer user-radius health-ring interface-width"
        >
          <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>

          {domains.map((domain, index) => {
            const startX = 120 + (index / domains.length) * 850;
            const centerX = 120 + ((index + 0.5) / domains.length) * 850;
            return (
              <g key={domain.id}>
                <line x1={startX} x2={startX} y1="46" y2="438" className="stroke-outline" vectorEffect="non-scaling-stroke" />
                <title>{domain.name}</title>
                <text x={centerX} y="26" textAnchor="middle" className="fill-primary font-mono text-label font-semibold">D{String(index + 1).padStart(2, "0")}</text>
              </g>
            );
          })}
          <line x1="970" x2="970" y1="46" y2="438" className="stroke-outline" vectorEffect="non-scaling-stroke" />
          {layers.map((layer, index) => {
            const y = 66 + ((index + 0.5) / layers.length) * 354;
            const boundaryY = 66 + (index / layers.length) * 354;
            return (
              <g key={layer.id}>
                <line x1="116" x2="970" y1={boundaryY} y2={boundaryY} className="stroke-outline" vectorEffect="non-scaling-stroke" />
                <title>{layer.name}</title>
                <text x="106" y={y + 4} textAnchor="end" className="fill-secondary font-mono text-label">L{String(index + 1).padStart(2, "0")}</text>
              </g>
            );
          })}
          <line x1="116" x2="970" y1="420" y2="420" className="stroke-outline" vectorEffect="non-scaling-stroke" />

          {/*
            線を全部描いてから、ラベルを全部描く。1 本ずつ「線・ラベル」で並べると、後の線が
            前のラベルの上に来る —— SVG は文書の順に描くので、縁取りは自分より前に描かれた
            ものしか止められない。実測で「受注・売上」が 2 本あとの線に切られていた。
          */}
          {drawnEdges.map(({ edge, path, touchesPriority }) => (
            <path
              key={edge.id}
              d={path}
              fill="none"
              className={touchesPriority ? "stroke-tertiary" : "stroke-primary"}
              strokeWidth={edge.strokeWidth}
              vectorEffect="non-scaling-stroke"
              markerEnd={`url(#${markerId})`}
              data-primary-edge={edge.id}
            />
          ))}
          {geometry.nodes.map((node) => {
            const circumference = 2 * Math.PI * (node.radius + 5);
            const share = node.health / 5;
            const isPriority = node.id === priority.id;
            const nodeNumber = geometry.nodes.indexOf(node) + 1;
            return (
              <g
                key={node.id}
                tabIndex={0}
                data-system-record={node.id}
                data-primary-record={node.id}
                data-geometry-a={node.radius}
                data-geometry-b={node.health}
                data-primary-emphasis={isPriority ? "true" : undefined}
              >
                <title>{`${node.name} · ${domains[node.domainIndex]?.name} · ${layers.find((item) => item.id === node.layerId)?.name} · ${node.userCount}人 · 健全性${node.health} · 重要度${node.criticality}`}</title>
                {isPriority && <circle cx={node.x} cy={node.y} r={node.radius + 13} className="fill-tertiary/15" />}
                <circle cx={node.x} cy={node.y} r={node.radius + 5} fill="none" className="stroke-outline" strokeWidth="2" />
                <circle
                  cx={node.x}
                  cy={node.y}
                  r={node.radius + 5}
                  fill="none"
                  className={isPriority ? "stroke-tertiary" : "stroke-primary"}
                  strokeWidth="4"
                  strokeDasharray={`${round(circumference * share)} ${round(circumference * (1 - share))}`}
                  transform={`rotate(-90 ${node.x} ${node.y})`}
                />
                <circle cx={node.x} cy={node.y} r={node.radius} className={isPriority ? "fill-tertiary" : "fill-primary"} />
                {/* 図が運ぶのは名前だけ。数値は「刷新優先」の台帳と携帯のリストが持つ。
                    2 行だと丸のまわりに 31px 要り、レイヤ 3 段の行間 118px では
                    両端の丸と線のラベルを引いた残りに収まらない —— 実測で統計行が
                    健全性の輪を 8px、優先の丸では 13.4px 横切って H1 が読めなかった。 */}
                <text data-primary-label={node.id} x={node.x} y={node.y + 3.5} textAnchor="middle" className="fill-on-primary font-mono font-semibold" fontSize="10">S{String(nodeNumber).padStart(2, "0")}</text>
              </g>
            );
          })}
        </svg>

        <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" />
          {systems.map((system) => {
            const isPriority = system.id === priority.id;
            return (
              <article key={system.id} data-system-record={system.id} data-system-mobile-record={system.id} className="relative border-b border-outline py-3 pl-3">
                <span className={`absolute -left-6 top-4 h-3 w-3 rounded-full ${isPriority ? "bg-tertiary ring-4 ring-tertiary/20" : "bg-primary"}`} aria-hidden="true" />
                <div className="flex justify-between gap-3"><strong>{system.name}</strong><span className="font-mono text-label">H{system.health} · C{system.criticality}</span></div>
                <p className="text-label text-secondary">{domains.find((item) => item.id === system.domainId)?.name} / {layers.find((item) => item.id === system.layerId)?.name} · {system.userCount}人</p>
              </article>
            );
          })}
          <div className="space-y-2 border-t border-outline pt-4" aria-label="連携一覧">
            {interfaces.map((item, index) => {
              const source = systems.find((system) => system.id === item.sourceId)!;
              const target = systems.find((system) => system.id === item.targetId)!;
              return <p key={`${item.sourceId}-${item.targetId}-${index}`} className="break-words text-label text-secondary"><strong className="font-mono text-primary">I{String(index + 1).padStart(2, "0")}</strong> · {source.name} → {target.name} · {item.label ?? "連携"} · 重要度{item.criticality}</p>;
            })}
          </div>
        </div>
      </div>

      <div className="system-landscape-key mt-5 hidden border-t border-outline pt-4 sm:block" aria-label="領域・層・システム・連携の一覧">
        <div className="grid gap-5 xl:grid-cols-2">
          <section><h2 className="mb-3 text-h3">業務領域</h2><ul className="grid gap-2 sm:grid-cols-2">{domains.map((item, index) => <li key={item.id} className="break-words border-b border-outline/60 py-2 text-label"><strong className="font-mono">D{String(index + 1).padStart(2, "0")}</strong> · {item.name}</li>)}</ul></section>
          <section><h2 className="mb-3 text-h3">システム層</h2><ul className="grid gap-2 sm:grid-cols-2">{layers.map((item, index) => <li key={item.id} className="break-words border-b border-outline/60 py-2 text-label"><strong className="font-mono">L{String(index + 1).padStart(2, "0")}</strong> · {item.name}</li>)}</ul></section>
        </div>
        <h2 className="mb-3 mt-5 text-h3">システム</h2>
        <div className="grid gap-x-6 gap-y-2 sm:grid-cols-2 xl:grid-cols-3">{systems.map((system, index) => <article key={system.id} data-system-key={system.id} className="min-w-0 break-words border-b border-outline/60 py-2 text-label"><strong className="font-mono">S{String(index + 1).padStart(2, "0")} · {system.name}</strong><p className="text-secondary">D{domains.findIndex((item) => item.id === system.domainId) + 1} / L{layers.findIndex((item) => item.id === system.layerId) + 1} · {system.userCount}人 · H{system.health} · C{system.criticality}</p></article>)}</div>
        <h2 className="mb-3 mt-5 text-h3">連携</h2>
        <div className="grid gap-x-6 gap-y-2 sm:grid-cols-2 xl:grid-cols-3">{interfaces.map((item, index) => { const source = systems.find((system) => system.id === item.sourceId)!; const target = systems.find((system) => system.id === item.targetId)!; return <article key={`${item.sourceId}-${item.targetId}-${index}`} className="min-w-0 break-words border-b border-outline/60 py-2 text-label"><strong className="font-mono">I{String(index + 1).padStart(2, "0")} · {item.label ?? "連携"}</strong><p className="text-secondary">S{systems.indexOf(source) + 1} {source.name} → S{systems.indexOf(target) + 1} {target.name} · 重要度{item.criticality}</p></article>; })}</div>
      </div>

      <div data-system-view="risk-ledger" className="mt-6 hidden group-has-[.system-view-risk:checked]/system:block">
        <div className="divide-y divide-outline border-y border-outline">
          {ranked.map((system, index) => (
            <article key={system.id} data-system-record={system.id} className={`grid grid-cols-[3rem_1fr_auto] items-center gap-3 py-3 ${index === 0 ? "border-l-4 border-tertiary pl-3" : "pl-4"}`}>
              <span className="font-mono text-label text-secondary">{String(index + 1).padStart(2, "0")}</span>
              {/* 規模は図から降ろしたので、卓上で数字として読めるのはここだけになる。
                  risk の式には入らない値なので、式とは中黒で分ける。 */}
              <div><strong>{system.name}</strong><p className="text-label text-secondary">{system.userCount}人 ・ 重要度{system.criticality} × 健全性不足{5 - system.health} + 接続{system.connectionCount}</p></div>
              <span className="font-mono text-h3">{system.risk}</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((item) => <p key={item.id} data-note-item="true" className="mt-3 text-body text-secondary"><strong className="text-primary">{item.name}:</strong> {item.note}</p>)}
        </details>
      )}
    </section>
  );
}
