import { useId } from "react";
import type { SwimlaneProcessProps, SwimlaneTask } from "./schema";

type Lane = SwimlaneProcessProps["lanes"][number];

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

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


/**
 * 図の中に置くラベルの縁取り。`stroke-surface` と組で使う。
 *
 * 塗りの前に地の色で縁を引くので、線はラベルの手前で途切れて見え、線の上にラベルが来ても
 * 字が残る。背後に何も無いところでは地と同じ色なので見えない。線を避けて置き場を解き直す
 * 道は取らない —— 密な図では解が無い入力が必ず残るが、縁取りは入力に依らず効く。
 */
const HALO = { paintOrder: "stroke", strokeWidth: 4, strokeLinejoin: "round" } as const;

export function taskGeometry(tasks: SwimlaneTask[]) {
  const maxEnd = Math.max(
    ...tasks.map((task) => task.start + task.duration),
    1,
  );
  return tasks.map((task) => ({
    id: task.id,
    startPercent: round((task.start / maxEnd) * 100),
    widthPercent: round((task.duration / maxEnd) * 100),
  }));
}

export function rankHandoffs(tasks: SwimlaneTask[]) {
  return tasks
    .slice(0, -1)
    .map((task, index) => {
      const next = tasks[index + 1];
      return {
        fromTaskId: task.id,
        toTaskId: next.id,
        wait: Math.max(0, next.start - (task.start + task.duration)),
        crossLane: task.laneId !== next.laneId,
        inputIndex: index,
      };
    })
    .sort((a, b) => b.wait - a.wait || a.inputIndex - b.inputIndex);
}

export function swimlaneGeometry(lanes: Lane[], tasks: SwimlaneTask[]) {
  const maxEnd = Math.max(...tasks.map((task) => task.start + task.duration), 1);
  const taskNodes = tasks.map((task) => {
    const laneIndex = lanes.findIndex((lane) => lane.id === task.laneId);
    const x = 120 + (task.start / maxEnd) * 830;
    const endX = 120 + ((task.start + task.duration) / maxEnd) * 830;
    return {
      ...task,
      x: round(x),
      endX: round(endX),
      width: round(endX - x),
      y: round(62 + ((laneIndex + 0.5) / lanes.length) * 270),
      strokeWidth: round(12 + Math.min(task.duration, 8) * 2),
    };
  });
  const handoffs = taskNodes.slice(0, -1).map((task, index) => {
    const next = taskNodes[index + 1];
    const wait = Math.max(0, next.start - (task.start + task.duration));
    return {
      id: joinIds(task.id, next.id),
      source: task,
      target: next,
      wait,
      crossLane: task.laneId !== next.laneId,
      strokeWidth: round(1 + wait * 0.6),
    };
  });
  return { tasks: taskNodes, handoffs, maxEnd };
}

export default function SwimlaneProcess({
  title,
  subtitle,
  timeUnit,
  lanes,
  tasks,
  instanceId,
}: SwimlaneProcessProps) {
  // 既定を固定文字列にすると、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)}_`,
    ) || "swimlane-process";
  const geometry = swimlaneGeometry(lanes, tasks);
  const handoffs = rankHandoffs(tasks);
  const priority = handoffs[0];
  const notes = tasks.filter((task) => task.note);
  const markerId = `${uid}-handoff-arrow`;
  const drawnHandoffs = geometry.handoffs.map((handoff) => ({
    handoff,
    isPriority: priority?.fromTaskId === handoff.source.id,
    bendX:
      handoff.source.endX +
      Math.max(8, (handoff.target.x - handoff.source.endX) / 2),
  }));

  return (
    <section
      className="asset group/swimlane overflow-hidden rounded-xl border border-outline bg-surface p-5 text-primary shadow-card sm:p-8"
      data-layout="swimlane-process"
    >
      <header className="border-b border-outline pb-5">
        <p className="font-mono text-label text-tertiary">CROSS-FUNCTIONAL FLOW</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 items-center justify-between gap-3">
        <div className="inline-flex rounded-full border border-outline bg-surface-subtle p-1 text-label">
          <label className="cursor-pointer rounded-full px-3 py-1.5 has-[:checked]:bg-primary has-[:checked]:text-on-primary">
            <input className="swimlane-view-process sr-only" type="radio" name={`${uid}-view`} defaultChecked />工程
          </label>
          <label className="cursor-pointer rounded-full px-3 py-1.5 has-[:checked]:bg-primary has-[:checked]:text-on-primary">
            <input className="swimlane-view-ledger sr-only" type="radio" name={`${uid}-view`} />滞留診断
          </label>
        </div>
        <p className="font-mono text-label text-secondary">LENGTH=DURATION · GAP=WAIT · Y=OWNER</p>
      </div>

      {priority && (
        <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-swimlane-default-priority="true"
        >
          <span className="font-mono text-label text-tertiary">LONGEST WAIT</span>
          <strong>{tasks.find((task) => task.id === priority.fromTaskId)?.label} → {tasks.find((task) => task.id === priority.toTaskId)?.label}</strong>
          <span className="font-mono text-h3">{priority.wait}{timeUnit}</span>
        </div>
      )}

      <div data-swimlane-view="process" className="mt-6 group-has-[.swimlane-view-ledger:checked]/swimlane:hidden">
        <svg
          className="hidden h-80 w-full sm:block"
          viewBox="0 0 1000 360"
          role="img"
          aria-label="担当レーンを横断し、工程期間と待機期間を時間比例で示す業務フロー"
          data-primary-geometry="swimlane-process"
          data-geometry-channels="time-position duration-length duration-thickness owner-lane wait-gap"
        >
          <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>

          {lanes.map((lane, index) => {
            const y = 62 + ((index + 0.5) / lanes.length) * 270;
            const boundaryY = 62 + (index / lanes.length) * 270;
            return (
              <g key={lane.id}>
                <line x1="112" x2="960" y1={boundaryY} y2={boundaryY} className="stroke-outline" vectorEffect="non-scaling-stroke" />
                <text x="102" y={y + 4} textAnchor="end" className="fill-secondary font-mono text-label">L{index + 1} · {lane.name}</text>
              </g>
            );
          })}
          <line x1="112" x2="960" y1="332" y2="332" className="stroke-outline" vectorEffect="non-scaling-stroke" />
          <text x="120" y="24" className="fill-secondary font-mono text-label">0{timeUnit}</text>
          <text x="950" y="24" textAnchor="end" className="fill-secondary font-mono text-label">{geometry.maxEnd}{timeUnit}</text>

          {drawnHandoffs.map(({ handoff, isPriority, bendX }) => (
            <path
              key={handoff.id}
              d={`M ${handoff.source.endX + 5} ${handoff.source.y} H ${bendX} V ${handoff.target.y} H ${handoff.target.x - 10}`}
              fill="none"
              className={isPriority ? "stroke-tertiary" : "stroke-primary"}
              strokeWidth={handoff.strokeWidth}
              strokeDasharray={handoff.wait > 0 ? "7 6" : undefined}
              vectorEffect="non-scaling-stroke"
              markerEnd={`url(#${markerId})`}
              data-primary-edge={handoff.id}
            />
          ))}

          {geometry.tasks.map((task, index) => {
            const isPriority = priority?.toTaskId === task.id;
            const centerX = (task.x + task.endX) / 2;
            return (
              <g
                key={task.id}
                tabIndex={0}
                data-swimlane-task={task.id}
                data-primary-record={task.id}
                data-geometry-a={task.start}
                data-geometry-b={task.duration}
                data-primary-emphasis={isPriority ? "true" : undefined}
              >
                {isPriority && <line x1={task.x - 7} x2={task.endX + 7} y1={task.y} y2={task.y} className="stroke-tertiary/20" strokeWidth={task.strokeWidth + 16} strokeLinecap="round" />}
                <line x1={task.x} x2={task.endX} y1={task.y} y2={task.y} className={isPriority ? "stroke-tertiary" : "stroke-primary"} strokeWidth={task.strokeWidth} strokeLinecap="round" />
                <circle cx={task.x} cy={task.y} r="5" className="fill-surface" />
                <circle cx={task.endX} cy={task.y} r="5" className="fill-surface" />
                <text data-primary-label={task.id} x={centerX} y={task.y - task.strokeWidth / 2 - 36} textAnchor="middle" className="fill-primary stroke-surface text-label font-semibold" {...HALO}>T{String(index + 1).padStart(2, "0")}</text>
                <text x={centerX} y={task.y + task.strokeWidth / 2 + 18} textAnchor="middle" className="fill-secondary stroke-surface font-mono text-label" {...HALO}>{task.start}–{task.start + task.duration}</text>
              </g>
            );
          })}

          {/*
            待ちのラベルは線より後に置く。SVG は文書の順に描くので、縁取りは自分より前に
            描かれたものしか止められない —— 引き渡しの線の中に混ぜると、後の引き渡しや
            工程の線がラベルの上に来る（system-landscape-map で同じ並びが実測の事故になった）。
          */}
          {drawnHandoffs.map(({ handoff, isPriority, bendX }) =>
            handoff.wait > 0 ? (
              <text key={handoff.id} x={bendX} y={(handoff.source.y + handoff.target.y) / 2 - 7} textAnchor="middle" className={isPriority ? "fill-tertiary stroke-surface font-mono text-label" : "fill-secondary stroke-surface font-mono text-label"} {...HALO}>WAIT {handoff.wait}{timeUnit}</text>
            ) : null,
          )}
        </svg>

        <div className="relative space-y-3 pl-7 sm:hidden">
          <span className="absolute bottom-2 left-2 top-2 w-px bg-outline" aria-hidden="true" />
          {tasks.map((task, index) => (
            <article key={task.id} data-swimlane-mobile-task={task.id} className="relative grid grid-cols-[auto_1fr_auto] gap-3 border-b border-outline py-3 pl-3">
              <span className={`absolute -left-6 top-4 h-3 w-3 rounded-full ${priority?.toTaskId === task.id ? "bg-tertiary ring-4 ring-tertiary/20" : "bg-primary"}`} aria-hidden="true" />
              <span className="font-mono text-label text-tertiary">{String(index + 1).padStart(2, "0")}</span>
              <div><strong>{task.label}</strong><p className="text-label text-secondary">{lanes.find((lane) => lane.id === task.laneId)?.name}</p></div>
              <span className="font-mono text-label">{task.start}→{task.start + task.duration}{timeUnit}</span>
            </article>
          ))}
        </div>
      </div>

      <div className="swimlane-process-key mt-4 hidden gap-x-6 gap-y-2 border-t border-outline pt-4 sm:grid sm:grid-cols-2 xl:grid-cols-3" aria-label="工程一覧">
        {tasks.map((task, index) => <div key={task.id} data-swimlane-key={task.id} data-swimlane-task={task.id} className="grid grid-cols-[3rem_1fr] gap-2 py-1">
          <strong className="font-mono text-label text-tertiary">T{String(index + 1).padStart(2, "0")}</strong>
          <span>{task.label}<span className="block text-label text-secondary">{lanes.find((lane) => lane.id === task.laneId)?.name} · {task.start}→{task.start + task.duration}{timeUnit}</span></span>
        </div>)}
      </div>

      <div data-swimlane-view="handoff-ledger" className="mt-6 hidden group-has-[.swimlane-view-ledger:checked]/swimlane:block">
        <div className="divide-y divide-outline border-y border-outline">
          {handoffs.map((handoff, index) => (
            <article key={handoff.fromTaskId} data-swimlane-task={handoff.toTaskId} 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>
              <div><strong>{tasks.find((task) => task.id === handoff.fromTaskId)?.label} → {tasks.find((task) => task.id === handoff.toTaskId)?.label}</strong><p className="text-label text-secondary">{handoff.crossLane ? "部門間hand-off" : "同一lane"}</p></div>
              <span className="font-mono text-h3">{handoff.wait}{timeUnit}</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>
          <div className="mt-3 space-y-2">{notes.map((task) => <p key={task.id} data-note-item="true" className="text-body text-secondary"><strong className="text-primary">{task.label}:</strong> {task.note}</p>)}</div>
        </details>
      )}
    </section>
  );
}
