import type { ReactNode } from 'react';
import { useState, useEffect, useCallback, useMemo } from 'react';
import { toast } from 'sonner';
import { useClient } from 'hooks/useClient';
import { usePanelChannel } from 'hooks/usePanelChannel';
import { RefreshCw, X, Radio, Folder, RotateCcw, Ban, Trash2 } from 'lucide-react';
import { Card, CardHeader, CardBody, SubCard, SubCardHeader, RowList } from './Cards';
import { UserMenu } from './UserMenu';
import {
  SLSKD_REFRESH_CHANNEL,
  basename,
  folderLabel,
  formatSize,
  formatSpeed,
  transferPhase,
  type SlskdApplication,
  type SlskdDownloadUser,
  type SlskdTransfer,
  type TransferPhase,
} from './shared';

// Right panel of the /soulseek workspace — the connection status strip plus the live download queue.
// Downloads are polled from slskd (GET /transfers/downloads, nested user → directories → files) and
// presented the same way as search results: a collapsible card per user, files grouped by folder.
// Enqueuing from the search panel bumps the refresh channel so the list updates without waiting for the
// next poll tick.

const POLL_MS = 1500;

type Row = SlskdTransfer & { phase: TransferPhase };
type DlDir = { directory: string; label: string; files: Row[]; size: number };
type DlUser = { username: string; dirs: DlDir[]; counts: Record<TransferPhase, number>; total: number };

const PHASE_ORDER: Record<TransferPhase, number> = { downloading: 0, queued: 1, failed: 2, done: 3 };
const PHASES: TransferPhase[] = ['downloading', 'queued', 'failed', 'done'];

const phaseStyle: Record<TransferPhase, { label: string; dot: string; bar: string }> = {
  downloading: { label: 'Downloading', dot: 'bg-blue-500', bar: 'bg-blue-500' },
  queued: { label: 'Queued', dot: 'bg-amber-500', bar: 'bg-amber-400' },
  done: { label: 'Done', dot: 'bg-green-500', bar: 'bg-green-500' },
  failed: { label: 'Failed', dot: 'bg-red-500', bar: 'bg-red-500' },
};

const isRemovable = (phase: TransferPhase) => phase === 'done' || phase === 'failed';

// Group raw download users → dirs → files, keeping only files whose phase passes `keep`. Users/dirs
// that end up empty are dropped, so passing a done/not-done predicate splits the queue into sections.
const group = (users: SlskdDownloadUser[], keep: (phase: TransferPhase) => boolean): DlUser[] => {
  const out: DlUser[] = [];
  for (const user of users) {
    const dirs: DlDir[] = [];
    for (const dir of user.directories ?? []) {
      const files: Row[] = (dir.files ?? [])
        .map((f) => ({ ...f, phase: transferPhase(f.state) }))
        .filter((f) => keep(f.phase));
      if (files.length === 0) continue;
      files.sort(
        (a, b) =>
          PHASE_ORDER[a.phase] - PHASE_ORDER[b.phase] || basename(a.filename).localeCompare(basename(b.filename)),
      );
      dirs.push({
        directory: dir.directory,
        label: folderLabel(dir.directory),
        files,
        size: files.reduce((n, f) => n + f.size, 0),
      });
    }
    if (dirs.length === 0) continue;
    dirs.sort((a, b) => a.directory.localeCompare(b.directory));
    const counts: Record<TransferPhase, number> = { downloading: 0, queued: 0, failed: 0, done: 0 };
    let total = 0;
    for (const dir of dirs) for (const f of dir.files) (counts[f.phase]++, total++);
    out.push({ username: user.username, dirs, counts, total });
  }
  // Active users first, then alphabetical.
  out.sort(
    (a, b) =>
      Number(b.counts.downloading > 0) - Number(a.counts.downloading > 0) || a.username.localeCompare(b.username),
  );
  return out;
};

export const SoulseekTransfers = () => {
  const client = useClient();
  const [refresh] = usePanelChannel<number>(SLSKD_REFRESH_CHANNEL, 0);

  const [app, setApp] = useState<SlskdApplication | null>(null);
  const [statusError, setStatusError] = useState<string | null>(null);
  const [statusLoading, setStatusLoading] = useState(true);
  const [data, setData] = useState<SlskdDownloadUser[]>([]);
  // Users are open by default; collapsing a live-updating list is opt-in so new users appear expanded.
  const [collapsed, setCollapsed] = useState<Set<string>>(new Set());

  // Two sections: everything still in flight (downloading / queued / failed) on top, finished
  // (succeeded) collected at the bottom. A user with a mix appears in both.
  const active = useMemo(() => group(data, (p) => p !== 'done'), [data]);
  const completed = useMemo(() => group(data, (p) => p === 'done'), [data]);
  const doneCount = useMemo(() => completed.reduce((n, u) => n + u.total, 0), [completed]);

  // Queue-wide phase tallies drive the bulk toolbar. Cancelled transfers collapse to `failed`, so the
  // three actions target disjoint sets: retry → failed, cancel → in-flight, remove → done.
  const totals = useMemo(() => {
    const t: Record<TransferPhase, number> = { downloading: 0, queued: 0, failed: 0, done: 0 };
    for (const u of data)
      for (const d of u.directories ?? []) for (const f of d.files ?? []) t[transferPhase(f.state)]++;
    return t;
  }, [data]);
  const inFlight = totals.downloading + totals.queued;

  const loadStatus = useCallback(() => {
    setStatusLoading(true);
    setStatusError(null);
    client
      .get<SlskdApplication>('/slskd/api/v0/application')
      .then((d) => setApp(d))
      .catch((err) => setStatusError(err instanceof Error ? err.message : String(err)))
      .finally(() => setStatusLoading(false));
    // useClient() is a fresh object each render — keep this callback stable.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  useEffect(() => {
    loadStatus();
  }, [loadStatus]);

  // Poll the download queue; also refetch immediately when the search panel bumps the refresh nonce.
  useEffect(() => {
    let cancelled = false;
    const tick = () =>
      client
        .get<SlskdDownloadUser[]>('/slskd/api/v0/transfers/downloads')
        .then((u) => !cancelled && setData(u))
        .catch(() => {});
    tick();
    const timer = setInterval(tick, POLL_MS);
    return () => {
      cancelled = true;
      clearInterval(timer);
    };
    // useClient() is a fresh object each render — poll on the refresh nonce only.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [refresh]);

  // Collapse keys are section-qualified ("a:<user>" / "c:<user>") so the same peer can be open in the
  // active section and collapsed in completed independently.
  const toggle = (key: string) =>
    setCollapsed((prev) => {
      const next = new Set(prev);
      if (next.has(key)) next.delete(key);
      else next.add(key);
      return next;
    });

  const dropIds = (ids: Set<string>) =>
    setData((prev) =>
      prev.map((u) => ({
        ...u,
        directories: (u.directories ?? []).map((d) => ({ ...d, files: (d.files ?? []).filter((f) => !ids.has(f.id)) })),
      })),
    );

  // For an active transfer this cancels it; for a finished/failed one it clears it from the queue.
  const remove = async (row: Row) => {
    const done = isRemovable(row.phase);
    try {
      await client.delete(
        `/slskd/api/v0/transfers/downloads/${encodeURIComponent(row.username)}/${row.id}${done ? '?remove=true' : ''}`,
      );
      dropIds(new Set([row.id]));
    } catch {
      /* next poll reconciles */
    }
  };

  const clearCompleted = async (user: DlUser) => {
    const finished = user.dirs.flatMap((d) => d.files).filter((f) => isRemovable(f.phase));
    if (finished.length === 0) return;
    await Promise.allSettled(
      finished.map((f) =>
        client.delete(`/slskd/api/v0/transfers/downloads/${encodeURIComponent(user.username)}/${f.id}?remove=true`),
      ),
    );
    dropIds(new Set(finished.map((f) => f.id)));
  };

  const clearAllCompleted = async () => {
    const finished = completed.flatMap((u) =>
      u.dirs.flatMap((d) => d.files.map((f) => ({ username: u.username, id: f.id }))),
    );
    if (finished.length === 0) return;
    await Promise.allSettled(
      finished.map((f) =>
        client.delete(`/slskd/api/v0/transfers/downloads/${encodeURIComponent(f.username)}/${f.id}?remove=true`),
      ),
    );
    dropIds(new Set(finished.map((f) => f.id)));
  };

  // Cancel every in-flight transfer (no ?remove — they become cancelled/failed and the poll reconciles).
  const cancelAll = async () => {
    const targets: { username: string; id: string }[] = [];
    for (const u of data)
      for (const d of u.directories ?? [])
        for (const f of d.files ?? []) {
          const p = transferPhase(f.state);
          if (p === 'downloading' || p === 'queued') targets.push({ username: u.username, id: f.id });
        }
    if (targets.length === 0) return;
    await Promise.allSettled(
      targets.map((t) => client.delete(`/slskd/api/v0/transfers/downloads/${encodeURIComponent(t.username)}/${t.id}`)),
    );
  };

  // Re-enqueue every errored/cancelled transfer, grouped into one POST per peer.
  const retryErrored = async () => {
    const byUser = new Map<string, { filename: string; size: number }[]>();
    for (const u of data)
      for (const d of u.directories ?? [])
        for (const f of d.files ?? [])
          if (transferPhase(f.state) === 'failed') {
            const files = byUser.get(u.username) ?? [];
            files.push({ filename: f.filename, size: f.size });
            byUser.set(u.username, files);
          }
    if (byUser.size === 0) return;
    const n = [...byUser.values()].reduce((s, a) => s + a.length, 0);
    try {
      await Promise.all(
        [...byUser].map(([username, files]) =>
          client.post(`/slskd/api/v0/transfers/downloads/${encodeURIComponent(username)}`, files),
        ),
      );
      toast.success(n === 1 ? 'Retrying 1 download' : `Retrying ${n} downloads`);
    } catch (err) {
      toast.error(`Retry failed: ${err instanceof Error ? err.message : String(err)}`);
    }
  };

  // Force slskd to re-request this download's place in the peer's queue, then patch the fresh value in.
  const refreshPosition = async (row: Row) => {
    try {
      const t = await client.get<SlskdTransfer>(
        `/slskd/api/v0/transfers/downloads/${encodeURIComponent(row.username)}/${row.id}/position`,
      );
      setData((prev) =>
        prev.map((u) =>
          u.username !== row.username
            ? u
            : {
                ...u,
                directories: (u.directories ?? []).map((d) => ({
                  ...d,
                  files: (d.files ?? []).map((f) => (f.id === row.id ? { ...f, placeInQueue: t.placeInQueue } : f)),
                })),
              },
        ),
      );
      toast.success(t.placeInQueue ? `Place in queue: #${t.placeInQueue}` : 'Queue position refreshed');
    } catch (err) {
      toast.error(`Couldn't fetch position: ${err instanceof Error ? err.message : String(err)}`);
    }
  };

  const server = app?.server;
  const connected = server?.isConnected ?? false;

  return (
    <div className="flex h-full flex-col overflow-hidden">
      <div className="flex shrink-0 items-center justify-between border-b p-3">
        <div className="flex min-w-0 items-center gap-2 text-sm">
          <Radio className="h-4 w-4 shrink-0 text-muted-foreground" />
          {statusLoading && !app ? (
            <span className="text-muted-foreground">Checking slskd…</span>
          ) : statusError ? (
            <span className="truncate text-red-500">slskd unreachable: {statusError}</span>
          ) : (
            <>
              <span
                className={`inline-block h-2 w-2 shrink-0 rounded-full ${connected ? 'bg-green-500' : 'bg-red-500'}`}
              />
              <span className="truncate">{server?.state ?? 'unknown'}</span>
              {server?.address && <span className="truncate text-muted-foreground">· {server.address}</span>}
              {app?.version?.current && (
                <span className="shrink-0 text-muted-foreground">· v{app.version.current}</span>
              )}
            </>
          )}
        </div>
        <button
          type="button"
          onClick={loadStatus}
          title="Refresh status"
          className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md border text-muted-foreground hover:bg-accent hover:text-foreground"
        >
          <RefreshCw className={`h-3.5 w-3.5 ${statusLoading ? 'animate-spin' : ''}`} />
        </button>
      </div>

      {(active.length > 0 || completed.length > 0) && (
        <div className="flex shrink-0 flex-wrap items-center gap-2 border-b px-3 py-2">
          <BulkButton
            icon={<RotateCcw className="h-3.5 w-3.5" />}
            label="Retry errored"
            count={totals.failed}
            onClick={retryErrored}
          />
          <BulkButton icon={<Ban className="h-3.5 w-3.5" />} label="Cancel all" count={inFlight} onClick={cancelAll} />
          <BulkButton
            icon={<Trash2 className="h-3.5 w-3.5" />}
            label="Remove completed"
            count={totals.done}
            onClick={clearAllCompleted}
          />
        </div>
      )}

      <div className="min-h-0 flex-1 overflow-y-auto p-3">
        {active.length === 0 && completed.length === 0 ? (
          <p className="text-sm text-muted-foreground">No downloads yet. Search and queue a file to see it here.</p>
        ) : (
          <div className="flex flex-col gap-4">
            {active.length > 0 && (
              <section className="flex flex-col gap-2">
                <div className="px-0.5 text-xs font-medium uppercase tracking-wide text-zinc-500">
                  Active ({active.length})
                </div>
                {active.map((user) => (
                  <UserCard
                    key={user.username}
                    user={user}
                    open={!collapsed.has(`a:${user.username}`)}
                    onToggle={() => toggle(`a:${user.username}`)}
                    onRemove={remove}
                    onPosition={refreshPosition}
                    onClearCompleted={() => clearCompleted(user)}
                  />
                ))}
              </section>
            )}
            {completed.length > 0 && (
              <section className="flex flex-col gap-2">
                <div className="flex items-center justify-between px-0.5">
                  <div className="text-xs font-medium uppercase tracking-wide text-zinc-500">
                    Completed ({doneCount})
                  </div>
                  <button
                    type="button"
                    onClick={clearAllCompleted}
                    className="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-xs text-zinc-500 transition hover:bg-white/5 hover:text-zinc-100"
                  >
                    <X className="h-3 w-3" /> Clear all
                  </button>
                </div>
                {completed.map((user) => (
                  <UserCard
                    key={user.username}
                    user={user}
                    open={!collapsed.has(`c:${user.username}`)}
                    onToggle={() => toggle(`c:${user.username}`)}
                    onRemove={remove}
                    onPosition={refreshPosition}
                    onClearCompleted={() => clearCompleted(user)}
                  />
                ))}
              </section>
            )}
          </div>
        )}
      </div>
    </div>
  );
};

type BulkButtonProps = { icon: ReactNode; label: string; count: number; onClick: () => void };

const BulkButton = ({ icon, label, count, onClick }: BulkButtonProps) => (
  <button
    type="button"
    onClick={onClick}
    disabled={count === 0}
    className="inline-flex items-center gap-1.5 rounded-md border border-white/10 bg-white/[0.02] px-2.5 py-1 text-xs text-zinc-300 transition hover:bg-white/5 hover:text-zinc-100 disabled:cursor-default disabled:opacity-40 disabled:hover:bg-white/[0.02] disabled:hover:text-zinc-300"
  >
    {icon}
    <span>{label}</span>
    {count > 0 && <span className="rounded bg-white/10 px-1 tabular-nums text-zinc-400">{count}</span>}
  </button>
);

type UserCardProps = {
  user: DlUser;
  open: boolean;
  onToggle: () => void;
  onRemove: (row: Row) => void;
  onPosition: (row: Row) => void;
  onClearCompleted: () => void;
};

const UserCard = ({ user, open, onToggle, onRemove, onPosition, onClearCompleted }: UserCardProps) => {
  const finished = user.counts.done + user.counts.failed;
  const meta = PHASES.filter((p) => user.counts[p] > 0).map((p) => (
    <span key={p} className="inline-flex items-center gap-1" title={phaseStyle[p].label}>
      <span className={`h-2 w-2 rounded-full ${phaseStyle[p].dot}`} />
      {user.counts[p]}
    </span>
  ));
  return (
    <Card>
      <CardHeader open={open} onToggle={onToggle} titleMenu={<UserMenu username={user.username} />} meta={meta} />
      {open && (
        <CardBody>
          {user.dirs.map((dir) => (
            <FolderBlock key={dir.directory} dir={dir} onRemove={onRemove} onPosition={onPosition} />
          ))}
          {finished > 0 && (
            <button
              type="button"
              onClick={onClearCompleted}
              className="flex items-center justify-center gap-1.5 rounded-lg border border-white/10 bg-white/[0.02] py-1.5 text-xs text-zinc-300 transition hover:bg-white/5 hover:text-zinc-100"
            >
              <X className="h-3.5 w-3.5" /> Clear completed ({finished})
            </button>
          )}
        </CardBody>
      )}
    </Card>
  );
};

type FolderBlockProps = { dir: DlDir; onRemove: (row: Row) => void; onPosition: (row: Row) => void };

const FolderBlock = ({ dir, onRemove, onPosition }: FolderBlockProps) => {
  const [open, setOpen] = useState(true);
  return (
    <SubCard>
      <SubCardHeader
        open={open}
        onToggle={() => setOpen((v) => !v)}
        icon={<Folder className="h-3.5 w-3.5 shrink-0 text-zinc-500" />}
        label={dir.label}
        title={dir.directory}
        meta={`${dir.files.length} · ${formatSize(dir.size)}`}
      />
      {open && (
        <RowList>
          {dir.files.map((row) => {
            const style = phaseStyle[row.phase];
            const pct = row.phase === 'done' ? 100 : Math.max(0, Math.min(100, Math.round(row.percentComplete)));
            const speed = row.phase === 'downloading' ? formatSpeed(row.averageSpeed) : '';
            return (
              <div key={row.id} className="px-3 py-2 pl-8 text-sm text-zinc-200">
                <div className="flex items-center gap-2">
                  <span className={`h-2 w-2 shrink-0 rounded-full ${style.dot}`} />
                  <span className="min-w-0 flex-1 truncate">{basename(row.filename)}</span>
                  <button
                    type="button"
                    onClick={() => onRemove(row)}
                    title={isRemovable(row.phase) ? 'Clear' : 'Cancel'}
                    className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-zinc-400 hover:bg-white/10 hover:text-zinc-100"
                  >
                    <X className="h-3.5 w-3.5" />
                  </button>
                </div>
                <div className="mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-white/10">
                  <div className={`h-full ${style.bar}`} style={{ width: `${pct}%` }} />
                </div>
                <div className="mt-1 flex flex-wrap items-center gap-x-2 text-xs text-zinc-500">
                  {row.phase === 'queued' ? (
                    <button
                      type="button"
                      onClick={() => onPosition(row)}
                      title={
                        row.placeInQueue
                          ? `Place in queue: #${row.placeInQueue} · click to refresh`
                          : 'Queued · click to fetch place in queue'
                      }
                      className="inline-flex items-center gap-1 rounded text-amber-400/90 transition hover:text-amber-300"
                    >
                      <RefreshCw className="h-3 w-3" />
                      {row.placeInQueue ? `#${row.placeInQueue} in queue` : 'Queued'}
                    </button>
                  ) : (
                    <span>{style.label}</span>
                  )}
                  {row.size > 0 && <span>· {formatSize(row.size)}</span>}
                  {speed && <span>· {speed}</span>}
                </div>
              </div>
            );
          })}
        </RowList>
      )}
    </SubCard>
  );
};
