import { useQuery } from "@tanstack/react-query";
import type { Node as ReactFlowNode } from "@xyflow/react";
import * as React from "react";
import { useLocation, useParams } from "react-router-dom";
import { Switch } from "../../../components/ui/switch";
import {
  fetchOrgChartSnapshot,
  fetchPublicOrgChart,
  type TenantSummary,
  type UserSummary,
} from "../../../lib/adminApi";
import { buildTenantFullTree, type TenantNode } from "../../../lib/tenantTree";
import {
  orderHanmacFamilyChildren,
  orderHanmacFamilyTenants,
} from "../hanmacFamilyOrder";
import { getOrgRankWeight } from "../rankPriority";
import { filterTenantsByVisibility, getOrgUnitType } from "../tenantVisibility";
import { getOrgChartUserDisplayName, getUserOrgProfile } from "../userDisplay";

export type OrgNode = {
  id: string;
  name: string;
  level: number;
  members: UserSummary[];
  children: OrgNode[];
  totalCount: number;
  totalMemberIds: Set<string>;
  companyCode?: string;
  companyColorDepth?: number;
  companyColorKey?: string;
  orgUnitType?: string;
  type?: string;
};

type ViewBox = {
  x: number;
  y: number;
  width: number;
  height: number;
};

type OrgChartLoadErrorDiagnostics = {
  cacheMode: "redis" | "public";
  code: string;
  message: string;
  route: string;
  status: number | null;
  tenantId: string;
};

type OrgSelectionDescendantOption = {
  depth: 1 | 2;
  id: string;
  label: string;
};

type OrgSelectionOption = {
  descendants: OrgSelectionDescendantOption[];
  id: string;
  label: string;
};

type VisualNode = {
  node: OrgNode;
  x: number;
  y: number;
  width: number;
  height: number;
  members: UserSummary[];
  collapsed: boolean;
};

type VisualEdge = {
  childId: string;
  key: string;
  parentId: string;
  path: string;
  visibleByDefault: boolean;
};

export type ChartLayout = {
  nodes: VisualNode[];
  edges: VisualEdge[];
  width: number;
  height: number;
};

export type ChildLayoutMode = "auto" | "topDown" | "threeColumn";

export type OrgChartLayoutOptions = {
  childLayoutMode: ChildLayoutMode;
};

export type SemanticZoomMode = "overview" | "compact" | "detail";

const NODE_WIDTH = 168;
const MEMBER_COLUMN_WIDTH = 128;
const MEMBER_COLUMN_GAP = 8;
const HEADER_HEIGHT = 42;
const MEMBER_ROW_HEIGHT = 24;
const NODE_PADDING_Y = 12;
const MEMBER_CARD_BASE_CHAR_COUNT = 8;
const MEMBER_CARD_CHAR_WIDTH = 12;
const MEMBER_CARD_TEXT_PADDING_X = 28;
const MEMBER_COLUMN_MAX_WIDTH = 280;
const MEMBER_GRID_TARGET_ASPECT_RATIO = 2;
const MAX_MEMBER_COLUMN_COUNT = 8;
const ROOT_GAP_X = 120;
const CHILD_GAP_Y = 96;
const SIBLING_GAP_X = 80;
const MIN_COMPRESSED_GAP_X = 28;
const MULTI_COLUMN_GAP_Y = 72;
const MIN_MULTI_COLUMN_GAP_X = 28;
const MAX_MULTI_COLUMN_GAP_X = 180;
const MIN_MULTI_COLUMN_GAP_Y = 48;
const MAX_MULTI_COLUMN_GAP_Y = 160;
const MAX_WIDTH_LIMIT = NODE_WIDTH * 4 + SIBLING_GAP_X * 3;
const TARGET_ASPECT_RATIO = 1.5;
const MIN_TARGET_ASPECT_RATIO = 1.41;
const MAX_TARGET_ASPECT_RATIO = 1.61;
const WIDE_ASPECT_RATIO = MAX_TARGET_ASPECT_RATIO;
const TALL_ASPECT_RATIO = 0.5;
const CHART_MARGIN = 72;
const MIN_SCALE = 0.08;
const MAX_SCALE = 32;
const ZOOM_SENSITIVITY = 0.0015;
const FAMILY_FILTER_ID = "hanmac-family";
const DEFAULT_LAYOUT_OPTIONS: OrgChartLayoutOptions = {
  childLayoutMode: "auto",
};

const childLayoutModeOptions: Array<{
  id: ChildLayoutMode;
  label: string;
}> = [
  { id: "auto", label: "자동" },
  { id: "topDown", label: "Top-down" },
  { id: "threeColumn", label: "3열" },
];

type DepthColorNodeData = {
  depth: number;
  headerFill: string;
};

type DepthColorNode = ReactFlowNode<DepthColorNodeData, "org-depth-color">;

function getRankWeight(
  user: UserSummary,
  tenant?: { id: string; slug: string },
) {
  const profile = getUserOrgProfile(user, tenant);
  return getOrgRankWeight(profile.grade);
}

function getComplementaryColor(hexColor: string) {
  const normalized = hexColor.trim().replace("#", "");
  if (!/^[\da-f]{6}$/i.test(normalized)) return "#f59e0b";

  const red = 255 - Number.parseInt(normalized.slice(0, 2), 16);
  const green = 255 - Number.parseInt(normalized.slice(2, 4), 16);
  const blue = 255 - Number.parseInt(normalized.slice(4, 6), 16);
  return `#${[red, green, blue]
    .map((value) => value.toString(16).padStart(2, "0"))
    .join("")}`;
}

function getDisplayTextWidthUnit(value: string) {
  return Array.from(value).reduce((sum, char) => {
    if (char === " ") return sum + 0.4;
    if (char.charCodeAt(0) <= 0x7f) return sum + 0.55;
    return sum + 1;
  }, 0);
}

function getMemberColumnWidth(
  members: UserSummary[],
  tenant?: { id: string; slug: string },
) {
  const baselineWidth =
    MEMBER_CARD_TEXT_PADDING_X +
    MEMBER_CARD_BASE_CHAR_COUNT * MEMBER_CARD_CHAR_WIDTH;
  const maxDisplayWidth = members.reduce((maxWidth, member) => {
    const displayName = getOrgChartUserDisplayName(member, tenant);
    const estimatedWidth =
      MEMBER_CARD_TEXT_PADDING_X +
      getDisplayTextWidthUnit(displayName) * MEMBER_CARD_CHAR_WIDTH;
    return Math.max(maxWidth, estimatedWidth);
  }, baselineWidth);

  return Math.min(
    MEMBER_COLUMN_MAX_WIDTH,
    Math.max(MEMBER_COLUMN_WIDTH, Math.ceil(maxDisplayWidth)),
  );
}

export function getMemberGridMetrics(
  memberCount: number,
  memberColumnWidth = MEMBER_COLUMN_WIDTH,
) {
  if (memberCount <= 0) return { columnCount: 1, rowCount: 1 };

  const maxColumnCount = Math.min(
    MAX_MEMBER_COLUMN_COUNT,
    Math.max(1, memberCount),
  );
  let best = { columnCount: 1, rowCount: memberCount };
  let bestScore = Number.POSITIVE_INFINITY;

  for (let columnCount = 1; columnCount <= maxColumnCount; columnCount += 1) {
    const rowCount = Math.ceil(memberCount / columnCount);
    const width =
      columnCount <= 1
        ? NODE_WIDTH
        : Math.max(
            NODE_WIDTH,
            NODE_PADDING_Y * 2 +
              columnCount * memberColumnWidth +
              (columnCount - 1) * MEMBER_COLUMN_GAP,
          );
    const height =
      HEADER_HEIGHT + NODE_PADDING_Y * 2 + rowCount * MEMBER_ROW_HEIGHT;
    const aspectRatio = width / height;
    const score = Math.abs(
      Math.log(aspectRatio / MEMBER_GRID_TARGET_ASPECT_RATIO),
    );

    if (
      score < bestScore ||
      (score === bestScore && rowCount < best.rowCount)
    ) {
      best = { columnCount, rowCount };
      bestScore = score;
    }
  }

  return best;
}

function getMemberColumnCount(memberCount: number, memberColumnWidth?: number) {
  return getMemberGridMetrics(memberCount, memberColumnWidth).columnCount;
}

function getMemberRowCount(memberCount: number, memberColumnWidth?: number) {
  return getMemberGridMetrics(memberCount, memberColumnWidth).rowCount;
}

function getNodeWidth(members: UserSummary[], memberColumnWidth: number) {
  const columnCount = getMemberColumnCount(members.length, memberColumnWidth);
  if (columnCount <= 1) {
    return Math.max(NODE_WIDTH, NODE_PADDING_Y * 2 + memberColumnWidth);
  }

  return Math.max(
    NODE_WIDTH,
    NODE_PADDING_Y * 2 +
      columnCount * memberColumnWidth +
      (columnCount - 1) * MEMBER_COLUMN_GAP,
  );
}

function getNodeHeight(members: UserSummary[], memberColumnWidth: number) {
  return (
    HEADER_HEIGHT +
    NODE_PADDING_Y * 2 +
    getMemberRowCount(members.length, memberColumnWidth) * MEMBER_ROW_HEIGHT
  );
}

export function clampScale(scale: number) {
  return Math.min(MAX_SCALE, Math.max(MIN_SCALE, scale));
}

function getCompressedSiblingGap(siblingCount: number) {
  if (siblingCount >= 12) return MIN_COMPRESSED_GAP_X;
  if (siblingCount >= 8) return 40;
  if (siblingCount >= 4) return 56;
  return SIBLING_GAP_X;
}

function resolveLayoutOptions(
  options?: Partial<OrgChartLayoutOptions>,
): OrgChartLayoutOptions {
  return {
    ...DEFAULT_LAYOUT_OPTIONS,
    ...options,
  };
}

function getExpectedHorizontalWidth(childLayouts: ChartLayout[], gap: number) {
  return (
    childLayouts.reduce((sum, layout) => sum + layout.width, 0) +
    gap * Math.max(childLayouts.length - 1, 0)
  );
}

function getExpectedHorizontalHeight(
  nodeHeight: number,
  childLayouts: ChartLayout[],
) {
  if (childLayouts.length === 0) return nodeHeight;
  return (
    nodeHeight +
    CHILD_GAP_Y +
    Math.max(...childLayouts.map((layout) => layout.height))
  );
}

function sumLayout(values: number[], gap: number) {
  return (
    values.reduce((sum, value) => sum + value, 0) +
    gap * Math.max(values.length - 1, 0)
  );
}

function shouldUseMultiColumnLayout(
  nodeHeight: number,
  childLayouts: ChartLayout[],
  options: OrgChartLayoutOptions,
) {
  if (childLayouts.length <= 1) return false;
  if (options.childLayoutMode === "topDown") return false;
  if (options.childLayoutMode === "threeColumn") return true;
  if (childLayouts.length <= 4) return false;

  const siblingGap = getCompressedSiblingGap(childLayouts.length);
  const expectedWidth = getExpectedHorizontalWidth(childLayouts, siblingGap);
  const expectedHeight = getExpectedHorizontalHeight(nodeHeight, childLayouts);
  const aspectRatio = expectedWidth / Math.max(expectedHeight, 1);

  if (aspectRatio < TALL_ASPECT_RATIO) return false;
  return expectedWidth > MAX_WIDTH_LIMIT || aspectRatio > WIDE_ASPECT_RATIO;
}

function getMultiColumnDimensions(
  childLayouts: ChartLayout[],
  columnCount: number,
  nodeHeight: number,
  columnGap = SIBLING_GAP_X,
  rowGap = MULTI_COLUMN_GAP_Y,
) {
  const rowCount = Math.ceil(childLayouts.length / columnCount);
  const columnWidths = Array.from({ length: columnCount }, (_, columnIndex) =>
    Math.max(
      ...childLayouts
        .filter((_, index) => index % columnCount === columnIndex)
        .map((layout) => layout.width),
    ),
  );
  const rowHeights = Array.from({ length: rowCount }, (_, rowIndex) =>
    Math.max(
      ...childLayouts
        .filter((_, index) => Math.floor(index / columnCount) === rowIndex)
        .map((layout) => layout.height),
    ),
  );
  const childrenWidth = sumLayout(columnWidths, columnGap);
  const childrenHeight = sumLayout(rowHeights, rowGap);

  return {
    columnGap,
    columnWidths,
    height: nodeHeight + CHILD_GAP_Y + childrenHeight,
    rowGap,
    rowHeights,
    width: Math.max(NODE_WIDTH, childrenWidth),
  };
}

function clampNumber(value: number, min: number, max: number) {
  return Math.min(max, Math.max(min, value));
}

function tuneMultiColumnGaps(
  childLayouts: ChartLayout[],
  columnCount: number,
  nodeHeight: number,
) {
  let dimensions = getMultiColumnDimensions(
    childLayouts,
    columnCount,
    nodeHeight,
  );
  let aspectRatio = dimensions.width / Math.max(dimensions.height, 1);

  if (
    aspectRatio < MIN_TARGET_ASPECT_RATIO ||
    aspectRatio > MAX_TARGET_ASPECT_RATIO
  ) {
    const columnGapCount = Math.max(columnCount - 1, 0);

    if (columnGapCount > 0) {
      const columnWidthSum = dimensions.columnWidths.reduce(
        (sum, width) => sum + width,
        0,
      );
      const targetWidth = dimensions.height * TARGET_ASPECT_RATIO;
      const nextColumnGap = clampNumber(
        (targetWidth - columnWidthSum) / columnGapCount,
        MIN_MULTI_COLUMN_GAP_X,
        MAX_MULTI_COLUMN_GAP_X,
      );

      dimensions = getMultiColumnDimensions(
        childLayouts,
        columnCount,
        nodeHeight,
        nextColumnGap,
        dimensions.rowGap,
      );
      aspectRatio = dimensions.width / Math.max(dimensions.height, 1);
    }
  }

  if (
    aspectRatio < MIN_TARGET_ASPECT_RATIO ||
    aspectRatio > MAX_TARGET_ASPECT_RATIO
  ) {
    const rowGapCount = Math.max(dimensions.rowHeights.length - 1, 0);

    if (rowGapCount > 0) {
      const fixedHeight =
        nodeHeight +
        CHILD_GAP_Y +
        dimensions.rowHeights.reduce((sum, height) => sum + height, 0);
      const targetHeight = dimensions.width / TARGET_ASPECT_RATIO;
      const nextRowGap = clampNumber(
        (targetHeight - fixedHeight) / rowGapCount,
        MIN_MULTI_COLUMN_GAP_Y,
        MAX_MULTI_COLUMN_GAP_Y,
      );

      dimensions = getMultiColumnDimensions(
        childLayouts,
        columnCount,
        nodeHeight,
        dimensions.columnGap,
        nextRowGap,
      );
    }
  }

  return dimensions;
}

function getAspectRatioPenalty(aspectRatio: number) {
  if (
    aspectRatio >= MIN_TARGET_ASPECT_RATIO &&
    aspectRatio <= MAX_TARGET_ASPECT_RATIO
  ) {
    return 0;
  }
  if (aspectRatio < MIN_TARGET_ASPECT_RATIO) {
    return MIN_TARGET_ASPECT_RATIO - aspectRatio;
  }
  return aspectRatio - MAX_TARGET_ASPECT_RATIO;
}

function getMultiColumnCount(
  childLayouts: ChartLayout[],
  nodeHeight: number,
  options: OrgChartLayoutOptions,
) {
  if (options.childLayoutMode === "threeColumn") {
    return Math.min(3, childLayouts.length);
  }

  let bestColumnCount = Math.max(2, Math.round(Math.sqrt(childLayouts.length)));
  let bestScore = Number.POSITIVE_INFINITY;

  for (
    let columnCount = 2;
    columnCount <= childLayouts.length;
    columnCount += 1
  ) {
    const dimensions = tuneMultiColumnGaps(
      childLayouts,
      columnCount,
      nodeHeight,
    );
    const aspectRatio = dimensions.width / Math.max(dimensions.height, 1);
    const penalty = getAspectRatioPenalty(aspectRatio);
    const targetDistance = Math.abs(aspectRatio - TARGET_ASPECT_RATIO);
    const score = penalty * 100 + targetDistance;

    if (score < bestScore) {
      bestScore = score;
      bestColumnCount = columnCount;
    }
  }

  return bestColumnCount;
}

function getColumnOffsets(widths: number[], gap: number) {
  const offsets: number[] = [];
  let cursor = 0;

  for (const width of widths) {
    offsets.push(cursor);
    cursor += width + gap;
  }

  return offsets;
}

function getRowOffsets(heights: number[], gap: number) {
  const offsets: number[] = [];
  let cursor = 0;

  for (const height of heights) {
    offsets.push(cursor);
    cursor += height + gap;
  }

  return offsets;
}

function getLayoutMaxDepth(layout: ChartLayout) {
  return Math.max(...layout.nodes.map((visualNode) => visualNode.node.level));
}

function orderChildLayoutsForMultiColumn(childLayouts: ChartLayout[]) {
  return childLayouts
    .map((layout, index) => ({
      depth: getLayoutMaxDepth(layout),
      index,
      layout,
    }))
    .sort((a, b) => b.depth - a.depth || a.index - b.index)
    .map((entry) => entry.layout);
}

function rangesOverlap(
  startA: number,
  endA: number,
  startB: number,
  endB: number,
) {
  return startA < endB && startB < endA;
}

function getRequiredShift(
  placedNodes: VisualNode[],
  candidateNodes: VisualNode[],
  gap: number,
) {
  let requiredShift = 0;

  for (const placed of placedNodes) {
    for (const candidate of candidateNodes) {
      if (
        rangesOverlap(
          placed.y,
          placed.y + placed.height,
          candidate.y,
          candidate.y + candidate.height,
        )
      ) {
        requiredShift = Math.max(
          requiredShift,
          placed.x + placed.width + gap - candidate.x,
        );
      }
    }
  }

  return Math.max(0, requiredShift);
}

function offsetNodes(nodes: VisualNode[], offsetX: number, offsetY: number) {
  return nodes.map((node) => ({
    ...node,
    x: node.x + offsetX,
    y: node.y + offsetY,
  }));
}

function offsetEdges(edges: VisualEdge[], offsetX: number, offsetY: number) {
  return edges.map((edge) => ({
    ...edge,
    path: offsetPath(edge.path, offsetX, offsetY),
  }));
}

function getEdgeBounds(edges: VisualEdge[]) {
  const xValues: number[] = [];
  const yValues: number[] = [];

  for (const edge of edges) {
    const numbers = edge.path.match(/-?\d+(\.\d+)?/g)?.map(Number) ?? [];
    numbers.forEach((value, index) => {
      if (index % 2 === 0) {
        xValues.push(value);
      } else {
        yValues.push(value);
      }
    });
  }

  return {
    minX: Math.min(0, ...xValues),
    minY: Math.min(0, ...yValues),
    maxX: Math.max(0, ...xValues),
    maxY: Math.max(0, ...yValues),
  };
}

function normalizeLayout(nodes: VisualNode[], edges: VisualEdge[]) {
  const edgeBounds = getEdgeBounds(edges);
  const minX = Math.min(0, edgeBounds.minX, ...nodes.map((node) => node.x));
  const minY = Math.min(0, ...nodes.map((node) => node.y));
  const normalizedNodes =
    minX === 0 && minY === 0 ? nodes : offsetNodes(nodes, -minX, -minY);
  const normalizedEdges =
    minX === 0 && minY === 0 ? edges : offsetEdges(edges, -minX, -minY);
  const maxX = Math.max(
    NODE_WIDTH,
    edgeBounds.maxX - minX,
    ...normalizedNodes.map((node) => node.x + node.width),
  );
  const maxY = Math.max(
    1,
    edgeBounds.maxY - minY,
    ...normalizedNodes.map((node) => node.y + node.height),
  );

  return {
    nodes: normalizedNodes,
    edges: normalizedEdges,
    width: maxX,
    height: maxY,
  };
}

function buildOrgNode(
  tenantNode: TenantNode,
  usersMap: Map<string, UserSummary[]>,
  depth: number,
  inheritedCompanyColorKey = "",
  inheritedCompanyColorDepth = 0,
): OrgNode {
  const slug = tenantNode.slug.toLowerCase();
  const shouldResetCompanyColor =
    tenantNode.type === "COMPANY" || tenantNode.type === "COMPANY_GROUP";
  const companyColorKey = shouldResetCompanyColor
    ? getCompanyColorKey(tenantNode)
    : inheritedCompanyColorKey || getCompanyColorKey(tenantNode);
  const companyColorDepth = shouldResetCompanyColor
    ? 0
    : inheritedCompanyColorDepth + 1;
  const members = usersMap.get(slug) || [];
  const children = orderHanmacFamilyChildren(
    tenantNode,
    tenantNode.children,
  ).map((child) =>
    buildOrgNode(
      child,
      usersMap,
      depth + 1,
      companyColorKey,
      companyColorDepth,
    ),
  );
  const totalMemberIds = new Set(members.map((member) => member.id));
  for (const child of children) {
    for (const memberId of child.totalMemberIds) {
      totalMemberIds.add(memberId);
    }
  }

  return {
    id: tenantNode.id,
    name: tenantNode.name,
    level: depth,
    members,
    children,
    totalCount: totalMemberIds.size,
    totalMemberIds,
    companyCode: slug,
    companyColorDepth,
    companyColorKey,
    orgUnitType: getOrgUnitType(tenantNode.config),
    type: tenantNode.type,
  };
}

function createEdgePath(
  parentCenterX: number,
  parentBottomY: number,
  childRoot: VisualNode,
) {
  const childCenterX = childRoot.x + childRoot.width / 2;
  const childTopY = childRoot.y;
  const midY = parentBottomY + (childTopY - parentBottomY) / 2;

  return `M ${parentCenterX} ${parentBottomY} L ${parentCenterX} ${midY} L ${childCenterX} ${midY} L ${childCenterX} ${childTopY}`;
}

function layoutMultiColumnChildren(
  childLayouts: ChartLayout[],
  nodeHeight: number,
  options: OrgChartLayoutOptions,
) {
  const orderedChildLayouts = orderChildLayoutsForMultiColumn(childLayouts);
  const columnCount = getMultiColumnCount(
    orderedChildLayouts,
    nodeHeight,
    options,
  );
  const { columnGap, columnWidths, rowGap, rowHeights } = tuneMultiColumnGaps(
    orderedChildLayouts,
    columnCount,
    nodeHeight,
  );
  const columnOffsets = getColumnOffsets(columnWidths, columnGap);
  const rowOffsets = getRowOffsets(rowHeights, rowGap);
  const childOffsetY = nodeHeight + CHILD_GAP_Y;
  const nodes: VisualNode[] = [];
  const edges: VisualEdge[] = [];
  const roots: VisualNode[] = [];

  for (const [index, childLayout] of orderedChildLayouts.entries()) {
    const columnIndex = index % columnCount;
    const rowIndex = Math.floor(index / columnCount);
    const offsetX = columnOffsets[columnIndex];
    const offsetY = childOffsetY + rowOffsets[rowIndex];
    const shiftedNodes = offsetNodes(childLayout.nodes, offsetX, offsetY);
    const shiftedEdges = offsetEdges(childLayout.edges, offsetX, offsetY);

    nodes.push(...shiftedNodes);
    edges.push(...shiftedEdges);
    roots.push(shiftedNodes[0]);
  }

  return { nodes, edges, roots };
}

function layoutTopDownChildren(
  childLayouts: ChartLayout[],
  nodeHeight: number,
) {
  const childOffsetY = nodeHeight + CHILD_GAP_Y;
  const siblingGap = getCompressedSiblingGap(childLayouts.length);
  const nodes: VisualNode[] = [];
  const edges: VisualEdge[] = [];
  const roots: VisualNode[] = [];

  for (const childLayout of childLayouts) {
    const candidateNodes = offsetNodes(childLayout.nodes, 0, childOffsetY);
    const requiredShift = getRequiredShift(nodes, candidateNodes, siblingGap);
    const shiftedNodes = offsetNodes(
      childLayout.nodes,
      requiredShift,
      childOffsetY,
    );
    const shiftedEdges = offsetEdges(
      childLayout.edges,
      requiredShift,
      childOffsetY,
    );

    nodes.push(...shiftedNodes);
    edges.push(...shiftedEdges);
    roots.push(shiftedNodes[0]);
  }

  return { nodes, edges, roots };
}

function layoutTree(
  node: OrgNode,
  collapsedIds: Set<string>,
  options: OrgChartLayoutOptions,
): ChartLayout {
  const tenantIdentity = { id: node.id, slug: node.companyCode ?? "" };
  const members = [...node.members].sort(
    (a, b) =>
      getRankWeight(a, tenantIdentity) - getRankWeight(b, tenantIdentity),
  );
  const memberColumnWidth = getMemberColumnWidth(members, tenantIdentity);
  const nodeHeight = getNodeHeight(members, memberColumnWidth);
  const collapsed = collapsedIds.has(node.id);
  const childLayouts = collapsed
    ? []
    : node.children.map((child) => layoutTree(child, collapsedIds, options));
  const useMultiColumn = shouldUseMultiColumnLayout(
    nodeHeight,
    childLayouts,
    options,
  );
  const childLayoutResult = useMultiColumn
    ? layoutMultiColumnChildren(childLayouts, nodeHeight, options)
    : layoutTopDownChildren(childLayouts, nodeHeight);
  const childRoots = childLayoutResult.roots;
  const firstChildSectionY = Math.min(
    Number.POSITIVE_INFINITY,
    ...childRoots.map((childRoot) => childRoot.y),
  );

  const childCenters = childRoots.map(
    (childRoot) => childRoot.x + childRoot.width / 2,
  );
  const nodeWidth = getNodeWidth(members, memberColumnWidth);
  const firstChildCenter =
    childCenters.length > 0 ? Math.min(...childCenters) : nodeWidth / 2;
  const lastChildCenter =
    childCenters.length > 0 ? Math.max(...childCenters) : nodeWidth / 2;
  const nodeX =
    childRoots.length > 0
      ? (firstChildCenter + lastChildCenter) / 2 - nodeWidth / 2
      : 0;
  const nodeY = 0;
  const visualNode: VisualNode = {
    node,
    x: nodeX,
    y: nodeY,
    width: nodeWidth,
    height: nodeHeight,
    members,
    collapsed,
  };

  const nodes: VisualNode[] = [visualNode];
  const edges: VisualEdge[] = [];

  for (const childRoot of childRoots) {
    const parentCenterX = nodeX + nodeWidth / 2;
    const parentBottomY = nodeY + nodeHeight;

    edges.push({
      childId: childRoot.node.id,
      key: `${node.id}->${childRoot.node.id}`,
      parentId: node.id,
      path: createEdgePath(parentCenterX, parentBottomY, childRoot),
      visibleByDefault: !useMultiColumn || childRoot.y === firstChildSectionY,
    });
  }
  nodes.push(...childLayoutResult.nodes);
  edges.push(...childLayoutResult.edges);

  return normalizeLayout(nodes, edges);
}

function offsetPath(path: string, offsetX: number, offsetY: number) {
  const numbers = path.match(/-?\d+(\.\d+)?/g)?.map(Number) ?? [];
  let index = 0;
  return path.replace(/-?\d+(\.\d+)?/g, () => {
    const value = numbers[index] ?? 0;
    const nextValue = index % 2 === 0 ? value + offsetX : value + offsetY;
    index += 1;
    return String(nextValue);
  });
}

export function layoutForest(
  nodes: OrgNode[],
  collapsedIds: Set<string>,
  options?: Partial<OrgChartLayoutOptions>,
): ChartLayout {
  const resolvedOptions = resolveLayoutOptions(options);
  const layouts = nodes.map((node) =>
    layoutTree(node, collapsedIds, resolvedOptions),
  );
  const contentWidth =
    layouts.reduce((sum, layout) => sum + layout.width, 0) +
    ROOT_GAP_X * Math.max(layouts.length - 1, 0);
  const contentHeight = Math.max(1, ...layouts.map((layout) => layout.height));
  const visualNodes: VisualNode[] = [];
  const edges: VisualEdge[] = [];
  let cursorX = CHART_MARGIN;

  for (const layout of layouts) {
    visualNodes.push(
      ...layout.nodes.map((node) => ({
        ...node,
        x: node.x + cursorX,
        y: node.y + CHART_MARGIN,
      })),
    );
    edges.push(
      ...layout.edges.map((edge) => ({
        ...edge,
        path: offsetPath(edge.path, cursorX, CHART_MARGIN),
      })),
    );
    cursorX += layout.width + ROOT_GAP_X;
  }

  return {
    nodes: visualNodes,
    edges,
    width: Math.max(contentWidth + CHART_MARGIN * 2, 960),
    height: Math.max(contentHeight + CHART_MARGIN * 2, 640),
  };
}

function makeInitialViewBox(
  layout: ChartLayout,
  viewport: DOMRect | null,
): ViewBox {
  const aspect =
    viewport && viewport.height > 0 ? viewport.width / viewport.height : 16 / 9;
  const width = Math.max(layout.width, 960);
  const height = Math.max(width / aspect, layout.height);
  return {
    x: 0,
    y: 0,
    width,
    height,
  };
}

function getViewBoxScale(layout: ChartLayout, viewBox: ViewBox) {
  return layout.width / viewBox.width;
}

function getHighlightedEdgeKeys(
  edges: VisualEdge[],
  hoveredNodeId: string | null,
) {
  if (!hoveredNodeId) return new Set<string>();

  const parentByChildId = new Map<string, VisualEdge>();
  for (const edge of edges) {
    parentByChildId.set(edge.childId, edge);
  }

  const highlighted = new Set<string>();
  let currentNodeId: string | null = hoveredNodeId;

  while (currentNodeId) {
    const edge = parentByChildId.get(currentNodeId);
    if (!edge) break;
    highlighted.add(edge.key);
    currentNodeId = edge.parentId;
  }

  return highlighted;
}

export function getSemanticZoomMode(scale: number): SemanticZoomMode {
  if (scale < 0.25) return "overview";
  if (scale < 0.55) return "compact";
  return "detail";
}

function getCompanyColorKey(
  tenant?: Pick<TenantSummary, "name" | "slug" | "type">,
) {
  const text = `${tenant?.slug ?? ""} ${tenant?.name ?? ""}`.toLowerCase();
  if (text.includes("hanmac-family") || text.includes("한맥가족")) {
    return "family";
  }
  if (text.includes("saman") || text.includes("삼안")) return "saman";
  if (
    (text.includes("hanmac") || text.includes("한맥기술")) &&
    !text.includes("hanmac-family")
  ) {
    return "hanmac";
  }
  if (text.includes("gpdtdc")) return "gpdtdc";
  return "baron";
}

function hexToRgb(hex: string) {
  const normalized = hex.replace("#", "");
  return {
    b: Number.parseInt(normalized.slice(4, 6), 16),
    g: Number.parseInt(normalized.slice(2, 4), 16),
    r: Number.parseInt(normalized.slice(0, 2), 16),
  };
}

function rgbToHex({ r, g, b }: { r: number; g: number; b: number }) {
  return `#${[r, g, b]
    .map((value) =>
      Math.max(0, Math.min(255, Math.round(value)))
        .toString(16)
        .padStart(2, "0"),
    )
    .join("")}`;
}

function mixHexColor(hex: string, target: string, amount: number) {
  const sourceRgb = hexToRgb(hex);
  const targetRgb = hexToRgb(target);
  return rgbToHex({
    b: sourceRgb.b + (targetRgb.b - sourceRgb.b) * amount,
    g: sourceRgb.g + (targetRgb.g - sourceRgb.g) * amount,
    r: sourceRgb.r + (targetRgb.r - sourceRgb.r) * amount,
  });
}

export function getOrgNodeHeaderFill(level: number, companyColorKey = "baron") {
  const baseColor =
    companyColorKey === "family"
      ? "#000000"
      : companyColorKey === "saman"
        ? "#f58220"
        : companyColorKey === "hanmac"
          ? "#1e489d"
          : companyColorKey === "gpdtdc"
            ? "#4b746d"
            : "#004cbf";
  const variants = [
    baseColor,
    mixHexColor(baseColor, "#000000", 0.16),
    mixHexColor(baseColor, "#000000", 0.28),
    mixHexColor(baseColor, "#ffffff", 0.14),
    mixHexColor(baseColor, "#000000", 0.4),
    mixHexColor(baseColor, "#ffffff", 0.24),
  ];
  return variants[Math.min(level, variants.length - 1)] ?? baseColor;
}

export function getReactFlowDepthColorNodes(): DepthColorNode[] {
  const fills = [
    "#0a2a22",
    "#1e4d3f",
    "#355e48",
    "#4d5f35",
    "#5b4d37",
    "#4f465e",
  ];

  return fills.map((headerFill, depth) => ({
    data: { depth, headerFill },
    id: `depth-color-${depth}`,
    position: { x: depth * 160, y: 0 },
    style: { backgroundColor: headerFill },
    type: "org-depth-color",
  }));
}

function isVisibleOrgChartUser(user: UserSummary) {
  return (
    !user.email.toLowerCase().endsWith("@hanmac.kr") &&
    !isSystemGlobalUser(user)
  );
}

function isOrgFrontTenantType(tenant: TenantSummary) {
  return ["COMPANY_GROUP", "COMPANY", "ORGANIZATION", "USER_GROUP"].includes(
    tenant.type.toUpperCase(),
  );
}

function isSystemGlobalTenant(
  tenant?: Pick<TenantSummary, "id" | "slug" | "type" | "name">,
) {
  if (!tenant) return false;
  const values = [tenant.id, tenant.slug, tenant.type, tenant.name].map(
    (value) => value.toLowerCase().replaceAll("_", "-"),
  );
  return values.some(
    (value) =>
      value === "system" ||
      value === "global" ||
      value === "system-global" ||
      value === "tenant-global" ||
      value === "시스템 전역",
  );
}

function isSystemGlobalUser(user: UserSummary) {
  const normalizedRole = user.role.toLowerCase().replaceAll("_", "-");

  return (
    normalizedRole === "super-admin" ||
    normalizedRole === "superadmin" ||
    normalizedRole === "system-admin" ||
    isSystemGlobalTenant(user.tenant) ||
    isSystemGlobalTenant({
      id: user.tenantSlug || "",
      slug: user.tenantSlug || "",
      type: user.role,
      name: user.role,
    })
  );
}

function findNodeByTenantId(
  nodes: TenantNode[],
  tenantId: string,
): TenantNode | null {
  for (const node of nodes) {
    if (node.id === tenantId) return node;
    const child = findNodeByTenantId(node.children, tenantId);
    if (child) return child;
  }
  return null;
}

function collectOrgSelectionDescendants(
  node: TenantNode,
  maxDepth: 1 | 2,
  depth: 1 | 2 = 1,
): OrgSelectionDescendantOption[] {
  if (depth > maxDepth) return [];

  return node.children.flatMap((child) => [
    { depth, id: child.id, label: child.name },
    ...(depth < maxDepth
      ? collectOrgSelectionDescendants(child, maxDepth, 2)
      : []),
  ]);
}

export function buildOrgSelectionOptions(
  familyRoot: TenantNode | null,
): OrgSelectionOption[] {
  return orderHanmacFamilyTenants(
    (familyRoot?.children ?? []).filter((node) =>
      ["COMPANY_GROUP", "COMPANY", "ORGANIZATION"].includes(node.type),
    ),
  ).map((node) => ({
    descendants: collectOrgSelectionDescendants(node, 2),
    id: node.id,
    label: node.name,
  }));
}

function getOrgSelectionLabel(
  familyRoot: TenantNode | null,
  selectedTenantId: string,
) {
  if (selectedTenantId === FAMILY_FILTER_ID) return "한맥가족";
  return findNodeByTenantId(familyRoot ? [familyRoot] : [], selectedTenantId)
    ?.name;
}

export function resolveOrgChartFamilyRoot(rootNodes: TenantNode[]) {
  return (
    rootNodes.find(
      (node) =>
        node.slug.toLowerCase() === "hanmac-family" || node.name === "한맥가족",
    ) ??
    rootNodes.find((node) => node.type === "COMPANY_GROUP") ??
    rootNodes[0] ??
    null
  );
}

export function filterSystemGlobalTenants(
  tenants: TenantSummary[],
  visibilityMode: "internal" | "public" = "public",
) {
  const excludedIds = new Set(
    tenants.filter(isSystemGlobalTenant).map((tenant) => tenant.id),
  );
  let changed = true;

  while (changed) {
    changed = false;
    for (const tenant of tenants) {
      if (
        tenant.parentId &&
        excludedIds.has(tenant.parentId) &&
        !excludedIds.has(tenant.id)
      ) {
        excludedIds.add(tenant.id);
        changed = true;
      }
    }
  }

  const filtered = tenants.filter(
    (tenant) => !excludedIds.has(tenant.id) && isOrgFrontTenantType(tenant),
  );
  return filterTenantsByVisibility(filtered, visibilityMode);
}

function filterOrgChartMembershipTenants(tenants: TenantSummary[]) {
  const excludedIds = new Set(
    tenants.filter(isSystemGlobalTenant).map((tenant) => tenant.id),
  );
  let changed = true;

  while (changed) {
    changed = false;
    for (const tenant of tenants) {
      if (
        tenant.parentId &&
        excludedIds.has(tenant.parentId) &&
        !excludedIds.has(tenant.id)
      ) {
        excludedIds.add(tenant.id);
        changed = true;
      }
    }
  }

  return tenants.filter(
    (tenant) => !excludedIds.has(tenant.id) && isOrgFrontTenantType(tenant),
  );
}

type TenantIndexes = {
  byId: Map<string, TenantNode>;
  bySlug: Map<string, TenantNode>;
};

type UserOrgAppointmentRef = {
  tenantId?: string;
  tenantSlug?: string;
};

function buildTenantIndexes(nodes: TenantNode[]): TenantIndexes {
  const byId = new Map<string, TenantNode>();
  const bySlug = new Map<string, TenantNode>();
  const visit = (node: TenantNode) => {
    byId.set(node.id, node);
    bySlug.set(node.slug.toLowerCase(), node);
    for (const child of node.children) visit(child);
  };

  for (const node of nodes) visit(node);
  return { byId, bySlug };
}

function normalizeOrgSlug(value: unknown) {
  return typeof value === "string" ? value.trim().toLowerCase() : "";
}

function readErrorText(value: unknown): string {
  return typeof value === "string" ? value : "";
}

function getOrgChartLoadErrorDiagnostics(
  error: unknown,
  options: { cacheMode: "redis" | "public"; route: string; tenantId: string },
): OrgChartLoadErrorDiagnostics {
  const maybeError = error as {
    config?: { url?: string };
    message?: string;
    response?: {
      data?: { code?: unknown; error?: unknown; message?: unknown };
      status?: number;
    };
  };
  const responseData = maybeError.response?.data;
  return {
    cacheMode: options.cacheMode,
    code: readErrorText(responseData?.code),
    message:
      readErrorText(responseData?.error) ||
      readErrorText(responseData?.message) ||
      readErrorText(maybeError.message),
    route: maybeError.config?.url || options.route,
    status: maybeError.response?.status ?? null,
    tenantId: options.tenantId,
  };
}

function getUserOrgAppointmentRefs(user: UserSummary): UserOrgAppointmentRef[] {
  const rawAppointments = user.metadata?.additionalAppointments;
  if (!Array.isArray(rawAppointments)) return [];

  return rawAppointments
    .filter(
      (item): item is Record<string, unknown> =>
        typeof item === "object" && item !== null,
    )
    .map((item) => ({
      tenantId: typeof item.tenantId === "string" ? item.tenantId.trim() : "",
      tenantSlug: normalizeOrgSlug(item.tenantSlug),
    }));
}

function addTenantSlugCandidate(
  slugs: Set<string>,
  tenantIndexes: TenantIndexes,
  slug: string,
) {
  const normalizedSlug = normalizeOrgSlug(slug);
  if (!normalizedSlug) return;
  if (!tenantIndexes.bySlug.has(normalizedSlug)) return;
  slugs.add(normalizedSlug);
}

function isDescendantTenant(
  candidate: TenantNode,
  ancestor: TenantNode,
  byId: Map<string, TenantNode>,
) {
  const visited = new Set<string>();
  let currentParentId = candidate.parentId;

  while (currentParentId) {
    if (currentParentId === ancestor.id) return true;
    if (visited.has(currentParentId)) return false;
    visited.add(currentParentId);
    currentParentId = byId.get(currentParentId)?.parentId;
  }

  return false;
}

function getLeafMembershipSlugs(
  slugs: Set<string>,
  tenantIndexes: TenantIndexes,
) {
  const memberships = Array.from(slugs);

  return memberships.filter((slug) => {
    const tenant = tenantIndexes.bySlug.get(slug);
    if (!tenant) return true;

    return !memberships.some((otherSlug) => {
      if (otherSlug === slug) return false;
      const otherTenant = tenantIndexes.bySlug.get(otherSlug);
      if (!otherTenant) return false;
      return isDescendantTenant(otherTenant, tenant, tenantIndexes.byId);
    });
  });
}

export function buildUsersMap(
  users: UserSummary[],
  rootNodes: TenantNode[],
  options: { activeOnly: boolean; membershipRootNodes?: TenantNode[] },
) {
  const visibleTenantIndexes = buildTenantIndexes(rootNodes);
  const membershipTenantIndexes = buildTenantIndexes(
    options.membershipRootNodes ?? rootNodes,
  );
  const map = new Map<string, UserSummary[]>();

  for (const user of users) {
    if (options.activeOnly && user.status !== "active") continue;
    if (!isVisibleOrgChartUser(user)) continue;

    const slugs = new Set<string>();
    const primarySlug = normalizeOrgSlug(user.tenantSlug);
    const legacyCompanySlug = normalizeOrgSlug(user.companyCode);
    if (
      primarySlug &&
      !isSystemGlobalTenant({
        id: primarySlug,
        slug: primarySlug,
        type: primarySlug,
        name: primarySlug,
      })
    ) {
      addTenantSlugCandidate(slugs, membershipTenantIndexes, primarySlug);
    }
    if (
      legacyCompanySlug &&
      !isSystemGlobalTenant({
        id: legacyCompanySlug,
        slug: legacyCompanySlug,
        type: legacyCompanySlug,
        name: legacyCompanySlug,
      })
    ) {
      addTenantSlugCandidate(slugs, membershipTenantIndexes, legacyCompanySlug);
    }
    if (user.tenant?.slug && !isSystemGlobalTenant(user.tenant)) {
      addTenantSlugCandidate(slugs, membershipTenantIndexes, user.tenant.slug);
    }
    for (const joinedTenant of user.joinedTenants || []) {
      if (joinedTenant.slug && !isSystemGlobalTenant(joinedTenant)) {
        addTenantSlugCandidate(
          slugs,
          membershipTenantIndexes,
          joinedTenant.slug,
        );
      }
    }
    for (const appointment of getUserOrgAppointmentRefs(user)) {
      if (appointment.tenantSlug) {
        addTenantSlugCandidate(
          slugs,
          membershipTenantIndexes,
          appointment.tenantSlug,
        );
        continue;
      }

      const tenantById = appointment.tenantId
        ? membershipTenantIndexes.byId.get(appointment.tenantId)
        : undefined;
      if (tenantById) {
        addTenantSlugCandidate(slugs, membershipTenantIndexes, tenantById.slug);
      }
    }

    for (const slug of getLeafMembershipSlugs(slugs, membershipTenantIndexes)) {
      if (!visibleTenantIndexes.bySlug.has(slug)) continue;
      const list = map.get(slug) || [];
      if (!list.some((existing) => existing.id === user.id)) list.push(user);
      map.set(slug, list);
    }
  }

  return map;
}

export function TenantOrgChartPage() {
  const viewportRef = React.useRef<HTMLDivElement>(null);
  const dragRef = React.useRef<{
    pointerId: number;
    startX: number;
    startY: number;
    startViewBox: ViewBox;
  } | null>(null);
  const { tenantId } = useParams<{ tenantId?: string }>();
  const location = useLocation();
  const searchParams = new URLSearchParams(location.search);
  const shareToken = searchParams.get("token");
  const [includeInternalTenants, setIncludeInternalTenants] = React.useState(
    () => !shareToken && searchParams.get("includeInternal") === "true",
  );
  const visibilityMode = includeInternalTenants ? "internal" : "public";
  const [selectedTenantFilter, setSelectedTenantFilter] =
    React.useState(FAMILY_FILTER_ID);
  const [collapsedIds, setCollapsedIds] = React.useState<Set<string>>(
    () => new Set(),
  );
  const [viewBox, setViewBox] = React.useState<ViewBox>({
    x: 0,
    y: 0,
    width: 1280,
    height: 720,
  });
  const [hasUserMovedCanvas, setHasUserMovedCanvas] = React.useState(false);
  const [isDragging, setIsDragging] = React.useState(false);
  const [childLayoutMode, setChildLayoutMode] =
    React.useState<ChildLayoutMode>("auto");
  const [hoveredNodeId, setHoveredNodeId] = React.useState<string | null>(null);

  const publicQuery = useQuery({
    queryKey: ["public-orgchart", shareToken],
    queryFn: () => {
      if (!shareToken) throw new Error("Missing share token");
      return fetchPublicOrgChart(shareToken);
    },
    enabled: !!shareToken,
  });

  const orgChartSnapshotQuery = useQuery({
    queryKey: ["orgchart-snapshot", { cache: "redis" }],
    queryFn: () => fetchOrgChartSnapshot(),
    enabled: !shareToken,
  });

  const { rootNodes, usersMap, sharedWith } = React.useMemo(() => {
    if (shareToken) {
      if (!publicQuery.data) {
        return {
          rootNodes: [],
          usersMap: new Map<string, UserSummary[]>(),
          sharedWith: "",
        };
      }

      const rootNodes = buildTenantFullTree(
        filterSystemGlobalTenants(publicQuery.data.tenants, "public"),
      ).subTree;
      const membershipRootNodes = buildTenantFullTree(
        filterOrgChartMembershipTenants(publicQuery.data.tenants),
      ).subTree;

      return {
        rootNodes,
        usersMap: buildUsersMap(publicQuery.data.users, rootNodes, {
          activeOnly: false,
          membershipRootNodes,
        }),
        sharedWith: publicQuery.data.sharedWith,
      };
    }

    if (!orgChartSnapshotQuery.data) {
      return {
        rootNodes: [],
        usersMap: new Map<string, UserSummary[]>(),
        sharedWith: "",
      };
    }

    const rootNodes = buildTenantFullTree(
      filterSystemGlobalTenants(
        orgChartSnapshotQuery.data.tenants,
        visibilityMode,
      ),
    ).subTree;
    const membershipRootNodes = buildTenantFullTree(
      filterOrgChartMembershipTenants(orgChartSnapshotQuery.data.tenants),
    ).subTree;

    return {
      rootNodes,
      usersMap: buildUsersMap(orgChartSnapshotQuery.data.users, rootNodes, {
        activeOnly: true,
        membershipRootNodes,
      }),
      sharedWith: "",
    };
  }, [
    publicQuery.data,
    shareToken,
    orgChartSnapshotQuery.data,
    visibilityMode,
  ]);

  const familyRoot = React.useMemo(() => {
    return resolveOrgChartFamilyRoot(rootNodes);
  }, [rootNodes]);

  const orgSelectionOptions = React.useMemo(
    () => buildOrgSelectionOptions(familyRoot),
    [familyRoot],
  );
  const selectedOrgLabel = React.useMemo(
    () => getOrgSelectionLabel(familyRoot, selectedTenantFilter) ?? "한맥가족",
    [familyRoot, selectedTenantFilter],
  );

  React.useEffect(() => {
    if (!tenantId) return;
    const searchRoots = familyRoot ? [familyRoot] : rootNodes;
    const match = searchRoots
      .flatMap((node) => [node, ...node.children])
      .find(
        (node) =>
          node.id === tenantId ||
          node.slug.toLowerCase() === tenantId.toLowerCase() ||
          node.name === tenantId,
      );
    if (match?.type === "COMPANY" || match?.type === "ORGANIZATION") {
      setSelectedTenantFilter(match.id);
    }
  }, [familyRoot, rootNodes, tenantId]);

  const targetNodes = React.useMemo(() => {
    if (!familyRoot) return [];
    if (selectedTenantFilter === FAMILY_FILTER_ID) {
      return [buildOrgNode(familyRoot, usersMap, 0)];
    }

    const companyNode = findNodeByTenantId([familyRoot], selectedTenantFilter);
    if (!companyNode) return [];

    return [buildOrgNode(companyNode, usersMap, 0)];
  }, [familyRoot, selectedTenantFilter, usersMap]);

  const layout = React.useMemo(
    () =>
      layoutForest(targetNodes, collapsedIds, {
        childLayoutMode,
      }),
    [childLayoutMode, collapsedIds, targetNodes],
  );
  const highlightedEdgeKeys = React.useMemo(
    () => getHighlightedEdgeKeys(layout.edges, hoveredNodeId),
    [hoveredNodeId, layout.edges],
  );
  const viewBoxScale = getViewBoxScale(layout, viewBox);
  const semanticZoomMode = getSemanticZoomMode(viewBoxScale);

  React.useLayoutEffect(() => {
    if (hasUserMovedCanvas) return;
    setViewBox(
      makeInitialViewBox(
        layout,
        viewportRef.current?.getBoundingClientRect() ?? null,
      ),
    );
  }, [hasUserMovedCanvas, layout]);

  const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
    if (event.button !== 0) return;
    dragRef.current = {
      pointerId: event.pointerId,
      startX: event.clientX,
      startY: event.clientY,
      startViewBox: viewBox,
    };
    event.currentTarget.setPointerCapture(event.pointerId);
    setIsDragging(true);
  };

  const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
    const dragState = dragRef.current;
    const rect = viewportRef.current?.getBoundingClientRect();
    if (!dragState || !rect || dragState.pointerId !== event.pointerId) return;

    const dx =
      ((event.clientX - dragState.startX) / rect.width) *
      dragState.startViewBox.width;
    const dy =
      ((event.clientY - dragState.startY) / rect.height) *
      dragState.startViewBox.height;
    setHasUserMovedCanvas(true);
    setViewBox({
      ...dragState.startViewBox,
      x: dragState.startViewBox.x - dx,
      y: dragState.startViewBox.y - dy,
    });
  };

  const finishDrag = (event: React.PointerEvent<HTMLDivElement>) => {
    const dragState = dragRef.current;
    if (!dragState || dragState.pointerId !== event.pointerId) return;
    if (event.currentTarget.hasPointerCapture(event.pointerId)) {
      event.currentTarget.releasePointerCapture(event.pointerId);
    }
    dragRef.current = null;
    setIsDragging(false);
  };

  const handleWheel = (event: React.WheelEvent<HTMLDivElement>) => {
    event.preventDefault();
    const rect = event.currentTarget.getBoundingClientRect();
    const currentScale = getViewBoxScale(layout, viewBox);
    const nextScale = clampScale(
      currentScale * Math.exp(-event.deltaY * ZOOM_SENSITIVITY),
    );
    const nextWidth = layout.width / nextScale;
    const nextHeight = nextWidth / (rect.width / rect.height);
    const pointX =
      viewBox.x + ((event.clientX - rect.left) / rect.width) * viewBox.width;
    const pointY =
      viewBox.y + ((event.clientY - rect.top) / rect.height) * viewBox.height;
    const ratioX = (pointX - viewBox.x) / viewBox.width;
    const ratioY = (pointY - viewBox.y) / viewBox.height;

    setHasUserMovedCanvas(true);
    setViewBox({
      x: pointX - nextWidth * ratioX,
      y: pointY - nextHeight * ratioY,
      width: nextWidth,
      height: nextHeight,
    });
  };

  const isLoading = shareToken
    ? publicQuery.isLoading
    : orgChartSnapshotQuery.isLoading;
  const isError = shareToken
    ? publicQuery.isError
    : orgChartSnapshotQuery.isError;
  const currentLoadError = shareToken
    ? publicQuery.error
    : orgChartSnapshotQuery.error;

  React.useEffect(() => {
    if (!currentLoadError) return;
    console.error(
      "[orgfront] Org chart load failed",
      getOrgChartLoadErrorDiagnostics(currentLoadError, {
        cacheMode: shareToken ? "public" : "redis",
        route: shareToken
          ? "/v1/public/orgchart"
          : "/v1/admin/orgchart/snapshot",
        tenantId:
          tenantId ?? window.localStorage.getItem("dev_tenant_id") ?? "",
      }),
    );
  }, [currentLoadError, shareToken, tenantId]);

  const totalUsers = React.useMemo(() => {
    const ids = new Set<string>();
    for (const node of targetNodes) {
      for (const memberId of node.totalMemberIds) {
        ids.add(memberId);
      }
    }
    return ids.size;
  }, [targetNodes]);

  if (isLoading) {
    return (
      <div className="p-8 text-center text-muted-foreground">로딩 중...</div>
    );
  }

  if (isError) {
    const errorMessage = shareToken
      ? "조직도를 불러올 수 없거나 만료된 링크입니다."
      : "조직도를 불러올 수 없습니다. 로그인 상태와 조직 권한을 확인해 주세요.";
    return (
      <div className="p-8 text-center text-red-500">
        {errorMessage}
      </div>
    );
  }

  const updateLayoutMode = (value: ChildLayoutMode) => {
    setChildLayoutMode(value);
    setHasUserMovedCanvas(false);
  };

  const updateSelectedOrg = (value: string) => {
    setSelectedTenantFilter(value);
    setCollapsedIds(new Set());
    setHoveredNodeId(null);
    setHasUserMovedCanvas(false);
  };

  const updateInternalTenantVisibility = (checked: boolean) => {
    setIncludeInternalTenants(checked);
    setSelectedTenantFilter(FAMILY_FILTER_ID);
    setCollapsedIds(new Set());
    setHoveredNodeId(null);
    setHasUserMovedCanvas(false);
  };

  return (
    <div
      className="flex h-full min-h-0 w-full flex-col overflow-hidden border-[#e0d5c1] bg-[#f6efe6]"
      data-testid="orgchart-dashboard-shell"
    >
      <header className="z-20 grid shrink-0 grid-cols-1 items-center gap-3 border-b border-[#f2c484]/30 bg-[linear-gradient(145deg,rgba(10,42,34,0.98)_0%,rgba(15,58,47,0.98)_52%,rgba(26,86,69,0.98)_100%)] px-6 py-4 lg:grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)]">
        <div className="flex min-w-0 justify-start">
          <OrgSelectionPicker
            onChange={updateSelectedOrg}
            options={orgSelectionOptions}
            selectedId={selectedTenantFilter}
            selectedLabel={selectedOrgLabel}
          />
        </div>
        <div className="text-center">
          {shareToken ? (
            <p className="text-xs font-bold uppercase tracking-wider text-[#f2c484]">
              공유된 조직도: {sharedWith}
            </p>
          ) : null}
          <h2 className="text-xl font-black text-[#f7f0e4]">조직 현황</h2>
        </div>
        <div className="custom-scrollbar flex min-w-0 max-w-full items-center justify-start gap-2 overflow-x-auto lg:justify-end">
          <LayoutOptionPicker
            label="배치"
            onChange={updateLayoutMode}
            options={childLayoutModeOptions}
            testId="orgchart-layout-mode-option"
            value={childLayoutMode}
          />
          <TotalUsersControl
            canToggleInternal={!shareToken}
            includeInternal={includeInternalTenants}
            onIncludeInternalChange={updateInternalTenantVisibility}
            totalUsers={totalUsers}
          />
        </div>
      </header>

      <div
        className={`relative flex-1 touch-none select-none overflow-hidden ${isDragging ? "cursor-grabbing" : "cursor-grab"}`}
        data-testid="orgchart-viewport"
        onPointerCancel={finishDrag}
        onPointerDown={handlePointerDown}
        onPointerMove={handlePointerMove}
        onPointerUp={finishDrag}
        onWheel={handleWheel}
        ref={viewportRef}
        style={{
          background:
            "radial-gradient(circle at top left, rgba(214, 138, 58, 0.08), transparent 24%), radial-gradient(circle at top right, rgba(47, 153, 115, 0.05), transparent 20%), linear-gradient(180deg, rgba(246, 239, 230, 0.98), rgba(241, 234, 223, 0.96))",
        }}
      >
        <svg
          className="h-full w-full"
          data-scale={viewBoxScale.toFixed(3)}
          data-semantic-zoom={semanticZoomMode}
          data-testid="orgchart-vector-svg"
          role="img"
          viewBox={`${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`}
        >
          <title>조직도 벡터 렌더링</title>
          <g data-testid="orgchart-canvas">
            {layout.edges.map((edge) => (
              <path
                d={edge.path}
                data-highlighted={
                  hoveredNodeId ? highlightedEdgeKeys.has(edge.key) : undefined
                }
                data-hidden-default={!edge.visibleByDefault || undefined}
                data-muted={
                  hoveredNodeId && edge.visibleByDefault
                    ? !highlightedEdgeKeys.has(edge.key)
                    : undefined
                }
                fill="none"
                key={edge.key}
                opacity={
                  hoveredNodeId
                    ? highlightedEdgeKeys.has(edge.key)
                      ? 1
                      : edge.visibleByDefault
                        ? 0.14
                        : 0
                    : edge.visibleByDefault
                      ? 0.72
                      : 0
                }
                stroke={
                  hoveredNodeId && highlightedEdgeKeys.has(edge.key)
                    ? "#0a2a22"
                    : "#bca58a"
                }
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth={
                  hoveredNodeId && highlightedEdgeKeys.has(edge.key) ? "3" : "2"
                }
                vectorEffect="non-scaling-stroke"
              />
            ))}
            {layout.nodes.map((visualNode) => (
              <SvgOrgNode
                isHovered={hoveredNodeId === visualNode.node.id}
                key={visualNode.node.id}
                onHoverEnd={() => setHoveredNodeId(null)}
                onHoverStart={() => setHoveredNodeId(visualNode.node.id)}
                semanticZoomMode={semanticZoomMode}
                visualNode={visualNode}
              />
            ))}
          </g>
        </svg>
      </div>
    </div>
  );
}

function TotalUsersControl({
  canToggleInternal,
  includeInternal,
  onIncludeInternalChange,
  totalUsers,
}: {
  canToggleInternal: boolean;
  includeInternal: boolean;
  onIncludeInternalChange: (checked: boolean) => void;
  totalUsers: number;
}) {
  if (!canToggleInternal) {
    return (
      <div
        className="ml-2 whitespace-nowrap rounded-full border border-[#f2c484]/30 bg-[#f2c484]/10 px-4 py-2 text-xs font-black text-[#f2c484] shadow-sm"
        data-testid="orgchart-total-users-control"
      >
        총 {totalUsers}명
      </div>
    );
  }

  return (
    <div className="group/total relative ml-2">
      <button
        aria-label={`전체 인원: 총 ${totalUsers}명`}
        className="whitespace-nowrap rounded-full border border-[#f2c484]/30 bg-[#f2c484]/10 px-4 py-2 text-xs font-black text-[#f2c484] shadow-sm transition-colors hover:border-[#f2c484]/60 hover:bg-[#f2c484]/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#f2c484]/70"
        data-testid="orgchart-total-users-control"
        type="button"
      >
        총 {totalUsers}명
      </button>
      <div className="absolute right-0 top-full z-40 mt-2 hidden w-[220px] rounded-md border border-[#8bd3b2]/35 bg-[#0d3d33] p-3 text-[#e1fff1] shadow-xl group-hover/total:block group-focus-within/total:block">
        <label className="flex items-center justify-between gap-3">
          <span className="text-xs font-bold">내부조직 보기</span>
          <Switch
            aria-label="내부조직 보기"
            checked={includeInternal}
            className="data-[state=checked]:bg-[#f2c484]"
            onCheckedChange={onIncludeInternalChange}
          />
        </label>
      </div>
    </div>
  );
}

function LayoutOptionPicker<T extends string>({
  label,
  onChange,
  options,
  testId,
  value,
}: {
  label: string;
  onChange: (value: T) => void;
  options: Array<{ id: T; label: string }>;
  testId: string;
  value: T;
}) {
  const selectedOption =
    options.find((option) => option.id === value) ?? options[0];
  const availableOptions = options.filter((option) => option.id !== value);

  return (
    <div
      className="group flex items-center gap-1 whitespace-nowrap rounded-full border border-[#f2c484]/30 bg-white/10 p-1 transition-colors hover:border-[#f2c484]/60 hover:bg-white/15 focus-within:border-[#f2c484]/60 focus-within:bg-white/15"
      data-testid={testId}
    >
      <button
        aria-label={`${label}: ${selectedOption.label}`}
        className="min-w-12 rounded-full bg-[#f2c484] px-3 py-1.5 text-xs font-black text-[#0a2a22] shadow-sm"
        type="button"
      >
        {selectedOption.label}
      </button>
      <div className="hidden items-center gap-1 group-hover:flex group-focus-within:flex">
        <span className="ml-1 border-l border-[#f2c484]/30 pl-3 pr-1 text-[11px] font-bold text-[#f7f0e4]/65">
          {label}
        </span>
        {availableOptions.map((option) => (
          <button
            className="rounded-full border border-transparent px-3 py-1.5 text-xs font-bold text-[#f7f0e4]/70 transition-all hover:border-[#f2c484]/30 hover:bg-white/10 hover:text-[#f7f0e4]"
            key={option.id}
            onClick={() => onChange(option.id)}
            type="button"
          >
            {option.label}
          </button>
        ))}
      </div>
    </div>
  );
}

function OrgSelectionPicker({
  onChange,
  options,
  selectedId,
  selectedLabel,
}: {
  onChange: (value: string) => void;
  options: OrgSelectionOption[];
  selectedId: string;
  selectedLabel: string;
}) {
  const isFamilySelected = selectedId === FAMILY_FILTER_ID;
  const selectedCompany = options.find((option) => option.id === selectedId);
  const showSelectedDescendant = !isFamilySelected && !selectedCompany;
  const baseButtonClass =
    "shrink-0 rounded-full border px-3 py-1.5 text-xs font-bold transition-all";
  const selectedButtonClass =
    "border-[#d6f4e6] bg-[#d6f4e6] text-[#073b2d] shadow-sm";
  const optionButtonClass =
    "border-[#8bd3b2]/28 bg-[#0c4b3c]/45 text-[#e1fff1]/85 hover:border-[#8bd3b2]/70 hover:bg-[#0e5a48]/75 hover:text-[#f4fff8]";

  return (
    <div
      className="flex min-w-0 flex-wrap items-center gap-1"
      data-testid="orgchart-org-selector"
    >
      <button
        aria-current={isFamilySelected ? "true" : undefined}
        aria-label={isFamilySelected ? "조직: 한맥가족" : undefined}
        className={`${baseButtonClass} ${
          isFamilySelected ? selectedButtonClass : optionButtonClass
        }`}
        onClick={() => onChange(FAMILY_FILTER_ID)}
        type="button"
      >
        한맥가족
      </button>
      {showSelectedDescendant ? (
        <button
          aria-current="true"
          aria-label={`조직: ${selectedLabel}`}
          className={`${baseButtonClass} max-w-[220px] truncate ${selectedButtonClass}`}
          onClick={() => onChange(selectedId)}
          type="button"
        >
          {selectedLabel}
        </button>
      ) : null}
      {options.map((option) => {
        const isSelected = selectedId === option.id;

        return (
          <div
            className="group/company relative shrink-0"
            data-testid={`orgchart-company-option-${option.id}`}
            key={option.id}
          >
            <button
              aria-current={isSelected ? "true" : undefined}
              aria-label={isSelected ? `조직: ${option.label}` : undefined}
              className={`${baseButtonClass} ${
                isSelected ? selectedButtonClass : optionButtonClass
              }`}
              onClick={() => onChange(option.id)}
              type="button"
            >
              {option.label}
            </button>
            {option.descendants.length > 0 ? (
              <div className="absolute left-0 top-full z-40 mt-2 hidden min-w-[220px] flex-col gap-1 rounded-md border border-[#8bd3b2]/35 bg-[#0d3d33] p-2 shadow-xl group-hover/company:flex group-focus-within/company:flex">
                {option.descendants
                  .filter((descendant) => descendant.id !== selectedId)
                  .map((descendant) => (
                    <button
                      aria-label={`${descendant.depth}뎁스 ${descendant.label}`}
                      className="rounded px-3 py-2 text-left text-xs font-bold text-[#e1fff1]/85 hover:bg-[#d6f4e6]/12 hover:text-[#f4fff8]"
                      key={descendant.id}
                      onClick={() => onChange(descendant.id)}
                      type="button"
                    >
                      <span className="mr-2 text-[10px] text-[#9ee7c2]/75">
                        {descendant.depth}뎁스
                      </span>
                      {descendant.label}
                    </button>
                  ))}
              </div>
            ) : null}
          </div>
        );
      })}
    </div>
  );
}

function SvgOrgNode({
  isHovered,
  onHoverEnd,
  onHoverStart,
  semanticZoomMode,
  visualNode,
}: {
  isHovered: boolean;
  onHoverEnd: () => void;
  onHoverStart: () => void;
  semanticZoomMode: SemanticZoomMode;
  visualNode: VisualNode;
}) {
  const { node, x, y, width, height, members, collapsed } = visualNode;
  const tenantIdentity = { id: node.id, slug: node.companyCode ?? "" };
  const headerFill = getOrgNodeHeaderFill(
    node.companyColorDepth ?? node.level,
    node.companyColorKey,
  );
  const memberColumnCount = getMemberColumnCount(
    members.length,
    getMemberColumnWidth(members, tenantIdentity),
  );
  const showMemberRows = semanticZoomMode === "detail";
  const showNodeName =
    semanticZoomMode === "detail" ||
    node.level <= 1 ||
    (semanticZoomMode === "compact" && node.level <= 2);
  const showCompactGlyph = !showNodeName;
  const titleClass =
    node.level <= 1
      ? "text-[17px] font-black leading-tight"
      : "text-[14px] font-extrabold leading-tight";

  return (
    <foreignObject
      data-node-id={node.id}
      data-node-level={node.level}
      data-hovered={isHovered || undefined}
      data-testid={`orgchart-node-${node.id}`}
      height={height}
      onPointerEnter={onHoverStart}
      onPointerLeave={onHoverEnd}
      width={width}
      x={x}
      y={y}
    >
      <div className="flex h-full w-full flex-col overflow-hidden rounded-[10px] border border-[#e0d5c1] bg-white shadow-[0_10px_24px_rgba(31,41,55,0.08)]">
        <div
          className="flex h-[42px] shrink-0 items-center gap-3 px-4 text-[#f7f0e4]"
          style={{ backgroundColor: headerFill }}
        >
          {showNodeName ? (
            <div className="flex min-w-0 flex-1 items-center gap-2">
              {node.orgUnitType ? (
                <span className="shrink-0 rounded bg-white/20 px-1.5 py-0.5 text-[11px] font-black text-white">
                  {node.orgUnitType}
                </span>
              ) : null}
              <div className={`${titleClass} min-w-0 truncate`}>
                {node.name}
              </div>
            </div>
          ) : (
            <div className="flex min-w-0 flex-1 items-center gap-2">
              <div className="h-3 w-8 rounded-full bg-[#f7f0e4]/65" />
              <div className="h-3 w-14 rounded-full bg-[#f7f0e4]/35" />
            </div>
          )}
          <div className="flex h-[22px] min-w-[52px] shrink-0 items-center justify-center rounded-full bg-black/25 px-2 text-xs font-bold text-white">
            {collapsed ? "+" : node.totalCount}
          </div>
        </div>
        {showCompactGlyph ? (
          <div className="flex flex-1 items-center justify-center px-4">
            <div className="flex items-center gap-2 rounded-full border border-[#e0d5c1] bg-[#f8fafc] px-4 py-2 text-xs font-black text-[#64748b]">
              <span className="h-2 w-2 rounded-full bg-[#94a3b8]" />
              {node.children.length > 0
                ? node.children.length
                : node.totalCount}
            </div>
          </div>
        ) : showMemberRows && members.length > 0 ? (
          <div
            className="grid gap-1 px-3 py-3"
            data-member-columns={memberColumnCount}
            style={{
              gridTemplateColumns: `repeat(${memberColumnCount}, minmax(0, 1fr))`,
            }}
          >
            {members.map((member) => {
              const profile = getUserOrgProfile(member, tenantIdentity);
              const isHighlighted = profile.isHighlighted === true;

              return (
                <div
                  className="flex h-5 items-center overflow-hidden rounded border border-[#e5e7eb] bg-white text-xs font-extrabold text-[#334155]"
                  data-highlighted={isHighlighted ? "true" : "false"}
                  data-testid={`orgchart-member-${member.id}`}
                  key={member.id}
                >
                  <div
                    className="h-full w-1 shrink-0"
                    style={{
                      backgroundColor: isHighlighted
                        ? getComplementaryColor(headerFill)
                        : "transparent",
                    }}
                  />
                  <div className="min-w-0 truncate px-2">
                    {getOrgChartUserDisplayName(member, tenantIdentity)}
                  </div>
                </div>
              );
            })}
          </div>
        ) : (
          <div className="flex flex-1 items-center px-4 text-xs font-bold text-[#94a3b8]">
            구성원 없음
          </div>
        )}
      </div>
    </foreignObject>
  );
}
