import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useLocation, useParams, Navigate, Link } from "react-router-dom";
import { toast } from "sonner";
import { AppHeader } from "../components/layout/AppHeader";
import Footer from "../components/layout/Footer";
import { ResearcherCard } from "../components/dashboard/ResearcherCard";
import { StatsRow } from "../components/dashboard/StatsRow";
import { PublicationsTable } from "../components/dashboard/PublicationsTable";
import { ExportDropdown } from "../components/dashboard/ExportDropdown";
import { SyncButton } from "../components/dashboard/SyncButton";
import { ArrowLeftIcon } from "../components/ui/Icons";
import {
downloadExport,
searchResearcher,
syncResearcher,
} from "../services/api";
import { isValidOrcid } from "../utils/orcid";
import {
DEFAULT_EXPORT_DESTINATION,
swordXmlFilename,
} from "../utils/exportProfiles";
import { useAuth } from "../contexts/AuthContext";
import {
markPublicationsAsDownloaded,
publicationsNeedDownloadFlags,
} from "../utils/downloadTracking";
const SUCCESS_FLASH_MS = 3000;
/** Minimum gap between sync requests (protects backend + avoids toast spam). */
const SYNC_COOLDOWN_MS = 5000;
const SYNC_TOAST_ID = "researcher-sync";
/** Minimum gap between export requests (protects backend + avoids toast spam). */
const EXPORT_COOLDOWN_MS = 5000;
const EXPORT_TOAST_ID = "researcher-export";
/**
* Researcher detail page. Owns:
* - Carga inicial vía `searchResearcher`. Si llegamos desde la landing
* usamos el bundle ya cargado en `location.state` para evitar
* duplicar la petición.
* - Re-sync manual (POST + actualización de estado in-place + toast).
* - Exportación SWORD/ZIP:
* · Si hay selección manual → exporta esos IDs.
* · Si el usuario está autenticado y sin selección → exporta solo
* los IDs con downloaded_by_me=false ("lo nuevo").
* · Si no está autenticado y sin selección → exporta todo.
*/
export function DashboardPage() {
const { orcid } = useParams();
const location = useLocation();
const { isAuthenticated } = useAuth();
const initialBundleRef = useRef(location.state?.bundle ?? null);
const consumedInitialBundleRef = useRef(false);
const initialBundle = initialBundleRef.current;
const [researcher, setResearcher] = useState(initialBundle?.researcher ?? null);
const [publications, setPublications] = useState(
initialBundle?.publications ?? [],
);
const [pubsLoading, setPubsLoading] = useState(!initialBundle);
const [pubsError, setPubsError] = useState(null);
const [syncStatus, setSyncStatus] = useState("idle"); // idle | loading | success
const [syncCooldownActive, setSyncCooldownActive] = useState(false);
const syncInFlightRef = useRef(false);
const syncCooldownUntilRef = useRef(0);
const syncCooldownTimerRef = useRef(null);
const [exportingFormat, setExportingFormat] = useState(null);
const [exportCooldownActive, setExportCooldownActive] = useState(false);
const exportInFlightRef = useRef(false);
const exportCooldownUntilRef = useRef(0);
const exportCooldownTimerRef = useRef(null);
const [exportDestination, setExportDestination] = useState(
DEFAULT_EXPORT_DESTINATION,
);
const [selectedIds, setSelectedIds] = useState(() => new Set());
// IDs de publicaciones que el usuario no ha descargado todavía
const newPublicationIds = useMemo(
() =>
isAuthenticated
? publications
.filter((p) => p.downloaded_by_me === false)
.map((p) => p.id)
: [],
[publications, isAuthenticated],
);
const loadBundle = useCallback(
async (signal) => {
setPubsLoading(true);
setPubsError(null);
try {
const bundle = await searchResearcher(orcid, { signal });
if (signal?.aborted) return;
setResearcher(bundle.researcher);
setPublications(bundle.publications);
setSelectedIds((prev) => {
if (prev.size === 0) return prev;
const alive = new Set(bundle.publications.map((p) => p.id));
const next = new Set();
for (const id of prev) if (alive.has(id)) next.add(id);
return next.size === prev.size ? prev : next;
});
} catch (err) {
if (signal?.aborted) return;
setPubsError(err);
toast.error("No se pudo cargar el investigador", {
description: err?.message ?? "Error desconocido.",
});
} finally {
if (!signal?.aborted) setPubsLoading(false);
}
},
[orcid],
);
useEffect(() => {
if (!isValidOrcid(orcid)) return;
const cachedBundle = initialBundleRef.current;
if (cachedBundle && !consumedInitialBundleRef.current) {
consumedInitialBundleRef.current = true;
initialBundleRef.current = null;
if (
!publicationsNeedDownloadFlags(
cachedBundle.publications,
isAuthenticated,
)
) {
return;
}
}
const ctrl = new AbortController();
loadBundle(ctrl.signal);
return () => ctrl.abort();
}, [orcid, loadBundle, isAuthenticated]);
useEffect(() => {
return () => {
if (syncCooldownTimerRef.current) {
clearTimeout(syncCooldownTimerRef.current);
}
if (exportCooldownTimerRef.current) {
clearTimeout(exportCooldownTimerRef.current);
}
};
}, []);
const syncDisabled = syncStatus !== "idle" || syncCooldownActive;
const exportDisabled = Boolean(exportingFormat) || exportCooldownActive;
function startSyncCooldown() {
syncCooldownUntilRef.current = Date.now() + SYNC_COOLDOWN_MS;
setSyncCooldownActive(true);
if (syncCooldownTimerRef.current) {
clearTimeout(syncCooldownTimerRef.current);
}
syncCooldownTimerRef.current = setTimeout(() => {
setSyncCooldownActive(false);
syncCooldownTimerRef.current = null;
}, SYNC_COOLDOWN_MS);
}
function startExportCooldown() {
exportCooldownUntilRef.current = Date.now() + EXPORT_COOLDOWN_MS;
setExportCooldownActive(true);
if (exportCooldownTimerRef.current) {
clearTimeout(exportCooldownTimerRef.current);
}
exportCooldownTimerRef.current = setTimeout(() => {
setExportCooldownActive(false);
exportCooldownTimerRef.current = null;
}, EXPORT_COOLDOWN_MS);
}
if (!isValidOrcid(orcid)) {
return