// Shared types/helpers for the /soulseek workspace panels, which talk to slskd through the /api/slskd
// auth proxy. What is open — the section, the peer, the search — is the URL; the one channel left here
// is a refresh signal, which is what channels are for.

import { useParams } from 'react-router';

// Bumped (to a fresh nonce) whenever a download is enqueued from the search panel, so the transfers
// panel refetches immediately instead of waiting for its next poll tick.
export const SLSKD_REFRESH_CHANNEL = 'soulseek:refresh';

export type SlskdApplication = {
  version?: { current?: string; full?: string; latest?: string; isUpdateAvailable?: boolean };
  server?: { state?: string; address?: string; isConnected?: boolean };
};

export type SlskdSearch = {
  id: string;
  isComplete: boolean;
  state: string;
  fileCount: number;
  responseCount: number;
};

export type SlskdFile = {
  filename: string;
  size: number;
  extension?: string;
  isLocked?: boolean;
  bitRate?: number;
  length?: number;
};

export type SlskdResponse = {
  username: string;
  fileCount: number;
  files: SlskdFile[];
  hasFreeUploadSlot: boolean;
  queueLength: number;
  uploadSpeed: number;
};

// One flat, rankable search result (a single file from a single peer).
export type ResultRow = {
  username: string;
  filename: string;
  size: number;
  extension: string;
  isLocked: boolean;
  bitRate?: number;
  hasFreeUploadSlot: boolean;
  queueLength: number;
  uploadSpeed: number;
};

// slskd presents results grouped by responder (user) → folder → files. We mirror that shape.
export type ResultFile = {
  filename: string; // full remote path (slskd needs this verbatim to enqueue)
  name: string; // basename for display
  size: number;
  extension: string;
  bitRate?: number;
  length?: number;
  isLocked: boolean;
};
export type ResultFolder = { path: string; label: string; files: ResultFile[]; size: number };
export type ResultUser = {
  username: string;
  hasFreeUploadSlot: boolean;
  queueLength: number;
  uploadSpeed: number;
  fileCount: number;
  folders: ResultFolder[];
};

// Group a search's responses into slskd's user → folder → file layout, best peers first.
export const groupResponses = (responses: SlskdResponse[]): ResultUser[] => {
  const users: ResultUser[] = [];
  for (const resp of responses) {
    const byFolder = new Map<string, ResultFile[]>();
    for (const f of resp.files ?? []) {
      const parts = f.filename.split(/[\\/]/);
      const name = parts.pop() ?? f.filename;
      const path = parts.join('\\') || '\\';
      const file: ResultFile = {
        filename: f.filename,
        name,
        size: f.size,
        extension: extOf(f),
        bitRate: f.bitRate,
        length: f.length,
        isLocked: !!f.isLocked,
      };
      const arr = byFolder.get(path);
      if (arr) arr.push(file);
      else byFolder.set(path, [file]);
    }
    const folders: ResultFolder[] = [...byFolder.entries()]
      .map(([path, files]) => {
        files.sort((a, b) => a.name.localeCompare(b.name));
        return { path, label: folderLabel(path), files, size: files.reduce((n, f) => n + f.size, 0) };
      })
      .sort((a, b) => a.path.localeCompare(b.path));
    users.push({
      username: resp.username,
      hasFreeUploadSlot: resp.hasFreeUploadSlot,
      queueLength: resp.queueLength,
      uploadSpeed: resp.uploadSpeed,
      fileCount: resp.fileCount ?? resp.files?.length ?? 0,
      folders,
    });
  }
  // Best peer first: free slot, then faster upload, then more files.
  users.sort(
    (a, b) =>
      Number(b.hasFreeUploadSlot) - Number(a.hasFreeUploadSlot) ||
      b.uploadSpeed - a.uploadSpeed ||
      b.fileCount - a.fileCount,
  );
  return users;
};

// A download transfer. GET /transfers/downloads nests these as user → directories → files.
export type SlskdTransfer = {
  id: string;
  username: string;
  direction: string;
  filename: string;
  size: number;
  state: string;
  bytesTransferred: number;
  averageSpeed: number;
  percentComplete: number;
  placeInQueue?: number;
};
export type SlskdDownloadDirectory = { directory: string; fileCount: number; files: SlskdTransfer[] };
export type SlskdDownloadUser = { username: string; directories: SlskdDownloadDirectory[] };
// Uploads (GET /transfers/uploads) use the same user → directories → files shape as downloads.
export type SlskdTransferUser = SlskdDownloadUser;

// Private conversations. GET /conversations lists them (one per peer); GET /conversations/{user}/messages
// returns that thread. A message's direction ('Out' = sent by us) drives self-alignment in the UI.
export type SlskdPrivateMessage = {
  id: number;
  timestamp: string;
  username: string;
  direction: string; // 'In' | 'Out'
  message: string;
  isAcknowledged?: boolean;
};
export type SlskdConversation = {
  username: string;
  isActive?: boolean;
  unAcknowledgedMessageCount?: number;
  hasUnAcknowledgedMessages?: boolean;
  messages?: SlskdPrivateMessage[];
};

// A peer, as seen through the Users section. GET /users/{u}/info + /status; /browse returns their shares.
export type SlskdUserInfo = {
  description?: string;
  hasFreeUploadSlot?: boolean;
  hasPicture?: boolean;
  queueLength?: number;
  uploadSlots?: number;
};
export type SlskdUserStatus = {
  isPrivileged?: boolean;
  presence?: string; // 'Offline' | 'Away' | 'Online'
};
export type SlskdBrowseDirectory = { name: string; fileCount: number; files?: SlskdFile[] };
// GET /users/{u}/browse wraps the share tree rather than returning a bare array, and inlines every
// file of every folder in one shot (real peers hit 18k folders / tens of MB). Note that a browsed
// file's `filename` is only the basename — unlike search results, where it's the full remote path —
// so enqueuing one means rejoining it to its directory name with a backslash.
export type SlskdBrowseResponse = {
  directories: SlskdBrowseDirectory[];
  directoryCount: number;
  lockedDirectories: SlskdBrowseDirectory[];
  lockedDirectoryCount: number;
};

// ── Cached share trees (Officer's own, from the sidecar's /_officer/browse routes) ──
//
// A live browse is one blocking slskd call carrying every file of every folder — 59 MB / 18k folders /
// 284k files for a real peer, thrown away the moment you navigate. So the sidecar fetches it in the
// background into Postgres and the UI reads paged slices. Timestamps arrive as ISO strings.

export type SoulseekBrowseStatus = 'pending' | 'ready' | 'failed';
export type SoulseekBrowseSnapshot = {
  username: string;
  status: SoulseekBrowseStatus;
  error: string | null;
  directoryCount: number;
  fileCount: number;
  totalSize: number;
  startedAt: string;
  completedAt: string | null;
};
// A node of the stored tree. The sidecar links slskd's flat backslash paths up at ingest, so `label` is
// already just the last segment and the subtree rollups are already summed — this side never derives
// either, which is the point: a collapsed row can show what's underneath it without fetching it.
export type SoulseekBrowseNode = {
  id: number;
  name: string;
  parentPath: string | null;
  label: string;
  depth: number;
  childCount: number;
  fileCount: number;
  totalSize: number;
  subtreeFileCount: number;
  subtreeSize: number;
};
export type SoulseekBrowseLevel = { nodes: SoulseekBrowseNode[]; total: number };
/** A filtered tree: the matches plus every ancestor holding them up, so the shape survives filtering. */
export type SoulseekBrowseSearch = {
  nodes: SoulseekBrowseNode[];
  matched: string[];
  total: number;
  truncated: boolean;
};
// Browse carries no bitrate/duration (real peers send empty `attributes`), so unlike a search result
// there's no quality metadata to show — just name, size, extension.
export type SoulseekBrowsedFile = { name: string; size: number; extension: string };

// Server connection state (GET /server, and the server block of GET /application).
export type SlskdServerState = { address?: string; state?: string; isConnected?: boolean; username?: string };

// Chat rooms. GET /rooms/joined/{name} inlines users + messages; GET /rooms/available lists the rest.
export type SlskdRoomUser = { username: string };
export type SlskdRoomMessage = {
  timestamp: string;
  username: string;
  message: string;
  roomName?: string;
  self?: boolean;
};
export type SlskdRoom = { name: string; isPrivate?: boolean; users?: SlskdRoomUser[]; messages?: SlskdRoomMessage[] };
export type SlskdRoomInfo = { name: string; userCount: number; isPrivate?: boolean };

// Which section is open is `/soulseek/:section` — the nav panel and the view panel both read the URL and
// neither publishes to the other. Mirrors slskd's own top-menu sections.
export type SoulseekSectionId =
  | 'dashboard'
  | 'search'
  | 'downloads'
  | 'uploads'
  | 'rooms'
  | 'chat'
  | 'users'
  | 'system'
  | 'connection';
export const SOULSEEK_SECTIONS: { id: SoulseekSectionId; label: string }[] = [
  { id: 'dashboard', label: 'Dashboard' },
  { id: 'search', label: 'Search' },
  { id: 'downloads', label: 'Downloads' },
  { id: 'uploads', label: 'Uploads' },
  { id: 'rooms', label: 'Rooms' },
  { id: 'chat', label: 'Chat' },
  { id: 'users', label: 'Users' },
  { id: 'system', label: 'System' },
  { id: 'connection', label: 'Connection' },
];

/** Where /soulseek lands, and where an unrecognised section redirects to. */
export const DEFAULT_SOULSEEK_SECTION: SoulseekSectionId = 'dashboard';

export const isSoulseekSection = (value: string | undefined): value is SoulseekSectionId =>
  SOULSEEK_SECTIONS.some((s) => s.id === value);

export const soulseekSectionPath = (section: SoulseekSectionId) => `/soulseek/${section}`;

// SoulseekScreen redirects anything unrecognised, so the fallback here only covers the instant before
// that lands.
export const useSoulseekSection = (): SoulseekSectionId => {
  const { section } = useParams();
  return isSoulseekSection(section) ? section : DEFAULT_SOULSEEK_SECTION;
};

// Which peer the Users section is showing — `/soulseek/users?user=<name>`.
//
// This was a `soulseek:user` channel carrying `{username, nonce}`: the username dropdown published a
// request, the Users panel consumed it once and cleared it, and the nonce existed so that asking for the
// SAME peer twice still counted as two requests. All of that machinery was a workaround for the peer not
// being in the URL — a link is idempotent, so there is nothing to consume and no nonce to disambiguate.
export const USER_PARAM = 'user';

export const soulseekUserPath = (username: string) =>
  `${soulseekSectionPath('users')}?${USER_PARAM}=${encodeURIComponent(username)}`;

// Which room the Rooms section is showing — `/soulseek/rooms?room=<name>` — and which thread the Chat
// section is showing — `/soulseek/chat?peer=<name>`. Two params rather than one because they are two
// sections: they never appear together, and a shared name would make `?thing=` mean different kinds of
// thing depending on the path.
//
// Both rails used to auto-select the first entry on load, which is why they held the selection locally:
// there was nowhere else to put an answer the user had not given. The bare section is now a real state —
// "nothing open" — and both panels already had the empty pane to say so.
export const ROOM_PARAM = 'room';
export const PEER_PARAM = 'peer';

export const soulseekRoomPath = (name: string) =>
  `${soulseekSectionPath('rooms')}?${ROOM_PARAM}=${encodeURIComponent(name)}`;

export const soulseekPeerPath = (username: string) =>
  `${soulseekSectionPath('chat')}?${PEER_PARAM}=${encodeURIComponent(username)}`;

// A past search, as listed by GET /searches (no responses inlined).
// Which search's results are open — `/soulseek/search?search=<id>`, read from the URL rather than held in a
// panel channel or local state. See docs/navigation-audit.md.
export const SEARCH_PARAM = 'search';

export type SlskdSearchSummary = {
  id: string;
  searchText: string;
  startedAt: string;
  endedAt?: string;
  fileCount: number;
  lockedFileCount: number;
  responseCount: number;
  isComplete: boolean;
  state: string;
};

export const basename = (path: string) => path.split(/[\\/]/).pop() ?? path;

// Compact folder label for grouped views — the last two path segments (slskd paths use backslashes).
export const folderLabel = (path: string) => {
  const segs = path.split(/[\\/]/).filter(Boolean);
  return segs.slice(-2).join(' / ') || path;
};

// Compact "time ago" for search-history rows.
export const formatWhen = (iso?: string): string => {
  if (!iso) return '';
  const t = new Date(iso).getTime();
  if (!Number.isFinite(t)) return '';
  const s = Math.round((Date.now() - t) / 1000);
  if (s < 60) return 'just now';
  const m = Math.floor(s / 60);
  if (m < 60) return `${m}m ago`;
  const h = Math.floor(m / 60);
  if (h < 24) return `${h}h ago`;
  const d = Math.floor(h / 24);
  return d < 7 ? `${d}d ago` : new Date(iso).toLocaleDateString();
};

// Wall-clock HH:MM for chat message timestamps.
export const formatClock = (iso?: string): string => {
  if (!iso) return '';
  const d = new Date(iso);
  return Number.isFinite(d.getTime()) ? d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
};

export const formatSize = (bytes: number) => {
  if (!bytes) return '0 B';
  const units = ['B', 'KB', 'MB', 'GB', 'TB'];
  const i = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024)));
  return `${(bytes / 1024 ** i).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
};

export const formatSpeed = (bytesPerSec: number) => (bytesPerSec > 0 ? `${formatSize(bytesPerSec)}/s` : '');

// Track length (seconds) → m:ss, as slskd shows it.
export const formatDuration = (sec?: number): string => {
  if (!sec || sec < 0) return '';
  const m = Math.floor(sec / 60);
  const s = Math.floor(sec % 60);
  return `${m}:${String(s).padStart(2, '0')}`;
};

export const extOf = (file: SlskdFile) =>
  (file.extension?.trim() || basename(file.filename).split('.').pop() || '').toLowerCase();

// slskd serialises TransferStates as a compound flag string ("Completed, Succeeded", "Queued, Remotely",
// "InProgress", "Requested", …). Collapse it to the one bucket we render.
export type TransferPhase = 'downloading' | 'queued' | 'done' | 'failed';
export const transferPhase = (state: string): TransferPhase => {
  const s = state.toLowerCase();
  if (s.includes('inprogress') || s.includes('initializing')) return 'downloading';
  if (s.includes('succeeded')) return 'done';
  if (
    s.includes('errored') ||
    s.includes('cancelled') ||
    s.includes('rejected') ||
    s.includes('timedout') ||
    s.includes('aborted')
  )
    return 'failed';
  return 'queued'; // Requested / Queued / anything not yet resolved
};
