import { useCallback, useEffect, useRef, useState } from "react";
import { api } from "../api/client";
import type { CbzPagesDto } from "../api/types";
import { clampReaderPage, orientedContainPageSize, readableViewportSize, readerPositionLabel, zoomReaderSize, type ReaderSize } from "./readerLayout";
import { majorityVisiblePage, type ReaderMode } from "./readerScroll";
import type { ReaderControls } from "./ReaderShell";

type PageCommitStrategy = "immediate" | "queued";

export function CbzReader({
  bookId,
  page,
  zoom,
  mode,
  onPageCommit,
  onControlsChange
}: {
  bookId: number;
  page: number;
  zoom: number;
  mode: ReaderMode;
  onPageCommit: (page: number, pages: number, anchor?: number, strategy?: PageCommitStrategy) => void;
  onControlsChange: (controls: ReaderControls) => void;
}) {
  const frameRef = useRef<HTMLDivElement>(null);
  const previousModeRef = useRef<ReaderMode>(mode);
  const [pages, setPages] = useState<CbzPagesDto | null>(null);
  const [documentError, setDocumentError] = useState<string>();
  const [pageError, setPageError] = useState<string>();
  const [documentAttempt, setDocumentAttempt] = useState(0);
  const [retryAttempt, setRetryAttempt] = useState(0);
  const [viewportSize, setViewportSize] = useState<ReaderSize | null>(null);
  const [imageSize, setImageSize] = useState<ReaderSize | null>(null);
  const [imageSizes, setImageSizes] = useState<Record<number, ReaderSize>>({});

  useEffect(() => {
    let alive = true;
    setDocumentError(undefined);
    setPageError(undefined);
    setPages(null);
    api
      .cbzPages(bookId)
      .then((nextPages) => {
        if (!alive) return;
        setPages(nextPages);
        if (page > nextPages.pageCount) onPageCommit(nextPages.pageCount, nextPages.pageCount, 1, "immediate");
      })
      .catch((error) => {
        if (!alive) return;
        setDocumentError(error instanceof Error && error.message ? `Archive indisponible: ${error.message}` : "Archive indisponible.");
      });
    return () => {
      alive = false;
    };
  }, [bookId, documentAttempt]);

  const pageCount = pages?.pageCount ?? 1;
  const currentPage = clampReaderPage(page, pageCount);
  const currentName = pages?.pages.find((item) => item.page === currentPage)?.name;
  const fittedSize = viewportSize && imageSize ? zoomReaderSize(orientedContainPageSize(viewportSize, imageSize), zoom) : null;
  const imageStyle = fittedSize ? { width: `${fittedSize.width}px`, height: `${fittedSize.height}px` } : undefined;
  const verticalImageStyle = useCallback(
    (pageNumber: number) => {
      const size = imageSizes[pageNumber];
      if (!viewportSize || !size) return undefined;
      const fitted = zoomReaderSize(orientedContainPageSize(viewportSize, size), zoom);
      return { width: `${fitted.width}px`, height: `${fitted.height}px` };
    },
    [imageSizes, viewportSize, zoom]
  );

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

  useEffect(() => {
    setPageError(undefined);
    setImageSize(null);
  }, [bookId, currentPage, retryAttempt]);

  useEffect(() => {
    setImageSizes({});
  }, [bookId, retryAttempt]);

  useEffect(() => {
    const frame = frameRef.current;
    const stage = frame?.closest(".reader-stage") as HTMLElement | null;
    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(() => {
    onControlsChange({
      canPrevious: Boolean(pages) && !documentError && !pageError && currentPage > 1,
      canNext: Boolean(pages) && !documentError && !pageError && currentPage < pageCount,
      positionLabel: pages ? readerPositionLabel(currentPage, pageCount) : "Chargement",
      onPrevious: () => go(currentPage - 1),
      onNext: () => go(currentPage + 1)
    });
  }, [currentPage, documentError, go, onControlsChange, pageCount, 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" || !pages) return;
    const stage = frameRef.current?.closest(".reader-stage") as HTMLElement | null;
    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.pageCount), pages.pageCount, 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]);

  if (documentError) {
    return (
      <div className="cbz-reader" ref={frameRef}>
        <div className="reader-fallback reader-fallback-error">
          <span>{documentError}</span>
          <button className="ghost-button" onClick={() => setDocumentAttempt((attempt) => attempt + 1)}>
            Reessayer
          </button>
        </div>
      </div>
    );
  }

  if (!pages) {
    return (
      <div className="cbz-reader" ref={frameRef}>
        <div className="reader-fallback">
          <span>Chargement de l'archive</span>
        </div>
      </div>
    );
  }

  if (pageError) {
    return (
      <div className="cbz-reader" ref={frameRef}>
        <div className="reader-fallback reader-fallback-error">
          <span>{pageError}</span>
          <button className="ghost-button" onClick={() => setRetryAttempt((attempt) => attempt + 1)}>
            Reessayer
          </button>
        </div>
      </div>
    );
  }

  return (
    <div className={`cbz-reader${mode === "vertical" ? " cbz-reader-vertical" : ""}`} ref={frameRef}>
      {mode === "vertical" ? (
        <div className="reader-vertical-stack">
          {pages.pages.map((item) => (
            <figure className="comic-page-frame comic-page-frame-vertical" data-reader-page={item.page} key={`${item.page}-${retryAttempt}`}>
              {!imageSizes[item.page] && (
                <div className="reader-fallback">
                  <span>Page {item.page}</span>
                </div>
              )}
              <img
                src={api.cbzPageUrl(bookId, item.page)}
                alt={item.name}
                loading="lazy"
                style={verticalImageStyle(item.page)}
                hidden={!imageSizes[item.page]}
                onLoad={(event) => {
                  setImageSizes((current) => ({
                    ...current,
                    [item.page]: { width: event.currentTarget.naturalWidth, height: event.currentTarget.naturalHeight }
                  }));
                }}
                onError={() => setPageError(`Page ${item.page} indisponible.`)}
              />
            </figure>
          ))}
        </div>
      ) : (
        <figure className="comic-page-frame" data-reader-page={currentPage}>
          {!imageSize && (
            <div className="reader-fallback">
              <span>Page {currentPage}</span>
            </div>
          )}
          <img
            key={`${currentPage}-${retryAttempt}`}
            src={api.cbzPageUrl(bookId, currentPage)}
            alt={currentName ?? `Page ${currentPage}`}
            style={imageStyle}
            hidden={!imageSize}
            onLoad={(event) => {
              setImageSize({ width: event.currentTarget.naturalWidth, height: event.currentTarget.naturalHeight });
            }}
            onError={() => setPageError(`Page ${currentPage} indisponible.`)}
          />
        </figure>
      )}
    </div>
  );
}
