import { useCallback, useEffect, useRef, useState } from "react";
import * as pdfjs from "pdfjs-dist";
import { ReaderError } from "./ReaderError";
import { pdfDocumentOptions } from "./pdfDocumentOptions";
import { classifyPdfCanvas, pdfCanvasVisible, pdfRenderScale, pdfSentinelBackground, type PdfRenderResult } from "./pdfRender";
import { configurePdfWorker } from "./pdfWorker";
import { clampReaderPage, readableViewportSize, readerPositionLabel, readerZoomFactor, type ReaderSize } from "./readerLayout";
import { isPageInRenderWindow, majorityVisiblePage, type ReaderMode } from "./readerScroll";
import type { ReaderControls } from "./ReaderShell";

configurePdfWorker(pdfjs);

type PageCommitStrategy = "immediate" | "queued";

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

type PdfFailure = {
  detail: string;
  technicalDetail: string;
};

type PdfPageStatus = "idle" | "loading" | PdfRenderResult | "render-failed";

function pdfTechnicalMessage(error: unknown) {
  if (error instanceof Error && error.message.trim()) return `${error.name}: ${error.message}`;
  if (typeof error === "string" && error.trim()) return error;
  return "Erreur inconnue.";
}

function pdfFailure(source: string, detail: string, context: Record<string, unknown>, error?: unknown): PdfFailure {
  return {
    detail,
    technicalDetail: JSON.stringify(
      {
        source,
        ...context,
        error: error === undefined ? undefined : pdfTechnicalMessage(error)
      },
      null,
      2
    )
  };
}

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

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

function afterNextPaint() {
  return new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
}

function PdfPageCanvas({
  documentProxy,
  pageNumber,
  viewportSize,
  zoom,
  active
}: {
  documentProxy: pdfjs.PDFDocumentProxy;
  pageNumber: number;
  viewportSize: ReaderSize | null;
  zoom: number;
  active: boolean;
}) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [loading, setLoading] = useState(false);
  const [rendered, setRendered] = useState(false);
  const [pageStatus, setPageStatus] = useState<PdfPageStatus>("idle");
  const [pageError, setPageError] = useState<string>();
  const showCanvas = active && pdfCanvasVisible(rendered, Boolean(pageError));

  useEffect(() => {
    if (!active || !viewportSize) return;
    const readerViewport = viewportSize;
    let cancelled = false;
    let renderTask: pdfjs.RenderTask | undefined;
    setLoading(true);
    setRendered(false);
    setPageStatus("loading");
    setPageError(undefined);

    async function renderPage() {
      try {
        const pdfPage = await documentProxy.getPage(pageNumber);
        if (cancelled) return;
        const canvas = canvasRef.current;
        if (!canvas) return;
        const baseViewport = pdfPage.getViewport({ scale: 1 });
        const fitScale = pdfRenderScale(readerViewport, { width: baseViewport.width, height: baseViewport.height });
        const pixelRatio = Math.min(2, window.devicePixelRatio || 1);
        const viewport = pdfPage.getViewport({ scale: Math.max(0.25, fitScale * readerZoomFactor(zoom)) * pixelRatio });
        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) throw new Error("PDF canvas context unavailable");
        context.fillStyle = pdfSentinelBackground();
        context.fillRect(0, 0, canvas.width, canvas.height);
        renderTask = pdfPage.render({ canvas, canvasContext: context, viewport });
        await renderTask.promise;
        await afterNextPaint();
        if (cancelled) return;
        setPageStatus(classifyPdfCanvas(context.getImageData(0, 0, canvas.width, canvas.height)));
        setRendered(true);
        setLoading(false);
      } catch (error) {
        if (cancelled || isPdfTransitionError(error)) return;
        setPageStatus("render-failed");
        setLoading(false);
        setPageError(`Page ${pageNumber} indisponible.`);
      }
    }

    void renderPage();
    return () => {
      cancelled = true;
      renderTask?.cancel();
    };
  }, [active, documentProxy, pageNumber, viewportSize, zoom]);

  return (
    <div className="pdf-page-frame pdf-page-frame-vertical" data-reader-page={pageNumber}>
      {(!active || !viewportSize || loading || !rendered || pageError) && (
        <div className={`reader-fallback${pageError ? " reader-fallback-error" : ""}`}>
          <span>{pageError ?? `Page ${pageNumber}`}</span>
        </div>
      )}
      <canvas ref={canvasRef} data-pdf-render-status={pageStatus} style={{ visibility: showCanvas ? "visible" : "hidden" }} />
    </div>
  );
}

export function PdfReader({ url, page, backHref, zoom, mode, onPageCommit, onControlsChange }: PdfReaderProps) {
  const frameRef = useRef<HTMLDivElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const previousModeRef = useRef<ReaderMode>(mode);
  const [documentProxy, setDocumentProxy] = useState<pdfjs.PDFDocumentProxy | null>(null);
  const [pages, setPages] = useState(1);
  const [documentError, setDocumentError] = useState<PdfFailure>();
  const [pageError, setPageError] = useState<PdfFailure>();
  const [loadingDocument, setLoadingDocument] = useState(true);
  const [loadingPage, setLoadingPage] = useState(false);
  const [pageRendered, setPageRendered] = useState(false);
  const [pageStatus, setPageStatus] = useState<PdfPageStatus>("idle");
  const [documentAttempt, setDocumentAttempt] = useState(0);
  const [pageAttempt, setPageAttempt] = useState(0);
  const [viewportSize, setViewportSize] = useState<ReaderSize | null>(null);

  const currentPage = clampReaderPage(page, pages);
  const showCanvas = pdfCanvasVisible(pageRendered, Boolean(pageError));

  const go = useCallback(
    (nextPage: number) => {
      const target = clampReaderPage(nextPage, pages);
      setPageError(undefined);
      if (mode === "vertical") {
        frameRef.current?.querySelector<HTMLElement>(`[data-reader-page="${target}"]`)?.scrollIntoView({ block: "start" });
      }
      onPageCommit(target, pages, 1, "immediate");
    },
    [mode, onPageCommit, pages]
  );

  useEffect(() => {
    onControlsChange({
      canPrevious: !loadingDocument && !documentError && !pageError && currentPage > 1,
      canNext: !loadingDocument && !documentError && !pageError && currentPage < pages,
      positionLabel: loadingDocument ? "Chargement" : readerPositionLabel(currentPage, pages),
      onPrevious: () => go(currentPage - 1),
      onNext: () => go(currentPage + 1)
    });
  }, [currentPage, documentError, go, loadingDocument, onControlsChange, pageError, pages]);

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

  useEffect(() => {
    if (mode !== "vertical") return;
    const stage = scrollContainerFor(frameRef.current);
    if (!stage) return;
    let frameId = 0;
    const updateVisiblePage = () => {
      frameId = 0;
      const stageRect = stage.getBoundingClientRect();
      const visiblePage = majorityVisiblePage(
        Array.from(stage.querySelectorAll<HTMLElement>("[data-reader-page]")).map((element) => {
          const rect = element.getBoundingClientRect();
          return { page: Number(element.dataset.readerPage), top: rect.top, bottom: rect.bottom };
        }),
        stageRect.top,
        stageRect.bottom
      );
      if (visiblePage && visiblePage !== currentPage) onPageCommit(clampReaderPage(visiblePage, pages), pages, 1, "queued");
    };
    const onScroll = () => {
      if (frameId) return;
      frameId = requestAnimationFrame(updateVisiblePage);
    };
    stage.addEventListener("scroll", onScroll, { passive: true });
    updateVisiblePage();
    return () => {
      if (frameId) cancelAnimationFrame(frameId);
      stage.removeEventListener("scroll", onScroll);
    };
  }, [currentPage, mode, onPageCommit, pages]);

  useEffect(() => {
    const frame = frameRef.current;
    const stage = scrollContainerFor(frame);
    const observedElement = stage ?? frame;
    if (!observedElement) return;
    const updateSize = () => {
      const rect = observedElement.getBoundingClientRect();
      const nextSize = readableViewportSize({ width: rect.width, height: rect.height });
      if (nextSize) setViewportSize(nextSize);
    };
    updateSize();
    const frameId = requestAnimationFrame(updateSize);
    const observer = new ResizeObserver(updateSize);
    observer.observe(observedElement);
    return () => {
      cancelAnimationFrame(frameId);
      observer.disconnect();
    };
  }, []);

  useEffect(() => {
    let cancelled = false;
    let loadingTask: pdfjs.PDFDocumentLoadingTask | undefined;
    setLoadingDocument(true);
    setDocumentError(undefined);
    setPageError(undefined);
    setPageRendered(false);
    setPageStatus("idle");
    setDocumentProxy(null);

    async function loadDocument() {
      try {
        try {
          new URL(url, window.location.href);
        } catch (error) {
          throw pdfFailure("uri", "PDF: URI de fichier invalide.", { url }, error);
        }
        configurePdfWorker(pdfjs);
        loadingTask = pdfjs.getDocument(pdfDocumentOptions({ url, withCredentials: true }, "browser"));
        const loadedDocument = await loadingTask.promise;
        if (cancelled) {
          await loadingTask.destroy();
          return;
        }
        setPages(loadedDocument.numPages);
        setDocumentProxy(loadedDocument);
        setLoadingDocument(false);
      } catch (error) {
        if (cancelled && isPdfTransitionError(error)) return;
        if (!cancelled) {
          setDocumentError(
            typeof error === "object" && error && "detail" in error && "technicalDetail" in error
              ? (error as PdfFailure)
              : pdfFailure(
                  "getDocument",
                  "PDF: getDocument() a échoué.",
                  {
                    url,
                    resolvedUrl: (() => {
                      try {
                        return new URL(url, window.location.href).toString();
                      } catch {
                        return null;
                      }
                    })(),
                    workerSrc: pdfjs.GlobalWorkerOptions.workerSrc,
                    hasWorkerPort: Boolean(pdfjs.GlobalWorkerOptions.workerPort)
                  },
                  error
                )
          );
          setLoadingDocument(false);
        }
      }
    }

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

  useEffect(() => {
    if (mode === "vertical") {
      setPageError(undefined);
      return;
    }
    if (!documentProxy || !viewportSize) return;
    const loadedDocument = documentProxy;
    const readerViewport = viewportSize;
    let cancelled = false;
    let renderTask: pdfjs.RenderTask | undefined;
    setLoadingPage(true);
    setPageError(undefined);
    setPageRendered(false);
    setPageStatus("loading");

    async function renderPage() {
      try {
        let pdfPage: pdfjs.PDFPageProxy;
        try {
          pdfPage = await loadedDocument.getPage(currentPage);
        } catch (error) {
          throw pdfFailure("getPage", `PDF: getPage(${currentPage}) a échoué.`, { pageNumber: currentPage }, error);
        }
        if (cancelled) return;
        const canvas = canvasRef.current;
        if (!canvas) {
          throw pdfFailure("react-canvas-ref", `PDF: canvas absent pour la page ${currentPage}.`, { pageNumber: currentPage });
        }
        const baseViewport = pdfPage.getViewport({ scale: 1 });
        const fitScale = pdfRenderScale(readerViewport, { width: baseViewport.width, height: baseViewport.height });
        const pixelRatio = Math.min(2, window.devicePixelRatio || 1);
        const zoomedScale = fitScale * readerZoomFactor(zoom);
        const renderScale = Math.max(0.25, zoomedScale) * 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) {
          throw pdfFailure("canvas-context", `PDF: contexte canvas 2D indisponible pour la page ${currentPage}.`, {
            pageNumber: currentPage,
            canvasWidth: canvas.width,
            canvasHeight: canvas.height
          });
        }
        if (canvas.width <= 0 || canvas.height <= 0 || cssWidth <= 0 || cssHeight <= 0) {
          throw pdfFailure("canvas-size", `PDF: dimensions canvas invalides pour la page ${currentPage}.`, {
            pageNumber: currentPage,
            canvasWidth: canvas.width,
            canvasHeight: canvas.height,
            cssWidth,
            cssHeight,
            readerViewport
          });
        }
        context.fillStyle = pdfSentinelBackground();
        context.fillRect(0, 0, canvas.width, canvas.height);
        try {
          renderTask = pdfPage.render({ canvas, canvasContext: context, viewport });
          await renderTask.promise;
        } catch (error) {
          throw pdfFailure(
            "render-failed",
            `PDF: render() a échoué pour la page ${currentPage}.`,
            {
              result: "render-failed",
              pageNumber: currentPage,
              baseViewport: { width: baseViewport.width, height: baseViewport.height },
              renderViewport: { width: viewport.width, height: viewport.height },
              canvasWidth: canvas.width,
              canvasHeight: canvas.height,
              cssWidth,
              cssHeight,
              readerViewport
            },
            error
          );
        }
        await afterNextPaint();
        if (cancelled) return;
        const renderResult = classifyPdfCanvas(context.getImageData(0, 0, canvas.width, canvas.height));
        if (renderResult === "blank-detected") {
          setPageStatus("blank-detected");
          setPageRendered(true);
          setLoadingPage(false);
          return;
        }
        if (!cancelled) {
          setPageStatus("rendered-ok");
          setPageRendered(true);
          setLoadingPage(false);
        }
      } catch (error) {
        if (cancelled || isPdfTransitionError(error)) return;
        setPageRendered(false);
        setPageStatus("render-failed");
        setLoadingPage(false);
        setPageError(
          typeof error === "object" && error && "detail" in error && "technicalDetail" in error
            ? (error as PdfFailure)
            : pdfFailure("render-unclassified", `PDF: erreur non classée pendant le rendu de la page ${currentPage}.`, { pageNumber: currentPage }, error)
        );
      }
    }

    void renderPage();
    return () => {
      cancelled = true;
      renderTask?.cancel();
    };
  }, [currentPage, documentProxy, mode, pageAttempt, viewportSize, zoom]);

  if (documentError) {
    return (
      <div className="pdf-reader" ref={frameRef}>
        <ReaderError
          title="Lecture PDF indisponible"
          detail={documentError.detail}
          technicalDetail={documentError.technicalDetail}
          downloadUrl={url}
          backHref={backHref}
          onRetry={() => setDocumentAttempt((value) => value + 1)}
        />
      </div>
    );
  }

  return (
    <div className={`pdf-reader${mode === "vertical" ? " pdf-reader-vertical" : ""}`} ref={frameRef}>
      {loadingDocument || !documentProxy ? (
        <div className="reader-fallback">
          <span>Ouverture PDF</span>
        </div>
      ) : mode === "vertical" ? (
        <div className="reader-vertical-stack">
          {Array.from({ length: pages }, (_, index) => {
            const pageNumber = index + 1;
            return (
              <PdfPageCanvas
                key={pageNumber}
                documentProxy={documentProxy}
                pageNumber={pageNumber}
                viewportSize={viewportSize}
                zoom={zoom}
                active={isPageInRenderWindow(pageNumber, currentPage, pages)}
              />
            );
          })}
        </div>
      ) : pageError ? (
        <div className="pdf-page-frame" data-reader-page={currentPage}>
          <div className="reader-fallback reader-fallback-error">
            <span>{pageError.detail}</span>
            <button className="ghost-button" onClick={() => setPageAttempt((value) => value + 1)}>
              Reessayer
            </button>
            <details>
              <summary>Détail technique</summary>
              <pre>{pageError.technicalDetail}</pre>
            </details>
          </div>
        </div>
      ) : (
        <div className="pdf-page-frame" data-reader-page={currentPage}>
          {(!viewportSize || loadingPage || !pageRendered) && (
            <div className="reader-fallback">
              <span>Page {currentPage}</span>
            </div>
          )}
          <canvas ref={canvasRef} data-pdf-render-status={pageStatus} style={{ visibility: showCanvas ? "visible" : "hidden" }} />
        </div>
      )}
    </div>
  );
}
