"use client";

import { createPortal } from "react-dom";
import {
  closestCenter,
  DndContext,
  DragOverlay,
  KeyboardSensor,
  PointerSensor,
  useSensor,
  useSensors,
} from "@dnd-kit/core";
import type { DragEndEvent, DragStartEvent } from "@dnd-kit/core";
import {
  arrayMove,
  SortableContext,
  sortableKeyboardCoordinates,
  useSortable,
  verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
import { trpc } from "~/lib/trpc/client.js";
import { useAnchoredOverlay } from "~/hooks/useAnchoredOverlay.js";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

type ClientRow = {
  id: string;
  name: string;
  code: string | null;
  parentId: string | null;
  sortOrder: number;
  isActive: boolean;
  tags: string[];
  _count?: { children: number; projects: number };
};

type TreeNode = ClientRow & {
  children: TreeNode[];
  depth: number;
};

// ---------------------------------------------------------------------------
// Tree helpers
// ---------------------------------------------------------------------------

function buildTree(clients: ClientRow[]): TreeNode[] {
  const map = new Map<string, TreeNode>();
  const roots: TreeNode[] = [];

  // First pass: create nodes
  for (const c of clients) {
    map.set(c.id, { ...c, children: [], depth: 0 });
  }

  // Second pass: link parents
  for (const c of clients) {
    const node = map.get(c.id)!;
    if (c.parentId && map.has(c.parentId)) {
      map.get(c.parentId)!.children.push(node);
    } else {
      roots.push(node);
    }
  }

  // Third pass: set depths and sort children
  function setDepths(nodes: TreeNode[], depth: number) {
    for (const n of nodes) {
      n.depth = depth;
      n.children.sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name));
      setDepths(n.children, depth + 1);
    }
  }
  roots.sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name));
  setDepths(roots, 0);

  return roots;
}

/** Flatten tree into display order, respecting collapsed state. */
function flattenTree(nodes: TreeNode[], collapsedIds: Set<string>): TreeNode[] {
  const result: TreeNode[] = [];
  function walk(list: TreeNode[]) {
    for (const n of list) {
      result.push(n);
      if (n.children.length > 0 && !collapsedIds.has(n.id)) {
        walk(n.children);
      }
    }
  }
  walk(nodes);
  return result;
}

/** Get all descendant IDs of a node (recursive). */
function getDescendantIds(clientId: string, clients: ClientRow[]): Set<string> {
  const ids = new Set<string>();
  function collect(parentId: string) {
    for (const c of clients) {
      if (c.parentId === parentId && !ids.has(c.id)) {
        ids.add(c.id);
        collect(c.id);
      }
    }
  }
  collect(clientId);
  return ids;
}

/** Get all ancestor IDs of a node (walk up). */
function getAncestorIds(clientId: string, clientMap: Map<string, ClientRow>): Set<string> {
  const ids = new Set<string>();
  let current = clientMap.get(clientId);
  while (current?.parentId) {
    ids.add(current.parentId);
    current = clientMap.get(current.parentId);
  }
  return ids;
}

// ---------------------------------------------------------------------------
// Tag color palette -- deterministic color from tag name hash
// ---------------------------------------------------------------------------

const TAG_COLORS = [
  { bg: "bg-purple-100 dark:bg-purple-900/40", text: "text-purple-700 dark:text-purple-300", border: "border-purple-200 dark:border-purple-700" },
  { bg: "bg-emerald-100 dark:bg-emerald-900/40", text: "text-emerald-700 dark:text-emerald-300", border: "border-emerald-200 dark:border-emerald-700" },
  { bg: "bg-amber-100 dark:bg-amber-900/40", text: "text-amber-700 dark:text-amber-300", border: "border-amber-200 dark:border-amber-700" },
  { bg: "bg-rose-100 dark:bg-rose-900/40", text: "text-rose-700 dark:text-rose-300", border: "border-rose-200 dark:border-rose-700" },
  { bg: "bg-sky-100 dark:bg-sky-900/40", text: "text-sky-700 dark:text-sky-300", border: "border-sky-200 dark:border-sky-700" },
  { bg: "bg-indigo-100 dark:bg-indigo-900/40", text: "text-indigo-700 dark:text-indigo-300", border: "border-indigo-200 dark:border-indigo-700" },
  { bg: "bg-teal-100 dark:bg-teal-900/40", text: "text-teal-700 dark:text-teal-300", border: "border-teal-200 dark:border-teal-700" },
  { bg: "bg-orange-100 dark:bg-orange-900/40", text: "text-orange-700 dark:text-orange-300", border: "border-orange-200 dark:border-orange-700" },
];

function hashString(str: string): number {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    hash = (hash << 5) - hash + str.charCodeAt(i);
    hash |= 0;
  }
  return Math.abs(hash);
}

function getTagColor(tag: string) {
  return TAG_COLORS[hashString(tag) % TAG_COLORS.length]!;
}

// ---------------------------------------------------------------------------
// Tag pill component
// ---------------------------------------------------------------------------

function TagPill({
  tag,
  onRemove,
}: {
  tag: string;
  onRemove?: () => void;
}) {
  const color = getTagColor(tag);
  return (
    <span
      className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium border ${color.bg} ${color.text} ${color.border}`}
    >
      {tag}
      {onRemove && (
        <button
          type="button"
          onClick={(e) => {
            e.stopPropagation();
            onRemove();
          }}
          className="hover:opacity-70 leading-none text-[10px] ml-0.5"
          aria-label={`Remove tag ${tag}`}
        >
          &times;
        </button>
      )}
    </span>
  );
}

// ---------------------------------------------------------------------------
// Inline tag adder with auto-suggest
// ---------------------------------------------------------------------------

function TagAdder({
  existingTags,
  allKnownTags,
  onAdd,
  onClose,
}: {
  existingTags: string[];
  allKnownTags: string[];
  onAdd: (tag: string) => void;
  onClose: () => void;
}) {
  const [value, setValue] = useState("");
  const inputRef = useRef<HTMLInputElement>(null);
  const [showSuggestions, setShowSuggestions] = useState(false);

  useEffect(() => {
    inputRef.current?.focus();
  }, []);

  const suggestions = useMemo(() => {
    if (!value.trim()) return allKnownTags.filter((t) => !existingTags.includes(t)).slice(0, 8);
    const lower = value.toLowerCase();
    return allKnownTags
      .filter((t) => t.toLowerCase().includes(lower) && !existingTags.includes(t))
      .slice(0, 8);
  }, [value, allKnownTags, existingTags]);
  const { panelRef, position } = useAnchoredOverlay<HTMLInputElement>({
    open: showSuggestions && suggestions.length > 0,
    onClose: () => {
      setShowSuggestions(false);
      if (!value.trim()) {
        onClose();
      }
    },
    align: "start",
    triggerRef: inputRef,
  });

  function submit(tag: string) {
    const trimmed = tag.trim();
    if (!trimmed || existingTags.includes(trimmed)) return;
    onAdd(trimmed);
    setValue("");
  }

  return (
    <div className="relative inline-block">
      <input
        ref={inputRef}
        type="text"
        value={value}
        onChange={(e) => {
          setValue(e.target.value);
          setShowSuggestions(true);
        }}
        onFocus={() => setShowSuggestions(true)}
        onBlur={() => {
          // Delay to allow click on suggestion
          setTimeout(() => {
            setShowSuggestions(false);
            if (!value.trim()) onClose();
          }, 200);
        }}
        onKeyDown={(e) => {
          if (e.key === "Enter") {
            e.preventDefault();
            submit(value);
          }
          if (e.key === "Escape") {
            onClose();
          }
        }}
        placeholder="Tag..."
        className="w-24 px-2 py-0.5 text-xs rounded-full border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-1 focus:ring-brand-400"
      />
      {showSuggestions && suggestions.length > 0 && typeof document !== "undefined"
        ? createPortal(
            <div
              ref={panelRef}
              className="fixed z-[9998] max-h-32 w-40 overflow-y-auto rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-gray-700 dark:bg-gray-800"
              style={{
                top: position.top,
                left: position.left,
              }}
            >
              {suggestions.map((s) => (
                <button
                  key={s}
                  type="button"
                  onMouseDown={(e) => e.preventDefault()}
                  onClick={() => {
                    submit(s);
                    setShowSuggestions(false);
                  }}
                  className="block w-full text-left px-3 py-1 text-xs text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
                >
                  <TagPill tag={s} />
                </button>
              ))}
            </div>,
            document.body,
          )
        : null}
    </div>
  );
}

// ---------------------------------------------------------------------------
// Drag handle icon
// ---------------------------------------------------------------------------

function GripIcon({ className }: { className?: string }) {
  return (
    <svg className={className} width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
      <circle cx="5" cy="3" r="1.5" />
      <circle cx="11" cy="3" r="1.5" />
      <circle cx="5" cy="8" r="1.5" />
      <circle cx="11" cy="8" r="1.5" />
      <circle cx="5" cy="13" r="1.5" />
      <circle cx="11" cy="13" r="1.5" />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// Chevron icon for expand/collapse
// ---------------------------------------------------------------------------

function ChevronIcon({ expanded, className }: { expanded: boolean; className?: string }) {
  return (
    <svg
      className={`transition-transform duration-150 ${expanded ? "rotate-90" : "rotate-0"} ${className ?? ""}`}
      width="14"
      height="14"
      viewBox="0 0 14 14"
      fill="none"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
    >
      <path d="M5 3l4 4-4 4" />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// Parent selector dropdown
// ---------------------------------------------------------------------------

function ParentSelector({
  clients,
  currentId,
  currentParentId,
  onChange,
}: {
  clients: ClientRow[];
  currentId?: string;
  currentParentId: string | null;
  onChange: (parentId: string | null) => void;
}) {
  // Exclude self and all descendants to prevent circular refs
  const excludeIds = useMemo(() => {
    if (!currentId) return new Set<string>();
    const ids = getDescendantIds(currentId, clients);
    ids.add(currentId);
    return ids;
  }, [currentId, clients]);

  const options = useMemo(() => {
    return clients
      .filter((c) => !excludeIds.has(c.id) && c.isActive)
      .sort((a, b) => a.name.localeCompare(b.name));
  }, [clients, excludeIds]);

  return (
    <select
      value={currentParentId ?? ""}
      onChange={(e) => onChange(e.target.value || null)}
      className="px-2 py-1 text-xs rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-700 dark:text-gray-300 focus:outline-none focus:ring-1 focus:ring-brand-400 max-w-[160px]"
    >
      <option value="">(Top Level)</option>
      {options.map((c) => (
        <option key={c.id} value={c.id}>
          {c.name}
        </option>
      ))}
    </select>
  );
}

// ---------------------------------------------------------------------------
// Sortable client card
// ---------------------------------------------------------------------------

function SortableClientCard({
  client,
  allClients,
  allKnownTags,
  onUpdateName,
  onUpdateSortOrder,
  onUpdateTags,
  onUpdateParent,
  onDelete,
  onToggleExpand,
  isExpanded,
  isDragOverlay,
}: {
  client: TreeNode;
  allClients: ClientRow[];
  allKnownTags: string[];
  onUpdateName: (id: string, name: string) => void;
  onUpdateSortOrder: (id: string, sortOrder: number) => void;
  onUpdateTags: (id: string, tags: string[]) => void;
  onUpdateParent: (id: string, parentId: string | null) => void;
  onDelete: (id: string) => void;
  onToggleExpand: (id: string) => void;
  isExpanded: boolean;
  isDragOverlay?: boolean;
}) {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id: client.id });

  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
  };

  const [editingName, setEditingName] = useState(false);
  const [nameValue, setNameValue] = useState(client.name);
  const [editingSortOrder, setEditingSortOrder] = useState(false);
  const [sortOrderValue, setSortOrderValue] = useState(client.sortOrder);
  const [addingTag, setAddingTag] = useState(false);
  const [confirmDelete, setConfirmDelete] = useState(false);
  const [showParentSelector, setShowParentSelector] = useState(false);
  const nameInputRef = useRef<HTMLInputElement>(null);
  const sortInputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    if (editingName) nameInputRef.current?.focus();
  }, [editingName]);

  useEffect(() => {
    if (editingSortOrder) sortInputRef.current?.focus();
  }, [editingSortOrder]);

  // Sync external updates
  useEffect(() => {
    setNameValue(client.name);
  }, [client.name]);

  useEffect(() => {
    setSortOrderValue(client.sortOrder);
  }, [client.sortOrder]);

  function saveName() {
    const trimmed = nameValue.trim();
    if (trimmed && trimmed !== client.name) {
      onUpdateName(client.id, trimmed);
    } else {
      setNameValue(client.name);
    }
    setEditingName(false);
  }

  function saveSortOrder() {
    if (sortOrderValue !== client.sortOrder) {
      onUpdateSortOrder(client.id, sortOrderValue);
    }
    setEditingSortOrder(false);
  }

  const hasChildren = (client._count?.children ?? client.children.length) > 0;
  const childCount = client._count?.children ?? client.children.length;
  const depth = client.depth;

  // Nesting depth styling: alternating subtle backgrounds
  const depthBg = depth > 0
    ? depth % 2 === 1
      ? "bg-gray-50/50 dark:bg-gray-800/80"
      : "bg-white dark:bg-gray-800/60"
    : "bg-white dark:bg-gray-800";

  const cardClasses = [
    "flex items-center gap-3 px-4 py-3 rounded-xl border transition-all group",
    isDragOverlay
      ? "bg-white dark:bg-gray-800 border-brand-500 shadow-xl scale-[1.02] ring-2 ring-brand-500"
      : isDragging
        ? "opacity-30 bg-gray-50 dark:bg-gray-900 border-gray-200 dark:border-gray-700"
        : `${depthBg} border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 hover:shadow-sm`,
  ].join(" ");

  // Indent padding: 0 for root, 32px per level
  const indentPx = depth * 32;

  // Find parent name for display
  const parentClient = client.parentId
    ? allClients.find((c) => c.id === client.parentId)
    : null;

  return (
    <div
      ref={setNodeRef}
      style={style}
      className={depth > 0 ? "relative" : undefined}
    >
      {/* Tree connector line */}
      {depth > 0 && !isDragOverlay && (
        <div
          className="absolute top-0 bottom-0 border-l-2 border-gray-300 dark:border-gray-600"
          style={{ left: `${indentPx - 16}px` }}
        />
      )}

      <div
        className={cardClasses}
        style={{ marginLeft: `${indentPx}px` }}
      >
        {/* Expand/collapse toggle */}
        <div className="flex-shrink-0 w-5">
          {hasChildren ? (
            <button
              type="button"
              onClick={() => onToggleExpand(client.id)}
              className="flex items-center justify-center w-5 h-5 rounded text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
              title={isExpanded ? "Collapse" : "Expand"}
            >
              <ChevronIcon expanded={isExpanded} />
            </button>
          ) : (
            <span className="block w-5" />
          )}
        </div>

        {/* Drag handle */}
        <button
          type="button"
          className="cursor-grab active:cursor-grabbing text-gray-300 dark:text-gray-600 hover:text-gray-500 dark:hover:text-gray-400 flex-shrink-0 touch-none"
          {...attributes}
          {...listeners}
        >
          <GripIcon />
        </button>

        {/* Name + parent info */}
        <div className="flex-1 min-w-0">
          {editingName ? (
            <input
              ref={nameInputRef}
              type="text"
              value={nameValue}
              onChange={(e) => setNameValue(e.target.value)}
              onKeyDown={(e) => {
                if (e.key === "Enter") saveName();
                if (e.key === "Escape") {
                  setNameValue(client.name);
                  setEditingName(false);
                }
              }}
              onBlur={saveName}
              className="w-full px-2 py-1 -ml-2 text-sm font-medium rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-brand-400"
            />
          ) : (
            <div className="flex items-center gap-2 min-w-0">
              <button
                type="button"
                onClick={() => setEditingName(true)}
                className="text-sm font-medium text-gray-900 dark:text-gray-100 hover:text-brand-600 dark:hover:text-brand-400 truncate text-left cursor-text"
                title="Click to edit name"
              >
                {client.name}
                {client.code && (
                  <span className="text-gray-400 dark:text-gray-500 font-mono ml-2 text-xs">
                    [{client.code}]
                  </span>
                )}
                {!client.isActive && (
                  <span className="ml-2 text-xs text-gray-400 italic">inactive</span>
                )}
              </button>

              {/* Children count badge */}
              {hasChildren && (
                <span className="inline-flex items-center px-1.5 py-0.5 rounded-full text-[10px] font-medium bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 tabular-nums flex-shrink-0">
                  {childCount}
                </span>
              )}
            </div>
          )}
        </div>

        {/* Parent selector */}
        <div className="flex-shrink-0">
          {showParentSelector ? (
            <div className="flex items-center gap-1">
              <ParentSelector
                clients={allClients}
                currentId={client.id}
                currentParentId={client.parentId}
                onChange={(newParentId) => {
                  if (newParentId !== client.parentId) {
                    onUpdateParent(client.id, newParentId);
                  }
                  setShowParentSelector(false);
                }}
              />
              <button
                type="button"
                onClick={() => setShowParentSelector(false)}
                className="text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
              >
                &times;
              </button>
            </div>
          ) : (
            <button
              type="button"
              onClick={() => setShowParentSelector(true)}
              className="text-[11px] text-gray-400 dark:text-gray-500 hover:text-brand-500 dark:hover:text-brand-400 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap"
              title="Change parent"
            >
              {parentClient ? `\u2190 ${parentClient.name}` : "\u2190 Move..."}
            </button>
          )}
        </div>

        {/* Tags */}
        <div className="flex items-center gap-1 flex-wrap flex-shrink-0">
          {client.tags.map((tag) => (
            <TagPill
              key={tag}
              tag={tag}
              onRemove={() => {
                onUpdateTags(client.id, client.tags.filter((t) => t !== tag));
              }}
            />
          ))}
          {addingTag ? (
            <TagAdder
              existingTags={client.tags}
              allKnownTags={allKnownTags}
              onAdd={(tag) => {
                onUpdateTags(client.id, [...client.tags, tag]);
              }}
              onClose={() => setAddingTag(false)}
            />
          ) : (
            <button
              type="button"
              onClick={() => setAddingTag(true)}
              className="inline-flex items-center justify-center w-5 h-5 rounded-full text-xs text-gray-400 dark:text-gray-500 hover:text-brand-500 dark:hover:text-brand-400 hover:bg-gray-100 dark:hover:bg-gray-700 opacity-0 group-hover:opacity-100 transition-opacity"
              title="Add tag"
            >
              +
            </button>
          )}
        </div>

        {/* Sort order */}
        <div className="flex-shrink-0 w-12">
          {editingSortOrder ? (
            <input
              ref={sortInputRef}
              type="number"
              value={sortOrderValue}
              onChange={(e) => setSortOrderValue(parseInt(e.target.value) || 0)}
              onKeyDown={(e) => {
                if (e.key === "Enter") saveSortOrder();
                if (e.key === "Escape") {
                  setSortOrderValue(client.sortOrder);
                  setEditingSortOrder(false);
                }
              }}
              onBlur={saveSortOrder}
              className="w-full px-1 py-0.5 text-xs text-center rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-600 dark:text-gray-400 focus:outline-none focus:ring-1 focus:ring-brand-400"
            />
          ) : (
            <button
              type="button"
              onClick={() => setEditingSortOrder(true)}
              className="w-full text-xs text-center text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 cursor-text tabular-nums"
              title="Click to edit sort order"
            >
              #{client.sortOrder}
            </button>
          )}
        </div>

        {/* Delete */}
        <div className="flex-shrink-0">
          {confirmDelete ? (
            <div className="flex items-center gap-1">
              <button
                type="button"
                onClick={() => {
                  onDelete(client.id);
                  setConfirmDelete(false);
                }}
                className="text-xs px-2 py-1 bg-red-600 text-white rounded hover:bg-red-700"
              >
                Delete
              </button>
              <button
                type="button"
                onClick={() => setConfirmDelete(false)}
                className="text-xs px-2 py-1 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
              >
                Cancel
              </button>
            </div>
          ) : (
            <button
              type="button"
              onClick={() => setConfirmDelete(true)}
              className="text-gray-300 dark:text-gray-600 hover:text-red-500 dark:hover:text-red-400 opacity-0 group-hover:opacity-100 transition-opacity"
              title="Delete client"
            >
              <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
                <path d="M2 4h12M5.33 4V2.67a1.33 1.33 0 011.34-1.34h2.66a1.33 1.33 0 011.34 1.34V4m2 0v9.33a1.33 1.33 0 01-1.34 1.34H4.67a1.33 1.33 0 01-1.34-1.34V4h9.34z" />
              </svg>
            </button>
          )}
        </div>
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------------
// Main component
// ---------------------------------------------------------------------------

export function ClientsAdminClient() {
  const [search, setSearch] = useState("");
  const [newName, setNewName] = useState("");
  const [newParentId, setNewParentId] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [activeId, setActiveId] = useState<string | null>(null);
  const [collapsedIds, setCollapsedIds] = useState<Set<string>>(new Set());
  const newInputRef = useRef<HTMLInputElement>(null);

  const utils = trpc.useUtils();
  const { data: rawList, isLoading } = trpc.clientEntity.list.useQuery();

  const clients = useMemo(() => {
    return ((rawList ?? []) as unknown as ClientRow[]).slice().sort(
      (a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name),
    );
  }, [rawList]);

  // Map for fast lookup
  const clientMap = useMemo(() => {
    const m = new Map<string, ClientRow>();
    for (const c of clients) m.set(c.id, c);
    return m;
  }, [clients]);

  // All known tags across all clients for auto-suggest
  const allKnownTags = useMemo(() => {
    const set = new Set<string>();
    for (const c of clients) {
      for (const t of c.tags ?? []) set.add(t);
    }
    return Array.from(set).sort();
  }, [clients]);

  // Build tree, then flatten with search filtering
  const tree = useMemo(() => buildTree(clients), [clients]);

  // When searching: filter matching clients and include their ancestors
  const displayNodes = useMemo(() => {
    if (!search.trim()) {
      return flattenTree(tree, collapsedIds);
    }

    const lower = search.toLowerCase();
    // Find IDs of clients matching the search
    const matchingIds = new Set<string>();
    for (const c of clients) {
      if (
        c.name.toLowerCase().includes(lower) ||
        (c.code ?? "").toLowerCase().includes(lower) ||
        (c.tags ?? []).some((t) => t.toLowerCase().includes(lower))
      ) {
        matchingIds.add(c.id);
      }
    }

    // Also include all ancestors of matching clients so tree path is visible
    const visibleIds = new Set(matchingIds);
    for (const id of matchingIds) {
      const ancestors = getAncestorIds(id, clientMap);
      for (const aid of ancestors) visibleIds.add(aid);
    }

    // Build tree from only visible clients
    const visibleClients = clients.filter((c) => visibleIds.has(c.id));
    const filteredTree = buildTree(visibleClients);
    // When searching, show all nodes expanded (don't respect collapsed state)
    return flattenTree(filteredTree, new Set());
  }, [search, tree, collapsedIds, clients, clientMap]);

  // Drag sensors
  const sensors = useSensors(
    useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
    useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
  );

  // Mutations
  const invalidateAll = useCallback(() => {
    void utils.clientEntity.list.invalidate();
    void utils.clientEntity.getTree.invalidate();
  }, [utils]);

  const createMut = trpc.clientEntity.create.useMutation({
    onSuccess: () => {
      invalidateAll();
      setNewName("");
      setNewParentId(null);
    },
    onError: (e) => setError(e.message),
  });

  const updateMut = trpc.clientEntity.update.useMutation({
    onSuccess: invalidateAll,
    onError: (e) => setError(e.message),
  });

  const deleteMut = trpc.clientEntity.delete.useMutation({
    onSuccess: invalidateAll,
    onError: (e) => setError(e.message),
  });

  const batchSortMut = trpc.clientEntity.batchUpdateSortOrder.useMutation({
    onSuccess: invalidateAll,
    onError: (e) => setError(e.message),
  });

  // Handlers
  function handleCreate() {
    const trimmed = newName.trim();
    if (!trimmed) return;
    // Compute max sortOrder among siblings
    const siblings = clients.filter((c) =>
      newParentId ? c.parentId === newParentId : !c.parentId,
    );
    const maxSort = siblings.reduce((max, c) => Math.max(max, c.sortOrder), 0);
    createMut.mutate({
      name: trimmed,
      sortOrder: maxSort + 1,
      ...(newParentId ? { parentId: newParentId } : {}),
    });
  }

  function handleUpdateName(id: string, name: string) {
    updateMut.mutate({ id, data: { name } });
  }

  function handleUpdateSortOrder(id: string, sortOrder: number) {
    updateMut.mutate({ id, data: { sortOrder } });
  }

  function handleUpdateTags(id: string, tags: string[]) {
    updateMut.mutate({ id, data: { tags } });
  }

  function handleUpdateParent(id: string, parentId: string | null) {
    updateMut.mutate({ id, data: { parentId } });
  }

  function handleDelete(id: string) {
    deleteMut.mutate({ id });
  }

  function handleToggleExpand(id: string) {
    setCollapsedIds((prev) => {
      const next = new Set(prev);
      if (next.has(id)) {
        next.delete(id);
      } else {
        next.add(id);
      }
      return next;
    });
  }

  // Drag and drop — moves the whole subtree with the dragged client
  function handleDragStart(event: DragStartEvent) {
    setActiveId(event.active.id as string);
  }

  function handleDragEnd(event: DragEndEvent) {
    setActiveId(null);
    const { active, over } = event;
    if (!over || active.id === over.id) return;

    const oldIndex = displayNodes.findIndex((c) => c.id === active.id);
    const newIndex = displayNodes.findIndex((c) => c.id === over.id);
    if (oldIndex === -1 || newIndex === -1) return;

    // Get the dragged node and all its descendants (the subtree)
    const draggedId = active.id as string;
    const descendantIds = getDescendantIds(draggedId, clients);

    // Filter display nodes to only siblings at the same parent level as the dragged node
    const draggedClient = clientMap.get(draggedId);
    if (!draggedClient) return;

    const overClient = displayNodes[newIndex];
    if (!overClient) return;

    // Only allow reorder among siblings (same parentId)
    if (draggedClient.parentId !== overClient.parentId) return;

    // Get sibling nodes in display order (excluding descendants of dragged)
    const siblings = displayNodes.filter(
      (c) => c.parentId === draggedClient.parentId && !descendantIds.has(c.id),
    );

    const sibOldIndex = siblings.findIndex((c) => c.id === active.id);
    const sibNewIndex = siblings.findIndex((c) => c.id === over.id);
    if (sibOldIndex === -1 || sibNewIndex === -1) return;

    const reordered = arrayMove(siblings, sibOldIndex, sibNewIndex);
    const updates = reordered.map((c, i) => ({ id: c.id, sortOrder: i }));

    // Optimistic update
    utils.clientEntity.list.setData(undefined, (prev) => {
      if (!prev) return prev;
      const sortMap = new Map(updates.map((u) => [u.id, u.sortOrder]));
      return prev.map((c) => {
        const newSort = sortMap.get(c.id);
        if (newSort !== undefined) return { ...c, sortOrder: newSort };
        return c;
      }) as typeof prev;
    });

    batchSortMut.mutate(updates);
  }

  const activeClient = activeId
    ? displayNodes.find((c) => c.id === activeId)
    : null;

  return (
    <div className="p-6 max-w-4xl mx-auto">
      {/* Header */}
      <div className="mb-6">
        <h1 className="text-2xl font-bold text-gray-900 dark:text-gray-50">Clients</h1>
        <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
          Manage clients for project assignment and chargeability reporting{" "}
          <InfoTooltip content="Clients are companies or brands that commission projects. Use the tree hierarchy for parent/child relationships. Drag to reorder within a level, click to edit inline, and use tags for categorization." />
        </p>
      </div>

      {/* Add new client -- sticky row */}
      <div className="mb-4 flex items-center gap-2">
        <input
          ref={newInputRef}
          type="text"
          value={newName}
          onChange={(e) => setNewName(e.target.value)}
          onKeyDown={(e) => {
            if (e.key === "Enter") handleCreate();
          }}
          placeholder="New client name..."
          className="flex-1 border border-gray-300 dark:border-gray-600 rounded-xl px-4 py-2.5 text-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-brand-400 focus:border-transparent"
        />
        <select
          value={newParentId ?? ""}
          onChange={(e) => setNewParentId(e.target.value || null)}
          className="border border-gray-300 dark:border-gray-600 rounded-xl px-3 py-2.5 text-sm bg-white dark:bg-gray-900 text-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-brand-400 max-w-[200px]"
          title="Parent client (optional)"
        >
          <option value="">No parent (top level)</option>
          {clients.filter((c) => c.isActive).map((c) => (
            <option key={c.id} value={c.id}>
              {c.name}
            </option>
          ))}
        </select>
        <button
          type="button"
          onClick={handleCreate}
          disabled={!newName.trim() || createMut.isPending}
          className="px-5 py-2.5 bg-brand-600 text-white rounded-xl hover:bg-brand-700 text-sm font-medium disabled:opacity-50 transition-colors flex-shrink-0"
        >
          {createMut.isPending ? "Adding..." : "Add"}
        </button>
      </div>

      {/* Search/filter */}
      <div className="mb-4 flex items-center gap-2">
        <input
          type="search"
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          placeholder="Filter by name or tag..."
          className="border border-gray-300 dark:border-gray-600 rounded-xl px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-400 w-full sm:w-72 bg-white dark:bg-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500"
        />
        {!search && clients.length > 0 && (
          <button
            type="button"
            onClick={() => {
              // Toggle collapse all / expand all
              const allWithChildren = clients.filter(
                (c) => (c._count?.children ?? 0) > 0,
              );
              if (collapsedIds.size === 0) {
                setCollapsedIds(new Set(allWithChildren.map((c) => c.id)));
              } else {
                setCollapsedIds(new Set());
              }
            }}
            className="text-xs text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 whitespace-nowrap px-2 py-1 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
          >
            {collapsedIds.size === 0 ? "Collapse all" : "Expand all"}
          </button>
        )}
      </div>

      {/* Error */}
      {error && (
        <div className="mb-4 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-700 px-4 py-3 text-sm text-red-700 dark:text-red-400 flex items-center justify-between">
          {error}
          <button
            type="button"
            onClick={() => setError(null)}
            className="text-red-400 hover:text-red-600 text-lg leading-none ml-4"
          >
            &times;
          </button>
        </div>
      )}

      {/* Client list */}
      <div className="space-y-2">
        {isLoading && (
          <div className="text-center py-12 text-gray-400 dark:text-gray-500">Loading...</div>
        )}

        {!isLoading && displayNodes.length === 0 && (
          <div className="text-center py-12 text-gray-400 dark:text-gray-500 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700">
            {search ? "No clients match your filter." : "No clients yet. Add one above."}
          </div>
        )}

        {!isLoading && displayNodes.length > 0 && (
          <DndContext
            sensors={sensors}
            collisionDetection={closestCenter}
            onDragStart={handleDragStart}
            onDragEnd={handleDragEnd}
          >
            <SortableContext
              items={displayNodes.map((c) => c.id)}
              strategy={verticalListSortingStrategy}
            >
              {displayNodes.map((client) => (
                <SortableClientCard
                  key={client.id}
                  client={client}
                  allClients={clients}
                  allKnownTags={allKnownTags}
                  onUpdateName={handleUpdateName}
                  onUpdateSortOrder={handleUpdateSortOrder}
                  onUpdateTags={handleUpdateTags}
                  onUpdateParent={handleUpdateParent}
                  onDelete={handleDelete}
                  onToggleExpand={handleToggleExpand}
                  isExpanded={!collapsedIds.has(client.id)}
                />
              ))}
            </SortableContext>

            <DragOverlay>
              {activeClient ? (
                <SortableClientCard
                  client={{ ...activeClient, depth: 0 }}
                  allClients={clients}
                  allKnownTags={allKnownTags}
                  onUpdateName={() => {}}
                  onUpdateSortOrder={() => {}}
                  onUpdateTags={() => {}}
                  onUpdateParent={() => {}}
                  onDelete={() => {}}
                  onToggleExpand={() => {}}
                  isExpanded={false}
                  isDragOverlay
                />
              ) : null}
            </DragOverlay>
          </DndContext>
        )}
      </div>

      {/* Count */}
      {!isLoading && clients.length > 0 && (
        <div className="mt-4 text-xs text-gray-400 dark:text-gray-500 text-right">
          {displayNodes.length} of {clients.length} client{clients.length !== 1 ? "s" : ""}
          {clients.filter((c) => c.parentId).length > 0 && (
            <span className="ml-2">
              ({clients.filter((c) => !c.parentId).length} top-level, {clients.filter((c) => c.parentId).length} nested)
            </span>
          )}
        </div>
      )}
    </div>
  );
}
