import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { folderMarkedStatus, rollupFolderStatus } from '../data/statusChips';
import { computeWindow, flattenVisible, sortNodes, toggleExpanded } from '../data/treeView';
import { toggleInSelection, useListKeyboardNav } from '../hooks/useListKeyboardNav';
import { StatusIcon } from './FileDialogs';

const FILE_ICON = 'assets/file.svg';
const FOLDER_ICON = 'assets/folder.svg';
const ARROW_RIGHT = 'assets/arrow_right.svg';
const ARROW_DOWN = 'assets/arrow_down.svg';

const ROW_HEIGHT = 22; // must match .tree-node height in components.css
const OVERSCAN = 12; // extra rows above/below the viewport (absorbs the header offset)

const COLUMNS = [
  { key: 'name', label: 'Name', className: 'col-name' },
  { key: 'status', label: 'Status', className: 'col-status' },
  { key: 'size', label: 'Size', className: 'col-size' },
  { key: 'modified', label: 'Modified', className: 'col-modified' },
];

export function FileTree({
  nodes,
  setNodes,
  mode,
  selectedIds,
  anchorId,
  onSelect,
  sort,
  onSort,
  scrollTop,
  onScroll,
  visible,
  rootId,
  statuses,
  onStatusClick,
  pulsePaths,
  deletedFolders,
  onContextMenuRow,
  onContextMenuEmpty,
  onOpenFile,
  dropTargetId,
  versionFractionOf,
}) {
  const containerRef = useRef(null);
  // Drag-to-range-select, like a real file browser: press on a row and drag
  // over others to select the range. `dragged` suppresses the click that would
  // otherwise fire after the drag and collapse the selection back to one row.
  const draggingRef = useRef(false);
  const dragAnchorRef = useRef(null);
  const draggedRef = useRef(false);

  useEffect(() => {
    const onUp = () => {
      draggingRef.current = false;
      dragAnchorRef.current = null;
    };
    window.addEventListener('mouseup', onUp);
    return () => window.removeEventListener('mouseup', onUp);
  }, []);

  // Every node id is rootId + depot-relative path; statuses/pulses are keyed
  // by that relative path so both tabs share them.
  const relPathOf = useCallback(
    (node) => (rootId && node.id.startsWith(rootId) ? node.id.slice(rootId.length) : null),
    [rootId]
  );
  const folderMarked = useCallback(
    (rel) =>
      Boolean(
        rel &&
          deletedFolders?.size &&
          [...deletedFolders].some((f) => rel === f || rel.startsWith(`${f}/`))
      ),
    [deletedFolders]
  );
  const statusOf = useCallback(
    (node) => {
      if (!statuses) return null;
      const rel = relPathOf(node);
      if (node.type === 'file') {
        return rel ? (statuses.get(rel) ?? null) : null;
      }
      if (node.type === 'folder' && !node.isRoot) {
        // A folder the user marked for delete carries its own status (and the
        // strikethrough) whether open or closed…
        if (folderMarked(rel)) return folderMarkedStatus();
        // …otherwise collapsed folders surface their contents' status so
        // nothing checked out or marked can hide inside a closed folder.
        if (!node.expanded) return rollupFolderStatus(node, statuses, rootId);
      }
      return null;
    },
    [statuses, relPathOf, rootId, folderMarked]
  );

  const sortedNodes = useMemo(
    () => sortNodes(nodes, sort, (node) => statusOf(node)?.label ?? ''),
    [nodes, sort, statusOf]
  );
  const flatRows = useMemo(() => flattenVisible(sortedNodes), [sortedNodes]);
  const itemIds = useMemo(() => flatRows.map((r) => r.id), [flatRows]);

  // 600 is only a first-paint stand-in — it's replaced by the ResizeObserver
  // below as soon as the container mounts and reports its real clientHeight.
  // It only ever governs the very first render's windowing math.
  const [viewport, setViewport] = useState({ top: scrollTop ?? 0, height: 600 });

  useEffect(() => {
    const el = containerRef.current;
    if (!el) return undefined;
    const measure = () =>
      setViewport((v) => (v.height === el.clientHeight ? v : { ...v, height: el.clientHeight }));
    measure();
    const ro = new ResizeObserver(measure);
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  const handleToggleSelect = useCallback(
    (id) => {
      onSelect(toggleInSelection(selectedIds, id), id);
    },
    [onSelect, selectedIds]
  );

  const handleSelectSingle = useCallback(
    (id) => {
      onSelect(new Set([id]), id);
    },
    [onSelect]
  );

  const scrollFallback = useCallback((index) => {
    const el = containerRef.current;
    if (!el || index < 0) return;
    const top = index * ROW_HEIGHT;
    if (top < el.scrollTop) el.scrollTop = top;
    else if (top + ROW_HEIGHT > el.scrollTop + el.clientHeight)
      el.scrollTop = top - el.clientHeight + ROW_HEIGHT;
  }, []);

  const {
    containerRef: listRef,
    handleKeyDown,
    registerItemRef,
    focusedId,
    setFocusById,
  } = useListKeyboardNav({
    itemIds,
    selectedIds,
    onToggleSelect: handleToggleSelect,
    onSelectSingle: handleSelectSingle,
    enabled: visible,
    scrollFallback,
  });

  const mergeContainerRef = (el) => {
    containerRef.current = el;
    listRef.current = el;
  };

  useEffect(() => {
    if (visible && containerRef.current && scrollTop != null) {
      containerRef.current.scrollTop = scrollTop;
      setViewport((v) => ({ ...v, top: scrollTop }));
    }
  }, [visible]);

  const handleScroll = () => {
    const el = containerRef.current;
    if (!el) return;
    setViewport((v) => ({ ...v, top: el.scrollTop }));
    onScroll(el.scrollTop);
  };

  const handleSort = (column) => {
    onSort({
      column,
      direction: sort.column === column && sort.direction === 'asc' ? 'desc' : 'asc',
    });
  };

  const handleToggle = useCallback(
    (id) => {
      setNodes((prev) => toggleExpanded(prev, id));
    },
    [setNodes]
  );

  // Begin a drag-select on a plain press (no modifier, not on the toggle/status
  // buttons). Modifier clicks stay click-based (shift = range, cmd = toggle).
  const handleRowMouseDown = (e, id) => {
    if (e.button !== 0 || e.target.closest('button')) return;
    if (e.shiftKey || e.metaKey || e.ctrlKey) return;
    draggingRef.current = true;
    draggedRef.current = false;
    dragAnchorRef.current = id;
    onSelect(new Set([id]), id);
    setFocusById(id);
  };

  const handleRowMouseEnter = (id) => {
    if (!draggingRef.current || dragAnchorRef.current == null) return;
    draggedRef.current = true;
    const ids = itemIds;
    const a = ids.indexOf(dragAnchorRef.current);
    const b = ids.indexOf(id);
    if (a === -1 || b === -1) return;
    const [start, end] = a < b ? [a, b] : [b, a];
    onSelect(new Set(ids.slice(start, end + 1)), dragAnchorRef.current);
  };

  const handleRowClick = (e, id) => {
    e.preventDefault();
    // Swallow the click that trails a real drag so it doesn't collapse the
    // range back to a single row.
    if (draggedRef.current) {
      draggedRef.current = false;
      return;
    }
    const multi = e.metaKey || e.ctrlKey;
    setFocusById(id);

    if (e.shiftKey && anchorId) {
      const ids = itemIds;
      const a = ids.indexOf(anchorId);
      const b = ids.indexOf(id);
      if (a === -1 || b === -1) {
        onSelect(new Set([id]), id);
        return;
      }
      const [start, end] = a < b ? [a, b] : [b, a];
      onSelect(new Set(ids.slice(start, end + 1)), anchorId);
      return;
    }

    if (multi) {
      const next = new Set(selectedIds);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      onSelect(next, id);
      return;
    }

    onSelect(new Set([id]), id);
    setFocusById(id);
  };

  const handleRowContextMenu = (e, node) => {
    e.preventDefault();
    if (!onContextMenuRow) return;
    // Right-click outside the current selection selects that row first, like
    // every native file browser; inside a multi-selection it keeps the set.
    if (!selectedIds.has(node.id)) {
      onSelect(new Set([node.id]), node.id);
      setFocusById(node.id);
    }
    onContextMenuRow(e, node);
  };

  const selectAll = useCallback(() => {
    if (!itemIds.length) return;
    onSelect(new Set(itemIds), itemIds[0]);
  }, [itemIds, onSelect]);

  // Right-click on blank space. The row handler doesn't stop propagation, so
  // this also fires for right-clicks on rows — bail on those, and on the sort
  // header, which leaves the scroll container plus the <ul>'s virtualization
  // padding as the only "empty space".
  const handleEmptyContextMenu = (e) => {
    if (!onContextMenuEmpty) return;
    if (e.target.closest('[data-node-id]') || e.target.closest('.file-tree-header')) return;
    e.preventDefault();
    // Empty space acts on the container, not on a selection — clearing it
    // stops the menu from implying it will act on rows the user can't see.
    onSelect(new Set(), null);
    onContextMenuEmpty(e, { selectAll });
  };

  const handleRowDoubleClick = (e, node) => {
    if (e.target.closest('button')) return; // toggle arrow / status chip
    if (node.type === 'folder') {
      if (node.children?.length) handleToggle(node.id);
      return;
    }
    onOpenFile?.(node);
  };

  // Enter mirrors double-click (folder toggles, file opens); everything else
  // stays with the list-nav hook, whose own Enter handling is bypassed here.
  const handleTreeKeyDown = (e) => {
    if ((e.metaKey || e.ctrlKey) && (e.key === 'a' || e.key === 'A') && !e.shiftKey && !e.altKey) {
      e.preventDefault();
      selectAll();
      return;
    }
    if (e.key === 'Enter' && focusedId) {
      e.preventDefault();
      const row = flatRows.find((r) => r.id === focusedId);
      if (!row) return;
      if (row.node.type === 'folder') {
        if (row.node.children?.length) handleToggle(focusedId);
      } else {
        handleSelectSingle(focusedId);
        onOpenFile?.(row.node);
      }
      return;
    }
    handleKeyDown(e);
  };

  // Clamp only the value fed to computeWindow, not viewport.top itself: if
  // flatRows shrank (e.g. a filter or collapse) while scrolled near the old
  // bottom, an unclamped top would window past the end of the list and leave
  // a blank pane until the next scroll event corrects viewport.top on its
  // own.
  const clampedTop = Math.min(
    viewport.top,
    Math.max(0, flatRows.length * ROW_HEIGHT - viewport.height)
  );
  const { start, end } = computeWindow(
    clampedTop,
    viewport.height,
    flatRows.length,
    ROW_HEIGHT,
    OVERSCAN
  );
  const windowRows = flatRows.slice(start, end);

  return (
    <div
      ref={mergeContainerRef}
      className="file-tree-container"
      style={{ display: visible ? undefined : 'none' }}
      tabIndex={visible ? 0 : -1}
      onScroll={handleScroll}
      onKeyDown={handleTreeKeyDown}
      onContextMenu={handleEmptyContextMenu}
      onMouseDown={() => containerRef.current?.focus()}
    >
      <div className="file-tree-header">
        <span className="file-tree-header-ribbon" aria-hidden="true" />
        {COLUMNS.map(({ key, label, className }) => (
          <button
            key={key}
            type="button"
            className={`file-tree-header-col ${className}${sort.column === key ? ' file-tree-header-col--active' : ''}`}
            onClick={() => handleSort(key)}
          >
            {label}
            {sort.column === key && (
              <span className="sort-indicator">{sort.direction === 'asc' ? ' ▲' : ' ▼'}</span>
            )}
          </button>
        ))}
      </div>
      <ul
        className="file-tree"
        style={{
          paddingTop: start * ROW_HEIGHT,
          paddingBottom: (flatRows.length - end) * ROW_HEIGHT,
        }}
      >
        {windowRows.map((row, i) => (
          <TreeRow
            key={row.id}
            node={row.node}
            depth={row.depth}
            mode={mode}
            index={start + i}
            selectedIds={selectedIds}
            focusedId={focusedId}
            registerItemRef={registerItemRef}
            onRowClick={handleRowClick}
            onRowMouseDown={handleRowMouseDown}
            onRowMouseEnter={handleRowMouseEnter}
            onToggle={handleToggle}
            relPathOf={relPathOf}
            statusOf={statusOf}
            onStatusClick={onStatusClick}
            pulsePaths={pulsePaths}
            onRowContextMenu={handleRowContextMenu}
            onRowDoubleClick={handleRowDoubleClick}
            dropTargetId={dropTargetId}
            versionFractionOf={versionFractionOf}
          />
        ))}
      </ul>
    </div>
  );
}

function TreeRow({
  node,
  mode,
  depth,
  index,
  selectedIds,
  focusedId,
  registerItemRef,
  onRowClick,
  onRowMouseDown,
  onRowMouseEnter,
  onToggle,
  relPathOf,
  statusOf,
  onStatusClick,
  pulsePaths,
  onRowContextMenu,
  onRowDoubleClick,
  dropTargetId,
  versionFractionOf,
}) {
  const hasChildren = node.type === 'folder' && node.children?.length > 0;
  const isRepo = mode === 'repo';
  const selected = selectedIds.has(node.id);
  const focused = focusedId === node.id;
  const relPath = relPathOf(node);
  const fraction =
    node.type === 'file' && relPath && versionFractionOf ? versionFractionOf(relPath) : null;
  const status = statusOf(node);
  const pulsing = Boolean(relPath && pulsePaths?.has(relPath));
  // Marked-for-delete rows (files, and folders marked as a whole) are already
  // gone from disk; the dimmed, struck-through row just shows what was there.
  // A folder whose files are merely all marked individually is NOT struck.
  const deleted = status?.kind === 'markedForDelete';

  return (
    <li className="tree-item">
      <div
        ref={(el) => registerItemRef(node.id, el)}
        data-node-id={node.id}
        className={`tree-node${index % 2 === 1 ? ' tree-node--alt' : ''}${selected ? ' tree-node--selected' : ''}${focused ? ' tree-node--focused' : ''}${node.isRoot ? ' tree-node--root' : ''}${deleted ? ' tree-node--deleted' : ''}${dropTargetId === node.id ? ' tree-node--drop-target' : ''}`}
        onClick={(e) => onRowClick(e, node.id)}
        onMouseDown={(e) => onRowMouseDown(e, node.id)}
        onMouseEnter={() => onRowMouseEnter(node.id)}
        onContextMenu={(e) => onRowContextMenu(e, node)}
        onDoubleClick={(e) => onRowDoubleClick(e, node)}
      >
        <span className={`tree-ribbon${selected ? ' tree-ribbon--on' : ''}`} />
        <div className="tree-node-main">
          <span style={{ width: depth * 16 }} className="tree-indent" />
          {node.type === 'folder' ? (
            <button
              type="button"
              className={`tree-toggle${hasChildren ? '' : ' tree-toggle--empty'}`}
              onClick={(e) => {
                e.stopPropagation();
                if (hasChildren) onToggle(node.id);
              }}
            >
              {hasChildren ? (
                <img
                  src={node.expanded ? ARROW_DOWN : ARROW_RIGHT}
                  alt=""
                  className="tree-toggle-icon"
                />
              ) : null}
            </button>
          ) : (
            <span className="tree-toggle tree-toggle--empty" />
          )}
          <img
            src={node.type === 'folder' ? FOLDER_ICON : FILE_ICON}
            alt=""
            className={`tree-icon${node.type === 'folder' ? (isRepo ? ' tree-icon--repo' : ' tree-icon--workspace') : ''}`}
          />
          <span className={`tree-label${pulsing ? ' tree-label--pulse' : ''}${deleted ? ' tree-label--deleted' : ''}`}>{node.name}</span>
          {fraction ? (
            <span className="tree-revision-count">
              ({fraction.your}/{fraction.server})
            </span>
          ) : null}
        </div>
        {status ? (
          <button
            type="button"
            className={`tree-col-status status-chip status-chip--${status.color}`}
            title={status.title}
            onClick={(e) => {
              e.stopPropagation();
              onStatusClick?.(status, relPath);
            }}
          >
            <StatusIcon icon={status.icon} color={status.color} className="status-chip-icon" />
            <span className="status-chip-label">{status.label}</span>
          </button>
        ) : (
          <span className="tree-col-status" />
        )}
        <span className="tree-col-size">{node.size ?? '--'}</span>
        <span className="tree-col-modified">{node.modified ?? '--'}</span>
      </div>
    </li>
  );
}

export function createBrowserState(tree, rootId) {
  return {
    tree: structuredClone(tree),
    selectedIds: new Set([rootId]),
    anchorId: rootId,
    sort: { column: 'name', direction: 'asc' },
    scrollTop: 0,
  };
}
