'use client';

import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import Link from 'next/link';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import {
  ChevronDown,
  ChevronRight,
  Download,
  FileText,
  Folder,
  Lock,
  Plus,
  Upload,
} from 'lucide-react';

import { Button } from '@/components/ui/button';
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogHeader,
  DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
import { apiFetch } from '@/lib/api/client';
import { StatusPill, type StatusPillStatus } from '@/components/ui/status-pill';
import { EmptyState } from '@/components/ui/empty-state';
import { FileUploadZone } from '@/components/files/file-upload-zone';
import { FilePreviewDialog } from '@/components/files/file-preview-dialog';
import { PageHeader } from '@/components/shared/page-header';
import { PermissionGate } from '@/components/shared/permission-gate';
import { usePaginatedQuery } from '@/hooks/use-paginated-query';
import { useRealtimeInvalidation } from '@/hooks/use-realtime-invalidation';
import { useDocumentFolders, type FolderNode } from '@/hooks/use-document-folders';
import { useMobileChrome } from '@/components/layout/mobile/mobile-layout-provider';
import { useUIStore } from '@/stores/ui-store';
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable';
import { FolderActionsMenu } from './folder-actions-menu';
import { FolderBreadcrumb } from './folder-breadcrumb';
import { FolderTreeBody, FolderTreeSidebar } from './folder-tree-sidebar';
import { HubRootView } from './hub-root-view';
import { EntityFolderView } from './entity-folder-view';
import { NewDocumentMenu } from './new-document-menu';

interface HubDoc {
  id: string;
  documentType: string;
  title: string;
  status: string;
  createdAt: string;
  signers?: Array<{ id: string; signerEmail: string; signerName: string; status: string }>;
}

/**
 * Lightweight files-table row used by FlatFolderListing's parallel files
 * query. Folder views show both signature documents (the `documents`
 * table) AND plain uploaded files (the `files` table) so reps see
 * everything that physically lives in the folder in one list. The two
 * sources merge into a unified `HubRow` for rendering.
 */
interface HubFile {
  id: string;
  filename: string;
  originalName: string | null;
  mimeType: string | null;
  sizeBytes: number;
  createdAt: string;
}

type HubRow = { kind: 'doc'; doc: HubDoc } | { kind: 'file'; file: HubFile };

const TYPE_LABELS: Record<string, string> = {
  eoi: 'EOI',
  contract: 'Contract',
  nda: 'NDA',
  reservation_agreement: 'Reservation Agreement',
  welcome_letter: 'Welcome Letter',
  handover_checklist: 'Handover',
  acknowledgment: 'Acknowledgment',
  correspondence: 'Correspondence',
  other: 'Other',
};

const STATUS_PILL_MAP: Record<string, StatusPillStatus> = {
  draft: 'draft',
  sent: 'sent',
  partially_signed: 'partial',
  completed: 'completed',
  signed: 'signed',
  expired: 'expired',
  cancelled: 'cancelled',
  rejected: 'rejected',
};

const SIGNER_STATUS_LABELS: Record<string, string> = {
  pending: 'Pending',
  sent: 'Sent',
  signed: 'Signed',
  declined: 'Declined',
  expired: 'Expired',
  cancelled: 'Cancelled',
};

// Runtime guard so we don't cast `entityType` from the FolderNode shape; if a
// future system folder shape leaks an unexpected entity type the UI falls
// back to FlatFolderListing instead of crashing on a bad route.
const ENTITY_TYPES = new Set(['client', 'company', 'yacht'] as const);
type EntityType = 'client' | 'company' | 'yacht';
function isEntityType(v: unknown): v is EntityType {
  return typeof v === 'string' && ENTITY_TYPES.has(v as EntityType);
}

interface DocumentsHubProps {
  portSlug: string;
}

function findInTree(nodes: FolderNode[], id: string): FolderNode | null {
  for (const n of nodes) {
    if (n.id === id) return n;
    const found = findInTree(n.children, id);
    if (found) return found;
  }
  return null;
}

// URL encoding for the folder selection tri-state:
//   no `folder` param   → undefined (hub root / "All documents")
//   `folder=root`       → null      (root folder only)
//   `folder=<uuid>`     → string    (specific folder)
function decodeFolderParam(raw: string | null): string | null | undefined {
  if (raw == null) return undefined;
  if (raw === 'root') return null;
  return raw;
}
function encodeFolderParam(value: string | null | undefined): string | null {
  if (value === undefined) return null;
  if (value === null) return 'root';
  return value;
}

export function DocumentsHub({ portSlug }: DocumentsHubProps) {
  // undefined = "All documents" (no folder selected / hub root)
  // null = root folder only
  // string = specific folder id
  const router = useRouter();
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const folderParam = searchParams.get('folder');
  const selectedFolderId = useMemo(() => decodeFolderParam(folderParam), [folderParam]);

  const { data: tree = [] } = useDocumentFolders();

  // Realtime invalidation covers ALL three render modes (HubRootView,
  // EntityFolderView, FlatFolderListing) so navigating between modes
  // doesn't tear down the subscription. The hook-level eventKeysSig
  // dedup means the inline literal is safe across re-renders.
  useRealtimeInvalidation({
    'document:created': [['documents']],
    'document:updated': [['documents']],
    'document:deleted': [['documents']],
    'document:sent': [['documents']],
    'document:completed': [['documents'], ['files']],
    'document:expired': [['documents']],
    'document:cancelled': [['documents']],
    'document:rejected': [['documents']],
    'document:signer:signed': [['documents']],
    // M-D01: server emits `file:uploaded` (see src/lib/services/files.ts);
    // every other consumer listens on that name. `file:created` was a
    // typo here, so the hub's file list never invalidated on upload.
    'file:uploaded': [['files']],
    'file:updated': [['files']],
    'file:deleted': [['files']],
    'folder:created': [['document-folders']],
    'folder:updated': [['document-folders']],
    'folder:deleted': [['document-folders']],
    'folder:moved': [['document-folders']],
  });

  const { setChrome } = useMobileChrome();
  useEffect(() => {
    setChrome({ title: 'Documents' });
    return () => setChrome({ title: null });
  }, [setChrome]);

  const selectedFolder =
    typeof selectedFolderId === 'string' ? findInTree(tree, selectedFolderId) : null;

  const folderEntityType = selectedFolder?.entityType;
  const isEntityFolder =
    selectedFolder?.systemManaged === true &&
    folderEntityType != null &&
    folderEntityType !== 'root' &&
    selectedFolder.entityId != null &&
    isEntityType(folderEntityType);

  const handleFolderSelect = useCallback(
    (id: string | null | undefined) => {
      const next = new URLSearchParams(searchParams.toString());
      const encoded = encodeFolderParam(id);
      if (encoded == null) {
        next.delete('folder');
      } else {
        next.set('folder', encoded);
      }
      const qs = next.toString();
      router.replace((qs ? `${pathname}?${qs}` : pathname) as never, { scroll: false });
    },
    [router, pathname, searchParams],
  );

  const sidebarFooter = (
    <PermissionGate resource="documents" action="manage_folders">
      <FolderActionsMenu
        selectedFolderId={selectedFolderId}
        onAfterDelete={() => handleFolderSelect(undefined)}
      />
    </PermissionGate>
  );

  const contentPane = (
    <div className="flex-1 min-w-0 p-4 space-y-4">
      {selectedFolderId !== undefined && (
        <div className="flex items-center justify-between gap-3">
          <FolderBreadcrumb selectedFolderId={selectedFolderId} onSelect={handleFolderSelect} />
          <NewDocumentMenu
            portSlug={portSlug}
            folderId={selectedFolderId}
            entityType={
              isEntityFolder && isEntityType(folderEntityType) ? folderEntityType : undefined
            }
            entityId={isEntityFolder ? (selectedFolder!.entityId ?? undefined) : undefined}
            size="sm"
          />
        </div>
      )}

      {selectedFolderId === undefined ? (
        <>
          <PageHeader
            title="Documents"
            description="Track signing status, chase pending signers, and audit completion."
            actions={<NewDocumentMenu portSlug={portSlug} />}
            variant="gradient"
          />
          <HubRootView portSlug={portSlug} />
        </>
      ) : isEntityFolder && isEntityType(folderEntityType) ? (
        <FolderDropZone
          folderId={selectedFolderId}
          entityType={folderEntityType}
          entityId={selectedFolder!.entityId!}
        >
          <EntityFolderView
            portSlug={portSlug}
            entityType={folderEntityType}
            entityId={selectedFolder!.entityId!}
          />
        </FolderDropZone>
      ) : (
        <FolderDropZone folderId={selectedFolderId}>
          <FlatFolderListing
            key={selectedFolderId ?? 'root'}
            portSlug={portSlug}
            folderId={selectedFolderId}
            childFolders={
              typeof selectedFolderId === 'string' ? (selectedFolder?.children ?? []) : (tree ?? [])
            }
            onFolderSelect={handleFolderSelect}
          />
        </FolderDropZone>
      )}
    </div>
  );

  return (
    // Escape the AppShell's desktop main padding (px-6 pt-3 pb-6) so the
    // folder column sits flush against the global app sidebar - reads
    // as an extension of navigation rather than a card-inside-a-page.
    // Inner content keeps its own padding so the right pane doesn't
    // run flush with the viewport edge.
    <div className="h-full sm:-mx-6 sm:-mt-3 sm:-mb-6">
      {/* Mobile: stacked, with a Sheet drawer for folders. */}
      <div className="flex flex-col h-full sm:hidden">
        <FolderTreeSidebar
          selectedFolderId={selectedFolderId}
          onSelect={handleFolderSelect}
          footer={sidebarFooter}
        />
        {contentPane}
      </div>

      {/* Desktop (sm+): resizable two-pane split. autoSaveId persists the
       *  user's chosen split width to localStorage so it survives reloads.
       *  Min/max defends against the user collapsing the sidebar to zero
       *  width or starving the content pane. */}
      <ResizablePanelGroup
        direction="horizontal"
        autoSaveId="documents-hub-split"
        className="hidden sm:flex h-full"
      >
        <ResizablePanel defaultSize={22} minSize={18} maxSize={40}>
          <aside className="h-full min-w-[180px] border-r bg-muted/40 p-2 overflow-y-auto">
            <div className="mb-2 px-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">
              Folders
            </div>
            <FolderTreeBody
              selectedFolderId={selectedFolderId}
              onSelect={handleFolderSelect}
              footer={sidebarFooter}
            />
          </aside>
        </ResizablePanel>
        <ResizableHandle withHandle />
        <ResizablePanel defaultSize={80} minSize={50}>
          <div className="h-full overflow-y-auto">{contentPane}</div>
        </ResizablePanel>
      </ResizablePanelGroup>
    </div>
  );
}

// ---------------------------------------------------------------------------
// FlatFolderListing - the original search + type-chip + document rows panel,
// now scoped to a specific folder (or null for root-only).
// ---------------------------------------------------------------------------

interface FlatFolderListingProps {
  portSlug: string;
  folderId: string | null;
  /** Direct children of the currently-viewed folder. Rendered above the
   *  document list as clickable cards so the rep can drill into subfolders
   *  from the main content area, not only the sidebar tree. */
  childFolders?: FolderNode[];
  onFolderSelect?: (id: string | null | undefined) => void;
}

function FlatFolderListing({
  portSlug,
  folderId,
  childFolders = [],
  onFolderSelect,
}: FlatFolderListingProps) {
  const [search, setSearch] = useState('');
  const [typeFilter, setTypeFilter] = useState<string | undefined>(undefined);
  const [expandedDocId, setExpandedDocId] = useState<string | null>(null);
  const [uploadOpen, setUploadOpen] = useState(false);
  // File selected for inline preview. Clicking a file row opens the shared
  // FilePreviewDialog rather than navigating the browser at the JSON-returning
  // `/files/[id]/download` endpoint (which used to dump raw `{data:{url}}`).
  const [previewFile, setPreviewFile] = useState<HubFile | null>(null);

  // Force-download a stored file: the `/download` route returns a presigned
  // URL (content-disposition=attachment) as a JSON envelope, so we fetch it
  // then click a hidden anchor. Avoids navigating the tab to the raw JSON.
  const downloadFile = useCallback(async (file: HubFile) => {
    try {
      const { data } = await apiFetch<{ data: { url: string; filename: string } }>(
        `/api/v1/files/${file.id}/download`,
      );
      const a = document.createElement('a');
      a.href = data.url;
      a.rel = 'noopener';
      a.download = file.originalName ?? file.filename;
      document.body.appendChild(a);
      a.click();
      a.remove();
    } catch {
      // apiFetch surfaces its own toast on failure; nothing else to do here.
    }
  }, []);
  const queryClient = useQueryClient();

  const queryParams = useMemo(() => {
    const params = new URLSearchParams();
    if (search) params.set('search', search);
    if (typeFilter) params.set('documentType', typeFilter);
    // folderId null = root, string = specific folder
    params.set('folderId', folderId ?? '');
    return params;
  }, [search, typeFilter, folderId]);

  const { data: documents, isLoading } = usePaginatedQuery<HubDoc>({
    queryKey: ['documents', 'hub', 'folder', queryParams.toString()],
    endpoint: `/api/v1/documents?${queryParams.toString()}`,
    filterDefinitions: [],
  });

  // Plain uploaded files in this folder. Lives in a separate table from
  // signature documents, so we query it in parallel and merge into the
  // unified row list. Skip the search/typeFilter for files (those filters
  // are doc-specific); applying them client-side would create a confusing
  // partial view where the search bar hides files unrelated to the query.
  // folderId === null → root; UUID → that folder; the endpoint resolves
  // `folderId=` as null to keep the URL idempotent across folder switches.
  const filesQueryString = useMemo(() => {
    const p = new URLSearchParams();
    p.set('folderId', folderId ?? '');
    p.set('limit', '50');
    p.set('sort', 'createdAt');
    p.set('order', 'desc');
    return p.toString();
  }, [folderId]);
  const { data: filesResp, isLoading: filesLoading } = useQuery<{ data: HubFile[] }>({
    queryKey: ['files', 'hub', 'folder', filesQueryString],
    queryFn: async (): Promise<{ data: HubFile[] }> => {
      const res = await fetch(`/api/v1/files?${filesQueryString}`, { credentials: 'include' });
      if (!res.ok) return { data: [] };
      return (await res.json()) as { data: HubFile[] };
    },
  });
  const folderFiles: HubFile[] = useMemo(() => filesResp?.data ?? [], [filesResp]);

  // Merge docs + files, sorted by createdAt desc so the most-recent item
  // (regardless of source) is at the top. Filter file rows out when the
  // type-chip is active (chips filter docs only — files have no
  // documentType column, treating them as "uploads" type means they'd
  // disappear under any chip selection, which is the right UX).
  const mergedRows: HubRow[] = useMemo(() => {
    const docRows = documents.map((doc) => ({ kind: 'doc' as const, doc }));
    const fileRows = typeFilter
      ? []
      : folderFiles
          // Client-side search across filenames so the search box covers
          // both sources uniformly.
          .filter((f) =>
            search
              ? (f.filename ?? '').toLowerCase().includes(search.toLowerCase()) ||
                (f.originalName ?? '').toLowerCase().includes(search.toLowerCase())
              : true,
          )
          .map((file) => ({ kind: 'file' as const, file }));
    return [...docRows, ...fileRows].sort((a, b) => {
      const aTime = a.kind === 'doc' ? a.doc.createdAt : a.file.createdAt;
      const bTime = b.kind === 'doc' ? b.doc.createdAt : b.file.createdAt;
      return bTime.localeCompare(aTime);
    });
  }, [documents, folderFiles, typeFilter, search]);

  // Realtime invalidation is lifted to DocumentsHub so it survives mode
  // switches (root / entity-folder / flat-folder). Don't re-subscribe here.

  const renderRow = (doc: HubDoc) => {
    const expanded = expandedDocId === doc.id;
    const totalSigners = doc.signers?.length ?? 0;
    const signedCount = doc.signers?.filter((s) => s.status === 'signed').length ?? 0;
    const pillStatus = STATUS_PILL_MAP[doc.status] ?? 'pending';

    const isNonSignature = [
      'welcome_letter',
      'handover_checklist',
      'acknowledgment',
      'correspondence',
    ].includes(doc.documentType);

    return (
      <li
        key={doc.id}
        className="border-b last:border-b-0 transition-colors hover:bg-gradient-brand-soft/40"
      >
        <div className="flex flex-wrap items-center gap-x-3 gap-y-1 px-4 py-3 text-sm sm:grid sm:grid-cols-[auto_1fr_auto_auto_auto_auto] sm:gap-3">
          {totalSigners > 0 ? (
            <button
              type="button"
              aria-label={`${expanded ? 'Collapse' : 'Expand'} signers for ${doc.title}`}
              aria-expanded={expanded}
              onClick={() => setExpandedDocId(expanded ? null : doc.id)}
              className="flex min-h-[44px] min-w-[44px] items-center justify-center text-muted-foreground transition-transform"
            >
              {expanded ? (
                <ChevronDown className="h-4 w-4" aria-hidden />
              ) : (
                <ChevronRight className="h-4 w-4" aria-hidden />
              )}
            </button>
          ) : (
            // Keep the column reserved so the grid layout stays aligned
            // across rows; this row has no signers to expand into.
            <span className="hidden h-[44px] w-[44px] sm:block" aria-hidden />
          )}
          <Link
            href={`/${portSlug}/documents/${doc.id}`}
            className="min-w-0 truncate font-medium text-foreground hover:text-brand"
          >
            {doc.title}
          </Link>
          <span className="text-xs text-muted-foreground">
            {TYPE_LABELS[doc.documentType] ?? doc.documentType}
          </span>
          <StatusPill
            status={isNonSignature && doc.status === 'sent' ? 'delivered' : pillStatus}
            withDot
          >
            {isNonSignature && doc.status === 'sent' ? 'Delivered' : doc.status.replace(/_/g, ' ')}
          </StatusPill>
          <span className="text-xs tabular-nums text-muted-foreground">
            {totalSigners > 0 ? `${signedCount}/${totalSigners} signed` : '-'}
          </span>
          <span className="text-xs text-muted-foreground">
            {new Date(doc.createdAt).toLocaleDateString(undefined)}
          </span>
        </div>
        {expanded && doc.signers && doc.signers.length > 0 ? (
          <div className="border-t bg-muted/30 px-12 py-2">
            <ul className="space-y-1">
              {doc.signers.map((signer) => (
                <li key={signer.id} className="flex items-center justify-between gap-2 text-xs">
                  <div className="flex min-w-0 items-center gap-2">
                    <span className="font-medium text-foreground">{signer.signerName}</span>
                    <span className="truncate text-muted-foreground">{signer.signerEmail}</span>
                  </div>
                  <StatusPill status={STATUS_PILL_MAP[signer.status] ?? 'pending'}>
                    {SIGNER_STATUS_LABELS[signer.status] ?? signer.status}
                  </StatusPill>
                </li>
              ))}
            </ul>
          </div>
        ) : null}
      </li>
    );
  };

  // Uploaded-file row — simpler than a signature doc since there's no
  // signer/status concept. Clicking the name opens an inline preview
  // (FilePreviewDialog); a dedicated download button saves the file. An
  // "Uploaded" type pill distinguishes it from signature workflows.
  const fileLabel = (file: HubFile) => file.originalName ?? file.filename;
  const renderFileRow = (file: HubFile) => {
    return (
      <li
        key={`file:${file.id}`}
        className="border-b last:border-b-0 transition-colors hover:bg-gradient-brand-soft/40"
      >
        <div className="flex flex-wrap items-center gap-x-3 gap-y-1 px-4 py-3 text-sm sm:grid sm:grid-cols-[auto_1fr_auto_auto_auto_auto] sm:gap-3">
          {/* Empty action column to align with doc-row layout */}
          <span className="hidden h-[44px] w-[44px] sm:block" aria-hidden />
          <button
            type="button"
            onClick={() => setPreviewFile(file)}
            className="min-w-0 truncate text-left font-medium text-foreground hover:text-brand"
            title={`Preview ${fileLabel(file)}`}
          >
            {fileLabel(file)}
          </button>
          <span className="text-xs text-muted-foreground">Uploaded file</span>
          <StatusPill status="completed" withDot>
            Stored
          </StatusPill>
          <span className="text-xs tabular-nums text-muted-foreground">
            {(file.sizeBytes / 1024).toFixed(0)} KB
          </span>
          <div className="flex items-center gap-2">
            <span className="text-xs tabular-nums text-muted-foreground">
              {new Date(file.createdAt).toLocaleDateString(undefined)}
            </span>
            <Button
              variant="ghost"
              size="icon"
              className="h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground"
              onClick={() => void downloadFile(file)}
              title={`Download ${fileLabel(file)}`}
              aria-label={`Download ${fileLabel(file)}`}
            >
              <Download className="h-4 w-4" aria-hidden />
            </Button>
          </div>
        </div>
      </li>
    );
  };

  return (
    <div className="space-y-4">
      <FilePreviewDialog
        open={!!previewFile}
        onOpenChange={(o) => {
          if (!o) setPreviewFile(null);
        }}
        fileId={previewFile?.id}
        fileName={previewFile ? fileLabel(previewFile) : undefined}
        mimeType={previewFile?.mimeType ?? undefined}
      />
      <div className="flex flex-wrap items-center gap-2">
        <Input
          placeholder="Search by title..."
          aria-label="Search documents by title"
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          className="max-w-xs h-9"
        />
        {(() => {
          const seenTypes = Array.from(new Set(documents.map((d) => d.documentType))).sort();
          if (seenTypes.length === 0) return null;
          return (
            <div className="flex flex-wrap gap-1.5">
              <button
                type="button"
                aria-pressed={typeFilter === undefined}
                className={cn(
                  'inline-flex min-h-[44px] items-center rounded-full border px-3 py-2 text-xs',
                  typeFilter === undefined ? 'bg-foreground text-background' : 'hover:bg-accent',
                )}
                onClick={() => setTypeFilter(undefined)}
              >
                All types
              </button>
              {seenTypes.map((t) => (
                <button
                  type="button"
                  key={t}
                  aria-pressed={typeFilter === t}
                  className={cn(
                    'inline-flex min-h-[44px] items-center rounded-full border px-3 py-2 text-xs',
                    typeFilter === t ? 'bg-foreground text-background' : 'hover:bg-accent',
                  )}
                  onClick={() => setTypeFilter(t)}
                >
                  {TYPE_LABELS[t] ?? t}
                </button>
              ))}
            </div>
          );
        })()}
      </div>

      {childFolders.length > 0 ? (
        <div>
          <p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-1.5">
            Subfolders
          </p>
          <div className="grid grid-cols-2 gap-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5">
            {childFolders.map((sub) => (
              <button
                key={sub.id}
                type="button"
                onClick={() => onFolderSelect?.(sub.id)}
                className="flex items-center gap-2 rounded-md border bg-white px-3 py-2 text-sm text-left transition-colors hover:border-primary/50 hover:bg-accent/30"
                title={sub.name}
              >
                <Folder className="h-4 w-4 shrink-0 text-muted-foreground" aria-hidden />
                <span className="truncate font-medium">{sub.name}</span>
                {sub.systemManaged ? (
                  <Lock className="ml-auto h-3 w-3 shrink-0 text-muted-foreground" aria-hidden />
                ) : null}
              </button>
            ))}
          </div>
        </div>
      ) : null}

      {isLoading || filesLoading ? (
        <ul className="rounded-md border bg-white">
          {[0, 1, 2, 3, 4].map((i) => (
            <li key={i} className="h-12 animate-pulse border-b last:border-b-0 bg-muted/40" />
          ))}
        </ul>
      ) : mergedRows.length === 0 && childFolders.length === 0 ? (
        <EmptyState
          icon={<FileText className="h-7 w-7" aria-hidden />}
          title="No documents in this folder"
          body="Upload a file, generate a signing flow, or move existing documents here."
          actions={
            <>
              <Button onClick={() => setUploadOpen(true)}>
                <Upload className="mr-1.5 h-4 w-4" aria-hidden />
                Upload file
              </Button>
              <Button asChild variant="outline">
                <Link href={`/${portSlug}/documents/new`}>
                  <Plus className="mr-1.5 h-4 w-4" aria-hidden />
                  Generate for signing
                </Link>
              </Button>
            </>
          }
        />
      ) : (
        <ul className="rounded-md border bg-white shadow-xs">
          {mergedRows.map((row) =>
            row.kind === 'doc' ? renderRow(row.doc) : renderFileRow(row.file),
          )}
        </ul>
      )}

      <Dialog open={uploadOpen} onOpenChange={setUploadOpen}>
        <DialogContent className="sm:max-w-2xl">
          <DialogHeader>
            <DialogTitle>Upload file</DialogTitle>
            <DialogDescription>
              {folderId === null
                ? 'File will be added to the root.'
                : 'File will be added to the current folder.'}
            </DialogDescription>
          </DialogHeader>
          <FileUploadZone
            folderId={folderId}
            onUploadComplete={(file) => {
              if (!file) {
                queryClient.invalidateQueries({ queryKey: ['files'] });
                queryClient.invalidateQueries({ queryKey: ['documents'] });
                setUploadOpen(false);
              }
            }}
          />
        </DialogContent>
      </Dialog>
    </div>
  );
}

// ---------------------------------------------------------------------------
// FolderDropZone - wraps the main content panel and accepts file drops onto
// the currently-viewed folder. Files dropped here upload with folder_id +
// entity FKs set so they land where the rep expects.
//
// Renders an overlay while a drag is in progress; the underlying content
// stays interactive. Multiple files at once are supported (Promise.all
// inside the upload loop). Errors surface inline; the toast layer picks
// them up via React Query / fetch error responses.
// ---------------------------------------------------------------------------

interface FolderDropZoneProps {
  folderId: string | null;
  entityType?: 'client' | 'company' | 'yacht';
  entityId?: string;
  children: React.ReactNode;
}

function FolderDropZone({ folderId, entityType, entityId, children }: FolderDropZoneProps) {
  const [dragActive, setDragActive] = useState(false);
  const [uploading, setUploading] = useState(false);
  // useRef for mutable per-drag counters - useMemo's value is supposed
  // to be immutable; React Compiler flags writes as a bug class.
  const dragCounter = useRef(0);
  const queryClient = useQueryClient();
  const portId = useUIStore((s) => s.currentPortId);

  const onDragEnter = useCallback((e: React.DragEvent<HTMLDivElement>) => {
    // Only react to drags that carry files. Avoids fighting with
    // text-selection / element drags inside the listing.
    if (!Array.from(e.dataTransfer.types).includes('Files')) return;
    e.preventDefault();
    dragCounter.current += 1;
    if (dragCounter.current === 1) setDragActive(true);
  }, []);

  const onDragLeave = useCallback((e: React.DragEvent<HTMLDivElement>) => {
    if (!Array.from(e.dataTransfer.types).includes('Files')) return;
    dragCounter.current -= 1;
    if (dragCounter.current <= 0) {
      dragCounter.current = 0;
      setDragActive(false);
    }
  }, []);

  const onDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
    if (!Array.from(e.dataTransfer.types).includes('Files')) return;
    e.preventDefault();
    e.dataTransfer.dropEffect = 'copy';
  }, []);

  const onDrop = useCallback(
    async (e: React.DragEvent<HTMLDivElement>) => {
      if (!Array.from(e.dataTransfer.types).includes('Files')) return;
      e.preventDefault();
      dragCounter.current = 0;
      setDragActive(false);
      const files = Array.from(e.dataTransfer.files);
      if (files.length === 0) return;
      setUploading(true);
      try {
        await Promise.all(
          files.map(async (file) => {
            const fd = new FormData();
            fd.append('file', file);
            fd.append('filename', file.name);
            if (folderId) fd.append('folderId', folderId);
            if (entityType) fd.append('entityType', entityType);
            if (entityId) fd.append('entityId', entityId);
            if (entityType === 'client' && entityId) fd.append('clientId', entityId);
            if (entityType === 'company' && entityId) fd.append('companyId', entityId);
            if (entityType === 'yacht' && entityId) fd.append('yachtId', entityId);
            const headers = new Headers();
            if (portId) headers.set('X-Port-Id', portId);
            await fetch('/api/v1/files/upload', {
              method: 'POST',
              headers,
              credentials: 'include',
              body: fd,
            });
          }),
        );
        queryClient.invalidateQueries({ queryKey: ['files'] });
        queryClient.invalidateQueries({ queryKey: ['documents'] });
      } finally {
        setUploading(false);
      }
    },
    [folderId, entityType, entityId, portId, queryClient],
  );

  return (
    <div
      onDragEnter={onDragEnter}
      onDragLeave={onDragLeave}
      onDragOver={onDragOver}
      onDrop={onDrop}
      className="relative"
    >
      {children}
      {(dragActive || uploading) && (
        <div className="pointer-events-none absolute inset-0 flex items-center justify-center rounded-md border-2 border-dashed border-primary bg-primary/5 backdrop-blur-[1px] z-10">
          <div className="flex flex-col items-center gap-2 text-sm font-medium text-primary">
            <Upload className="h-8 w-8" aria-hidden />
            {uploading ? 'Uploading…' : 'Drop to upload to this folder'}
          </div>
        </div>
      )}
    </div>
  );
}
