import React, { useCallback, useEffect, useMemo, useState } from 'react';
import {
  DownloadIcon,
  EditIcon,
  ArrowLeftIcon,
  ArrowRightIcon,
  ChevronsRightIcon,
  AnalyzeIcon,
  WindowMaximizeIcon,
  TextScanIcon,
} from '../ui/icons';
import { getTagColorStyle } from '../utils/colors';
import { formatFileSize } from '../utils/format';
import { resolveDocumentAssetUrl, createAssetView } from '../asset_manager';
import { useAssetNavigator } from '../hooks/useAssetNavigator';
import PreviewZoomOverlay from './PreviewZoomOverlay';
import { openOcrTextInNewTab } from '../utils/ocr';

const MAX_PREVIEW_STACK_ITEMS = 15;

const derivePreviewOrientation = (metadata) => {
  const width = Number(metadata?.width);
  const height = Number(metadata?.height);
  if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
    return width >= height ? 'landscape' : 'portrait';
  }
  return 'landscape';
};

const sortCorrespondents = (entries = []) =>
  entries
    .map((entry) => ({
      id: entry.id,
      name: entry.name || '',
      count: entry.count,
    }))
    .sort((a, b) => (a.name || '').localeCompare(b.name || ''));

const CorrespondentPills = ({ entries = [], onRemove, showCount = false }) => (
  <div className="correspondent-list">
    {entries.length ? (
      entries.map((entry, index) => {
        const key = entry.id ?? `${entry.name}-${index}`;
        return (
          <span key={key} className="correspondent-pill">
            <span className="correspondent-pill__label">
              <span>
                {entry.name}
                {showCount && entry.count ? ` (${entry.count})` : ''}
              </span>
            </span>
            {onRemove ? (
              <button
                type="button"
                className="correspondent-pill__remove"
                onClick={() => onRemove(entry)}
                aria-label={`Remove ${entry.name}`}
              >
                ×
              </button>
            ) : null}
          </span>
        );
      })
    ) : (
      <span className="meta">No correspondents yet.</span>
    )}
  </div>
);

const TagSection = ({
  title,
  tags = [],
  onRemove,
  onAdd,
  emptyMessage = 'No tags yet.',
  addPlaceholder = 'Add or create tag',
  addButtonLabel = 'Add',
  datalistId,
  datalistOptions = [],
  className,
}) => (
  <div className={className}>
    <dt>{title}</dt>
    <div className="tag-list">
      {tags.length ? (
        tags.map((tag) => {
          const key = tag.id ?? tag.label;
          const style = getTagColorStyle(tag.color);
          const removable = Boolean(onRemove);
          const className = removable ? 'badge tag-chip tag-chip--removable' : 'badge tag-chip';
          return (
            <span key={key} className={className} style={style || undefined}>
              <span className="tag-chip__label">{tag.label}</span>
              {removable ? (
                <button
                  type="button"
                  className="tag-chip__remove"
                  onClick={() => onRemove(tag)}
                  aria-label={`Remove tag ${tag.label}`}
                >
                  ×
                </button>
              ) : null}
            </span>
          );
        })
      ) : (
        <span className="meta">{emptyMessage}</span>
      )}
    </div>
    {onAdd ? (
      <form
        className="inline"
        onSubmit={(event) => {
          event.preventDefault();
          const input = event.currentTarget.elements.tag;
          const value = input.value.trim();
          if (!value) return;
          onAdd({ value, input });
        }}
      >
        <input name="tag" placeholder={addPlaceholder} list={datalistId} />
        <button type="submit">{addButtonLabel}</button>
        {datalistId ? (
          <datalist id={datalistId}>
            {datalistOptions.map((option) => (
              <option key={option.id} />
            ))}
          </datalist>
        ) : null}
      </form>
    ) : null}
  </div>
);

const CorrespondentSection = ({
  title,
  entries = [],
  onRemove,
  onAdd,
  showCount = false,
  addPlaceholder = 'Add or create correspondent',
  addButtonLabel = 'Add',
  datalistId,
  datalistOptions = [],
  className,
}) => (
  <div className={className}>
    <dt>{title}</dt>
    <CorrespondentPills entries={entries} onRemove={onRemove} showCount={showCount} />
    {onAdd ? (
      <form
        className="correspondent-form"
        onSubmit={(event) => {
          event.preventDefault();
          const form = event.currentTarget;
          const nameInput = form.elements.correspondent;
          const value = nameInput.value.trim();
          if (!value) return;
          onAdd({ name: value, input: nameInput });
          form.reset();
        }}
      >
        <input
          name="correspondent"
          placeholder={addPlaceholder}
          list={datalistId}
        />
        <button type="submit">{addButtonLabel}</button>
        {datalistId ? (
          <datalist id={datalistId}>
            {datalistOptions.map((name) => (
              <option key={name} value={name} />
            ))}
          </datalist>
        ) : null}
      </form>
    ) : null}
  </div>
);

const computeStackAngle = (docId, index) => {
  if (index === 0) return 0;
  let hash = 0;
  const source = docId || `stack-${index}`;
  for (let i = 0; i < source.length; i += 1) {
    hash = (hash * 31 + source.charCodeAt(i)) % 997;
  }
  const magnitude = Math.max(3, (hash % 13) + 3);
  const sign = index % 2 === 0 ? 1 : -1;
  return magnitude * sign;
};

const PreviewStack = ({
  items = [],
  maxItems = MAX_PREVIEW_STACK_ITEMS,
  emptyMessage = 'Preview unavailable',
  onItemActivate,
  onOpenPreview,
  onZoomPreview,
}) => {
  const limited = useMemo(() => items.slice(0, maxItems), [items, maxItems]);
  const hasMultiple = limited.length > 1;
  const preparedItems = useMemo(
    () =>
      limited.map((entry, index) => ({
        entry,
        angle: index === 0 ? 0 : computeStackAngle(entry.id, index),
      })),
    [limited],
  );

  if (!limited.length) {
    return <span className="meta">{emptyMessage}</span>;
  }

  return (
    <div className="preview-stack preview-stack--stacked">
      {preparedItems.map(({ entry, angle }, index) => {
        const transform = hasMultiple
          ? `translate(-50%, -50%) rotate(${angle}deg)`
          : 'translate(-50%, -50%)';
        const isFront = index === 0;
        return (
          <div
            key={entry.id || index}
            className={`preview-stack__item orientation-${entry.orientation || 'landscape'}`}
            style={{
              zIndex: preparedItems.length - index,
              transform,
            }}
            aria-hidden={
              hasMultiple && !onItemActivate && !onOpenPreview && !onZoomPreview
                ? 'true'
                : undefined
            }
          >
            <img
              src={entry.url}
              alt={entry.alt || ''}
              className="preview-stack__image"
              onClick={(event) => {
                event.stopPropagation();
                if (isFront) {
                  if (onZoomPreview) {
                    onZoomPreview(entry);
                  } else if (onOpenPreview) {
                    onOpenPreview(entry.id);
                  } else if (onItemActivate) {
                    onItemActivate(entry.id);
                  }
                } else if (onItemActivate) {
                  onItemActivate(entry.id);
                }
              }}
              onKeyDown={(event) => {
                if (!onItemActivate && !onOpenPreview && !onZoomPreview) return;
                if (event.key === 'Enter' || event.key === ' ') {
                  event.preventDefault();
                  event.stopPropagation();
                  if (isFront) {
                    if (onZoomPreview) {
                      onZoomPreview(entry);
                    } else if (onOpenPreview) {
                      onOpenPreview(entry.id);
                    } else {
                      onItemActivate?.(entry.id);
                    }
                  } else {
                    onItemActivate?.(entry.id);
                  }
                }
              }}
            />
          </div>
        );
      })}
    </div>
  );
};

const DetailPanel = ({
  selectedDocuments = [],
  tags = [],
  tagLookupById = new Map(),
  onTagAdd,
  onTagRemove,
  onRegenerateThumbnails,
  onOpenPreview,
  onBulkTagAdd,
  onBulkTagRemove,
  onBulkReanalyze,
  onBulkCorrespondentAdd,
  onBulkCorrespondentRemove,
  onPromoteSelection,
  onUpdateTitle = async () => false,
  ensureAssetUrl = null,
  getDocumentAsset = () => null,
  ensurePreviewData = () => Promise.resolve(),
  correspondents = [],
  onCorrespondentAdd,
  onCorrespondentRemove,
  resolveApiPath,
  onFolderNavigate = null,
  resolveFolderPath = null,
  onClose = () => {},
}) => {
  const selectedCount = selectedDocuments.length;
  const singleDoc = selectedCount === 1 ? selectedDocuments[0] : null;
  const singleDocId = singleDoc?.id || null;
  const selectionKey = useMemo(
    () => selectedDocuments.map((doc) => doc?.id ?? '').join('|'),
    [selectedDocuments],
  );

  const singleDownloadHref = useMemo(() => {
    if (!singleDoc) return null;
    const downloadPath = singleDoc.current_version?.download_path;
    if (!downloadPath || !resolveApiPath) {
      return null;
    }
    return resolveApiPath(downloadPath);
  }, [singleDoc, resolveApiPath]);

  const [titleEditDocId, setTitleEditDocId] = useState(null);
  const [titleDraft, setTitleDraft] = useState('');
  const [titleSaving, setTitleSaving] = useState(false);
  const [titleError, setTitleError] = useState(null);
  const [zoomedPreview, setZoomedPreview] = useState(null);

  const bulkDocumentIds = useMemo(
    () => selectedDocuments.map((doc) => doc?.id).filter(Boolean),
    [selectedDocuments],
  );

  useEffect(() => {
    if (!singleDoc) {
      setTitleEditDocId(null);
      setTitleDraft('');
      setTitleError(null);
      setTitleSaving(false);
      return;
    }

    if (titleEditDocId && titleEditDocId !== singleDoc.id) {
      setTitleEditDocId(null);
      setTitleDraft('');
      setTitleError(null);
      setTitleSaving(false);
    }
  }, [singleDoc, titleEditDocId]);

  useEffect(() => {
    setZoomedPreview(null);
  }, [selectionKey]);

  const startTitleEdit = useCallback(() => {
    if (!singleDoc) return;
    setTitleEditDocId(singleDoc.id);
    setTitleDraft(singleDoc.title || singleDoc.original_name || '');
    setTitleError(null);
  }, [singleDoc]);

  const cancelTitleEdit = useCallback(() => {
    setTitleEditDocId(null);
    setTitleDraft('');
    setTitleError(null);
    setTitleSaving(false);
  }, []);

  const submitTitleEdit = useCallback(
    async (event) => {
      event.preventDefault();
      if (!singleDoc) return;
      const trimmed = titleDraft.trim();
      if (!trimmed) {
        setTitleError('Title cannot be empty.');
        return;
      }
      setTitleSaving(true);
      try {
        const ok = await onUpdateTitle(singleDoc.id, trimmed);
        if (ok) {
          setTitleEditDocId(null);
          setTitleDraft('');
          setTitleError(null);
        } else {
          setTitleError('Failed to update title.');
        }
      } finally {
        setTitleSaving(false);
      }
    },
    [singleDoc, titleDraft, onUpdateTitle],
  );

  const handlePreviewActivate = useCallback(
    (docId) => {
      if (!docId) return;
      onPromoteSelection?.(docId);
    },
    [onPromoteSelection],
  );

  const hasOcrAsset = useMemo(
    () => Boolean(singleDoc && getDocumentAsset(singleDoc, 'ocr-text')),
    [singleDoc, getDocumentAsset],
  );

  const openOcr = useCallback(async () => {
    if (!singleDoc) {
      return;
    }
    try {
      await openOcrTextInNewTab({
        document: singleDoc,
        ensurePreviewData,
        getDocumentAsset,
        ensureAssetUrl,
      });
    } catch (error) {
      /* noop */
    }
  }, [singleDoc, ensurePreviewData, getDocumentAsset, ensureAssetUrl]);

  const singlePreviewNavigator = useAssetNavigator({
    document: singleDoc,
    assetType: 'preview',
    ensureAssetUrl,
    getAsset: getDocumentAsset,
    prefetch: 3,
  });

  const makePreviewItem = useCallback(
    (doc, ordinal = 1) => {
      if (!doc) return null;
      const asset = getDocumentAsset(doc, 'preview');
      const assetView = createAssetView(asset);
      const object = assetView.getObject(ordinal);
      let url = object?.url || null;
      if (!url) {
        url = resolveDocumentAssetUrl(doc, 'preview', {
          ensureAssetUrl,
          getAsset: getDocumentAsset,
          ensureOptions: { start: ordinal, limit: 1 },
          objectOrdinal: ordinal,
        });
      }
      if (!url) {
        return null;
      }
      const metadata = object?.metadata || assetView.getPrimaryMetadata() || {};
      const orientation = derivePreviewOrientation(metadata);
      return {
        id: doc.id,
        url,
        orientation,
        alt: doc.title,
      };
    },
    [ensureAssetUrl, getDocumentAsset],
  );

  const stackDocuments = useMemo(() => {
    if (!selectedDocuments.length) return [];
    const seen = new Set();
    const ordered = [];
    for (let index = selectedDocuments.length - 1; index >= 0; index -= 1) {
      const doc = selectedDocuments[index];
      if (!doc?.id || seen.has(doc.id)) continue;
      seen.add(doc.id);
      ordered.push(doc);
      if (ordered.length >= MAX_PREVIEW_STACK_ITEMS) {
        break;
      }
    }
    return ordered;
  }, [selectedDocuments]);

  const stackTopDocument = stackDocuments[0] || null;
  const stackTopDocId = stackTopDocument?.id || null;
  const stackPreviewNavigator = useAssetNavigator({
    document: stackTopDocument,
    assetType: 'preview',
    ensureAssetUrl,
    getAsset: getDocumentAsset,
    prefetch: 3,
  });

  const singlePreviewItems = useMemo(() => {
    if (!singleDoc) return [];
    const url = singlePreviewNavigator.currentUrl;
    if (!url) {
      return [];
    }
    const orientation = derivePreviewOrientation(singlePreviewNavigator.currentMetadata);
    return [
      {
        id: singleDoc.id,
        url,
        orientation,
        alt: singleDoc.title,
      },
    ];
  }, [singleDoc, singlePreviewNavigator.currentUrl, singlePreviewNavigator.currentMetadata]);

  const stackPreviews = useMemo(() => {
    if (!stackDocuments.length) {
      return [];
    }
    return stackDocuments
      .map((doc) => {
        if (!doc) return null;
        if (stackTopDocument && doc.id === stackTopDocument.id) {
          const url = stackPreviewNavigator.currentUrl;
          if (!url) {
            return null;
          }
          const orientation = derivePreviewOrientation(stackPreviewNavigator.currentMetadata);
          return {
            id: doc.id,
            url,
            orientation,
            alt: doc.title,
          };
        }
        return makePreviewItem(doc, 1);
      })
      .filter(Boolean);
  }, [
    stackDocuments,
    stackTopDocument,
    stackPreviewNavigator.currentUrl,
    stackPreviewNavigator.currentMetadata,
    makePreviewItem,
  ]);

  const singleCardinality = singlePreviewNavigator.cardinality;
  const singleEffectiveCardinality = singleCardinality || (singlePreviewNavigator.currentUrl ? 1 : 0);
  const singleHasPreview = Boolean(singlePreviewNavigator.currentUrl);

  const topDocId = stackTopDocument?.id || null;
  const topCardinality = stackPreviewNavigator.cardinality;
  const topEffectiveCardinality = topCardinality || (stackPreviewNavigator.currentUrl ? 1 : 0);
  const topHasPreview = Boolean(stackPreviewNavigator.currentUrl);

  const bulkTagUnion = useMemo(() => {
    if (!selectedDocuments.length) return [];
    const tagMap = new Map();
    selectedDocuments.forEach((doc) => {
      (doc.tags || []).forEach((tag) => {
        const label = (tag?.label || '').trim();
        if (!label) return;
        if (!tagMap.has(label)) {
          const fallback = tagLookupById.get(tag.id) || {};
          tagMap.set(label, {
            id: tag.id,
            label,
            color: tag.color || fallback.color,
          });
        }
      });
    });
    return [...tagMap.values()].sort((a, b) => a.label.localeCompare(b.label));
  }, [selectedDocuments, tagLookupById]);

  const stackTotalSizeBytes = useMemo(() => {
    if (!stackPreviews.length) return 0;
    const byId = new Map(selectedDocuments.map((doc) => [doc.id, doc]));
    return stackPreviews.reduce((sum, item) => {
      const source = byId.get(item.id);
      const bytes = source?.current_version?.size_bytes;
      return sum + (typeof bytes === 'number' ? bytes : 0);
    }, 0);
  }, [stackPreviews, selectedDocuments]);

  const availableCorrespondents = useMemo(
    () => (Array.isArray(correspondents) ? correspondents : []),
    [correspondents],
  );

  const correspondentOptions = useMemo(() => {
    const seen = new Set();
    return availableCorrespondents
      .map((entry) => (entry?.name || '').trim())
      .filter((name) => {
        if (!name) return false;
        const lower = name.toLowerCase();
        if (seen.has(lower)) {
          return false;
        }
        seen.add(lower);
        return true;
      });
  }, [availableCorrespondents]);

  const singleCorrespondents = useMemo(() => {
    if (!singleDoc) return [];
    return sortCorrespondents(singleDoc.correspondents || []);
  }, [singleDoc]);

  const singleFolderPath = useMemo(() => {
    if (!singleDoc?.folder_id) {
      return null;
    }
    if (typeof resolveFolderPath !== 'function') {
      return null;
    }
    const segments = resolveFolderPath(singleDoc.folder_id);
    if (!Array.isArray(segments) || !segments.some((segment) => segment?.id && segment.id !== 'root')) {
      return null;
    }
    return segments;
  }, [singleDoc?.folder_id, resolveFolderPath]);

  const bulkCorrespondents = useMemo(() => {
    if (selectedDocuments.length <= 1) {
      const doc = selectedDocuments[0];
      return doc ? sortCorrespondents(doc.correspondents || []) : [];
    }

    const map = new Map();
    selectedDocuments.forEach((doc) => {
      if (!doc?.id) return;
      (doc.correspondents || []).forEach((entry) => {
        if (!entry?.id) return;
        if (!map.has(entry.id)) {
          map.set(entry.id, {
            id: entry.id,
            name: entry.name || '',
            documentIds: new Set(),
          });
        }
        map.get(entry.id).documentIds.add(doc.id);
      });
    });

    return [...map.values()]
      .map((entry) => ({
        id: entry.id,
        name: entry.name,
        documentIds: [...entry.documentIds],
        count: entry.documentIds.size,
      }))
      .sort((a, b) => (a.name || '').localeCompare(b.name || ''));
  }, [selectedDocuments]);

  const handleBulkCorrespondentRemove = useCallback(
    (entry) => {
      if (!entry?.id) return;
      if (onBulkCorrespondentRemove) {
        return onBulkCorrespondentRemove({
          assignments: [
            {
              correspondent_id: entry.id,
            },
          ],
          documentIds: entry.documentIds,
        });
      }

      if (!onCorrespondentRemove) return;
      const targets = entry.documentIds && entry.documentIds.length
        ? entry.documentIds
        : selectedDocuments
            .filter((doc) => (doc.correspondents || []).some((item) => item.id === entry.id))
            .map((doc) => doc.id);

      return Promise.all(
        targets.map((documentId) =>
          onCorrespondentRemove({
            documentId,
            correspondentId: entry.id,
          }),
        ),
      ).catch(() => {});
    },
    [onBulkCorrespondentRemove, onCorrespondentRemove, selectedDocuments],
  );

  const openZoomPreview = useCallback((config) => {
    if (!config) return;
    setZoomedPreview({
      mode: config.mode,
      docId: config.docId ?? null,
    });
  }, []);

  const closeZoomPreview = useCallback(() => {
    setZoomedPreview(null);
  }, []);

  const handleSingleZoom = useCallback(
    (entry) => {
      if (!singleHasPreview) return;
      const targetId = entry?.id ?? singleDocId;
      if (!targetId) return;
      openZoomPreview({ mode: 'single', docId: targetId });
    },
    [openZoomPreview, singleHasPreview, singleDocId],
  );

  const handleStackZoom = useCallback(
    (entry) => {
      if (!stackTopDocId || entry?.id !== stackTopDocId) return;
      if (!topHasPreview) return;
      openZoomPreview({ mode: 'stack', docId: stackTopDocId });
    },
    [openZoomPreview, stackTopDocId, topHasPreview],
  );

  const zoomDisplay = useMemo(() => {
    if (!zoomedPreview) {
      return null;
    }

    if (
      zoomedPreview.mode === 'single' &&
      singleDocId &&
      singleDoc &&
      singleHasPreview &&
      zoomedPreview.docId === singleDocId
    ) {
      return {
        url: singlePreviewNavigator.currentUrl,
        alt: singleDoc.title,
        canGoPrev:
          singleEffectiveCardinality > 1 && Boolean(singlePreviewNavigator.canGoPrev),
        canGoNext:
          singleEffectiveCardinality > 1 && Boolean(singlePreviewNavigator.canGoNext),
        goPrev: singlePreviewNavigator.goPrev,
        goNext: singlePreviewNavigator.goNext,
      };
    }

    if (
      zoomedPreview.mode === 'stack' &&
      stackTopDocId &&
      stackTopDocument &&
      zoomedPreview.docId === stackTopDocId &&
      topHasPreview
    ) {
      return {
        url: stackPreviewNavigator.currentUrl,
        alt: stackTopDocument.title,
        canGoPrev:
          topEffectiveCardinality > 1 && Boolean(stackPreviewNavigator.canGoPrev),
        canGoNext:
          topEffectiveCardinality > 1 && Boolean(stackPreviewNavigator.canGoNext),
        goPrev: stackPreviewNavigator.goPrev,
        goNext: stackPreviewNavigator.goNext,
      };
    }

    return null;
  }, [
    zoomedPreview,
    singleDoc,
    singleDocId,
    singleHasPreview,
    singlePreviewNavigator.currentUrl,
    singlePreviewNavigator.canGoPrev,
    singlePreviewNavigator.canGoNext,
    singlePreviewNavigator.goPrev,
    singlePreviewNavigator.goNext,
    singleEffectiveCardinality,
    stackTopDocument,
    stackTopDocId,
    topHasPreview,
    stackPreviewNavigator.currentUrl,
    stackPreviewNavigator.canGoPrev,
    stackPreviewNavigator.canGoNext,
    stackPreviewNavigator.goPrev,
    stackPreviewNavigator.goNext,
    topEffectiveCardinality,
  ]);

  useEffect(() => {
    if (zoomedPreview && !zoomDisplay) {
      setZoomedPreview(null);
    }
  }, [zoomedPreview, zoomDisplay]);

  const {
    documentId: singleNavigatorDocId,
    asset: singleNavigatorAsset,
    ordinal: singleNavigatorOrdinal,
    canGoPrev: singleNavigatorCanGoPrev,
    canGoNext: singleNavigatorCanGoNext,
    cardinality: singleNavigatorCardinality,
  } = singlePreviewNavigator;

  const {
    documentId: stackNavigatorDocId,
    asset: stackNavigatorAsset,
    ordinal: stackNavigatorOrdinal,
    canGoPrev: stackNavigatorCanGoPrev,
    canGoNext: stackNavigatorCanGoNext,
    cardinality: stackNavigatorCardinality,
  } = stackPreviewNavigator;

  useEffect(() => {
    if (typeof ensureAssetUrl !== 'function') {
      return;
    }

    const warmNavigator = (navigator) => {
      const {
        documentId,
        asset,
        ordinal,
        canGoPrev,
        canGoNext,
        cardinality,
      } = navigator;
      if (!documentId || !asset || !Number.isFinite(ordinal)) {
        return;
      }

      const requests = [];
      if (canGoPrev) {
        const prevOrdinal = Math.max(1, ordinal - 1);
        if (!cardinality || prevOrdinal <= cardinality) {
          requests.push(
            ensureAssetUrl(documentId, asset, {
              start: prevOrdinal,
              limit: 1,
              objectOrdinal: prevOrdinal,
            }),
          );
        }
      }
      if (canGoNext) {
        const nextOrdinal = ordinal + 1;
        if (!cardinality || nextOrdinal <= cardinality) {
          requests.push(
            ensureAssetUrl(documentId, asset, {
              start: nextOrdinal,
              limit: 1,
              objectOrdinal: nextOrdinal,
            }),
          );
        }
      }

      requests.forEach((promise) => promise?.catch?.(() => {}));
    };

    const singleWarmState = {
      documentId: singleNavigatorDocId,
      asset: singleNavigatorAsset,
      ordinal: singleNavigatorOrdinal,
      canGoPrev: singleNavigatorCanGoPrev,
      canGoNext: singleNavigatorCanGoNext,
      cardinality: singleNavigatorCardinality,
    };

    const stackWarmState = {
      documentId: stackNavigatorDocId,
      asset: stackNavigatorAsset,
      ordinal: stackNavigatorOrdinal,
      canGoPrev: stackNavigatorCanGoPrev,
      canGoNext: stackNavigatorCanGoNext,
      cardinality: stackNavigatorCardinality,
    };

    warmNavigator(singleWarmState);
    warmNavigator(stackWarmState);
  }, [
    ensureAssetUrl,
    singleNavigatorDocId,
    singleNavigatorAsset,
    singleNavigatorOrdinal,
    singleNavigatorCanGoPrev,
    singleNavigatorCanGoNext,
    singleNavigatorCardinality,
    stackNavigatorDocId,
    stackNavigatorAsset,
    stackNavigatorOrdinal,
    stackNavigatorCanGoPrev,
    stackNavigatorCanGoNext,
    stackNavigatorCardinality,
  ]);

  const renderSingle = () => {
    if (!singleDoc) {
      return <p className="meta">Select a document to view metadata, tags and actions.</p>;
    }

    const displayName = singleDoc.title || singleDoc.original_name;
    const isEditingTitle = titleEditDocId === singleDoc.id;
    const sizeBytes = Number(singleDoc.current_version?.size_bytes) || 0;
    const sizeLabel = sizeBytes > 0 ? formatFileSize(sizeBytes) : '—';
    const issuedAt = singleDoc.issued_at
      ? new Date(singleDoc.issued_at).toLocaleString()
      : '—';
    const tagsForDoc = Array.isArray(singleDoc.tags) ? singleDoc.tags : [];
    const pageCountRaw = singleDoc.current_version?.metadata?.page_count;
    const pageCountValue =
      typeof pageCountRaw === 'number'
        ? pageCountRaw
        : pageCountRaw != null && pageCountRaw !== ''
        ? Number.parseInt(pageCountRaw, 10)
        : null;
    const hasPageCount = Number.isFinite(pageCountValue) && pageCountValue >= 0;
    const metadata =
      singleDoc.metadata && Object.keys(singleDoc.metadata).length > 0 ? singleDoc.metadata : null;
    const effectiveCardinality = singleEffectiveCardinality;
    const canGoPrev = singlePreviewNavigator.canGoPrev;
    const canGoNext = singlePreviewNavigator.canGoNext;
    const hasPreviewImage = singleHasPreview;
    const interceptNavPointer = (event) => {
      event.preventDefault();
      event.stopPropagation();
    };

    return (
      <>
        <div className="preview-pane preview-pane--stack">
          <PreviewStack
            items={singlePreviewItems}
            maxItems={1}
            emptyMessage="Preview loading…"
            onItemActivate={handlePreviewActivate}
            onOpenPreview={onOpenPreview}
            onZoomPreview={handleSingleZoom}
          />
          {hasPreviewImage && (effectiveCardinality > 1 || canGoPrev || canGoNext) ? (
              <div className="preview-pane__nav preview-pane__nav--overlay">
                <button
                  type="button"
                  className="preview-pane__nav-button preview-pane__nav-button--prev"
                  onClick={(event) => {
                    event.preventDefault();
                    event.stopPropagation();
                    singlePreviewNavigator.goPrev();
                  }}
                  onPointerDown={interceptNavPointer}
                  onPointerUp={interceptNavPointer}
                  onMouseDown={interceptNavPointer}
                  onMouseUp={interceptNavPointer}
                  disabled={!canGoPrev}
                  aria-label="Previous preview"
                >
                  <ArrowLeftIcon />
                </button>
              <button
                type="button"
                  className="preview-pane__nav-button preview-pane__nav-button--next"
                  onClick={(event) => {
                    event.preventDefault();
                    event.stopPropagation();
                    singlePreviewNavigator.goNext();
                  }}
                  onPointerDown={interceptNavPointer}
                  onPointerUp={interceptNavPointer}
                  onMouseDown={interceptNavPointer}
                  onMouseUp={interceptNavPointer}
                  disabled={!canGoNext}
                  aria-label="Next preview"
                >
                  <ArrowRightIcon />
                </button>
            </div>
          ) : null}
        </div>
        <div className="doc-title-row">
          {isEditingTitle ? (
            <form className="doc-title-edit" onSubmit={submitTitleEdit}>
              <input
                value={titleDraft}
                onChange={(event) => {
                  setTitleDraft(event.target.value);
                  if (titleError) {
                    setTitleError(null);
                  }
                }}
                onKeyDown={(event) => {
                  if (event.key === 'Escape') {
                    event.preventDefault();
                    cancelTitleEdit();
                  }
                }}
                aria-label="Document title"
                autoFocus
              />
              <button type="submit" disabled={titleSaving}>
                Save
              </button>
              <button
                type="button"
                className="secondary"
                onClick={cancelTitleEdit}
                disabled={titleSaving}
              >
                Cancel
              </button>
            </form>
          ) : (
            <>
              <h3 style={{ margin: 0 }}>{displayName}</h3>
              <button
                type="button"
                className="icon-button"
                onClick={startTitleEdit}
                aria-label="Edit title"
                title="Edit title"
              >
                <EditIcon className="icon-inline" />
              </button>
            </>
          )}
        </div>
        {titleError ? <div className="status-inline error">{titleError}</div> : null}
        <div className="meta">
          <div>
            <strong>Uploaded:</strong>{' '}
            {singleDoc.uploaded_at ? new Date(singleDoc.uploaded_at).toLocaleString() : '—'}
          </div>
          <div>
            <strong>Size:</strong>{' '}
            {sizeLabel}
          </div>
          <div>
            <strong>Type:</strong> {singleDoc.content_type || 'Unknown'}
          </div>
          <div>
            <strong>Issued:</strong> {issuedAt}
          </div>
          {hasPageCount ? (
            <div>
              <strong>Pages:</strong> {pageCountValue}
            </div>
          ) : null}
          {singleFolderPath?.length ? (
            <div>
              <strong>Folder:</strong>{' '}
              <span className="detail-folder-path">
                {singleFolderPath.map((segment, index) => {
                  const label = segment?.name || '…';
                  const targetId = segment?.id || null;
                  const key = `${targetId || label}-${index}`;
                  const isClickable = Boolean(targetId) && typeof onFolderNavigate === 'function';
                  const href = !isClickable
                    ? null
                    : targetId === 'root'
                    ? '/documents'
                    : `/documents/folder/${targetId}`;
                  return (
                    <React.Fragment key={key}>
                      {index > 0 ? <span className="detail-folder-path__separator">/</span> : null}
                      {isClickable ? (
                        <a
                          href={href}
                          className="detail-folder-path__link"
                          onClick={(event) => {
                            if (
                              event.button !== 0 ||
                              event.metaKey ||
                              event.ctrlKey ||
                              event.shiftKey ||
                              event.altKey
                            ) {
                              return;
                            }
                            event.preventDefault();
                            event.stopPropagation();
                            onFolderNavigate(targetId);
                          }}
                        >
                          {label}
                        </a>
                      ) : (
                        <span className="detail-folder-path__segment">{label}</span>
                      )}
                    </React.Fragment>
                  );
                })}
              </span>
            </div>
          ) : null}
          <div>
            <strong>Original filename:</strong>{' '}
            {singleDoc.original_name}
          </div>
        </div>
        <TagSection
          title="Tags"
          tags={tagsForDoc.map((tag) => ({
            id: tag.id,
            label: tag.label,
            color: tag.color || tagLookupById.get(tag.id)?.color,
          }))}
          onRemove={(tag) => onTagRemove(singleDoc.id, tag.id)}
          onAdd={({ value, input }) => onTagAdd(singleDoc, value, input)}
          datalistId="tag-catalog-single"
          datalistOptions={tags}
        />
        <CorrespondentSection
          title="Correspondents"
          entries={singleCorrespondents}
          onRemove={(entry) =>
            onCorrespondentRemove?.({
              documentId: singleDoc.id,
              correspondentId: entry.id,
            })
          }
          onAdd={({ name, input }) =>
            onCorrespondentAdd?.({
              document: singleDoc,
              name,
              input,
            })
          }
          datalistId="correspondent-catalog-single"
          datalistOptions={correspondentOptions}
        />
        {metadata && (
          <div>
            <dt>Metadata</dt>
            <pre className="detail-metadata__block">{JSON.stringify(metadata, null, 2)}</pre>
          </div>
        )}
      </>
    );
  };

  const renderBulk = () => {
    const countLabel = `${selectedCount} document${selectedCount === 1 ? '' : 's'}`;
    const sizeLabel = stackTotalSizeBytes ? formatFileSize(stackTotalSizeBytes) : '—';
    const topDocIdLocal = topDocId;
    const topCardinalityLocal = topEffectiveCardinality;
    const topHasPreview = Boolean(stackPreviewNavigator.currentUrl);
    const topCanGoPrev = stackPreviewNavigator.canGoPrev;
    const topCanGoNext = stackPreviewNavigator.canGoNext;
    const interceptTopNavPointer = (event) => {
      event.preventDefault();
      event.stopPropagation();
    };
    const documentIds = bulkDocumentIds;

    return (
      <>
        <div className="preview-pane preview-pane--stack">
          <PreviewStack
            items={stackPreviews}
            emptyMessage="No previews available."
            onItemActivate={handlePreviewActivate}
            onOpenPreview={onOpenPreview}
            onZoomPreview={handleStackZoom}
          />
          {topDocIdLocal && topHasPreview && (topCardinalityLocal > 1 || topCanGoPrev || topCanGoNext) ? (
            <div className="preview-pane__nav preview-pane__nav--overlay">
                <button
                  type="button"
                  className="preview-pane__nav-button preview-pane__nav-button--prev"
                  onClick={(event) => {
                    event.preventDefault();
                    event.stopPropagation();
                    stackPreviewNavigator.goPrev();
                  }}
                  onPointerDown={interceptTopNavPointer}
                  onPointerUp={interceptTopNavPointer}
                  onMouseDown={interceptTopNavPointer}
                  onMouseUp={interceptTopNavPointer}
                  disabled={!topCanGoPrev}
                  aria-label="Previous preview"
                >
                  <ArrowLeftIcon />
                </button>
              <button
                type="button"
                  className="preview-pane__nav-button preview-pane__nav-button--next"
                  onClick={(event) => {
                    event.preventDefault();
                    event.stopPropagation();
                    stackPreviewNavigator.goNext();
                  }}
                  onPointerDown={interceptTopNavPointer}
                  onPointerUp={interceptTopNavPointer}
                  onMouseDown={interceptTopNavPointer}
                  onMouseUp={interceptTopNavPointer}
                  disabled={!topCanGoNext}
                  aria-label="Next preview"
                >
                  <ArrowRightIcon />
                </button>
            </div>
          ) : null}
        </div>
        <h3 style={{ margin: 0 }}>{countLabel}</h3>
        <div className="meta">
          <div>
            <strong>Total size (stack):</strong> {sizeLabel}
          </div>
        </div>
        <TagSection
          title="Tags"
          tags={bulkTagUnion}
          emptyMessage="No tags assigned."
          onRemove={(tag) => onBulkTagRemove?.({ label: tag.label, documentIds })}
          onAdd={({ value, input }) =>
            onBulkTagAdd?.({ label: value, input, documentIds })
          }
          addPlaceholder="Add tag to selection"
          addButtonLabel="Add tag"
          datalistId="tag-catalog-bulk"
          datalistOptions={tags}
          className="bulk-tags"
        />
        <CorrespondentSection
          title="Correspondents"
          entries={bulkCorrespondents}
          onRemove={handleBulkCorrespondentRemove}
          onAdd={({ name, input }) =>
            onBulkCorrespondentAdd?.({ name, input, documentIds })
          }
          addPlaceholder="Add correspondent to selection"
          datalistId="correspondent-catalog-bulk"
          datalistOptions={correspondentOptions}
          showCount
          className="bulk-correspondents"
        />
      </>
    );
  };

  const isBulkSelection = selectedCount > 1;
  const showOcrAction = Boolean(singleDoc && hasOcrAsset);

  return (
    <>
      <aside className="detail-panel panel">
        <div className="panel-header">
          <div className="panel-actions">
          <button
            type="button"
            className="icon-button"
            onClick={onClose}
            aria-label="Close detail panel"
            title="Close detail panel"
          >
            <ChevronsRightIcon />
          </button>
          {singleDoc ? (
            <button
              type="button"
              className="icon-button"
              onClick={(event) => {
                event.stopPropagation();
                onOpenPreview(singleDoc.id);
              }}
              aria-label="Open preview"
              title="Open preview"
              disabled={!singleHasPreview}
            >
              <WindowMaximizeIcon />
            </button>
          ) : null}
          <div className="spacer" />
          {isBulkSelection && onBulkReanalyze ? (
            <button
              type="button"
              className="icon-button"
              onClick={(event) => {
                event.stopPropagation();
                onBulkReanalyze(bulkDocumentIds);
              }}
              aria-label="Re-run analysis for selection"
              title="Re-run analysis for selection"
            >
              <AnalyzeIcon />
            </button>
          ) : null}
          {singleDoc && singleDownloadHref ? (
            <a
              className="icon-button"
              href={singleDownloadHref}
              target="_blank"
              rel="noopener noreferrer"
              aria-label="Download document"
              title="Download document"
              onClick={(event) => event.stopPropagation()}
            >
              <DownloadIcon />
            </a>
          ) : null}
          {showOcrAction ? (
            <button
              type="button"
              className="icon-button"
              onClick={(event) => {
                event.stopPropagation();
                openOcr();
              }}
              aria-label="View OCR text"
              title="View OCR text"
            >
              <TextScanIcon />
            </button>
          ) : null}
          {singleDoc ? (
            <button
              type="button"
              className="icon-button"
              onClick={(event) => {
                event.stopPropagation();
                onRegenerateThumbnails(singleDoc.id);
              }}
              aria-label="Re-run analysis"
              title="Re-run analysis"
            >
              <AnalyzeIcon />
            </button>
          ) : null}
        </div>
      </div>
      <div className="panel-body">
        {selectedCount <= 1 ? renderSingle() : renderBulk()}
      </div>
      </aside>
      <PreviewZoomOverlay
        open={Boolean(zoomDisplay?.url)}
        display={zoomDisplay}
        onClose={closeZoomPreview}
      />
    </>
  );
};

export default DetailPanel;
