// App: coordinates Room → Gallery → (TV zoom) → Reader

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "vhsCount": 8
}/*EDITMODE-END*/;

// Read tracking — phase 2 lives in Supabase via /api/read.
// Local cache key kept around just for warm starts before the fetch resolves.
const READ_CACHE_KEY = "trooper_read_ids_cache_v3";

function loadReadCache() {
  try {
    const raw = localStorage.getItem(READ_CACHE_KEY);
    if (!raw) return new Set();
    return new Set(JSON.parse(raw));
  } catch (e) { return new Set(); }
}
function saveReadCache(set) {
  try { localStorage.setItem(READ_CACHE_KEY, JSON.stringify([...set])); } catch (e) {}
}
async function fetchReadIds() {
  try {
    const r = await fetch("/api/read", { credentials: "same-origin" });
    if (r.status === 401) return new Set();
    if (!r.ok) throw new Error(`HTTP ${r.status}`);
    const { ids } = await r.json();
    const set = new Set(ids || []);
    saveReadCache(set);
    return set;
  } catch (e) {
    console.error("[read] fetch failed", e);
    return loadReadCache();
  }
}
async function postReadId(chapterId) {
  try {
    await fetch("/api/read", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      credentials: "same-origin",
      body: JSON.stringify({ chapterId }),
    });
  } catch (e) {
    console.error("[read] mark failed", e);
  }
}

// Catches render errors from any subtree (mainly AdminPanel) and shows them
// on screen so we don't get a silent black void.
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null, info: null };
  }
  static getDerivedStateFromError(error) {
    return { error };
  }
  componentDidCatch(error, info) {
    console.error("[ErrorBoundary]", error, info);
    this.setState({ info });
  }
  render() {
    if (this.state.error) {
      return (
        <div style={{
          position: "fixed", inset: 0, zIndex: 999,
          background: "#0a0812", color: "#ff5aa0",
          fontFamily: "JetBrains Mono, monospace",
          padding: 32, overflow: "auto", fontSize: 12,
        }}>
          <h2 style={{ color: "#fff", fontSize: 20, marginTop: 0, letterSpacing: "0.2em" }}>
            ◉ ADMIN CRASH
          </h2>
          <pre style={{ whiteSpace: "pre-wrap", color: "#ff5aa0" }}>
            {String(this.state.error?.stack || this.state.error?.message || this.state.error)}
          </pre>
          {this.state.info?.componentStack && (
            <>
              <h3 style={{ color: "#fff", fontSize: 14, marginTop: 24, letterSpacing: "0.2em" }}>
                COMPONENT STACK
              </h3>
              <pre style={{ whiteSpace: "pre-wrap", color: "rgba(239,233,220,0.7)", fontSize: 11 }}>
                {this.state.info.componentStack}
              </pre>
            </>
          )}
          <button
            onClick={() => { this.setState({ error: null, info: null }); window.location.reload(); }}
            style={{
              marginTop: 20, padding: "10px 18px",
              background: "#fff", color: "#0a0812", border: 0, cursor: "pointer",
              fontFamily: "inherit", letterSpacing: "0.2em", fontSize: 11,
            }}
          >
            ▸ RECARGAR
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

function App() {
  const [view, setView] = React.useState("room");
  const [selectedChapter, setSelectedChapter] = React.useState(null);
  const [selectedArc, setSelectedArc] = React.useState(null);
  const [galleryInitialId, setGalleryInitialId] = React.useState(null);

  const [tweaksOn, setTweaksOn] = React.useState(false);
  const [vhsCount, setVhsCount] = React.useState(TWEAK_DEFAULTS.vhsCount);
  const [readIds, setReadIds] = React.useState(() => loadReadCache());

  // TV menu auth state. Initial value reads localStorage cache so the UI doesn't
  // flicker on reload; the useEffect below verifies against the server (Supabase
  // session cookie) and corrects the state if the cache is stale.
  const [user, setUser] = React.useState(() => loadUser());
  const [tvScreen, setTvScreen] = React.useState(() => (loadUser() ? "home" : "start"));

  React.useEffect(() => {
    if (typeof window.refreshUser !== "function") return;
    window.refreshUser().then((serverUser) => {
      if (JSON.stringify(serverUser) !== JSON.stringify(user)) {
        setUser(serverUser);
        setTvScreen(serverUser ? "home" : "start");
      }
    });
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // When user logs in → fetch read ids from server. Logged out → clear.
  React.useEffect(() => {
    if (!user) {
      setReadIds(new Set());
      saveReadCache(new Set());
      try {
        localStorage.removeItem("trooper_user_cards_cache_v3");
        localStorage.removeItem("trooper_novel_cache_v3");
      } catch (e) {}
    } else {
      fetchReadIds().then((set) => setReadIds(set));
    }
    // Tell card/novel hooks to refresh from server too.
    window.dispatchEvent(new CustomEvent("trooper:user-changed"));
  }, [user?.handle]);

  // Toast for card-add errors (so we don't silently lose drops in production).
  const [cardError, setCardError] = React.useState(null);
  React.useEffect(() => {
    const onErr = (e) => {
      const msg = e?.detail?.errorMsg || "Error guardando carta";
      setCardError(msg);
      setTimeout(() => setCardError(null), 8000);
    };
    window.addEventListener("trooper:card-error", onErr);
    return () => window.removeEventListener("trooper:card-error", onErr);
  }, []);

  // Admin mode toggle: "lector" | "admin" — only visible when user.isAdmin
  const [adminMode, setAdminMode] = React.useState("lector");

  // Card system state
  const [novelStore] = window.NovelStore.useNovelStore();
  const userCards = window.CardSystem.useUserCards();
  const [albumOpen, setAlbumOpen] = React.useState(false);
  const [dropQueue, setDropQueue] = React.useState([]);

  // Defensive: refresca user cards y novel store al togglear EDITOR↔LECTOR
  // por si quedó algún listener desincronizado tras los remounts.
  const adminModeMountedRef = React.useRef(false);
  React.useEffect(() => {
    if (!adminModeMountedRef.current) {
      adminModeMountedRef.current = true;
      return;
    }
    window.dispatchEvent(new CustomEvent("trooper:user-changed"));
  }, [adminMode]);

  const allCards = novelStore.cards || [];
  // Solo cuenta entradas que aún apuntan a una carta existente (evita huérfanos
  // de user_cards cuando una carta fue borrada vía admin antes del cascade).
  const ownedCount = allCards.filter(c => userCards[c.id]).length;

  // Clear admin mode when logging out
  React.useEffect(() => {
    if (!user?.isAdmin) setAdminMode("lector");
  }, [user]);

  // When the user logs out, return to the room view
  React.useEffect(() => {
    if (!user) {
      setView("room");
      setGalleryInitialId(null);
      setSelectedChapter(null);
      setSelectedArc(null);
    }
  }, [user]);

  const markRead = (chapterId) => {
    setReadIds(prev => {
      if (prev.has(chapterId)) return prev;
      const next = new Set(prev);
      next.add(chapterId);
      saveReadCache(next);
      postReadId(chapterId);
      return next;
    });
  };

  React.useEffect(() => {
    const onMsg = (e) => {
      const d = e.data || {};
      if (d.type === "__activate_edit_mode") setTweaksOn(true);
      if (d.type === "__deactivate_edit_mode") setTweaksOn(false);
    };
    window.addEventListener("message", onMsg);
    window.parent.postMessage({ type: "__edit_mode_available" }, "*");
    return () => window.removeEventListener("message", onMsg);
  }, []);

  const updateVhsCount = (n) => {
    setVhsCount(n);
    window.parent.postMessage({ type: "__edit_mode_set_keys", edits: { vhsCount: n } }, "*");
  };

  const openGallery = (id) => {
    setGalleryInitialId(id);
    setView("gallery");
  };

  const closeGallery = () => {
    setView("room");
    setGalleryInitialId(null);
  };

  // Solo transición visual al lector. La lógica de "marcar leído + dropear cartas"
  // ahora vive en handleScrolledToBottom — se dispara cuando el usuario llega al
  // final del capítulo, no al abrirlo.
  const startRead = (chapter, arc) => {
    setSelectedChapter(chapter);
    setSelectedArc(arc);
    setView("zooming");
    setTimeout(() => setView("reader"), 1200);
  };

  // Llamado por el Reader cuando el scroll llega al fondo del capítulo (o el
  // contenido entra completo en pantalla sin necesidad de scroll). Solo dispara
  // drops la PRIMERA vez que el cap se completa.
  const handleScrolledToBottom = (chapter, arc) => {
    if (!chapter || !arc) return;
    const wasRead = readIds.has(chapter.id);
    markRead(chapter.id);
    if (wasRead) return;
    const cards = novelStore.cards || [];
    const drops = window.CardSystem.evaluateChapterDrops(cards, arc.id, chapter.id);
    const arcAfter = (novelStore.arcs || []).find(a => String(a.id) === String(arc.id));
    const allChapterIds = (arcAfter?.chapters || []).map(c => c.id);
    const newReadSet = new Set([...readIds, chapter.id]);
    const arcComplete = allChapterIds.length > 0 && allChapterIds.every(id => newReadSet.has(id));
    const arcDrops = arcComplete ? window.CardSystem.evaluateArcDrops(cards, arc.id) : [];
    const all = [...drops, ...arcDrops];
    if (all.length) {
      const queue = all.map(c => {
        const isDup = !!userCards[c.id];
        window.CardSystem.addCardToCollection(c.id);
        return { ...c, _duplicate: isDup };
      });
      setDropQueue(queue);
    }
  };

  // Compute the "next chapter" (in the same arc, or first chapter of the next
  // arc) based on the reader-filtered arc list (so we never offer to navigate
  // to an unwritten chapter).
  const computeNext = (chapter, arc) => {
    if (!chapter || !arc) return null;
    const filtered = window.NovelStore.filterForReader(novelStore);
    const arcIdx = filtered.findIndex(a => String(a.id) === String(arc.id));
    if (arcIdx === -1) return null;
    const cur = filtered[arcIdx];
    const chIdx = cur.chapters.findIndex(c => String(c.id) === String(chapter.id));
    if (chIdx === -1) return null;
    if (chIdx + 1 < cur.chapters.length) {
      return { chapter: cur.chapters[chIdx + 1], arc: cur, sameArc: true };
    }
    if (arcIdx + 1 < filtered.length) {
      const nextArc = filtered[arcIdx + 1];
      if (nextArc.chapters.length) {
        return { chapter: nextArc.chapters[0], arc: nextArc, sameArc: false };
      }
    }
    return null;
  };

  // Compute the previous chapter (in same arc, or last chapter of the previous
  // arc), mirroring computeNext.
  const computePrevious = (chapter, arc) => {
    if (!chapter || !arc) return null;
    const filtered = window.NovelStore.filterForReader(novelStore);
    const arcIdx = filtered.findIndex(a => String(a.id) === String(arc.id));
    if (arcIdx === -1) return null;
    const cur = filtered[arcIdx];
    const chIdx = cur.chapters.findIndex(c => String(c.id) === String(chapter.id));
    if (chIdx === -1) return null;
    if (chIdx > 0) {
      return { chapter: cur.chapters[chIdx - 1], arc: cur, sameArc: true };
    }
    if (arcIdx > 0) {
      const prevArc = filtered[arcIdx - 1];
      if (prevArc.chapters.length) {
        return {
          chapter: prevArc.chapters[prevArc.chapters.length - 1],
          arc: prevArc,
          sameArc: false,
        };
      }
    }
    return null;
  };

  // Swap chapter in the open reader (no zoom transition).
  const swapChapter = (chapter, arc) => {
    setSelectedChapter(chapter);
    setSelectedArc(arc);
  };

  // closeReader regresa a la galería (no al cuarto), preservando el arco
  // actual como inicial pa' que la galería abra enfocada en él. La galería
  // mantiene su propio estado interno (vista ARCOS / LÍNEA DE TIEMPO).
  const closeReader = () => {
    const arcId = selectedArc?.id ?? galleryInitialId;
    setSelectedChapter(null);
    setSelectedArc(null);
    setGalleryInitialId(arcId);
    setView("gallery");
  };

  const next = computeNext(selectedChapter, selectedArc);
  const previous = computePrevious(selectedChapter, selectedArc);

  return (
    <div className="stage">
      {user?.isAdmin && view !== "reader" && (
        <div className="admin-toggle">
          <button
            className={"admin-toggle-btn" + (adminMode === "lector" ? " active" : "")}
            onClick={() => setAdminMode("lector")}
          >
            LECTOR
          </button>
          <div className="admin-toggle-sep">/</div>
          <button
            className={"admin-toggle-btn" + (adminMode === "admin" ? " active" : "")}
            onClick={() => setAdminMode("admin")}
          >
            EDITOR
          </button>
        </div>
      )}

      {user?.isAdmin && adminMode === "admin" ? (
        <ErrorBoundary>
          <AdminPanel user={user} onExitAdmin={() => setAdminMode("lector")} />
        </ErrorBoundary>
      ) : (
        <>
          <Room
            vhsCount={vhsCount}
            onOpenGallery={openGallery}
            onClickTV={() => {
              if (user) openGallery(null);
            }}
            onOpenAlbum={() => setAlbumOpen(true)}
            dim={view === "gallery"}
            zooming={view === "zooming"}
            tvScreen={tvScreen}
            setTvScreen={setTvScreen}
            user={user}
            setUser={setUser}
            ownedCount={ownedCount}
            totalCards={allCards.length}
          />

          <Gallery
            open={view === "gallery"}
            initialId={galleryInitialId}
            onClose={closeGallery}
            onRead={startRead}
            readIds={readIds}
          />

          <Reader
            open={view === "reader"}
            chapter={selectedChapter}
            arc={selectedArc}
            onClose={closeReader}
            onContinue={next ? () => swapChapter(next.chapter, next.arc) : undefined}
            onPrevious={previous ? () => swapChapter(previous.chapter, previous.arc) : undefined}
            onScrolledToBottom={handleScrolledToBottom}
            nextLabel={next ? (next.sameArc ? "CONTINUAR" : `CONTINUAR · ${next.arc.title}`) : undefined}
          />
        </>
      )}

      <Tweaks show={tweaksOn} vhsCount={vhsCount} setVhsCount={updateVhsCount} />

      {/* Card collection album */}
      <window.CardAlbum
        open={albumOpen}
        onClose={() => setAlbumOpen(false)}
        allCards={allCards}
      />

      {/* Card drop pop-up */}
      {dropQueue.length > 0 && (
        <window.CardDropOverlay
          queue={dropQueue}
          onDismiss={() => setDropQueue([])}
          onOpenAlbum={() => setAlbumOpen(true)}
          onVolver={closeReader}
          onContinue={next ? () => swapChapter(next.chapter, next.arc) : undefined}
        />
      )}

      {/* Toast: error guardando carta (visible en pantalla, no silencioso) */}
      {cardError && (
        <div style={{
          position: "fixed", top: 18, left: "50%", transform: "translateX(-50%)",
          zIndex: 2000, maxWidth: 640,
          background: "#2a0a14", border: "1px solid #ff5aa0", color: "#ffd0e0",
          padding: "12px 18px", borderRadius: 4,
          fontFamily: "'Geist Sans', system-ui, sans-serif", fontSize: 12,
          letterSpacing: "0.05em", lineHeight: 1.4,
          boxShadow: "0 12px 30px rgba(0,0,0,0.6)",
        }}>
          <div style={{ fontWeight: 700, marginBottom: 4, color: "#ff5aa0" }}>
            ◉ NO SE PUDO GUARDAR LA CARTA
          </div>
          <div style={{ fontFamily: "monospace", fontSize: 11, opacity: 0.9 }}>
            {cardError}
          </div>
          <button
            onClick={() => setCardError(null)}
            style={{
              marginTop: 8, padding: "4px 10px",
              background: "transparent", color: "#ffd0e0",
              border: "1px solid #ff5aa0", borderRadius: 2, cursor: "pointer",
              fontFamily: "inherit", fontSize: 10, letterSpacing: "0.2em",
            }}
          >
            CERRAR
          </button>
        </div>
      )}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);
