import { useState, useEffect, useMemo } from 'react';
import {
  FolderOpen,
  Folder,
  Loader2,
  Search,
  RefreshCw,
  Download,
  ChevronRight,
  ChevronDown,
  TriangleAlert,
  Trash2,
  FileAudio,
} from 'lucide-react';
import {
  useSoulseekBrowseSnapshots,
  useSoulseekBrowseLevel,
  useSoulseekBrowseSearch,
  useSoulseekBrowseFiles,
  useSoulseekEnqueue,
} from './useSoulseekBrowse';
import { formatSize, formatWhen, type SoulseekBrowseNode, type SoulseekBrowseSnapshot } from './shared';

// A peer's shared folders, read from Officer's cache rather than browsed live, and rendered as a TREE.
//
// The live slskd call returns the entire tree in one blocking response (59 MB / 18k folders / 284k files
// for a real peer) and takes minutes, because it round-trips to that peer. So nothing here triggers it
// inline: you ask the sidecar to fetch, it keeps going without the tab, and this reads back levels.
//
// slskd's response is flat — every folder is a full backslash-delimited path — which made the old list
// unreadable: 26 rows all beginning with the same 31 characters. The sidecar links the paths into a tree
// at ingest, so a row here shows one segment and one level loads at a time.
//
// Filtering never flattens that. The search route returns matches WITH their ancestors, and this renders
// that skeleton pre-expanded: you see where a hit lives, not just that it exists. Because matching runs
// against the whole path, a folder matching implies all its descendants match too — so a matched subtree
// arrives complete.

// One level's page size. Fan-out is not gentle: the widest folder measured on a real peer has 1,181
// children, so a level can ask for more (up to the route's cap) rather than paging with Previous/Next —
// there's nothing to page through once you're inside one folder.
const LEVEL_LIMIT = 200;
const LEVEL_LIMIT_MAX = 2000;

/** Debounce the filter box so a keystroke doesn't become a request. */
function useDebounced<T>(value: T, ms: number): T {
  const [held, setHeld] = useState(value);
  useEffect(() => {
    const timer = setTimeout(() => setHeld(value), ms);
    return () => clearTimeout(timer);
  }, [value, ms]);
  return held;
}

/** Cache state for one peer, compact enough for a favourites row. */
export const BrowseStateChip = ({ snapshot }: { snapshot: SoulseekBrowseSnapshot | null }) => {
  if (!snapshot) return <span className="shrink-0 text-xs text-zinc-600">not cached</span>;
  if (snapshot.status === 'pending')
    return (
      <span className="flex shrink-0 items-center gap-1 text-xs text-primary">
        <Loader2 className="h-3 w-3 animate-spin" />
        fetching
      </span>
    );
  if (snapshot.status === 'failed' && !snapshot.directoryCount)
    return (
      <span className="flex shrink-0 items-center gap-1 text-xs text-red-400" title={snapshot.error ?? undefined}>
        <TriangleAlert className="h-3 w-3" />
        failed
      </span>
    );
  return (
    <span className="shrink-0 text-xs tabular-nums text-zinc-500">
      {snapshot.directoryCount.toLocaleString()} folders
    </span>
  );
};

/** Queue-this button, shown on row hover. Kept out of the row's own button — nesting the two is invalid. */
type QueueButtonProps = { onQueue: () => void; busy: boolean; title: string };

const QueueButton = ({ onQueue, busy, title }: QueueButtonProps) => (
  <button
    type="button"
    onClick={onQueue}
    disabled={busy}
    title={title}
    className="mr-3 flex h-6 w-6 shrink-0 items-center justify-center rounded text-zinc-600 opacity-0 transition hover:bg-white/10 hover:text-primary focus:opacity-100 disabled:opacity-40 group-hover:opacity-100"
  >
    {busy ? <Loader2 className="h-3 w-3 animate-spin" /> : <Download className="h-3 w-3" />}
  </button>
);

/** The filtered tree, indexed for rendering: children by parent path, plus which rows actually matched. */
type Filtered = { childrenOf: Map<string, SoulseekBrowseNode[]>; matched: Set<string> };

export const SharesBrowser = ({ username }: { username: string }) => {
  const { snapshotOf, fetchShares, dropShares } = useSoulseekBrowseSnapshots();
  const [filter, setFilter] = useState('');
  const q = useDebounced(filter.trim(), 250);

  const snapshot = snapshotOf(username);
  // Folder rows survive a failed or in-flight refresh, so drive the tree off the data existing rather
  // than off the status — a stale tree is still worth browsing while a new one is being fetched.
  const hasCache = !!snapshot && snapshot.directoryCount > 0;

  const roots = useSoulseekBrowseLevel({ username, parent: null, limit: LEVEL_LIMIT, enabled: hasCache && !q });
  const search = useSoulseekBrowseSearch({ username, q, enabled: hasCache });

  useEffect(() => {
    setFilter('');
  }, [username]);

  const filtered = useMemo<Filtered | null>(() => {
    if (!q || !search.data) return null;
    const childrenOf = new Map<string, SoulseekBrowseNode[]>();
    for (const node of search.data.nodes) {
      if (!node.parentPath) continue;
      const siblings = childrenOf.get(node.parentPath);
      if (siblings) siblings.push(node);
      else childrenOf.set(node.parentPath, [node]);
    }
    return { childrenOf, matched: new Set(search.data.matched) };
  }, [q, search.data]);

  const topNodes = q ? (search.data?.nodes.filter((n) => !n.parentPath) ?? []) : (roots.data?.nodes ?? []);
  const pending = snapshot?.status === 'pending';
  const busy = q ? search.isFetching : roots.isFetching;

  return (
    <div className="rounded-xl border border-white/10 bg-zinc-950">
      <div className="flex flex-wrap items-center gap-2 border-b border-white/10 px-4 py-2.5">
        <FolderOpen className="h-4 w-4 shrink-0 text-zinc-400" />
        <span className="text-sm font-medium text-zinc-100">Shared folders</span>
        {snapshot && !!snapshot.directoryCount && (
          <span className="text-xs tabular-nums text-zinc-500">
            · {snapshot.directoryCount.toLocaleString()} folders · {snapshot.fileCount.toLocaleString()} files ·{' '}
            {formatSize(snapshot.totalSize)}
          </span>
        )}
        <div className="ml-auto flex items-center gap-1">
          <button
            type="button"
            onClick={() => fetchShares(username)}
            disabled={pending}
            title={hasCache ? 'Fetch again in the background' : 'Fetch this share tree in the background'}
            className="flex h-7 items-center gap-1.5 rounded-md px-2 text-xs font-medium text-zinc-300 transition hover:bg-white/10 hover:text-zinc-100 disabled:opacity-50"
          >
            {pending ? (
              <Loader2 className="h-3.5 w-3.5 animate-spin" />
            ) : hasCache ? (
              <RefreshCw className="h-3.5 w-3.5" />
            ) : (
              <Download className="h-3.5 w-3.5" />
            )}
            {pending ? 'Fetching…' : hasCache ? 'Refresh' : 'Fetch shares'}
          </button>
          {hasCache && (
            <button
              type="button"
              onClick={() => dropShares(username)}
              title="Drop this cached share tree"
              className="flex h-7 w-7 items-center justify-center rounded-md text-zinc-500 transition hover:bg-red-500/10 hover:text-red-400"
            >
              <Trash2 className="h-3.5 w-3.5" />
            </button>
          )}
        </div>
      </div>

      {/* Status banners — a fetch runs server-side, so leaving the panel is safe. */}
      {pending && (
        <div className="flex items-center gap-2 border-b border-white/10 bg-primary/5 px-4 py-2 text-xs text-zinc-300">
          <Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin text-primary" />
          Fetching this peer's shares in the background — it can take a few minutes for a large share, and it keeps
          going if you navigate away.
        </div>
      )}
      {snapshot?.status === 'failed' && (
        <div className="flex items-start gap-2 border-b border-white/10 bg-red-500/5 px-4 py-2 text-xs text-red-300">
          <TriangleAlert className="mt-0.5 h-3.5 w-3.5 shrink-0" />
          <span>
            Last fetch failed{snapshot.error ? `: ${snapshot.error}` : ''}.{' '}
            {hasCache && 'Showing the previously cached tree.'}
          </span>
        </div>
      )}

      {!hasCache && !pending ? (
        <div className="flex flex-col items-center gap-2 px-4 py-10 text-center">
          <Folder className="h-8 w-8 text-zinc-700" />
          <p className="max-w-sm text-sm text-zinc-500">
            No cached shares for this peer yet. Fetching runs on the server and survives you closing the tab — come back
            and it'll be here.
          </p>
        </div>
      ) : (
        hasCache && (
          <>
            <div className="flex items-center gap-2 border-b border-white/10 px-4 py-2">
              <Search className="h-3.5 w-3.5 shrink-0 text-zinc-500" />
              <input
                value={filter}
                onChange={(ev) => setFilter(ev.target.value)}
                placeholder="Filter folders…"
                className="h-7 min-w-0 flex-1 bg-transparent text-sm text-zinc-100 outline-none placeholder:text-zinc-600"
              />
              {busy && <Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin text-zinc-500" />}
              {q && (
                <span className="shrink-0 text-xs tabular-nums text-zinc-500">
                  {(search.data?.total ?? 0).toLocaleString()} matching
                </span>
              )}
            </div>

            {/* Never a silent cap: a truncated search still renders a valid tree, just not all of it. */}
            {search.data?.truncated && q && (
              <div className="border-b border-white/10 bg-amber-500/5 px-4 py-1.5 text-xs text-amber-300/80">
                Showing the first {search.data.matched.length.toLocaleString()} of {search.data.total.toLocaleString()}{' '}
                matches — narrow the filter to see the rest.
              </div>
            )}

            {q && search.data && search.data.total === 0 ? (
              <p className="px-4 py-6 text-center text-sm text-zinc-500">No folders match “{q}”.</p>
            ) : (
              // Keyed by the term so switching between browsing and a filtered view remounts the tree:
              // every row's open state is local, and reusing instances across modes would strand it.
              <div key={q} className="max-h-[32rem] overflow-y-auto py-1">
                {topNodes.map((node) => (
                  <TreeNode
                    key={node.id}
                    username={username}
                    node={node}
                    filtered={filtered}
                    indent={0}
                    initialOpen={!!filtered || topNodes.length === 1}
                  />
                ))}
              </div>
            )}

            {snapshot?.completedAt && (
              <div className="border-t border-white/5 px-4 py-1.5 text-right text-xs text-zinc-600">
                cached {formatWhen(snapshot.completedAt)}
              </div>
            )}
          </>
        )
      )}
    </div>
  );
};

type TreeNodeProps = {
  username: string;
  node: SoulseekBrowseNode;
  /** Non-null in filter mode: children come from this set, not from a request, and rows start open. */
  filtered: Filtered | null;
  indent: number;
  initialOpen: boolean;
};

const TreeNode = ({ username, node, filtered, indent, initialOpen }: TreeNodeProps) => {
  const [limit, setLimit] = useState(LEVEL_LIMIT);
  const [revealed, setRevealed] = useState(false);
  const enqueue = useSoulseekEnqueue(username);

  // Filtering stops at a revealed row: from here down it's ordinary lazy browsing, so opening a matched
  // folder shows what's really inside it rather than the filtered skeleton — which is why you searched.
  const scoped = revealed ? null : filtered;
  const scopedChildren = scoped?.childrenOf.get(node.name) ?? [];

  // A filtered row starts open only when the filter matched something below it. One that matched on its own
  // name has nothing of its own to show, so it stays shut and behaves like any other collapsed folder.
  const [open, setOpen] = useState(() => (filtered ? scopedChildren.length > 0 : initialOpen));

  const expandable = node.childCount > 0 || node.fileCount > 0;
  const level = useSoulseekBrowseLevel({
    username,
    parent: node.name,
    limit,
    enabled: !scoped && open && node.childCount > 0,
  });

  const children = scoped ? scopedChildren : (level.data?.nodes ?? []);
  // In the filtered skeleton no level was fetched, so the row's own count is what says how many exist.
  const total = scoped ? node.childCount : (level.data?.total ?? node.childCount);
  const capped = !scoped && limit >= LEVEL_LIMIT_MAX;
  // Not while a level is in flight: `total` falls back to the row's own count then, so every loading row
  // would flash a "show all" button for children it's already fetching.
  const more = open && total > children.length && (!!scoped || !level.isPending);
  const isMatch = filtered?.matched.has(node.name) ?? false;

  const toggle = () => {
    // Opening a matched row with nothing matched below it leaves the filter behind, rather than opening
    // into an empty folder.
    if (!open && scoped && !scopedChildren.length) setRevealed(true);
    setOpen((v) => !v);
  };

  return (
    <div>
      <div className="group flex items-center transition-colors hover:bg-white/[0.03]">
        <button
          type="button"
          onClick={() => expandable && toggle()}
          style={{ paddingLeft: 12 + indent * 14 }}
          className={`flex min-w-0 flex-1 items-center gap-1.5 py-1.5 text-left text-sm ${
            expandable ? '' : 'cursor-default'
          }`}
        >
          {expandable ? (
            open ? (
              <ChevronDown className="h-3.5 w-3.5 shrink-0 text-zinc-500" />
            ) : (
              <ChevronRight className="h-3.5 w-3.5 shrink-0 text-zinc-600" />
            )
          ) : (
            <span className="h-3.5 w-3.5 shrink-0" />
          )}
          {open ? (
            <FolderOpen className="h-3.5 w-3.5 shrink-0 text-zinc-500" />
          ) : (
            <Folder className="h-3.5 w-3.5 shrink-0 text-zinc-600" />
          )}
          <span
            className={`min-w-0 flex-1 truncate ${isMatch ? 'font-medium text-primary' : 'text-zinc-200'}`}
            title={node.name}
          >
            {node.label}
          </span>
          <span className="shrink-0 text-xs tabular-nums text-zinc-500">
            {node.childCount > 0 && `${node.childCount.toLocaleString()} folders · `}
            {node.subtreeFileCount.toLocaleString()} files · {formatSize(node.subtreeSize)}
          </span>
        </button>
        {node.subtreeFileCount > 0 && (
          <QueueButton
            onQueue={() => enqueue.mutate({ path: node.name })}
            busy={enqueue.isPending}
            title={`Queue all ${node.subtreeFileCount.toLocaleString()} files below ${node.label}`}
          />
        )}
      </div>

      {open && (
        <>
          {!scoped && level.isPending && node.childCount > 0 && (
            <p
              style={{ paddingLeft: 12 + (indent + 1) * 14 }}
              className="flex items-center gap-1.5 py-1 text-xs text-zinc-500"
            >
              <Loader2 className="h-3 w-3 animate-spin" /> Loading folders…
            </p>
          )}
          {children.map((child) => (
            <TreeNode
              key={child.id}
              username={username}
              node={child}
              filtered={scoped}
              indent={indent + 1}
              initialOpen={!!scoped}
            />
          ))}
          {/* One button for both modes: in the filtered skeleton it drops this row out of the filter, in
              browse mode it widens the level. Wide levels load in one go — there's nothing to page through. */}
          {more && !capped && (
            <button
              type="button"
              onClick={() => (scoped ? setRevealed(true) : setLimit(Math.min(total, LEVEL_LIMIT_MAX)))}
              style={{ paddingLeft: 12 + (indent + 1) * 14 }}
              className="py-1 text-xs text-primary/80 transition hover:text-primary"
            >
              Show all {total.toLocaleString()} folders
            </button>
          )}
          {more && capped && (
            <p style={{ paddingLeft: 12 + (indent + 1) * 14 }} className="py-1 text-xs text-zinc-600">
              Showing {children.length.toLocaleString()} of {total.toLocaleString()} folders.
            </p>
          )}
          {node.fileCount > 0 && <DirFiles username={username} dirId={node.id} path={node.name} indent={indent + 1} />}
        </>
      )}
    </div>
  );
};

type DirFilesProps = { username: string; dirId: number; path: string; indent: number };

const DirFiles = ({ username, dirId, path, indent }: DirFilesProps) => {
  const { data, isPending, error } = useSoulseekBrowseFiles(username, dirId);
  const enqueue = useSoulseekEnqueue(username);
  const pad = { paddingLeft: 12 + indent * 14 };

  if (isPending)
    return (
      <p style={pad} className="flex items-center gap-1.5 py-1 text-xs text-zinc-500">
        <Loader2 className="h-3 w-3 animate-spin" /> Loading files…
      </p>
    );
  if (error)
    return (
      <p style={pad} className="py-1 text-xs text-red-400">
        Could not load this folder.
      </p>
    );
  if (!data?.length) return null;

  return (
    <div className="max-h-72 overflow-y-auto">
      {data.map((file) => (
        <div
          key={file.name}
          style={pad}
          className="group flex items-center gap-1.5 py-1 text-xs transition-colors hover:bg-white/[0.03]"
        >
          <span className="h-3.5 w-3.5 shrink-0" />
          <FileAudio className="h-3 w-3 shrink-0 text-zinc-600" />
          <span className="min-w-0 flex-1 truncate text-zinc-400" title={file.name}>
            {file.name}
          </span>
          <span className="shrink-0 tabular-nums text-zinc-500">{formatSize(file.size)}</span>
          <QueueButton
            onQueue={() => enqueue.mutate({ path, file: file.name })}
            busy={enqueue.isPending}
            title={`Queue ${file.name}`}
          />
        </div>
      ))}
    </div>
  );
};
