import React, { useEffect, useRef, useState } from 'react';
import Layout from './components/Layout';
import FileLoader from './components/FileLoader';
import ReorderPanel from './components/ReorderPanel';
import ActionsPanel from './components/ActionsPanel';
import PagePreviewModal from './components/PagePreviewModal';
import { PDFDocument } from 'pdf-lib';
import type { PageRef, PdfFile, SplitResult } from './pdf/pdfTypes';
import {
  loadPdfFromFile,
  mergePdfFiles,
  splitIntoSinglePages,
  exportPages,
} from './pdf/pdfService';
import {
  generateThumbnailsProgressive,
  generateThumbnailsWithRotationsProgressive,
} from './pdf/pdfThumbnailService';

const THUMBNAIL_MAX_HEIGHT = 150;
const THUMBNAIL_MAX_WIDTH = 140;
const THUMBNAIL_CONCURRENCY = 3;

function createPageRefId(): string {
  return Math.random().toString(36).slice(2);
}

function createInitialPageRefs(pageCount: number): PageRef[] {
  return Array.from({ length: pageCount }, (_, sourcePageIndex) => ({
    id: createPageRefId(),
    sourcePageIndex,
    rotation: 0,
  }));
}

function normalizeRotation(rotation: number | undefined): number {
  return (((rotation ?? 0) % 360) + 360) % 360;
}

function thumbnailCacheKey(
  pdfId: string,
  sourcePageIndex: number,
  rotation: number
): string {
  return [
    pdfId,
    sourcePageIndex,
    normalizeRotation(rotation),
    THUMBNAIL_MAX_WIDTH,
    THUMBNAIL_MAX_HEIGHT,
  ].join(':');
}

function isEditableKeyboardTarget(target: EventTarget | null): boolean {
  if (!(target instanceof HTMLElement)) return false;

  const tagName = target.tagName.toLowerCase();
  return (
    target.isContentEditable ||
    tagName === 'input' ||
    tagName === 'textarea' ||
    tagName === 'select'
  );
}

const App: React.FC = () => {
  const [pdf, setPdf] = useState<PdfFile | null>(null);
  const [isBusy, setIsBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const [pages, setPages] = useState<PageRef[]>([]);
  const [reorderThumbnails, setReorderThumbnails] = useState<Record<string, string>>({});

  const [selectedPageIds, setSelectedPageIds] = useState<string[]>([]);
  const [lastSelectedVisualIndex, setLastSelectedVisualIndex] = useState<number | null>(null);

  const [splitResults, setSplitResults] = useState<SplitResult[]>([]);
  const [subsetUrl, setSubsetUrl] = useState<string | null>(null);
  const [subsetFilename, setSubsetFilename] = useState<string | null>(null);
  const [exportUrl, setExportUrl] = useState<string | null>(null);
  const [exportFilename, setExportFilename] = useState<string | null>(null);

  const [previewPageId, setPreviewPageId] = useState<string | null>(null);

  const [pendingFile, setPendingFile] = useState<File | null>(null);
  const [showMergeOptions, setShowMergeOptions] = useState(false);
  const [mergeMode, setMergeMode] = useState<'overwrite' | 'append' | 'insertAt'>('append');
  const [mergeInsertAt, setMergeInsertAt] = useState<string>('');

  const thumbnailCacheRef = useRef<Map<string, string>>(new Map());
  const latestPagesRef = useRef<PageRef[]>([]);
  const previousPageRotationsRef = useRef<Map<string, number>>(new Map());

  const resetGeneratedUrls = () => {
    if (subsetUrl) {
      URL.revokeObjectURL(subsetUrl);
      setSubsetUrl(null);
      setSubsetFilename(null);
    }

    if (exportUrl) {
      URL.revokeObjectURL(exportUrl);
      setExportUrl(null);
      setExportFilename(null);
    }
  };

  const resetWorkspaceState = () => {
    setSplitResults([]);
    setSelectedPageIds([]);
     setLastSelectedVisualIndex(null);
    resetGeneratedUrls();
    setReorderThumbnails({});
    thumbnailCacheRef.current.clear();
    previousPageRotationsRef.current.clear();
    latestPagesRef.current = [];
    setPages([]);
    setPreviewPageId(null);
  };

  const loadFileAsNew = async (file: File) => {
    setError(null);
    resetWorkspaceState();

    setIsBusy(true);
    try {
      const loaded = await loadPdfFromFile(file);
      const initialPages = createInitialPageRefs(loaded.pageCount);

      setPdf(loaded);
      setPages(initialPages);
      latestPagesRef.current = initialPages;
    } catch (e) {
      console.error(e);
      setError('Failed to load PDF (see console).');
    } finally {
      setIsBusy(false);
    }
  };

  const handleFileLoaded = (file: File) => {
    if (!pdf || pages.length === 0) {
      void loadFileAsNew(file);
    } else {
      setPendingFile(file);
      setShowMergeOptions(true);
      setMergeMode('append');
      setMergeInsertAt(String(pages.length + 1));
    }
  };

  const handleMergeCancel = () => {
    setPendingFile(null);
    setShowMergeOptions(false);
  };

  const handleMergeConfirm = async () => {
    if (!pendingFile) return;

    if (!pdf || mergeMode === 'overwrite') {
      await loadFileAsNew(pendingFile);
      setPendingFile(null);
      setShowMergeOptions(false);
      return;
    }

    setError(null);
    setIsBusy(true);

    try {
      // 1) Materialize the current in-memory workspace (page refs + rotations)
      const currentBlob = await exportPages(pdf, pages);
      const currentArrayBuffer = await currentBlob.arrayBuffer();
      const currentDoc = await PDFDocument.load(currentArrayBuffer);
      const currentPdf: PdfFile = {
        id: pdf.id,
        name: pdf.name,
        doc: currentDoc,
        arrayBuffer: currentArrayBuffer,
        pageCount: pages.length,
      };

      // 2) Load the new PDF
      const newPdf = await loadPdfFromFile(pendingFile);

      // 3) Determine insert position (0-based)
      let insertAt = pages.length; // default: append at end
      if (mergeMode === 'insertAt') {
        const parsed = parseInt(mergeInsertAt, 10);
        if (Number.isFinite(parsed)) {
          insertAt = Math.min(Math.max(parsed - 1, 0), pages.length);
        }
      } else if (mergeMode === 'append') {
        insertAt = pages.length;
      }

      // 4) Merge
      const mergedPdf = await mergePdfFiles(currentPdf, newPdf, insertAt);
      const mergedPages = createInitialPageRefs(mergedPdf.pageCount);

      // 5) Reset state to the merged document
      setPdf(mergedPdf);

      setPages(mergedPages);
      latestPagesRef.current = mergedPages;
      setSelectedPageIds([]);
      setLastSelectedVisualIndex(null);
      setSplitResults([]);
      resetGeneratedUrls();
      setReorderThumbnails({});
      thumbnailCacheRef.current.clear();
      previousPageRotationsRef.current.clear();
      setPreviewPageId(null);
    } catch (e) {
      console.error(e);
      setError('Failed to merge PDF (see console).');
    } finally {
      setIsBusy(false);
      setPendingFile(null);
      setShowMergeOptions(false);
    }
  };

  useEffect(() => {
    latestPagesRef.current = pages;
  }, [pages]);

  useEffect(() => {
    if (!pdf) {
      setReorderThumbnails({});
      thumbnailCacheRef.current.clear();
      previousPageRotationsRef.current.clear();
      return;
    }

    const controller = new AbortController();

    latestPagesRef.current = pages;
    thumbnailCacheRef.current.clear();
    previousPageRotationsRef.current = new Map(
      pages.map((page) => [page.id, normalizeRotation(page.rotation)])
    );
    setReorderThumbnails({});

    void generateThumbnailsProgressive(pdf.arrayBuffer, {
      maxHeight: THUMBNAIL_MAX_HEIGHT,
      maxWidth: THUMBNAIL_MAX_WIDTH,
      concurrency: THUMBNAIL_CONCURRENCY,
      signal: controller.signal,
      onThumbnail: ({ pageIndex, dataUrl }) => {
        if (controller.signal.aborted) return;

        thumbnailCacheRef.current.set(
          thumbnailCacheKey(pdf.id, pageIndex, 0),
          dataUrl
        );

        const currentPages = latestPagesRef.current;
        const updates: Record<string, string> = {};

        for (const page of currentPages) {
          if (
            page.sourcePageIndex === pageIndex &&
            normalizeRotation(page.rotation) === 0
          ) {
            updates[page.id] = dataUrl;
          }
        }

        if (Object.keys(updates).length === 0) return;

        setReorderThumbnails((prev) => ({
          ...prev,
          ...updates,
        }));
      },
    }).catch((e) => {
      if (!controller.signal.aborted) {
        console.error(e);
        setError('Failed to generate thumbnails (see console).');
      }
    });

    return () => {
      controller.abort();
    };
  }, [pdf]);

  useEffect(() => {
    if (!pdf) {
      previousPageRotationsRef.current.clear();
      return;
    }

    const previousRotations = previousPageRotationsRef.current;
    const changedPages = pages.filter(
      (page) =>
        normalizeRotation(previousRotations.get(page.id)) !==
        normalizeRotation(page.rotation)
    );

    previousPageRotationsRef.current = new Map(
      pages.map((page) => [page.id, normalizeRotation(page.rotation)])
    );

    if (changedPages.length === 0) return;

    const cachedUpdates: Record<string, string> = {};
    const pagesToRender: PageRef[] = [];

    for (const page of changedPages) {
      const rotation = normalizeRotation(page.rotation);
      const cached = thumbnailCacheRef.current.get(
        thumbnailCacheKey(pdf.id, page.sourcePageIndex, rotation)
      );

      if (cached) {
        cachedUpdates[page.id] = cached;
      } else {
        pagesToRender.push(page);
      }
    }

    if (Object.keys(cachedUpdates).length > 0) {
      setReorderThumbnails((prev) => ({
        ...prev,
        ...cachedUpdates,
      }));
    }

    if (pagesToRender.length === 0) return;

    const controller = new AbortController();
    const groups = new Map<number, PageRef[]>();

    for (const page of pagesToRender) {
      const rotation = normalizeRotation(page.rotation);
      const group = groups.get(rotation) ?? [];
      group.push(page);
      groups.set(rotation, group);
    }

    const renderGroups = async () => {
      for (const [rotation, groupPages] of groups) {
        if (controller.signal.aborted) return;

        const pageIndices = Array.from(
          new Set(groupPages.map((page) => page.sourcePageIndex))
        );
        const rotationsBySourcePage: Record<number, number> = {};

        for (const pageIndex of pageIndices) {
          rotationsBySourcePage[pageIndex] = rotation;
        }

        await generateThumbnailsWithRotationsProgressive(
          pdf.arrayBuffer,
          rotationsBySourcePage,
          {
            maxHeight: THUMBNAIL_MAX_HEIGHT,
            maxWidth: THUMBNAIL_MAX_WIDTH,
            concurrency: Math.min(THUMBNAIL_CONCURRENCY, pageIndices.length),
            pageIndices,
            signal: controller.signal,
            onThumbnail: ({ pageIndex, dataUrl }) => {
              if (controller.signal.aborted) return;

              thumbnailCacheRef.current.set(
                thumbnailCacheKey(pdf.id, pageIndex, rotation),
                dataUrl
              );

              const updates: Record<string, string> = {};

              for (const page of latestPagesRef.current) {
                if (
                  page.sourcePageIndex === pageIndex &&
                  normalizeRotation(page.rotation) === rotation
                ) {
                  updates[page.id] = dataUrl;
                }
              }

              if (Object.keys(updates).length === 0) return;

              setReorderThumbnails((prev) => ({
                ...prev,
                ...updates,
              }));
            },
          }
        );
      }
    };

    void renderGroups().catch((e) => {
      if (!controller.signal.aborted) {
        console.error(e);
        setError('Failed to generate rotated thumbnails (see console).');
      }
    });

    return () => {
      controller.abort();
    };
  }, [pdf, pages]);

  useEffect(() => {
    if (previewPageId != null && !pages.some((page) => page.id === previewPageId)) {
      setPreviewPageId(null);
    }
  }, [previewPageId, pages]);

  const hasPdf = !!pdf;

  // === UI interactions ===

  const handleRotatePageClockwise = (pageId: string) => {
    setPages((prev) =>
      prev.map((page) =>
        page.id === pageId
          ? { ...page, rotation: (normalizeRotation(page.rotation) + 90) % 360 }
          : page
      )
    );
  };

  const handleRotatePageCounterclockwise = (pageId: string) => {
    setPages((prev) =>
      prev.map((page) =>
        page.id === pageId
          ? { ...page, rotation: (normalizeRotation(page.rotation) + 270) % 360 }
          : page
      )
    );
  };

  const handleDeletePage = (pageId: string) => {
    setPages((prev) => prev.filter((page) => page.id !== pageId));
    setSelectedPageIds((prev) => prev.filter((id) => id !== pageId));
  };

  const handleReorder = (newPages: PageRef[]) => {
    setPages(newPages);
  };

  const handleToggleSelect = (
    pageId: string,
    visualIndex: number,
    e: React.MouseEvent<HTMLButtonElement>
  ) => {
    setSelectedPageIds((prev) => {
      if (e.shiftKey && lastSelectedVisualIndex !== null && pages.length > 0) {
        const from = Math.min(lastSelectedVisualIndex, visualIndex);
        const to = Math.max(lastSelectedVisualIndex, visualIndex);
        const rangeIds = pages.slice(from, to + 1).map((page) => page.id);

        const set = new Set(prev);
        rangeIds.forEach((id) => set.add(id));
        return Array.from(set);
      }

      if (prev.includes(pageId)) {
        return prev.filter((id) => id !== pageId);
      }

      return [...prev, pageId];
    });

    setLastSelectedVisualIndex(visualIndex);
  };

  const handleSelectAll = () => {
    setSelectedPageIds(pages.map((page) => page.id));
    setLastSelectedVisualIndex(null);
  };

  const handleClearSelection = () => {
    setSelectedPageIds([]);
    setLastSelectedVisualIndex(null);
  };
  
  const handleDeleteSelected = () => {
    if (selectedPageIds.length === 0) return;
    const selectedSet = new Set(selectedPageIds);
    setPages((prev) => prev.filter((page) => !selectedSet.has(page.id)));
    setSelectedPageIds([]);
    setLastSelectedVisualIndex(null);
  };

  const handleCopyPagesToSlot = (pageIds: string[], insertSlot: number) => {
    if (!pdf || pageIds.length === 0) return;

    const pageIdSet = new Set(pageIds);

    // Copy in current visual order, not in arbitrary selectedPageIds order.
    const sourcePages = pages.filter((page) => pageIdSet.has(page.id));

    if (sourcePages.length === 0) return;

    const copiedPages: PageRef[] = sourcePages.map((page) => ({
      ...page,
      id: createPageRefId(),
    }));

    const clampedSlot = Math.min(Math.max(insertSlot, 0), pages.length);

    const thumbnailUpdates: Record<string, string> = {};

    sourcePages.forEach((sourcePage, index) => {
      const copiedPage = copiedPages[index];

      const thumbnail =
        reorderThumbnails[sourcePage.id] ??
        thumbnailCacheRef.current.get(
          thumbnailCacheKey(
            pdf.id,
            sourcePage.sourcePageIndex,
            sourcePage.rotation
          )
        );

      if (thumbnail) {
        thumbnailUpdates[copiedPage.id] = thumbnail;
      }
    });

    setPages((prev) => {
      const slot = Math.min(Math.max(clampedSlot, 0), prev.length);

      return [
        ...prev.slice(0, slot),
        ...copiedPages,
        ...prev.slice(slot),
      ];
    });

    // Select the newly created copies.
    setSelectedPageIds(copiedPages.map((page) => page.id));
    setLastSelectedVisualIndex(null);

    if (Object.keys(thumbnailUpdates).length > 0) {
      setReorderThumbnails((prev) => ({
        ...prev,
        ...thumbnailUpdates,
      }));
    }

    // Existing generated outputs no longer represent the current workspace.
    setSplitResults([]);
    resetGeneratedUrls();
  };

  const handleOpenPreview = (pageId: string) => {
    setPreviewPageId(pageId);
  };

  const handleClosePreview = () => {
    setPreviewPageId(null);
  };

  const handlePreviewPrevious = () => {
    setPreviewPageId((current) => {
      if (current == null || pages.length === 0) return current;

      const visualIndex = pages.findIndex((page) => page.id === current);
      if (visualIndex <= 0) return current;

      return pages[visualIndex - 1].id;
    });
  };

  const handlePreviewNext = () => {
    setPreviewPageId((current) => {
      if (current == null || pages.length === 0) return current;

      const visualIndex = pages.findIndex((page) => page.id === current);
      if (visualIndex < 0 || visualIndex >= pages.length - 1) return current;

      return pages[visualIndex + 1].id;
    });
  };

  useEffect(() => {
    if (!hasPdf || previewPageId !== null) return;

    const handleKeyDown = (e: KeyboardEvent) => {
      if (isEditableKeyboardTarget(e.target)) return;

      const key = e.key.toLowerCase();

      if ((e.ctrlKey || e.metaKey) && key === 'a') {
        e.preventDefault();
        setSelectedPageIds(pages.map((page) => page.id));
        setLastSelectedVisualIndex(null);
        return;
      }

      if ((e.key === 'Delete' || e.key === 'Backspace') && selectedPageIds.length > 0) {
        e.preventDefault();
        const selectedSet = new Set(selectedPageIds);
        setPages((prev) => prev.filter((page) => !selectedSet.has(page.id)));
        setSelectedPageIds([]);
        setLastSelectedVisualIndex(null);
        return;
      }

      if (e.key === 'Escape' && selectedPageIds.length > 0) {
        e.preventDefault();
        setSelectedPageIds([]);
        setLastSelectedVisualIndex(null);
      }
    };

    window.addEventListener('keydown', handleKeyDown);

    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, [hasPdf, previewPageId, pages, selectedPageIds]);

  const handleSplit = async () => {
    if (!pdf) return;
    setError(null);
    setIsBusy(true);
    try {
      const result = await splitIntoSinglePages(pdf);
      setSplitResults(result);
    } catch (e) {
      console.error(e);
      setError('Error while splitting PDF (see console).');
    } finally {
      setIsBusy(false);
    }
  };

  const handleExtractSelected = async () => {
    if (!pdf || selectedPageIds.length === 0) return
    setError(null);
    setIsBusy(true);

    if (subsetUrl) {
      URL.revokeObjectURL(subsetUrl);
      setSubsetUrl(null);
      setSubsetFilename(null);
    }

    try {
      const selectedSet = new Set(selectedPageIds);
      const selectedPages = pages.filter((page) => selectedSet.has(page.id));
      const blob = await exportPages(pdf, selectedPages);
      const url = URL.createObjectURL(blob);
      const base = pdf.name.replace(/\.pdf$/i, '');
      const filename = `${base}_selected.pdf`;
      setSubsetUrl(url);
      setSubsetFilename(filename);
    } catch (e) {
      console.error(e);
      setError('Error while extracting selected pages (see console).');
    } finally {
      setIsBusy(false);
    }
  };

  const handleExportReordered = async () => {
    if (!pdf || pages.length === 0) return;
    setError(null);
    setIsBusy(true);

    if (exportUrl) {
      URL.revokeObjectURL(exportUrl);
      setExportUrl(null);
      setExportFilename(null);
    }

    try {
      const blob = await exportPages(pdf, pages);
      const url = URL.createObjectURL(blob);
      const base = pdf.name.replace(/\.pdf$/i, '');
      const filename = `${base}_reordered.pdf`;
      setExportUrl(url);
      setExportFilename(filename);
    } catch (e) {
      console.error(e);
      setError('Error while exporting reordered PDF (see console).');
    } finally {
      setIsBusy(false);
    }
  };

  const previewVisualIndex =
    previewPageId != null
      ? pages.findIndex((page) => page.id === previewPageId)
      : -1;
  const previewPage =
    previewVisualIndex >= 0 ? pages[previewVisualIndex] : null;

  const canPreviewPrevious = previewVisualIndex > 0;
  const canPreviewNext =
    previewVisualIndex >= 0 && previewVisualIndex < pages.length - 1;

  return (
    <Layout>
      <FileLoader pdf={pdf} onFileLoaded={handleFileLoaded} />

      {showMergeOptions && pendingFile && pdf && pages.length > 0 && (
        <div
          className="card"
          style={{ border: '1px solid #bfdbfe', background: '#eff6ff' }}
        >
          <h2>Open file: merge or replace?</h2>
          <p style={{ fontSize: '0.85rem', color: '#374151' }}>
            You already have <strong>{pdf.name}</strong> with {pages.length}{' '}
            pages open. What should happen with{' '}
            <strong>{pendingFile.name}</strong>?
          </p>

          <div
            style={{
              display: 'flex',
              flexDirection: 'column',
              gap: '0.5rem',
              marginTop: '0.5rem',
              fontSize: '0.9rem',
            }}
          >
            <label
              style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}
            >
              <input
                type="radio"
                name="mergeMode"
                value="overwrite"
                checked={mergeMode === 'overwrite'}
                onChange={() => setMergeMode('overwrite')}
              />
              <span>Replace current document</span>
            </label>

            <label
              style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}
            >
              <input
                type="radio"
                name="mergeMode"
                value="append"
                checked={mergeMode === 'append'}
                onChange={() => setMergeMode('append')}
              />
              <span>Merge and append pages at the end</span>
            </label>

            <label
              style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}
            >
              <input
                type="radio"
                name="mergeMode"
                value="insertAt"
                checked={mergeMode === 'insertAt'}
                onChange={() => setMergeMode('insertAt')}
              />
              <span>
                Merge and insert starting at position{' '}
                <input
                  type="number"
                  min={1}
                  max={pages.length + 1}
                  value={mergeInsertAt}
                  onChange={(e) => setMergeInsertAt(e.target.value)}
                  style={{
                    width: '4rem',
                    padding: '0.15rem 0.3rem',
                    fontSize: '0.85rem',
                  }}
                />{' '}
                <span style={{ color: '#6b7280' }}>
                  (1 = before first page, {pages.length + 1} = after last page)
                </span>
              </span>
            </label>
          </div>

          <div className="button-row" style={{ marginTop: '0.75rem' }}>
            <button
              className="secondary"
              type="button"
              onClick={handleMergeCancel}
              disabled={isBusy}
            >
              Cancel
            </button>
            <button
              className="primary"
              type="button"
              onClick={handleMergeConfirm}
              disabled={isBusy}
            >
              {isBusy ? 'Working…' : 'Continue'}
            </button>
          </div>
        </div>
      )}

      <ReorderPanel
        pages={pages}
        thumbnails={reorderThumbnails}
        isBusy={isBusy}
        hasPdf={hasPdf}
        selectedPageIds={selectedPageIds}
        onRotateClockwise={handleRotatePageClockwise}
        onRotateCounterclockwise={handleRotatePageCounterclockwise}
        onDelete={handleDeletePage}
        onReorder={handleReorder}
        onCopyPagesToSlot={handleCopyPagesToSlot}
        onToggleSelect={handleToggleSelect}
        onSelectAll={handleSelectAll}
        onOpenPreview={handleOpenPreview}
        onClearSelection={handleClearSelection}
        onDeleteSelected={handleDeleteSelected}
      />


      <ActionsPanel
        hasPdf={hasPdf}
        isBusy={isBusy}
        selectedCount={selectedPageIds.length}
        onSplit={handleSplit}
        onExtractSelected={handleExtractSelected}
        onExportReordered={handleExportReordered}
        splitResults={splitResults}
        subsetDownloadUrl={subsetUrl}
        subsetFilename={subsetFilename}
        exportDownloadUrl={exportUrl}
        exportFilename={exportFilename}
      />

      {error && (
        <div
          className="card"
          style={{ border: '1px solid #fecaca', background: '#fef2f2' }}
        >
          <strong>Error:</strong> {error}
        </div>
      )}

      <PagePreviewModal
        isOpen={previewPage !== null}
        pdf={pdf}
        pageIndex={previewPage?.sourcePageIndex ?? null}
        rotation={previewPage?.rotation ?? 0}
        visualIndex={previewVisualIndex >= 0 ? previewVisualIndex : null}
        totalPages={pages.length}
        canGoPrevious={canPreviewPrevious}
        canGoNext={canPreviewNext}
        onPrevious={handlePreviewPrevious}
        onNext={handlePreviewNext}
        onClose={handleClosePreview}
      />
    </Layout>
  );
};

export default App;
