import { useCallback, useEffect, useRef, useState } from "react";
import * as pdfjs from "pdfjs-dist";
import { ReaderError } from "./ReaderError";
import { pdfCanvasHasVisibleInk, pdfRenderScale } from "./pdfRender";
import { configurePdfWorker } from "./pdfWorker";
import type { ReaderControls } from "./ReaderShell";
import type { ReaderMode } from "../api/types";

configurePdfWorker(pdfjs);

type PageCommitStrategy = "immediate" | "queued";

type PdfReaderProps = {
  url: string;
  page: number;
  backHref: string;
  mode: ReaderMode;
  onPageCommit: (page: number, pages: number, anchor?: number, strategy?: PageCommitStrategy) => void;
  onControlsChange: (controls: ReaderControls) => void;
};

type PdfPageCanvasProps = {
  document: pdfjs.PDFDocumentProxy;
  pageNumber: number;
  mode: ReaderMode;
  frameSize: { width: number; height: number };
  onError: (message: string) => void;
};

let pdfLifecycle = Promise.resolve();

function enqueuePdfLifecycle<T>(work: () => Promise<T>) {
  const run = pdfLifecycle.then(work, work);
  pdfLifecycle = run.then(() => undefined, () => undefined);
  return run;
}

function isPdfTransitionError(error: unknown) {
  return error instanceof Error && (error.message.includes("worker is being destroyed") || error.message.includes("Rendering cancelled"));
}

function pdfReaderErrorMessage(error: unknown) {
  if (isPdfTransitionError(error)) return "Le lecteur PDF termine une ouverture precedente.";
  return "Le PDF n'a pas pu s'ouvrir.";
}

function scrollContainerFor(element: HTMLElement | null): HTMLElement | null {
  return element?.closest(".reader-stage") as HTMLElement | null;
}

function PdfPageCanvas({ document, pageNumber, mode, frameSize, onError }: PdfPageCanvasProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const wrapperRef = useRef<HTMLDivElement>(null);
  const [visible, setVisible] = useState(mode === "horizontal");
  const [rendered, setRendered] = useState(false);

  useEffect(() => {
    if (mode === "horizontal") {
      setVisible(true);
      return;
    }
    const wrapper = wrapperRef.current;
    if (!wrapper) return;
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries.some((entry) => entry.isIntersecting)) setVisible(true);
      },
      { root: scrollContainerFor(wrapper), rootMargin: "900px 0px" }
    );
    observer.observe(wrapper);
    return () => observer.disconnect();
  }, [mode]);

  useEffect(() => {
    if (!visible) return;
    let cancelled = false;
    let renderTask: pdfjs.RenderTask | undefined;
    setRendered(false);

    async function renderPage() {
      try {
        const pdfPage = await document.getPage(pageNumber);
        if (cancelled) return;
        const canvas = canvasRef.current;
        if (!canvas) return;
        const baseViewport = pdfPage.getViewport({ scale: 1 });
        const fitScale = pdfRenderScale(mode, frameSize, { width: baseViewport.width, height: baseViewport.height });
        const pixelRatio = Math.min(2, window.devicePixelRatio || 1);
        const renderScale = Math.max(0.25, Math.min(3, fitScale)) * pixelRatio;
        const viewport = pdfPage.getViewport({ scale: renderScale });
        const cssWidth = Math.floor(viewport.width / pixelRatio);
        const cssHeight = Math.floor(viewport.height / pixelRatio);
        canvas.width = Math.max(1, Math.floor(viewport.width));
        canvas.height = Math.max(1, Math.floor(viewport.height));
        canvas.style.width = `${cssWidth}px`;
        canvas.style.height = `${cssHeight}px`;
        const context = canvas.getContext("2d", { alpha: false });
        if (!context) return;
        context.fillStyle = "#f7f0df";
        context.fillRect(0, 0, canvas.width, canvas.height);
        renderTask = pdfPage.render({ canvas, canvasContext: context, viewport });
        await renderTask.promise;
        if (!pdfCanvasHasVisibleInk(context.getImageData(0, 0, canvas.width, canvas.height))) {
          throw new Error(`La page PDF ${pageNumber} est blanche apres rendu.`);
        }
        if (!cancelled) setRendered(true);
      } catch (error) {
        if (!cancelled && !isPdfTransitionError(error)) onError(pdfReaderErrorMessage(error));
      }
    }

    void enqueuePdfLifecycle(renderPage);
    return () => {
      cancelled = true;
      renderTask?.cancel();
    };
  }, [document, frameSize.height, frameSize.width, mode, onError, pageNumber, visible]);

  return (
    <div className="pdf-page-frame" data-reader-page={pageNumber} ref={wrapperRef}>
      {!rendered && (
        <div className="reader-fallback">
          <span>Page {pageNumber}</span>
        </div>
      )}
      <canvas ref={canvasRef} />
    </div>
  );
}

export function PdfReader({ url, page, backHref, mode, onPageCommit, onControlsChange }: PdfReaderProps) {
  const frameRef = useRef<HTMLDivElement>(null);
  const restoredRef = useRef(false);
  const [documentProxy, setDocumentProxy] = useState<pdfjs.PDFDocumentProxy | null>(null);
  const [pages, setPages] = useState(1);
  const [visiblePage, setVisiblePage] = useState(page);
  const [error, setError] = useState<string>();
  const [loading, setLoading] = useState(true);
  const [attempt, setAttempt] = useState(0);
  const [frameSize, setFrameSize] = useState({ width: 980, height: 900 });

  const currentPage = Math.max(1, Math.min(page, pages));
  const displayedPage = mode === "vertical" ? Math.max(1, Math.min(visiblePage, pages)) : currentPage;

  const go = useCallback(
    (nextPage: number) => {
      const target = Math.max(1, Math.min(nextPage, pages));
      if (mode === "vertical") {
        frameRef.current?.querySelector(`[data-reader-page="${target}"]`)?.scrollIntoView({ block: "start" });
        onPageCommit(target, pages, 0, "immediate");
        setVisiblePage(target);
      } else {
        onPageCommit(target, pages, 1, "immediate");
      }
    },
    [mode, onPageCommit, pages]
  );

  const goTop = useCallback(() => go(1), [go]);
  const handlePageError = useCallback((message: string) => {
    setError(message);
  }, []);

  useEffect(() => {
    onControlsChange({
      canPrevious: !loading && !error && displayedPage > 1,
      canNext: !loading && !error && displayedPage < pages,
      canTop: !loading && !error && displayedPage > 1,
      positionLabel: loading ? "Ouverture PDF" : `${displayedPage} / ${pages}`,
      onPrevious: () => go(displayedPage - 1),
      onNext: () => go(displayedPage + 1),
      onTop: goTop
    });
  }, [displayedPage, error, go, goTop, loading, onControlsChange, pages]);

  useEffect(() => {
    const frame = frameRef.current;
    if (!frame) return;
    const updateSize = () => {
      const rect = frame.getBoundingClientRect();
      setFrameSize({ width: Math.max(320, rect.width), height: Math.max(320, rect.height) });
    };
    updateSize();
    const observer = new ResizeObserver(updateSize);
    observer.observe(frame);
    return () => observer.disconnect();
  }, []);

  useEffect(() => {
    let cancelled = false;
    let loadingTask: pdfjs.PDFDocumentLoadingTask | undefined;
    setLoading(true);
    setError(undefined);
    setDocumentProxy(null);
    restoredRef.current = false;

    async function loadDocument() {
      try {
        configurePdfWorker(pdfjs);
        loadingTask = pdfjs.getDocument({ url, withCredentials: true });
        const loadedDocument = await loadingTask.promise;
        if (cancelled) {
          await loadingTask.destroy();
          return;
        }
        setPages(loadedDocument.numPages);
        setDocumentProxy(loadedDocument);
        setLoading(false);
      } catch (loadError) {
        if (cancelled && isPdfTransitionError(loadError)) return;
        if (!cancelled) {
          setError(pdfReaderErrorMessage(loadError));
          setLoading(false);
        }
      }
    }

    void enqueuePdfLifecycle(loadDocument);
    return () => {
      cancelled = true;
      void loadingTask?.destroy();
    };
  }, [attempt, url]);

  useEffect(() => {
    if (mode !== "vertical") return;
    const root = scrollContainerFor(frameRef.current);
    if (!root || !documentProxy) return;
    const handleScroll = () => {
      const rootRect = root.getBoundingClientRect();
      const frames = Array.from(frameRef.current?.querySelectorAll<HTMLElement>("[data-reader-page]") ?? []);
      const active = frames
        .map((element) => {
          const rect = element.getBoundingClientRect();
          const overlap = Math.min(rootRect.bottom, rect.bottom) - Math.max(rootRect.top, rect.top);
          const anchor = Math.max(0, Math.min(1, (rootRect.top - rect.top) / Math.max(1, rect.height)));
          return { page: Number(element.dataset.readerPage), overlap, anchor };
        })
        .filter((item) => Number.isInteger(item.page) && item.overlap > 0)
        .sort((left, right) => right.overlap - left.overlap)[0];
      if (!active) return;
      setVisiblePage(active.page);
      onPageCommit(active.page, pages, active.anchor, "queued");
    };
    root.addEventListener("scroll", handleScroll, { passive: true });
    handleScroll();
    return () => root.removeEventListener("scroll", handleScroll);
  }, [documentProxy, mode, onPageCommit, pages]);

  useEffect(() => {
    if (mode !== "vertical" || !documentProxy || restoredRef.current) return;
    restoredRef.current = true;
    requestAnimationFrame(() => {
      frameRef.current?.querySelector(`[data-reader-page="${currentPage}"]`)?.scrollIntoView({ block: "start" });
      setVisiblePage(currentPage);
    });
  }, [currentPage, documentProxy, mode]);

  return (
    <div className={`pdf-reader pdf-reader-${mode}`} ref={frameRef}>
      {error ? (
        <ReaderError
          title="Lecture PDF indisponible"
          detail={error}
          downloadUrl={url}
          backHref={backHref}
          onRetry={() => setAttempt((value) => value + 1)}
        />
      ) : loading || !documentProxy ? (
        <div className="reader-fallback">
          <span>Ouverture PDF</span>
        </div>
      ) : mode === "vertical" ? (
        <div className="pdf-strip">
          {Array.from({ length: pages }, (_, index) => (
            <PdfPageCanvas key={index + 1} document={documentProxy} pageNumber={index + 1} mode={mode} frameSize={frameSize} onError={handlePageError} />
          ))}
        </div>
      ) : (
        <PdfPageCanvas document={documentProxy} pageNumber={currentPage} mode={mode} frameSize={frameSize} onError={handlePageError} />
      )}
    </div>
  );
}
