import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
import {
  useContext,
  useEffect,
  useState,
  useCallback,
  useRef,
  useMemo,
} from 'react';
import DJClientContext from '../../providers/djclient';
import { useCurrentUser } from '../../providers/UserProvider';
import AddNodeDropdown from '../../components/AddNodeDropdown';
import NodeListActions from '../../components/NodeListActions';
import NamespaceHeader from '../../components/NamespaceHeader';
import SplitFilter from '../../components/SplitFilter';
import Tooltip from '../../components/Tooltip';
import {
  secondaryButtonStyle,
  onSecondaryHover,
  onSecondaryOut,
} from '../../components/buttonStyles';
import LoadingIcon from '../../icons/LoadingIcon';
import CompactSelect from './CompactSelect';
import NamespaceNav from './NamespaceNav';
import { isHiddenNamespace } from './namespaceOptions';
import { NODE_TYPE_ORDER, NODE_TYPE_COLORS } from './nodeTypes';
import { getDJUrl } from '../../services/DJService';

import 'styles/node-list.css';
import 'styles/sorted-table.css';

const AVATAR_COLORS = [
  ['#dbeafe', '#1e40af'],
  ['#dcfce7', '#166534'],
  ['#fef3c7', '#92400e'],
  ['#fce7f3', '#9d174d'],
  ['#ede9fe', '#5b21b6'],
  ['#ffedd5', '#9a3412'],
  ['#fee2e2', '#991b1b'],
  ['#d1fae5', '#065f46'],
];

function avatarColorIndex(username) {
  let hash = 0;
  for (let i = 0; i < username.length; i++) {
    hash = (hash * 31 + username.charCodeAt(i)) >>> 0;
  }
  return hash % AVATAR_COLORS.length;
}

const FIELD_CONFIG = {
  name: { label: 'Name' },
  displayName: { label: 'Display name' },
  type: { label: 'Type' },
  status: {
    label: 'Validation',
    tooltip: 'Whether this node validates successfully.',
  },
  mode: {
    label: 'Publish state',
    tooltip: 'Whether this node is published or still a draft.',
  },
  owners: { label: 'Owners' },
  updatedAt: { label: 'Updated' },
};

const CELL_STYLE = {
  padding: '8px 16px',
};

const MIDDLE_CELL_STYLE = {
  ...CELL_STYLE,
  verticalAlign: 'middle',
};

const NODE_TYPE_DESCRIPTIONS = {
  CUBE: 'A curated set of metrics and dimensions for querying together.',
  DIMENSION: 'Descriptive attributes used to group, filter, and join metrics.',
  METRIC: 'A reusable business calculation or measure.',
  SOURCE: 'A physical table registered in DJ as a source node.',
  TRANSFORM: 'A SQL-defined node derived from sources or other DJ nodes.',
};

function splitNodeName(nodeName) {
  const lastDot = nodeName.lastIndexOf('.');
  if (lastDot === -1) {
    return { leafName: nodeName, namespacePath: '' };
  }
  return {
    leafName: nodeName.slice(lastDot + 1),
    namespacePath: nodeName.slice(0, lastDot),
  };
}

function nodeState(current) {
  const isPublished = current.mode === 'PUBLISHED';
  const isValid = current.status === 'VALID';
  return {
    isPublished,
    isValid,
    publishLabel: isPublished ? 'Published' : 'Draft',
    validationLabel: isValid ? 'Valid' : 'Needs fixes',
    publishTooltip: isPublished
      ? 'This version is published and expected to be usable by downstream nodes.'
      : 'This version is a draft and may still be incomplete.',
    validationTooltip: isValid
      ? 'This node validates successfully.'
      : 'This node has validation errors. Open it to see what needs to be fixed.',
    needsAttention: !isPublished || !isValid,
    borderColor: isValid && isPublished ? '#28a745' : '#d39e00',
    backgroundColor: isValid && isPublished ? '#eaf7ee' : '#fff7df',
    color: isValid && isPublished ? '#1f7a3a' : '#8a5b00',
  };
}

function exactDateTime(value) {
  return new Date(value).toLocaleString('en-us', {
    year: 'numeric',
    month: 'short',
    day: 'numeric',
    hour: 'numeric',
    minute: '2-digit',
  });
}

function relativeDate(value) {
  const updatedAt = new Date(value);
  const diffMs = Date.now() - updatedAt.getTime();
  const absMs = Math.abs(diffMs);
  const units = [
    ['year', 365 * 24 * 60 * 60 * 1000],
    ['month', 30 * 24 * 60 * 60 * 1000],
    ['week', 7 * 24 * 60 * 60 * 1000],
    ['day', 24 * 60 * 60 * 1000],
    ['hour', 60 * 60 * 1000],
    ['minute', 60 * 1000],
  ];

  for (const [unit, ms] of units) {
    if (absMs >= ms) {
      const valueForUnit = Math.round(absMs / ms);
      return `${valueForUnit} ${unit}${valueForUnit === 1 ? '' : 's'} ${
        diffMs >= 0 ? 'ago' : 'from now'
      }`;
    }
  }
  return 'just now';
}

export function NamespacePage() {
  const ASC = 'ascending';
  const DESC = 'descending';

  const fields = [
    'name',
    'displayName',
    'type',
    'status',
    'mode',
    'owners',
    'updatedAt',
  ];

  const djClient = useContext(DJClientContext).DataJunctionAPI;
  const { currentUser } = useCurrentUser();
  var { namespace } = useParams();
  const navigate = useNavigate();
  const [searchParams, setSearchParams] = useSearchParams();

  // Data for select options. These populate filter dropdowns only, so they're
  // fetched lazily on first dropdown open instead of on page load — except when a
  // users/tags filter is already active from the URL, where we need the options to
  // render the selected label.
  const [users, setUsers] = useState([]);
  const [tags, setTags] = useState([]);
  const [usersLoading, setUsersLoading] = useState(false);
  const [tagsLoading, setTagsLoading] = useState(false);
  const usersRequested = useRef(false);
  const tagsRequested = useRef(false);

  const ensureUsers = useCallback(async () => {
    if (usersRequested.current) return;
    usersRequested.current = true;
    setUsersLoading(true);
    try {
      const data = await djClient.users();
      setUsers(data || []);
    } finally {
      setUsersLoading(false);
    }
  }, [djClient]);

  const ensureTags = useCallback(async () => {
    if (tagsRequested.current) return;
    tagsRequested.current = true;
    setTagsLoading(true);
    try {
      const data = await djClient.listTags();
      setTags(data || []);
    } finally {
      setTagsLoading(false);
    }
  }, [djClient]);

  // Eagerly fetch only what an already-applied filter needs to render its label.
  useEffect(() => {
    if (searchParams.get('editedBy') || searchParams.get('ownedBy')) {
      ensureUsers().catch(console.error);
    }
    if (searchParams.get('tags')) {
      ensureTags().catch(console.error);
    }
  }, [searchParams, ensureUsers, ensureTags]);

  // Parse all filters from URL
  const getFiltersFromUrl = useCallback(
    () => ({
      node_type: searchParams.get('type') || '',
      tags: searchParams.get('tags') ? searchParams.get('tags').split(',') : [],
      edited_by: searchParams.get('editedBy') || '',
      mode: searchParams.get('mode') || '',
      ownedBy: searchParams.get('ownedBy') || '',
      statuses: searchParams.get('statuses') || '',
      missingDescription: searchParams.get('missingDescription') === 'true',
      hasMaterialization: searchParams.get('hasMaterialization') === 'true',
      orphanedDimension: searchParams.get('orphanedDimension') === 'true',
    }),
    [searchParams],
  );

  const [filters, setFilters] = useState(getFiltersFromUrl);
  const [moreFiltersOpen, setMoreFiltersOpen] = useState(false);

  // Sync filters state when the URL changes. `filters` is already seeded from the
  // URL via useState, so we skip the initial run — otherwise it would replace
  // filters with an equal-but-new object and trigger a redundant node refetch.
  const didSyncFilters = useRef(false);
  useEffect(() => {
    if (!didSyncFilters.current) {
      didSyncFilters.current = true;
      return;
    }
    setFilters(getFiltersFromUrl());
  }, [searchParams, getFiltersFromUrl]);

  // Update URL when filters change
  const updateFilters = useCallback(
    newFilters => {
      const params = new URLSearchParams();

      if (newFilters.node_type) params.set('type', newFilters.node_type);
      if (newFilters.tags?.length)
        params.set('tags', newFilters.tags.join(','));
      if (newFilters.edited_by) params.set('editedBy', newFilters.edited_by);
      if (newFilters.mode) params.set('mode', newFilters.mode);
      if (newFilters.ownedBy) params.set('ownedBy', newFilters.ownedBy);
      if (newFilters.statuses) params.set('statuses', newFilters.statuses);
      if (newFilters.missingDescription)
        params.set('missingDescription', 'true');
      if (newFilters.hasMaterialization)
        params.set('hasMaterialization', 'true');
      if (newFilters.orphanedDimension) params.set('orphanedDimension', 'true');

      setSearchParams(params);
    },
    [setSearchParams],
  );

  const clearAllFilters = () => {
    setSearchParams(new URLSearchParams());
  };

  // Check if any filters are active
  const hasActiveFilters =
    filters.node_type ||
    filters.tags?.length ||
    filters.edited_by ||
    filters.mode ||
    filters.ownedBy ||
    filters.statuses ||
    filters.missingDescription ||
    filters.hasMaterialization ||
    filters.orphanedDimension;

  // Quick presets
  const presets = [
    {
      id: 'my-nodes',
      label: 'My Nodes',
      filters: { ownedBy: currentUser?.username },
    },
    {
      id: 'needs-attention',
      label: 'Needs Attention',
      filters: { ownedBy: currentUser?.username, statuses: 'INVALID' },
    },
    {
      id: 'drafts',
      label: 'Drafts',
      filters: { ownedBy: currentUser?.username, mode: 'draft' },
    },
  ];

  const applyPreset = preset => {
    const newFilters = {
      node_type: '',
      tags: [],
      edited_by: '',
      mode: preset.filters.mode || '',
      ownedBy: preset.filters.ownedBy || '',
      statuses: preset.filters.statuses || '',
      missingDescription: preset.filters.missingDescription || false,
      hasMaterialization: preset.filters.hasMaterialization || false,
      orphanedDimension: preset.filters.orphanedDimension || false,
    };
    updateFilters(newFilters);
  };

  // Check if a preset is active
  const isPresetActive = preset => {
    const pf = preset.filters;
    return (
      (pf.ownedBy || '') === (filters.ownedBy || '') &&
      (pf.statuses || '') === (filters.statuses || '') &&
      (pf.mode || '') === (filters.mode || '') &&
      !filters.node_type &&
      !filters.tags?.length &&
      !filters.edited_by &&
      !filters.missingDescription &&
      !filters.hasMaterialization &&
      !filters.orphanedDimension
    );
  };

  const [state, setState] = useState({
    namespace: namespace ? namespace : '',
    nodes: [],
  });
  const [retrieved, setRetrieved] = useState(false);

  const [namespaceHierarchy, setNamespaceHierarchy] = useState([]);
  const [rawNamespaces, setRawNamespaces] = useState([]);
  const [gitRoots, setGitRoots] = useState(new Set());
  // Use undefined to indicate "not yet loaded", null means "loaded but no config"
  const [gitConfig, setGitConfig] = useState(undefined);

  const [sortConfig, setSortConfig] = useState({
    key: 'updatedAt',
    direction: DESC,
  });

  const [before, setBefore] = useState(null);
  const [after, setAfter] = useState(null);
  const [prevCursor, setPrevCursor] = useState(true);
  const [nextCursor, setNextCursor] = useState(true);

  const [hasNextPage, setHasNextPage] = useState(true);
  const [hasPrevPage, setHasPrevPage] = useState(true);

  const [nodeSearch, setNodeSearch] = useState('');
  const [debouncedSearch, setDebouncedSearch] = useState('');
  useEffect(() => {
    const t = setTimeout(() => setDebouncedSearch(nodeSearch.trim()), 300);
    return () => clearTimeout(t);
  }, [nodeSearch]);
  // Changing the search term restarts paging from the first page; a cursor from
  // a previous page would otherwise be sent with the new query.
  useEffect(() => {
    setBefore(null);
    setAfter(null);
  }, [debouncedSearch]);

  const [typeCounts, setTypeCounts] = useState(null);
  // undefined until NamespaceHeader reports the read-only verdict, so Add Node stays hidden until known
  const [headerReadOnly, setHeaderReadOnly] = useState(undefined);

  // Only show edit/add controls once git config has loaded and namespace is not git-only
  const gitConfigLoaded = gitConfig !== undefined;
  // Descendants inherit github_repo_path via cascade, so compare against
  // git_root_namespace to know if this namespace IS the git root.
  const isGitRoot =
    gitConfigLoaded &&
    !!gitConfig?.github_repo_path &&
    gitConfig?.git_root_namespace === namespace;
  // Node-edit controls follow the single read-only verdict from NamespaceHeader
  // (`headerReadOnly`), which correctly treats the default branch, 1:1/root git
  // namespaces, git_only, and git-deployed non-feature content as read-only
  // while leaving feature branches (and plain namespaces) editable. It's
  // `undefined` until the header reports, so controls stay hidden until the
  // verdict is known (no flash). Deriving a second, shape-based verdict here is
  // what let the default branch (shape 'branch') wrongly show edit controls.
  const showEditControls = headerReadOnly === false;
  // Sub-namespaces can be created from the rail only for plain (non-git-backed)
  // namespaces; git-backed ones are managed via git, not the UI. The git config
  // endpoint returns an object with null fields (not null) for non-git
  // namespaces, so check the actual git markers — a repo (root or cascaded
  // descendant) or a branch namespace.
  const isGitBacked = !!(
    gitConfig?.github_repo_path || gitConfig?.branch_namespace
  );
  const canCreateNamespace = gitConfigLoaded && !isGitBacked;
  const createSubNamespace = async fullNamespace => {
    const response = await djClient.addNamespace(fullNamespace);
    if (response.status === 200 || response.status === 201) {
      // The cached namespace list is now stale — refetch it on next mount.
      djClient.invalidateNamespacesCache?.();
      navigate(`/namespaces/${fullNamespace}`);
      return {};
    }
    return {
      _error: true,
      message: response.json?.message || 'Failed to create namespace',
    };
  };
  // Distinguish a *child-spawning* git root (owns the repo, has no branch of its
  // own — its nodes live on `<root>.<default_branch>`) from a *flat* git-backed
  // namespace (has its own `git_branch`, e.g. tracks `main` directly, with no
  // `<ns>.<branch>` children). Only the former is browsed via / redirected to its
  // default branch; a flat namespace is its own branch and must stay put.
  const isChildSpawningRoot = isGitRoot && !gitConfig?.git_branch;

  // A child-spawning root has no nodes of its own — they live on its default
  // branch — so the node table browses `<root>.<default_branch>`. A flat
  // namespace browses itself.
  const tableNamespace =
    isChildSpawningRoot && gitConfig?.default_branch
      ? `${namespace}.${gitConfig.default_branch}`
      : namespace;

  // A child-spawning root isn't a browsable branch — redirect to its default
  // branch so the URL is the branch (giving the breadcrumb its branch switcher)
  // and everything is scoped consistently. A flat git-backed namespace is NOT
  // redirected: it *is* the branch, and `<ns>.<default_branch>` doesn't exist.
  useEffect(() => {
    if (isChildSpawningRoot && gitConfig?.default_branch) {
      navigate(`/namespaces/${namespace}.${gitConfig.default_branch}`, {
        replace: true,
      });
    }
  }, [isChildSpawningRoot, gitConfig, namespace, navigate]);

  // Per-type node counts (recursive) for the current namespace, shown inline in
  // the TYPE filter options (e.g. "Metric (342)").
  useEffect(() => {
    // Wait until git config resolves so tableNamespace is final — otherwise a
    // git root fetches its provisional namespace, then refetches after redirect.
    if (!gitConfigLoaded) return;
    if (isGitRoot && gitConfig?.default_branch) return;
    let cancelled = false;
    if (!tableNamespace) {
      setTypeCounts(null);
      return;
    }
    djClient
      .nodeTypeCounts(tableNamespace, NODE_TYPE_ORDER)
      .then(byType => {
        if (!cancelled) setTypeCounts(byType);
      })
      .catch(() => {
        if (!cancelled) setTypeCounts(null);
      });
    return () => {
      cancelled = true;
    };
  }, [djClient, tableNamespace, gitConfigLoaded, isGitRoot, gitConfig]);

  const requestSort = key => {
    let direction = ASC;
    if (sortConfig.key === key && sortConfig.direction === ASC) {
      direction = DESC;
    }
    if (sortConfig.key !== key || sortConfig.direction !== direction) {
      setSortConfig({ key, direction });
    }
  };

  const getClassNamesFor = name => {
    if (sortConfig.key === name) {
      return sortConfig.direction;
    }
    return undefined;
  };

  const createNamespaceHierarchy = namespaceList => {
    const hierarchy = [];
    // Map lookups (path -> node's children map) keep insertion O(n · depth); we sort
    // each level once at the end rather than on every insert (which was O(n² log n)).
    const rootMap = new Map();
    const childMaps = new Map();

    for (const item of namespaceList) {
      const segments = item.namespace.split('.');
      let currentLevel = hierarchy;
      let levelMap = rootMap;
      let path = '';
      for (const ns of segments) {
        path += ns;
        let node = levelMap.get(ns);
        if (!node) {
          node = { namespace: ns, children: [], path };
          currentLevel.push(node);
          levelMap.set(ns, node);
          childMaps.set(path, new Map());
        }
        currentLevel = node.children;
        levelMap = childMaps.get(path);
        path += '.';
      }
    }

    const sortLevel = level => {
      level.sort((a, b) => a.namespace.localeCompare(b.namespace));
      level.forEach(node => sortLevel(node.children));
    };
    sortLevel(hierarchy);
    return hierarchy;
  };

  useEffect(() => {
    const fetchData = async () => {
      const all = await djClient.listNamespacesWithGit();
      // Hide system/internal scratch namespaces from all browse navigation
      // (rail list, header switcher, folders).
      const namespaces = all.filter(ns => !isHiddenNamespace(ns.namespace));
      setRawNamespaces(namespaces);
      const hierarchy = createNamespaceHierarchy(namespaces);
      setNamespaceHierarchy(hierarchy);

      const roots = new Set(
        namespaces
          .filter(ns => ns.git?.__typename === 'GitRootConfig')
          .map(ns => ns.namespace),
      );
      setGitRoots(roots);
    };
    fetchData().catch(console.error);
  }, [djClient]);

  useEffect(() => {
    // Wait until git config resolves so tableNamespace is final — otherwise a
    // git root fetches its provisional namespace, then refetches after redirect.
    if (!gitConfigLoaded) return;
    if (isGitRoot && gitConfig?.default_branch) return;
    const fetchData = async () => {
      setRetrieved(false);

      // Build extended filters for API
      const extendedFilters = {
        ownedBy: filters.ownedBy || null,
        statuses: filters.statuses ? [filters.statuses] : null,
        missingDescription: filters.missingDescription,
        hasMaterialization: filters.hasMaterialization,
        orphanedDimension: filters.orphanedDimension,
        // The table shows every node under this namespace (all descendants),
        // matching the rail's per-type counts. The rail's Folders drill DOWN to
        // re-scope; search narrows within the current scope.
        search: debouncedSearch || null,
      };

      const nodes = await djClient.listNodesForLanding(
        tableNamespace,
        filters.node_type ? [filters.node_type.toUpperCase()] : [],
        filters.tags,
        filters.edited_by,
        before,
        after,
        50,
        sortConfig,
        filters.mode ? filters.mode.toUpperCase() : null,
        extendedFilters,
      );

      setState({
        namespace: namespace,
        nodes: nodes.data
          ? nodes.data.findNodesPaginated.edges.map(n => n.node)
          : [],
      });
      if (nodes.data) {
        setPrevCursor(
          nodes.data ? nodes.data.findNodesPaginated.pageInfo.startCursor : '',
        );
        setNextCursor(
          nodes.data ? nodes.data.findNodesPaginated.pageInfo.endCursor : '',
        );
        setHasPrevPage(
          nodes.data
            ? nodes.data.findNodesPaginated.pageInfo.hasPrevPage
            : false,
        );
        setHasNextPage(
          nodes.data
            ? nodes.data.findNodesPaginated.pageInfo.hasNextPage
            : false,
        );
      }
      setRetrieved(true);
    };
    fetchData().catch(console.error);
  }, [
    djClient,
    filters,
    before,
    after,
    sortConfig.key,
    sortConfig.direction,
    tableNamespace,
    namespace,
    debouncedSearch,
    gitConfigLoaded,
    isGitRoot,
    gitConfig,
  ]);

  const loadNext = () => {
    if (nextCursor) {
      setAfter(nextCursor);
      setBefore(null);
    }
  };
  const loadPrev = () => {
    if (prevCursor) {
      setAfter(null);
      setBefore(prevCursor);
    }
  };

  // Select options. TYPE options carry the recursive per-type node count for the
  // current namespace (e.g. "Metric (342)") — this replaces the old rail counts.
  const TYPE_LABELS = {
    metric: 'Metric',
    cube: 'Cube',
    dimension: 'Dimension',
    transform: 'Transform',
    source: 'Source',
  };
  const typeOptions = NODE_TYPE_ORDER.map(type => ({
    value: type,
    label: TYPE_LABELS[type],
    count: typeCounts?.[type] ?? null,
  }));

  // Renders a TYPE option as "<name> <colored count pill>" (pill colors match the
  // node-type badges). Right-aligns the pill in the menu; inline in the control.
  const formatTypeOption = (option, meta) => {
    const pill =
      option.count != null ? (
        <span
          style={{
            backgroundColor: NODE_TYPE_COLORS[option.value]?.bg ?? '#f1f5f9',
            color: NODE_TYPE_COLORS[option.value]?.color ?? '#475569',
            borderRadius: '8px',
            padding: '1px 8px',
            fontSize: '11px',
            fontWeight: 600,
            fontVariantNumeric: 'tabular-nums',
          }}
        >
          {option.count}
        </span>
      ) : null;
    return (
      <span
        style={{
          display: 'flex',
          alignItems: 'center',
          gap: '8px',
          justifyContent:
            meta?.context === 'menu' ? 'space-between' : 'flex-start',
          width: meta?.context === 'menu' ? '100%' : 'auto',
        }}
      >
        <span>{option.label}</span>
        {pill}
      </span>
    );
  };

  const modeOptions = [
    { value: 'published', label: 'Published' },
    { value: 'draft', label: 'Draft' },
  ];

  const statusOptions = [
    { value: 'VALID', label: 'Valid' },
    { value: 'INVALID', label: 'Needs fixes' },
  ];

  const userOptions = useMemo(
    () => users.map(u => ({ value: u.username, label: u.username })),
    [users],
  );
  const tagOptions = useMemo(
    () => tags.map(t => ({ value: t.name, label: t.display_name })),
    [tags],
  );

  const nodesList = retrieved ? (
    state.nodes.length > 0 ? (
      state.nodes.map(node => {
        const { leafName, namespacePath } = splitNodeName(node.name);
        const stateInfo = nodeState(node.current);
        const owners = node.owners || [];
        return (
          <tr
            key={node.name}
            style={{
              backgroundColor: stateInfo.needsAttention ? '#fffbeb' : undefined,
            }}
          >
            <td
              style={{
                ...CELL_STYLE,
                maxWidth: '300px',
                overflow: 'hidden',
                whiteSpace: 'nowrap',
                textOverflow: 'ellipsis',
              }}
            >
              <div>
                <a
                  href={'/nodes/' + node.name}
                  className="link-table"
                  title={node.name}
                  style={{ fontWeight: 600, color: '#334155' }}
                >
                  {leafName}
                </a>
                <span
                  className="rounded-pill badge bg-secondary-soft"
                  style={{ marginLeft: '0.5rem' }}
                >
                  {node.currentVersion}
                </span>
              </div>
              {namespacePath ? (
                <div
                  title={namespacePath}
                  style={{
                    color: '#64748b',
                    fontSize: '12px',
                    marginTop: '2px',
                    overflow: 'hidden',
                    textOverflow: 'ellipsis',
                  }}
                >
                  {namespacePath}
                </div>
              ) : null}
            </td>
            <td
              style={{
                ...MIDDLE_CELL_STYLE,
                maxWidth: '250px',
                overflow: 'hidden',
                whiteSpace: 'nowrap',
                textOverflow: 'ellipsis',
                color: '#64748b',
              }}
            >
              <a
                href={'/nodes/' + node.name}
                className="link-table"
                style={{ color: '#475569', fontWeight: 400 }}
              >
                {node.type !== 'source' ? node.current.displayName : ''}
              </a>
            </td>
            <td style={MIDDLE_CELL_STYLE}>
              <Tooltip content={NODE_TYPE_DESCRIPTIONS[node.type]}>
                <span
                  className={
                    'node_type__' + node.type.toLowerCase() + ' badge node_type'
                  }
                >
                  {node.type}
                </span>
              </Tooltip>
            </td>
            <td style={MIDDLE_CELL_STYLE}>
              <Tooltip content={stateInfo.validationTooltip}>
                <span
                  style={{
                    display: 'inline-flex',
                    alignItems: 'center',
                    borderRadius: '999px',
                    border: `1px solid ${
                      stateInfo.isValid ? '#28a745' : '#d39e00'
                    }`,
                    backgroundColor: stateInfo.isValid ? '#eaf7ee' : '#fff7df',
                    color: stateInfo.isValid ? '#1f7a3a' : '#8a5b00',
                    fontWeight: '600',
                    fontSize: '12px',
                    lineHeight: 1.2,
                    padding: '6px 10px',
                    whiteSpace: 'nowrap',
                  }}
                >
                  {stateInfo.validationLabel}
                </span>
              </Tooltip>
            </td>
            <td style={MIDDLE_CELL_STYLE}>
              <Tooltip content={stateInfo.publishTooltip}>
                <span
                  style={{
                    display: 'inline-flex',
                    alignItems: 'center',
                    borderRadius: '999px',
                    border: `1px solid ${
                      stateInfo.isPublished ? '#28a745' : '#d39e00'
                    }`,
                    backgroundColor: stateInfo.isPublished
                      ? '#eaf7ee'
                      : '#fff7df',
                    color: stateInfo.isPublished ? '#1f7a3a' : '#8a5b00',
                    fontWeight: '600',
                    fontSize: '12px',
                    lineHeight: 1.2,
                    padding: '6px 10px',
                    whiteSpace: 'nowrap',
                  }}
                >
                  {stateInfo.publishLabel}
                </span>
              </Tooltip>
            </td>
            <td style={MIDDLE_CELL_STYLE}>
              {owners.length > 0 && (
                <div style={{ display: 'flex', gap: '2px' }}>
                  {owners.slice(0, 3).map(owner => {
                    const initials = owner.username
                      .split('@')[0]
                      .slice(0, 2)
                      .toUpperCase();
                    const [bg, fg] =
                      AVATAR_COLORS[avatarColorIndex(owner.username)];
                    return (
                      <span
                        key={owner.username}
                        title={owner.username}
                        style={{
                          display: 'inline-flex',
                          alignItems: 'center',
                          justifyContent: 'center',
                          width: '24px',
                          height: '24px',
                          borderRadius: '50%',
                          backgroundColor: bg,
                          color: fg,
                          fontSize: '9px',
                          fontWeight: '600',
                          flexShrink: 0,
                        }}
                      >
                        {initials}
                      </span>
                    );
                  })}
                </div>
              )}
            </td>
            <td style={MIDDLE_CELL_STYLE}>
              <Tooltip content={exactDateTime(node.current.updatedAt)}>
                <span className="status">
                  {relativeDate(node.current.updatedAt)}
                </span>
              </Tooltip>
            </td>
            {showEditControls && (
              <td style={MIDDLE_CELL_STYLE}>
                <NodeListActions nodeName={node?.name} />
              </td>
            )}
          </tr>
        );
      })
    ) : (
      <tr>
        <td colSpan={fields.length + (showEditControls ? 1 : 0)}>
          <span
            style={{
              display: 'block',
              marginTop: '2rem',
              marginLeft: '2rem',
              fontSize: '16px',
            }}
          >
            {hasActiveFilters ? (
              <>
                No nodes match the current filters.
                <a
                  href="#"
                  onClick={e => {
                    e.preventDefault();
                    clearAllFilters();
                  }}
                  style={{ marginLeft: '0.5rem' }}
                >
                  Clear filters
                </a>
              </>
            ) : (
              'No nodes in this namespace yet.'
            )}
          </span>
        </td>
      </tr>
    )
  ) : (
    <tr>
      <td>
        <span style={{ display: 'block', marginTop: '2rem' }}>
          <LoadingIcon />
        </span>
      </td>
    </tr>
  );

  // Count active filters tucked behind "More filters".
  const moreFiltersCount = [
    !!filters.edited_by,
    filters.missingDescription,
    filters.hasMaterialization,
    filters.orphanedDimension,
  ].filter(Boolean).length;

  return (
    <div className="mid">
      <div className="card">
        <div className="card-header">
          <div
            style={{
              display: 'flex',
              justifyContent: 'space-between',
              alignItems: 'center',
              marginBottom: '1rem',
            }}
          >
            <h2 style={{ margin: 0 }}>Browse</h2>
          </div>

          {/* Unified Filter Bar — always shown; a git root browses its default branch */}
          {gitConfigLoaded && (
            <div
              style={{
                marginBottom: '1rem',
                padding: '1rem',
                backgroundColor: '#f8fafc',
                borderRadius: '8px',
              }}
            >
              {/* Top row: Quick presets + Clear all */}
              <div
                style={{
                  display: 'flex',
                  alignItems: 'center',
                  gap: '12px',
                  marginBottom: '12px',
                }}
              >
                <div
                  style={{ display: 'flex', alignItems: 'center', gap: '6px' }}
                >
                  <span
                    style={{
                      fontSize: '11px',
                      fontWeight: '600',
                      textTransform: 'uppercase',
                      letterSpacing: '0.5px',
                      color: '#64748b',
                    }}
                  >
                    Quick
                  </span>
                  {presets.map(preset => (
                    <button
                      key={preset.id}
                      onClick={() => applyPreset(preset)}
                      style={{
                        padding: '4px 10px',
                        fontSize: '11px',
                        border: '1px solid',
                        borderColor: isPresetActive(preset)
                          ? '#1976d2'
                          : '#ddd',
                        borderRadius: '12px',
                        backgroundColor: isPresetActive(preset)
                          ? '#e3f2fd'
                          : 'white',
                        color: isPresetActive(preset) ? '#1976d2' : '#666',
                        cursor: 'pointer',
                        fontWeight: isPresetActive(preset) ? '600' : '400',
                      }}
                    >
                      {preset.label}
                    </button>
                  ))}
                  {hasActiveFilters && (
                    <button
                      onClick={clearAllFilters}
                      style={{
                        padding: '4px 10px',
                        fontSize: '11px',
                        border: 'none',
                        backgroundColor: 'transparent',
                        color: '#dc3545',
                        cursor: 'pointer',
                      }}
                    >
                      Clear all ×
                    </button>
                  )}
                </div>
              </div>

              {/* Bottom row: Dropdowns */}
              <div
                style={{
                  display: 'flex',
                  alignItems: 'flex-end',
                  gap: '12px',
                }}
              >
                <CompactSelect
                  label="Type"
                  name="type"
                  options={typeOptions}
                  value={filters.node_type}
                  onChange={e =>
                    updateFilters({ ...filters, node_type: e?.value || '' })
                  }
                  formatOptionLabel={formatTypeOption}
                  flex={1}
                  minWidth="80px"
                  testId="select-node-type"
                />
                <CompactSelect
                  label="Owner"
                  name="owner"
                  options={userOptions}
                  value={filters.ownedBy}
                  onChange={e =>
                    updateFilters({ ...filters, ownedBy: e?.value || '' })
                  }
                  isLoading={usersLoading}
                  onMenuOpen={ensureUsers}
                  flex={1}
                  minWidth="80px"
                />
                <CompactSelect
                  label="Tags"
                  name="tags"
                  options={tagOptions}
                  value={filters.tags}
                  onChange={e =>
                    updateFilters({
                      ...filters,
                      tags: e ? e.map(t => t.value) : [],
                    })
                  }
                  isMulti
                  isLoading={tagsLoading}
                  onMenuOpen={ensureTags}
                  flex={1.2}
                  minWidth="100px"
                  testId="select-tag"
                />
                <SplitFilter
                  label="Publish state"
                  options={modeOptions}
                  value={filters.mode}
                  onChange={value => updateFilters({ ...filters, mode: value })}
                />
                <SplitFilter
                  label="Validation"
                  options={statusOptions}
                  value={filters.statuses}
                  onChange={value =>
                    updateFilters({ ...filters, statuses: value })
                  }
                />

                {/* More Filters */}
                <div
                  style={{ position: 'relative', flex: 0, minWidth: 'auto' }}
                >
                  <div
                    style={{
                      display: 'flex',
                      flexDirection: 'column',
                      gap: '2px',
                    }}
                  >
                    <label
                      style={{
                        fontSize: '10px',
                        fontWeight: '600',
                        color: '#666',
                        textTransform: 'uppercase',
                        letterSpacing: '0.5px',
                      }}
                    >
                      More filters
                    </label>
                    <button
                      onClick={() => setMoreFiltersOpen(!moreFiltersOpen)}
                      style={{
                        height: '32px',
                        minHeight: '32px',
                        padding: '0 12px',
                        fontSize: '12px',
                        border: '1px solid #ccc',
                        borderRadius: '4px',
                        backgroundColor:
                          moreFiltersCount > 0 ? '#e3f2fd' : 'white',
                        color: '#666',
                        cursor: 'pointer',
                        display: 'flex',
                        alignItems: 'center',
                        gap: '4px',
                        whiteSpace: 'nowrap',
                      }}
                    >
                      {moreFiltersCount > 0
                        ? `${moreFiltersCount} active`
                        : 'Filters'}
                      <span style={{ fontSize: '8px' }}>
                        {moreFiltersOpen ? '▲' : '▼'}
                      </span>
                    </button>
                  </div>

                  {moreFiltersOpen && (
                    <div
                      style={{
                        position: 'absolute',
                        top: '100%',
                        right: 0,
                        marginTop: '4px',
                        padding: '12px',
                        backgroundColor: 'white',
                        border: '1px solid #ddd',
                        borderRadius: '8px',
                        boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
                        zIndex: 1000,
                        minWidth: '320px',
                      }}
                    >
                      <div
                        style={{
                          display: 'grid',
                          gap: '12px',
                          marginBottom: '12px',
                        }}
                      >
                        <CompactSelect
                          label="Edited By"
                          name="editedBy"
                          options={userOptions}
                          value={filters.edited_by}
                          onChange={e =>
                            updateFilters({
                              ...filters,
                              edited_by: e?.value || '',
                            })
                          }
                          isLoading={usersLoading}
                          onMenuOpen={ensureUsers}
                          flex="none"
                          minWidth="100%"
                          testId="select-user"
                        />
                      </div>
                      <div
                        style={{
                          borderTop: '1px solid #e2e8f0',
                          paddingTop: '12px',
                        }}
                      >
                        <label
                          style={{
                            display: 'flex',
                            alignItems: 'center',
                            gap: '8px',
                            fontSize: '12px',
                            color: '#444',
                            marginBottom: '8px',
                            cursor: 'pointer',
                          }}
                        >
                          <input
                            type="checkbox"
                            checked={filters.missingDescription}
                            onChange={e =>
                              updateFilters({
                                ...filters,
                                missingDescription: e.target.checked,
                              })
                            }
                          />
                          Missing Description
                        </label>
                        <label
                          style={{
                            display: 'flex',
                            alignItems: 'center',
                            gap: '8px',
                            fontSize: '12px',
                            color: '#444',
                            marginBottom: '8px',
                            cursor: 'pointer',
                          }}
                        >
                          <input
                            type="checkbox"
                            checked={filters.orphanedDimension}
                            onChange={e =>
                              updateFilters({
                                ...filters,
                                orphanedDimension: e.target.checked,
                              })
                            }
                          />
                          Orphaned Dimensions
                        </label>
                        <label
                          style={{
                            display: 'flex',
                            alignItems: 'center',
                            gap: '8px',
                            fontSize: '12px',
                            color: '#444',
                            cursor: 'pointer',
                          }}
                        >
                          <input
                            type="checkbox"
                            checked={filters.hasMaterialization}
                            onChange={e =>
                              updateFilters({
                                ...filters,
                                hasMaterialization: e.target.checked,
                              })
                            }
                          />
                          Has Materialization
                        </label>
                      </div>
                    </div>
                  )}
                </div>
              </div>
            </div>
          )}

          <NamespaceHeader
            namespace={namespace}
            onGitConfigLoaded={setGitConfig}
            onReadOnlyChange={setHeaderReadOnly}
            namespaceOptions={rawNamespaces}
            currentNamespace={namespace}
          >
            {namespace && (
              <Tooltip
                content={`Download every node in "${namespace}" as a YAML project (.zip) you can version in git and re-deploy with the DJ client.`}
              >
                <button
                  type="button"
                  onClick={async () => {
                    const response = await fetch(
                      `${getDJUrl()}/namespaces/${namespace}/export/yaml`,
                      { method: 'POST', credentials: 'include' },
                    );
                    if (!response.ok) {
                      return;
                    }
                    const blob = await response.blob();
                    const url = URL.createObjectURL(blob);
                    const link = document.createElement('a');
                    const safeName = namespace.replace(/\./g, '_');
                    link.href = url;
                    link.download = `${safeName}_export.zip`;
                    document.body.appendChild(link);
                    link.click();
                    document.body.removeChild(link);
                    URL.revokeObjectURL(url);
                  }}
                  style={secondaryButtonStyle}
                  onMouseOver={onSecondaryHover}
                  onMouseOut={onSecondaryOut}
                  aria-label="Export namespace to YAML"
                >
                  <svg
                    width="14"
                    height="14"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth="2"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  >
                    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
                    <polyline points="7 10 12 15 17 10"></polyline>
                    <line x1="12" y1="15" x2="12" y2="3"></line>
                  </svg>
                  Export YAML
                </button>
              </Tooltip>
            )}
            {showEditControls && <AddNodeDropdown namespace={namespace} />}
          </NamespaceHeader>

          <div className="table-responsive">
            {/* The rail is always present for layout consistency, even on a leaf
                namespace with no sub-namespaces (where its Folders list is empty). */}
            <div
              className="sidebar"
              style={{
                borderRight: '1px solid #e2e8f0',
                paddingRight: '1rem',
              }}
            >
              <NamespaceNav
                namespaces={rawNamespaces}
                hierarchy={namespaceHierarchy}
                currentNamespace={namespace}
                gitRoots={gitRoots}
                onSelect={value =>
                  navigate(value ? `/namespaces/${value}` : '/')
                }
                canCreateNamespace={canCreateNamespace}
                onCreateNamespace={createSubNamespace}
              />
            </div>
            <div
              style={{
                flex: 1,
                minWidth: 0,
                marginLeft: '1.5rem',
              }}
            >
              {gitConfigLoaded && (
                <input
                  type="text"
                  className="dj-node-search"
                  value={nodeSearch}
                  onChange={e => setNodeSearch(e.target.value)}
                  placeholder="Search nodes in this namespace…"
                  aria-label="Search nodes in this namespace"
                />
              )}

              {/* NODES: nodes that live directly in this namespace */}
              {!gitConfigLoaded ? null : (
                <table className="card-table table" style={{ marginBottom: 0 }}>
                  <thead>
                    <tr>
                      {fields.map(field => {
                        const fieldConfig = FIELD_CONFIG[field] || {};
                        const sortable = fieldConfig.sortable !== false;
                        const thStyle = {
                          fontFamily:
                            "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
                          fontSize: '11px',
                          fontWeight: '600',
                          textTransform: 'uppercase',
                          letterSpacing: '0.5px',
                          color: '#64748b',
                          padding: '12px 16px',
                          borderBottom: '1px solid #e2e8f0',
                          backgroundColor: 'transparent',
                        };
                        return (
                          <th key={field} style={thStyle}>
                            <Tooltip
                              content={fieldConfig.tooltip}
                              placement="top"
                            >
                              {sortable ? (
                                <button
                                  type="button"
                                  onClick={() => requestSort(field)}
                                  className={
                                    'sortable ' + getClassNamesFor(field)
                                  }
                                  style={{
                                    fontSize: 'inherit',
                                    fontWeight: 'inherit',
                                    letterSpacing: 'inherit',
                                    textTransform: 'inherit',
                                    fontFamily: 'inherit',
                                  }}
                                >
                                  {fieldConfig.label ||
                                    field.replace(/([a-z](?=[A-Z]))/g, '$1 ')}
                                </button>
                              ) : (
                                <span>
                                  {fieldConfig.label ||
                                    field.replace(/([a-z](?=[A-Z]))/g, '$1 ')}
                                </span>
                              )}
                            </Tooltip>
                          </th>
                        );
                      })}
                      {showEditControls && (
                        <th
                          style={{
                            fontFamily:
                              "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
                            fontSize: '11px',
                            fontWeight: '600',
                            textTransform: 'uppercase',
                            letterSpacing: '0.5px',
                            color: '#64748b',
                            padding: '12px 16px',
                            borderBottom: '1px solid #e2e8f0',
                            backgroundColor: 'transparent',
                          }}
                        >
                          Actions
                        </th>
                      )}
                    </tr>
                  </thead>
                  <tbody className="nodes-table-body">{nodesList}</tbody>
                  <tfoot>
                    <tr>
                      <td>
                        {retrieved && hasPrevPage ? (
                          <a
                            onClick={loadPrev}
                            className="previous round pagination"
                          >
                            ← Previous
                          </a>
                        ) : (
                          ''
                        )}
                        {retrieved && hasNextPage ? (
                          <a
                            onClick={loadNext}
                            className="next round pagination"
                          >
                            Next →
                          </a>
                        ) : (
                          ''
                        )}
                      </td>
                    </tr>
                  </tfoot>
                </table>
              )}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}
