import { Component, useCallback, useEffect, useMemo, useState, type ErrorInfo, type ReactNode } from "react";
import type { BookDto } from "@readabook/shared";
import { api } from "../api/client";
import { CbzReader } from "../reader/CbzReader";
import { EpubReader } from "../reader/EpubReader";
import { pageLocator, parseCbrPageLocator, parseCbzPageLocator, parsePdfPageLocator, pdfPagePercent } from "../reader/locators";
import { PdfReader } from "../reader/PdfReader";
import { ReaderShell, type ReaderControls, type ReaderModeControls, type ReaderZoomAnchor, type ReaderZoomControls } from "../reader/ReaderShell";
import { clampReaderZoom, READER_ZOOM_DEFAULT, READER_ZOOM_STEP } from "../reader/readerLayout";
import { majorityVisiblePage, type ReaderMode } from "../reader/readerScroll";
import { useReaderPreferences } from "../reader/useReaderPreferences";
import { useReaderProgress } from "../reader/useReaderProgress";
import { navigate } from "../router";
import { EmptyState, LoadingState, Panel } from "../components/ui";

const idleControls: ReaderControls = {
  canPrevious: false,
  canNext: false,
  positionLabel: "Chargement",
  onPrevious: () => undefined,
  onNext: () => undefined
};

type ReaderCrashBoundaryProps = {
  resetKey: string;
  onError: (error: Error) => void;
  fallbackRender: (error: Error, retry: () => void) => ReactNode;
  children: ReactNode;
};

type ReaderCrashBoundaryState = {
  error: Error | null;
};

class ReaderCrashBoundary extends Component<ReaderCrashBoundaryProps, ReaderCrashBoundaryState> {
  state: ReaderCrashBoundaryState = { error: null };

  static getDerivedStateFromError(error: Error) {
    return { error };
  }

  componentDidCatch(error: Error, _errorInfo: ErrorInfo) {
    this.props.onError(error);
  }

  componentDidUpdate(previousProps: ReaderCrashBoundaryProps) {
    if (previousProps.resetKey !== this.props.resetKey && this.state.error) {
      this.setState({ error: null });
    }
  }

  retry = () => {
    this.setState({ error: null });
  };

  render() {
    if (this.state.error) return this.props.fallbackRender(this.state.error, this.retry);
    return this.props.children;
  }
}

export function ReaderPage({ bookId }: { bookId: number }) {
  const [book, setBook] = useState<BookDto | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string>();
  const [page, setPage] = useState(1);
  const [zoom, setZoom] = useState(READER_ZOOM_DEFAULT);
  const [mode, setMode] = useState<ReaderMode>("horizontal");
  const [readerControls, setReaderControls] = useState<ReaderControls>(idleControls);
  const { progress, error: progressError, save, queueSave } = useReaderProgress(bookId);
  const { preferences, setMode: saveReaderMode, error: preferencesError } = useReaderPreferences(bookId);

  async function loadBook() {
    setLoading(true);
    setError(undefined);
    try {
      setBook(await api.book(bookId));
    } catch {
      setBook(null);
      setError("Ce livre n'existe pas dans le catalogue.");
    } finally {
      setLoading(false);
    }
  }

  useEffect(() => {
    void loadBook();
  }, [bookId]);

  useEffect(() => {
    setReaderControls(idleControls);
    setPage(1);
    setZoom(READER_ZOOM_DEFAULT);
    setMode("horizontal");
  }, [bookId]);

  useEffect(() => {
    if (book?.format === "pdf" || book?.format === "cbz" || book?.format === "cbr") setMode(preferences.mode);
    else setMode("horizontal");
  }, [book?.format, preferences.mode]);

  useEffect(() => {
    const nextPage = parsePdfPageLocator(progress?.locator) ?? parseCbzPageLocator(progress?.locator) ?? parseCbrPageLocator(progress?.locator);
    if (nextPage) setPage((current) => (current === nextPage ? current : nextPage));
  }, [progress]);

  const fileUrl = useMemo(() => api.bookFileUrl(bookId), [bookId]);
  const backHref = useMemo(() => (book ? `/book/${book.id}` : "/home"), [book]);
  const savePdfPage = useCallback(
    (nextPage: number, pages: number, anchor = 1, strategy: "immediate" | "queued" = "immediate") => {
      setPage(nextPage);
      const locator = pageLocator("pdf", nextPage, anchor);
      const percent = pdfPagePercent(nextPage, pages, anchor);
      if (strategy === "queued") queueSave(locator, percent);
      else void save(locator, percent);
    },
    [queueSave, save]
  );
  const saveEpubLocator = useCallback((locator: string, percent: number) => void save(locator, percent), [save]);
  const saveComicPage = useCallback(
    (nextPage: number, pages: number, anchor = 1, strategy: "immediate" | "queued" = "immediate") => {
      setPage(nextPage);
      const prefix = book?.format === "cbr" ? "cbr" : "cbz";
      const locator = pageLocator(prefix, nextPage, anchor);
      const percent = pdfPagePercent(nextPage, pages, anchor);
      if (strategy === "queued") queueSave(locator, percent);
      else void save(locator, percent);
    },
    [book?.format, queueSave, save]
  );

  const readerError = error ?? progressError ?? preferencesError;
  const supportsZoom = book?.format === "pdf" || book?.format === "cbz" || book?.format === "cbr";
  const supportsMode = supportsZoom;
  const currentVisiblePage = useCallback(() => {
    const stage = document.querySelector(".reader-stage") as HTMLElement | null;
    if (!stage) return null;
    const stageRect = stage.getBoundingClientRect();
    const pages = Array.from(stage.querySelectorAll<HTMLElement>("[data-reader-page]"))
      .map((element) => {
        const rect = element.getBoundingClientRect();
        const pageNumber = Number(element.dataset.readerPage);
        return Number.isFinite(pageNumber) ? { page: pageNumber, top: rect.top, bottom: rect.bottom } : null;
      })
      .filter((item): item is { page: number; top: number; bottom: number } => Boolean(item));
    return majorityVisiblePage(pages, stageRect.top, stageRect.bottom);
  }, []);
  const changeMode = useCallback(
    (nextMode: ReaderMode) => {
      const anchorPage = currentVisiblePage() ?? page;
      setPage(anchorPage);
      setMode(nextMode);
      saveReaderMode(nextMode);
    },
    [currentVisiblePage, page, saveReaderMode]
  );
  const returnToPagedMode = useCallback(() => {
    setPage(currentVisiblePage() ?? page);
    setMode("horizontal");
    saveReaderMode("horizontal");
  }, [currentVisiblePage, page, saveReaderMode]);
  const changeZoom = useCallback((nextZoom: number | ((currentZoom: number) => number), anchor?: ReaderZoomAnchor) => {
    const stage = document.querySelector(".reader-stage") as HTMLElement | null;
    const scrollRatioX = stage && stage.scrollWidth > stage.clientWidth ? (stage.scrollLeft + stage.clientWidth / 2) / stage.scrollWidth : 0.5;
    const scrollRatioY = stage && stage.scrollHeight > stage.clientHeight ? stage.scrollTop / (stage.scrollHeight - stage.clientHeight) : 0;

    setZoom((currentZoom) => clampReaderZoom(typeof nextZoom === "function" ? nextZoom(currentZoom) : nextZoom));

    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        if (!stage) return;
        if (anchor) {
          const target = stage.querySelector<HTMLElement>(`[data-reader-page="${anchor.page}"]`);
          if (!target) return;
          const rect = target.getBoundingClientRect();
          stage.scrollLeft += rect.left + anchor.offsetX - anchor.clientX;
          stage.scrollTop += rect.top + anchor.offsetY - anchor.clientY;
          return;
        }
        stage.scrollLeft = Math.max(0, stage.scrollWidth * scrollRatioX - stage.clientWidth / 2);
        stage.scrollTop = Math.max(0, (stage.scrollHeight - stage.clientHeight) * scrollRatioY);
      });
    });
  }, []);
  const zoomControls = useMemo<ReaderZoomControls | undefined>(
    () =>
      supportsZoom
        ? {
            zoom,
            onZoomChange: (nextZoom, anchor) => changeZoom(nextZoom, anchor),
            onZoomOut: () => changeZoom((currentZoom) => currentZoom - READER_ZOOM_STEP),
            onZoomIn: () => changeZoom((currentZoom) => currentZoom + READER_ZOOM_STEP),
            onZoomReset: () => changeZoom(READER_ZOOM_DEFAULT)
          }
        : undefined,
    [changeZoom, supportsZoom, zoom]
  );
  const modeControls = useMemo<ReaderModeControls | undefined>(
    () =>
      supportsMode
        ? {
            mode,
            onModeChange: changeMode
          }
        : undefined,
    [changeMode, mode, supportsMode]
  );

  if (loading && !book) return <LoadingState />;

  if (!book) {
    return (
      <section className="reader-missing-page" role="alert">
        <Panel>
          <EmptyState title="Livre introuvable" detail={error ?? "Le serveur n'a pas renvoye cet ouvrage."} />
          <div className="reader-error-actions">
            <button className="ghost-button" onClick={() => navigate("/home")}>
              Retour à l'accueil
            </button>
            <button className="ghost-button" onClick={() => void loadBook()}>
              Réessayer
            </button>
          </div>
        </Panel>
      </section>
    );
  }

  return (
    <ReaderShell
      title={book?.title ?? "Ouverture du lecteur"}
      backHref={backHref}
      error={readerError}
      onRetry={error ? () => void loadBook() : undefined}
      controls={readerControls}
      zoomControls={zoomControls}
      modeControls={modeControls}
    >
      <ReaderCrashBoundary
        resetKey={`${bookId}:${book?.format ?? "loading"}:${mode}`}
        onError={() => setReaderControls(idleControls)}
        fallbackRender={(crashError, retry) => (
          <div className="reader-error" role="alert">
            <div>
              <h2>Le lecteur a rencontré une erreur.</h2>
              <p>La page reste ouverte. Vous pouvez réessayer, revenir au mode page par page ou retourner à la fiche du livre.</p>
            </div>
            <div className="reader-error-actions">
              <button className="ghost-button" onClick={retry}>
                Réessayer
              </button>
              <button
                className="ghost-button"
                onClick={() => {
                  returnToPagedMode();
                  retry();
                }}
              >
                Revenir au mode page par page
              </button>
              <button className="ghost-button" onClick={() => navigate(backHref)}>
                Retour à la fiche
              </button>
            </div>
            {crashError.message && (
              <details>
                <summary>Détail technique</summary>
                <pre>{crashError.message}</pre>
              </details>
            )}
          </div>
        )}
      >
        {book.format === "pdf" ? (
          <PdfReader
            url={fileUrl}
            page={page}
            backHref={backHref}
            zoom={zoom}
            mode={mode}
            onPageCommit={savePdfPage}
            onControlsChange={setReaderControls}
          />
        ) : book.format === "cbz" || book.format === "cbr" ? (
          <CbzReader
            bookId={book.id}
            page={page}
            zoom={zoom}
            mode={mode}
            onPageCommit={saveComicPage}
            onControlsChange={setReaderControls}
          />
        ) : (
          <EpubReader
            url={fileUrl}
            locator={progress?.locator}
            backHref={backHref}
            mode="horizontal"
            onLocatorChange={saveEpubLocator}
            onControlsChange={setReaderControls}
          />
        )}
      </ReaderCrashBoundary>
    </ReaderShell>
  );
}
