/**
 * Pure helpers behind FileTree's rendering: sorting, visible-row flattening,
 * expansion toggling, and the virtual-window slice. Kept free of React so the
 * math is unit-testable and the component stays a thin renderer.
 */

export function parseSize(size) {
  if (!size || size === '--') return 0;
  const match = size.match(/^([\d.]+)\s*(B|KB|MB|GB|TB)?$/i);
  if (!match) return 0;
  const n = parseFloat(match[1]);
  const unit = (match[2] || 'B').toUpperCase();
  const mult = { B: 1, KB: 1024, MB: 1024 ** 2, GB: 1024 ** 3, TB: 1024 ** 4 };
  return n * (mult[unit] || 1);
}

// statusLabelOf can be expensive for collapsed folders (it rolls up the whole
// subtree), so labels are cached per node id for the duration of one sort.
export function sortNodes(nodes, sort, statusLabelOf) {
  const labels = new Map();
  const labelOf = (node) => {
    if (!labels.has(node.id)) labels.set(node.id, statusLabelOf(node));
    return labels.get(node.id);
  };
  const sortLevel = (level) => {
    const sorted = [...level].sort((a, b) => {
      let av = a[sort.column];
      let bv = b[sort.column];
      if (sort.column === 'size') {
        av = a.type === 'folder' ? -1 : parseSize(a.size);
        bv = b.type === 'folder' ? -1 : parseSize(b.size);
      }
      if (sort.column === 'status') {
        av = labelOf(a);
        bv = labelOf(b);
      }
      if (typeof av === 'string') av = av.toLowerCase();
      if (typeof bv === 'string') bv = bv.toLowerCase();
      if (av < bv) return sort.direction === 'asc' ? -1 : 1;
      if (av > bv) return sort.direction === 'asc' ? 1 : -1;
      return 0;
    });
    return sorted.map((node) =>
      node.children ? { ...node, children: sortLevel(node.children) } : node
    );
  };
  return sortLevel(nodes);
}

/** Depth-tagged flat list of the rows expansion makes visible. */
export function flattenVisible(nodes, depth = 0, rows = []) {
  for (const node of nodes) {
    rows.push({ id: node.id, node, depth });
    if (node.type === 'folder' && node.expanded && node.children?.length) {
      flattenVisible(node.children, depth + 1, rows);
    }
  }
  return rows;
}

/**
 * Immutable expand/collapse: clone only the toggled node and its ancestor
 * chain so a 100k-node tree isn't structuredClone'd per click. Node ids are
 * path-prefixed, so `${folderId}/` prefixing identifies the branch to follow
 * (root ids already end with '/').
 */
export function toggleExpanded(nodes, id) {
  let changed = false;
  const next = nodes.map((node) => {
    if (node.id === id) {
      if (node.type !== 'folder') return node;
      changed = true;
      return { ...node, expanded: !node.expanded };
    }
    if (node.type === 'folder' && node.children?.length) {
      const prefix = node.id.endsWith('/') ? node.id : `${node.id}/`;
      if (id.startsWith(prefix)) {
        const children = toggleExpanded(node.children, id);
        if (children !== node.children) {
          changed = true;
          return { ...node, children };
        }
      }
    }
    return node;
  });
  return changed ? next : nodes;
}

/**
 * Recursive expand/collapse for one folder's subtree, immutably (only the
 * ancestor chain and the target subtree are cloned). Expanding opens the
 * target too; collapsing closes only its descendants — "Collapse All
 * Children" leaves the folder you clicked open.
 */
export function setSubtreeExpanded(nodes, id, expanded) {
  const applyAll = (node) => {
    if (node.type !== 'folder') return node;
    return {
      ...node,
      expanded,
      children: node.children?.length ? node.children.map(applyAll) : node.children,
    };
  };
  const walk = (list) => {
    let listChanged = false;
    const newList = list.map((node) => {
      let newNode = node;
      if (node.id === id) {
        if (node.type === 'folder') {
          listChanged = true;
          newNode = {
            ...node,
            expanded: expanded ? true : node.expanded,
            children: node.children?.length ? node.children.map(applyAll) : node.children,
          };
        }
      } else if (node.type === 'folder' && node.children?.length) {
        const prefix = node.id.endsWith('/') ? node.id : `${node.id}/`;
        if (id.startsWith(prefix)) {
          const children = walk(node.children);
          if (children !== node.children) {
            listChanged = true;
            newNode = { ...node, children };
          }
        }
      }
      return newNode;
    });
    return listChanged ? newList : list;
  };
  return walk(nodes);
}

/** Which flat rows to actually render for the current scroll position. */
export function computeWindow(scrollTop, viewportHeight, totalRows, rowHeight, overscan) {
  const start = Math.max(0, Math.floor(scrollTop / rowHeight) - overscan);
  const end = Math.min(totalRows, Math.ceil((scrollTop + viewportHeight) / rowHeight) + overscan);
  return { start, end };
}
