<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";

import AppPage from "@/components/layout/AppPage.vue";
import AsyncStateGate from "@/components/patterns/AsyncStateGate.vue";
import RequestStateAlerts from "@/components/patterns/RequestStateAlerts.vue";
import AppBadge from "@/components/ui/AppBadge.vue";
import AppButton from "@/components/ui/AppButton.vue";
import AppCard from "@/components/ui/AppCard.vue";
import AppHelpHint from "@/components/ui/AppHelpHint.vue";
import AppInput from "@/components/ui/AppInput.vue";
import AppRefreshButton from "@/components/ui/AppRefreshButton.vue";
import AppSelect from "@/components/ui/AppSelect.vue";
import AppTable from "@/components/ui/AppTable.vue";
import {
  listPublications,
  markAllRead,
  markSelectedRead,
  retryPublicationPdf,
  togglePublicationFavorite,
  type PublicationItem,
  type PublicationMode,
  type PublicationsResult,
} from "@/features/publications";
import { listScholars, type ScholarProfile } from "@/features/scholars";
import { ApiRequestError } from "@/lib/api/errors";
import { useRunStatusStore } from "@/stores/run_status";
import { useUserSettingsStore } from "@/stores/user_settings";

type PublicationSortKey =
  | "title"
  | "scholar"
  | "year"
  | "citations"
  | "first_seen"
  | "pdf_status";

type BulkAction =
  | "mark_selected_read"
  | "mark_all_unread_read"
  | "select_all_visible_unread"
  | "clear_selection";
type BulkActionOption = { value: BulkAction; label: string };

const loading = ref(true);
const publishingAll = ref(false);
const publishingSelected = ref(false);
const mode = ref<PublicationMode>("all");
const favoriteOnly = ref(false);
const bulkAction = ref<BulkAction>("mark_all_unread_read");
const selectedScholarFilter = ref("");
const searchQuery = ref("");
const sortKey = ref<PublicationSortKey>("first_seen");
const sortDirection = ref<"asc" | "desc">("desc");
const currentPage = ref(1);
const pageSize = ref("50");
const publicationSnapshot = ref<string | null>(null);

const scholars = ref<ScholarProfile[]>([]);
const listState = ref<PublicationsResult | null>(null);
const selectedPublicationKeys = ref<Set<string>>(new Set());
const retryingPublicationKeys = ref<Set<string>>(new Set());
const favoriteUpdatingKeys = ref<Set<string>>(new Set());

const errorMessage = ref<string | null>(null);
const errorRequestId = ref<string | null>(null);
const successMessage = ref<string | null>(null);
const route = useRoute();
const router = useRouter();
const textCollator = new Intl.Collator(undefined, { sensitivity: "base", numeric: true });
const runStatus = useRunStatusStore();
const userSettings = useUserSettingsStore();
const PUBLICATIONS_RUN_STATUS_SYNC_INTERVAL_MS = 5000;
let runStatusSyncTimer: ReturnType<typeof setInterval> | null = null;

function normalizeScholarFilterQuery(value: unknown): string {
  if (Array.isArray(value)) {
    return normalizeScholarFilterQuery(value[0]);
  }
  if (typeof value !== "string") {
    return "";
  }
  const parsed = Number(value);
  return Number.isInteger(parsed) && parsed > 0 ? String(parsed) : "";
}

function normalizeFavoriteOnlyQuery(value: unknown): boolean {
  if (Array.isArray(value)) {
    return normalizeFavoriteOnlyQuery(value[0]);
  }
  if (typeof value !== "string") {
    return false;
  }
  const normalized = value.trim().toLowerCase();
  return normalized === "1" || normalized === "true" || normalized === "yes";
}

function normalizePageQuery(value: unknown): number {
  if (Array.isArray(value)) {
    return normalizePageQuery(value[0]);
  }
  if (typeof value !== "string") {
    return 1;
  }
  const parsed = Number(value);
  return Number.isInteger(parsed) && parsed > 0 ? parsed : 1;
}

function syncFiltersFromRoute(): boolean {
  let changed = false;
  const nextScholar = normalizeScholarFilterQuery(route.query.scholar);
  const nextFavoriteOnly = normalizeFavoriteOnlyQuery(route.query.favorite);
  const nextPage = normalizePageQuery(route.query.page);
  if (selectedScholarFilter.value !== nextScholar) {
    selectedScholarFilter.value = nextScholar;
    changed = true;
  }
  if (favoriteOnly.value !== nextFavoriteOnly) {
    favoriteOnly.value = nextFavoriteOnly;
    changed = true;
  }
  if (currentPage.value !== nextPage) {
    currentPage.value = nextPage;
    changed = true;
  }
  return changed;
}

async function syncFiltersToRoute(): Promise<void> {
  const nextScholar = selectedScholarFilter.value.trim();
  const currentScholar = normalizeScholarFilterQuery(route.query.scholar);
  const currentFavoriteOnly = normalizeFavoriteOnlyQuery(route.query.favorite);
  const currentPageQuery = normalizePageQuery(route.query.page);

  if (
    nextScholar === currentScholar
    && favoriteOnly.value === currentFavoriteOnly
    && currentPage.value === currentPageQuery
  ) {
    return;
  }

  await router.replace({
    query: {
      ...route.query,
      scholar: nextScholar || undefined,
      favorite: favoriteOnly.value ? "1" : undefined,
      page: currentPage.value > 1 ? String(currentPage.value) : undefined,
    },
  });
}

function formatDate(value: string): string {
  const asDate = new Date(value);
  if (Number.isNaN(asDate.getTime())) {
    return value;
  }
  return asDate.toLocaleDateString();
}

function publicationKey(item: PublicationItem): string {
  return `${item.scholar_profile_id}:${item.publication_id}`;
}

function scholarLabel(item: ScholarProfile): string {
  return item.display_name || item.scholar_id;
}

function publicationPrimaryUrl(item: PublicationItem): string | null {
  return item.pub_url || item.pdf_url;
}

function publicationIdentifierUrl(item: PublicationItem): string | null {
  if (!item.display_identifier?.url) {
    return null;
  }
  return item.display_identifier.url;
}

function publicationIdentifierLabel(item: PublicationItem): string | null {
  return item.display_identifier?.label ?? null;
}

function startRunStatusSyncLoop(): void {
  if (runStatusSyncTimer !== null) {
    return;
  }
  runStatusSyncTimer = setInterval(() => {
    if (runStatus.isRunActive) {
      return;
    }
    void runStatus.syncLatest();
  }, PUBLICATIONS_RUN_STATUS_SYNC_INTERVAL_MS);
}

function stopRunStatusSyncLoop(): void {
  if (runStatusSyncTimer === null) {
    return;
  }
  clearInterval(runStatusSyncTimer);
  runStatusSyncTimer = null;
}

const selectedScholarName = computed(() => {
  const selectedId = Number(selectedScholarFilter.value);
  if (!Number.isInteger(selectedId) || selectedId <= 0) {
    return "all scholars";
  }
  const profile = scholars.value.find((item) => item.id === selectedId);
  return profile ? scholarLabel(profile) : "the selected scholar";
});

const filteredPublications = computed(() => {
  let stream = [...runStatus.livePublications];
  if (favoriteOnly.value) {
    stream = stream.filter((p) => p.is_favorite);
  }
  if (mode.value === "unread") {
    stream = stream.filter((p) => !p.is_read);
  }
  const selectedScholarId = Number(selectedScholarFilter.value);
  if (Number.isInteger(selectedScholarId) && selectedScholarId > 0) {
    stream = stream.filter((p) => p.scholar_profile_id === selectedScholarId);
  }

  const base = listState.value?.publications ?? [];
  const merged = [...stream, ...base];
  const seenKeys = new Set<string>();
  const deduped: typeof base = [];
  for (const item of merged) {
    const key = publicationKey(item);
    if (!seenKeys.has(key)) {
      seenKeys.add(key);
      deduped.push(item);
    }
  }

  const normalized = searchQuery.value.trim().toLowerCase();
  if (!normalized) {
    return deduped;
  }
  // Client-side fallback: filter live-discovered publications that haven't been
  // server-round-tripped yet. The main server query already filters by search.
  return deduped;
});

function publicationSortValue(item: PublicationItem, key: PublicationSortKey): number | string {
  if (key === "title") {
    return item.title;
  }
  if (key === "scholar") {
    return item.scholar_label;
  }
  if (key === "year") {
    return item.year ?? -1;
  }
  if (key === "citations") {
    return item.citation_count;
  }
  if (key === "pdf_status") {
    if (item.pdf_url || item.pdf_status === "resolved") {
      return 4;
    }
    if (item.pdf_status === "running") {
      return 3;
    }
    if (item.pdf_status === "queued") {
      return 2;
    }
    if (item.pdf_status === "failed") {
      return 0;
    }
    return 1;
  }
  const timestamp = Date.parse(item.first_seen_at);
  return Number.isNaN(timestamp) ? 0 : timestamp;
}

const sortedPublications = computed(() => {
  const direction = sortDirection.value === "asc" ? 1 : -1;
  return [...filteredPublications.value].sort((left, right) => {
    const leftValue = publicationSortValue(left, sortKey.value);
    const rightValue = publicationSortValue(right, sortKey.value);
    let comparison = 0;
    if (typeof leftValue === "string" && typeof rightValue === "string") {
      comparison = textCollator.compare(leftValue, rightValue);
    } else {
      comparison = Number(leftValue) - Number(rightValue);
    }
    if (comparison !== 0) {
      return comparison * direction;
    }
    return right.publication_id - left.publication_id;
  });
});

const visibleUnreadKeys = computed(() => {
  const keys = new Set<string>();
  for (const item of sortedPublications.value) {
    if (!item.is_read) {
      keys.add(publicationKey(item));
    }
  }
  return keys;
});

const pageSizeValue = computed(() => {
  const parsed = Number(pageSize.value);
  if (!Number.isInteger(parsed)) {
    return 50;
  }
  return Math.max(10, Math.min(200, parsed));
});
const hasNextPage = computed(() => Boolean(listState.value?.has_next));
const hasPrevPage = computed(() => Boolean(listState.value?.has_prev));
const totalPages = computed(() => {
  if (!listState.value) {
    return 1;
  }
  return Math.max(1, Math.ceil(listState.value.total_count / Math.max(listState.value.page_size, 1)));
});

const selectedCount = computed(() => selectedPublicationKeys.value.size);
const totalCount = computed(() => listState.value?.total_count ?? 0);
const visibleCount = computed(() => sortedPublications.value.length);
const visibleUnreadCount = computed(() => visibleUnreadKeys.value.size);
const visibleFavoriteCount = computed(
  () => sortedPublications.value.filter((item) => item.is_favorite).length,
);
const actionBusy = computed(() => loading.value || publishingAll.value || publishingSelected.value);
const startRunDisabledReason = computed(() => {
  if (!userSettings.manualRunAllowed) {
    return "Manual runs are disabled by policy.";
  }
  if (runStatus.safetyState.cooldown_active) {
    return runStatus.safetyState.cooldown_reason_label || "Safety cooldown is active.";
  }
  if (runStatus.isRunActive) {
    return "A run is already in progress.";
  }
  return null;
});
const startRunButtonLabel = computed(() => {
  if (runStatus.isSubmitting) {
    return "Starting...";
  }
  if (!userSettings.manualRunAllowed) {
    return "Manual runs disabled";
  }
  if (runStatus.safetyState.cooldown_active) {
    return "Safety cooldown";
  }
  if (runStatus.isLikelyRunning) {
    return "Run active";
  }
  return "Start run";
});
const startRunDisabled = computed(() => Boolean(startRunDisabledReason.value) || runStatus.isSubmitting);
const hasSelection = computed(() => selectedCount.value > 0);
const bulkActionOptions = computed<BulkActionOption[]>(() => {
  if (hasSelection.value) {
    return [
      {
        value: "mark_selected_read",
        label: `Mark selected as read (${selectedCount.value})`,
      },
      {
        value: "clear_selection",
        label: `Clear selection (${selectedCount.value})`,
      },
    ];
  }
  return [
    { value: "mark_all_unread_read", label: "Mark all unread as read" },
    { value: "select_all_visible_unread", label: "Select all visible unread" },
  ];
});
const bulkApplyDisabled = computed(() => {
  if (actionBusy.value) {
    return true;
  }
  if (bulkAction.value === "mark_selected_read" || bulkAction.value === "clear_selection") {
    return selectedCount.value === 0;
  }
  if (bulkAction.value === "mark_all_unread_read" || bulkAction.value === "select_all_visible_unread") {
    return visibleUnreadCount.value === 0;
  }
  return false;
});
const bulkApplyLabel = computed(() => {
  if (publishingSelected.value || publishingAll.value) {
    return "Applying...";
  }
  if (bulkAction.value === "select_all_visible_unread") {
    return "Select";
  }
  if (bulkAction.value === "clear_selection") {
    return "Clear";
  }
  return "Apply";
});
const showingEmptyList = computed(() => Boolean(listState.value) && sortedPublications.value.length === 0);
const scopeLabel = computed(() => {
  if (mode.value === "latest" && favoriteOnly.value) {
    return "New + Favorites";
  }
  if (mode.value === "latest") {
    return "New";
  }
  if (mode.value === "unread" && favoriteOnly.value) {
    return "Unread + Favorites";
  }
  if (mode.value === "unread") {
    return "Unread";
  }
  if (favoriteOnly.value) {
    return "All + Favorites";
  }
  return "All";
});

const emptyTitle = computed(() =>
  searchQuery.value.trim().length > 0 ? "No publications match this search" : "No publications found",
);

const emptyBody = computed(() => {
  if (searchQuery.value.trim().length > 0) {
    return "Try another title, scholar, venue, or year.";
  }
  if (favoriteOnly.value && mode.value === "latest") {
    return `No new favorites for ${selectedScholarName.value}.`;
  }
  if (mode.value === "latest") {
    return `No new publications in the latest run for ${selectedScholarName.value}.`;
  }
  if (favoriteOnly.value && mode.value === "unread") {
    return `No unread favorites for ${selectedScholarName.value}.`;
  }
  if (favoriteOnly.value) {
    return `No favorites for ${selectedScholarName.value}.`;
  }
  if (mode.value === "unread") {
    return `No unread publications for ${selectedScholarName.value}.`;
  }
  return `No publication records for ${selectedScholarName.value}.`;
});

const allVisibleUnreadSelected = computed(() => {
  if (visibleUnreadKeys.value.size === 0) {
    return false;
  }
  for (const key of visibleUnreadKeys.value) {
    if (!selectedPublicationKeys.value.has(key)) {
      return false;
    }
  }
  return true;
});

watch(sortedPublications, (items) => {
  const validKeys = new Set(items.filter((item) => !item.is_read).map((item) => publicationKey(item)));
  const next = new Set<string>();
  for (const key of selectedPublicationKeys.value) {
    if (validKeys.has(key)) {
      next.add(key);
    }
  }
  if (next.size !== selectedPublicationKeys.value.size) {
    selectedPublicationKeys.value = next;
  }
});

watch(hasSelection, (nextHasSelection) => {
  const validValues = new Set(bulkActionOptions.value.map((item) => item.value));
  if (validValues.has(bulkAction.value)) {
    return;
  }
  bulkAction.value = nextHasSelection ? "mark_selected_read" : "mark_all_unread_read";
});

async function toggleSort(nextKey: PublicationSortKey): Promise<void> {
  if (sortKey.value === nextKey) {
    sortDirection.value = sortDirection.value === "asc" ? "desc" : "asc";
  } else {
    sortKey.value = nextKey;
    sortDirection.value = nextKey === "first_seen" || nextKey === "pdf_status" ? "desc" : "asc";
  }
  currentPage.value = 1;
  publicationSnapshot.value = null;
  await loadPublications();
}

function sortMarker(key: PublicationSortKey): string {
  if (sortKey.value !== key) {
    return "↕";
  }
  return sortDirection.value === "asc" ? "↑" : "↓";
}

async function loadScholarFilters(): Promise<void> {
  try {
    scholars.value = await listScholars();
  } catch {
    scholars.value = [];
  }
}

function selectedScholarId(): number | undefined {
  const parsed = Number(selectedScholarFilter.value);
  return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
}

async function loadPublications(): Promise<void> {
  loading.value = true;
  errorMessage.value = null;
  errorRequestId.value = null;

  try {
    listState.value = await listPublications({
      mode: mode.value,
      favoriteOnly: favoriteOnly.value,
      scholarProfileId: selectedScholarId(),
      search: searchQuery.value.trim() || undefined,
      sortBy: sortKey.value,
      sortDir: sortDirection.value,
      page: currentPage.value,
      pageSize: pageSizeValue.value,
      snapshot: publicationSnapshot.value ?? undefined,
    });
    publicationSnapshot.value = listState.value.snapshot;
    currentPage.value = listState.value.page;
    pageSize.value = String(listState.value.page_size);
    selectedPublicationKeys.value = new Set();
  } catch (error) {
    listState.value = null;
    selectedPublicationKeys.value = new Set();
    if (error instanceof ApiRequestError) {
      errorMessage.value = error.message;
      errorRequestId.value = error.requestId;
    } else {
      errorMessage.value = "Unable to load publications.";
    }
  } finally {
    loading.value = false;
  }
}

async function onModeChanged(): Promise<void> {
  currentPage.value = 1;
  publicationSnapshot.value = null;
  await syncFiltersToRoute();
  await loadPublications();
}

async function onScholarFilterChanged(): Promise<void> {
  currentPage.value = 1;
  publicationSnapshot.value = null;
  await syncFiltersToRoute();
  await loadPublications();
}

async function onFavoriteOnlyChanged(): Promise<void> {
  favoriteOnly.value = !favoriteOnly.value;
  currentPage.value = 1;
  publicationSnapshot.value = null;
  await syncFiltersToRoute();
  await loadPublications();
}

async function onPageSizeChanged(): Promise<void> {
  currentPage.value = 1;
  publicationSnapshot.value = null;
  await syncFiltersToRoute();
  await loadPublications();
}

async function onPrevPage(): Promise<void> {
  if (!hasPrevPage.value || currentPage.value <= 1) {
    return;
  }
  currentPage.value -= 1;
  await syncFiltersToRoute();
  await loadPublications();
}

async function onNextPage(): Promise<void> {
  if (!hasNextPage.value) {
    return;
  }
  currentPage.value += 1;
  await syncFiltersToRoute();
  await loadPublications();
}

function selectAllVisibleUnread(): void {
  selectedPublicationKeys.value = new Set(visibleUnreadKeys.value);
}

function clearSelection(): void {
  selectedPublicationKeys.value = new Set();
}

async function onApplyBulkAction(): Promise<void> {
  if (bulkApplyDisabled.value) {
    return;
  }
  if (bulkAction.value === "select_all_visible_unread") {
    selectAllVisibleUnread();
    return;
  }
  if (bulkAction.value === "clear_selection") {
    clearSelection();
    return;
  }
  if (bulkAction.value === "mark_selected_read") {
    await onMarkSelectedRead();
    return;
  }
  await onMarkAllRead();
}

function onToggleAllVisible(event: Event): void {
  const checked = (event.target as HTMLInputElement).checked;
  const next = new Set(selectedPublicationKeys.value);
  for (const key of visibleUnreadKeys.value) {
    if (checked) {
      next.add(key);
    } else {
      next.delete(key);
    }
  }
  selectedPublicationKeys.value = next;
}

function onToggleRowSelection(item: PublicationItem, event: Event): void {
  const checked = (event.target as HTMLInputElement).checked;
  const key = publicationKey(item);
  const next = new Set(selectedPublicationKeys.value);
  if (checked) {
    next.add(key);
  } else {
    next.delete(key);
  }
  selectedPublicationKeys.value = next;
}

function isRetryingPublication(item: PublicationItem): boolean {
  return retryingPublicationKeys.value.has(publicationKey(item));
}

function isFavoriteUpdating(item: PublicationItem): boolean {
  return favoriteUpdatingKeys.value.has(publicationKey(item));
}

function canRetryPublicationPdf(item: PublicationItem): boolean {
  return !item.pdf_url && item.pdf_status === "failed" && item.pdf_attempt_count > 0;
}

function pdfPendingLabel(item: PublicationItem): string {
  if (item.pdf_status === "queued") {
    return "Queued";
  }
  if (item.pdf_status === "running") {
    return "Resolving...";
  }
  if (item.pdf_status === "failed") {
    return "Missing";
  }
  return "Untracked";
}

function replacePublication(updated: PublicationItem): void {
  if (!listState.value) {
    return;
  }
  listState.value.publications = listState.value.publications.map((item) => {
    if (publicationKey(item) !== publicationKey(updated)) {
      return item;
    }
    return updated;
  });
}

function withFavorite(item: PublicationItem, isFavorite: boolean): PublicationItem {
  return {
    ...item,
    is_favorite: isFavorite,
  };
}

async function onToggleFavorite(item: PublicationItem): Promise<void> {
  if (isFavoriteUpdating(item)) {
    return;
  }
  const key = publicationKey(item);
  const nextFavorite = !item.is_favorite;
  const nextUpdating = new Set(favoriteUpdatingKeys.value);
  nextUpdating.add(key);
  favoriteUpdatingKeys.value = nextUpdating;
  errorMessage.value = null;
  errorRequestId.value = null;

  replacePublication(withFavorite(item, nextFavorite));

  try {
    const response = await togglePublicationFavorite(
      item.publication_id,
      item.scholar_profile_id,
      nextFavorite,
    );
    replacePublication(response.publication);
  } catch (error) {
    replacePublication(withFavorite(item, item.is_favorite));
    if (error instanceof ApiRequestError) {
      errorMessage.value = error.message;
      errorRequestId.value = error.requestId;
    } else {
      errorMessage.value = "Unable to update favorite state.";
    }
  } finally {
    const cleared = new Set(favoriteUpdatingKeys.value);
    cleared.delete(key);
    favoriteUpdatingKeys.value = cleared;
  }
}

async function onRetryPdf(item: PublicationItem): Promise<void> {
  if (item.pdf_url || !canRetryPublicationPdf(item)) {
    return;
  }
  const key = publicationKey(item);
  const next = new Set(retryingPublicationKeys.value);
  next.add(key);
  retryingPublicationKeys.value = next;
  errorMessage.value = null;
  errorRequestId.value = null;
  successMessage.value = null;

  try {
    const response = await retryPublicationPdf(item.publication_id, item.scholar_profile_id);
    replacePublication(response.publication);
    successMessage.value = response.message;
  } catch (error) {
    if (error instanceof ApiRequestError) {
      errorMessage.value = error.message;
      errorRequestId.value = error.requestId;
    } else {
      errorMessage.value = "Unable to retry PDF lookup for this publication.";
    }
  } finally {
    const cleared = new Set(retryingPublicationKeys.value);
    cleared.delete(key);
    retryingPublicationKeys.value = cleared;
  }
}

async function onMarkSelectedRead(): Promise<void> {
  if (selectedPublicationKeys.value.size === 0 || !listState.value) {
    return;
  }

  const selectedLookup = new Set(selectedPublicationKeys.value);
  const selections = listState.value.publications
    .filter((item) => selectedLookup.has(publicationKey(item)))
    .map((item) => ({
      scholar_profile_id: item.scholar_profile_id,
      publication_id: item.publication_id,
    }));

  publishingSelected.value = true;
  successMessage.value = null;
  errorMessage.value = null;
  errorRequestId.value = null;

  try {
    const response = await markSelectedRead(selections);
    successMessage.value = `${response.updated_count} publication${response.updated_count === 1 ? "" : "s"} marked as read.`;
    await loadPublications();
  } catch (error) {
    if (error instanceof ApiRequestError) {
      errorMessage.value = error.message;
      errorRequestId.value = error.requestId;
    } else {
      errorMessage.value = "Unable to mark selected publications as read.";
    }
  } finally {
    publishingSelected.value = false;
  }
}

async function onMarkAllRead(): Promise<void> {
  publishingAll.value = true;
  successMessage.value = null;
  errorMessage.value = null;
  errorRequestId.value = null;

  try {
    const response = await markAllRead();
    successMessage.value = `${response.updated_count} publication${response.updated_count === 1 ? "" : "s"} marked as read.`;
    await loadPublications();
  } catch (error) {
    if (error instanceof ApiRequestError) {
      errorMessage.value = error.message;
      errorRequestId.value = error.requestId;
    } else {
      errorMessage.value = "Unable to mark publications as read.";
    }
  } finally {
    publishingAll.value = false;
  }
}

async function onStartRun(): Promise<void> {
  if (startRunDisabled.value) {
    return;
  }
  successMessage.value = null;
  errorMessage.value = null;
  errorRequestId.value = null;

  const result = await runStatus.startManualCheck();
  if (result.kind === "started") {
    successMessage.value = `Run #${result.runId} started.`;
    return;
  }
  if (result.kind === "already_running") {
    successMessage.value = result.runId
      ? `Run #${result.runId} is already in progress.`
      : "A run is already in progress.";
    return;
  }
  errorMessage.value = result.message;
  errorRequestId.value = result.requestId;
}

function resetSearchQuery(): void {
  searchQuery.value = "";
}

let searchDebounceTimer: ReturnType<typeof setTimeout> | null = null;
watch(searchQuery, () => {
  if (searchDebounceTimer !== null) {
    clearTimeout(searchDebounceTimer);
  }
  searchDebounceTimer = setTimeout(() => {
    currentPage.value = 1;
    publicationSnapshot.value = null;
    void loadPublications();
  }, 300);
});

onMounted(() => {
  syncFiltersFromRoute();
  startRunStatusSyncLoop();
  void Promise.all([loadScholarFilters(), loadPublications(), runStatus.syncLatest()]);
});

onUnmounted(() => {
  stopRunStatusSyncLoop();
});

watch(
  () => runStatus.latestRun,
  async (nextRun, previousRun) => {
    const nextRunId = nextRun && (nextRun.status === "running" || nextRun.status === "resolving")
      ? nextRun.id
      : null;
    const previousRunId = previousRun && (previousRun.status === "running" || previousRun.status === "resolving")
      ? previousRun.id
      : null;
    if (nextRunId === null || nextRunId === previousRunId) {
      return;
    }
    publicationSnapshot.value = null;
    currentPage.value = 1;
    await loadPublications();
  },
);

watch(
  () => [route.query.scholar, route.query.favorite, route.query.page],
  async () => {
    const previousScholar = selectedScholarFilter.value;
    const previousFavorite = favoriteOnly.value;
    const changed = syncFiltersFromRoute();
    if (!changed) {
      return;
    }
    if (
      selectedScholarFilter.value !== previousScholar
      || favoriteOnly.value !== previousFavorite
    ) {
      publicationSnapshot.value = null;
    }
    await loadPublications();
  },
);
</script>

<template>
  <AppPage
    title="Publications"
    subtitle="Review discoveries, open PDFs, and update read/favorite state in one workspace."
    fill
  >
    <div class="flex min-h-0 flex-1 flex-col gap-4 xl:overflow-hidden">
      <AppCard class="flex min-h-0 flex-1 flex-col gap-4">
      <div class="grid gap-3 xl:grid-cols-[minmax(0,13rem)_minmax(0,18rem)_minmax(0,1fr)_auto] xl:items-end">
        <div class="grid gap-1 text-xs text-secondary">
          <div class="flex items-center gap-1">
            <span>Status</span>
            <AppHelpHint text="All shows full history. Unread and New narrow the dataset before search." />
          </div>
          <AppSelect v-model="mode" :disabled="actionBusy" @change="onModeChanged">
            <option value="all">All records</option>
            <option value="unread">Unread</option>
            <option value="latest">New (latest run)</option>
          </AppSelect>
        </div>

        <label class="grid gap-1 text-xs text-secondary" for="publications-scholar-filter">
          <span class="inline-flex items-center gap-1">
            Scholar
            <AppHelpHint text="Filter to one tracked scholar profile. Filter is synced to URL query." />
          </span>
          <AppSelect
            id="publications-scholar-filter"
            v-model="selectedScholarFilter"
            :disabled="actionBusy"
            @change="onScholarFilterChanged"
          >
            <option value="">All scholars</option>
            <option v-for="scholar in scholars" :key="scholar.id" :value="String(scholar.id)">
              {{ scholarLabel(scholar) }}
            </option>
          </AppSelect>
        </label>

        <label class="grid gap-1 text-xs text-secondary" for="publications-search-input">
          <span class="inline-flex items-center gap-1">
            Search
            <AppHelpHint text="Searches title, scholar name, and venue." />
          </span>
          <div class="flex min-w-0 items-center gap-2">
            <AppInput
              id="publications-search-input"
              v-model="searchQuery"
              placeholder="Search title, scholar, venue, year"
              :disabled="loading"
            />
            <AppButton
              v-if="searchQuery.trim().length > 0"
              variant="secondary"
              class="shrink-0"
              :disabled="loading"
              @click="resetSearchQuery"
            >
              Clear
            </AppButton>
          </div>
        </label>

        <div class="flex flex-wrap items-end justify-end gap-2">
          <button
            type="button"
            class="favorite-filter-button"
            :class="favoriteOnly ? 'favorite-filter-on' : 'favorite-filter-off'"
            :disabled="actionBusy"
            :title="favoriteOnly ? 'Favorites-only filter is active' : 'Show only favorites'"
            :aria-pressed="favoriteOnly"
            :aria-label="favoriteOnly ? 'Disable favorites-only filter' : 'Enable favorites-only filter'"
            @click="onFavoriteOnlyChanged"
          >
            <span aria-hidden="true">{{ favoriteOnly ? "★" : "☆" }}</span>
          </button>
          <AppButton
            variant="secondary"
            :disabled="startRunDisabled"
            :title="startRunDisabledReason || undefined"
            @click="onStartRun"
          >
            {{ startRunButtonLabel }}
          </AppButton>
          <AppRefreshButton
            variant="ghost"
            :disabled="loading"
            :loading="loading"
            title="Refresh publications"
            loading-title="Refreshing publications"
            @click="loadPublications"
          />
        </div>
      </div>

      <RequestStateAlerts
        :success-message="successMessage"
        success-title="Publication update complete"
        :error-message="errorMessage"
        :error-request-id="errorRequestId"
        error-title="Publication request failed"
        @dismiss-success="successMessage = null"
      />

      <div class="flex flex-wrap items-center justify-between gap-2 border-t border-stroke-default pt-3">
        <div class="flex items-center gap-1">
          <h3 class="text-base font-semibold text-ink-primary">Results</h3>
          <AppHelpHint
            text="Star to favorite, open PDF when available, and retry only after failed PDF lookup attempts."
          />
        </div>
        <div class="flex flex-wrap items-center justify-end gap-2 text-[11px] text-secondary">
          <span>Scope: {{ scopeLabel }} | Showing {{ selectedScholarName }}</span>
          <div class="flex items-center gap-1">
            <label for="publications-page-size" class="sr-only">Page size</label>
            <AppSelect
              id="publications-page-size"
              v-model="pageSize"
              :disabled="loading"
              class="max-w-[8rem] !py-1.5 !text-xs"
              @change="onPageSizeChanged"
            >
              <option value="25">25 / page</option>
              <option value="50">50 / page</option>
              <option value="100">100 / page</option>
              <option value="200">200 / page</option>
            </AppSelect>
            <AppHelpHint
              text="When no rows are selected, actions target all unread results. After selecting rows, the menu switches to selected-row actions."
            />
            <label for="publications-bulk-action" class="sr-only">Bulk task</label>
            <AppSelect
              id="publications-bulk-action"
              v-model="bulkAction"
              :disabled="actionBusy"
              class="max-w-[14rem] !py-1.5 !text-xs"
            >
              <option v-for="option in bulkActionOptions" :key="option.value" :value="option.value">
                {{ option.label }}
              </option>
            </AppSelect>
            <AppButton
              variant="secondary"
              class="h-8 min-h-8 shrink-0 px-2 text-xs"
              :disabled="bulkApplyDisabled"
              @click="onApplyBulkAction"
            >
              {{ bulkApplyLabel }}
            </AppButton>
          </div>
        </div>
      </div>

      <AsyncStateGate
        :loading="loading"
        :loading-lines="8"
        :empty="showingEmptyList"
        :empty-title="emptyTitle"
        :empty-body="emptyBody"
        :show-empty="!errorMessage"
      >
        <AppTable v-if="listState" label="Publication list table">
          <thead>
            <tr>
              <th scope="col" class="w-10">
                <input
                  type="checkbox"
                  class="h-4 w-4 rounded border-stroke-interactive bg-surface-input text-brand-600 focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-focus-offset"
                  :checked="allVisibleUnreadSelected"
                  :disabled="visibleUnreadKeys.size === 0"
                  aria-label="Select all visible unread publications"
                  @change="onToggleAllVisible"
                />
              </th>
              <th scope="col" class="w-12 text-left font-semibold text-ink-primary">★</th>
              <th scope="col" class="w-[44%] min-w-[24rem]">
                <button type="button" class="table-sort" @click="toggleSort('title')">
                  Title <span aria-hidden="true" class="sort-marker">{{ sortMarker('title') }}</span>
                </button>
              </th>
              <th scope="col" class="w-[16%] min-w-[12rem]">
                <button type="button" class="table-sort" @click="toggleSort('scholar')">
                  Scholar <span aria-hidden="true" class="sort-marker">{{ sortMarker('scholar') }}</span>
                </button>
              </th>
              <th scope="col" class="w-[8.5rem] whitespace-nowrap">
                <button type="button" class="table-sort" @click="toggleSort('pdf_status')">
                  PDF <span aria-hidden="true" class="sort-marker">{{ sortMarker('pdf_status') }}</span>
                </button>
              </th>
              <th scope="col" class="w-16 whitespace-nowrap">
                <button type="button" class="table-sort" @click="toggleSort('year')">
                  Year <span aria-hidden="true" class="sort-marker">{{ sortMarker('year') }}</span>
                </button>
              </th>
              <th scope="col" class="w-24 whitespace-nowrap">
                <button type="button" class="table-sort" @click="toggleSort('citations')">
                  Citations <span aria-hidden="true" class="sort-marker">{{ sortMarker('citations') }}</span>
                </button>
              </th>
              <th scope="col" class="w-44 whitespace-nowrap text-left font-semibold text-ink-primary">Read status</th>
              <th scope="col" class="w-32 whitespace-nowrap">
                <button type="button" class="table-sort" @click="toggleSort('first_seen')">
                  First seen <span aria-hidden="true" class="sort-marker">{{ sortMarker('first_seen') }}</span>
                </button>
              </th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="item in sortedPublications" :key="publicationKey(item)">
              <td>
                <input
                  type="checkbox"
                  class="h-4 w-4 rounded border-stroke-interactive bg-surface-input text-brand-600 focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-focus-offset"
                  :checked="selectedPublicationKeys.has(publicationKey(item))"
                  :disabled="item.is_read"
                  :aria-label="`Select publication ${item.title}`"
                  @change="onToggleRowSelection(item, $event)"
                />
              </td>
              <td>
                <button
                  type="button"
                  class="favorite-star-button"
                  :class="item.is_favorite ? 'favorite-star-on' : 'favorite-star-off'"
                  :aria-label="item.is_favorite ? `Remove ${item.title} from favorites` : `Add ${item.title} to favorites`"
                  :aria-pressed="item.is_favorite"
                  :disabled="isFavoriteUpdating(item)"
                  @click="onToggleFavorite(item)"
                >
                  {{ item.is_favorite ? "★" : "☆" }}
                </button>
              </td>
              <td class="max-w-0">
                <div class="grid min-w-0 gap-1">
                  <a
                    v-if="publicationPrimaryUrl(item)"
                    :href="publicationPrimaryUrl(item) || ''"
                    target="_blank"
                    rel="noreferrer"
                    class="link-inline block truncate font-medium"
                    :title="item.title"
                  >
                    {{ item.title }}
                  </a>
                  <span v-else class="block truncate font-medium" :title="item.title">{{ item.title }}</span>
                  <a
                    v-if="publicationIdentifierUrl(item)"
                    :href="publicationIdentifierUrl(item) || ''"
                    target="_blank"
                    rel="noreferrer"
                    class="link-inline block truncate text-xs"
                    :title="publicationIdentifierLabel(item) || ''"
                  >
                    {{ publicationIdentifierLabel(item) }}
                  </a>
                </div>
              </td>
              <td>
                <span class="block truncate" :title="item.scholar_label">{{ item.scholar_label }}</span>
              </td>
              <td class="whitespace-nowrap">
                <a
                  v-if="item.pdf_url"
                  :href="item.pdf_url"
                  target="_blank"
                  rel="noreferrer"
                  class="pdf-link-button"
                  title="Open PDF"
                >
                  <svg class="mr-1 h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
                  </svg>
                  Available
                </a>
                <button
                  v-else-if="canRetryPublicationPdf(item)"
                  type="button"
                  class="pdf-retry-button"
                  :disabled="isRetryingPublication(item)"
                  @click="onRetryPdf(item)"
                >
                  <svg v-if="isRetryingPublication(item)" class="mr-1 h-3 w-3 animate-spin" viewBox="0 0 24 24" fill="none">
                    <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
                    <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                  </svg>
                  {{ isRetryingPublication(item) ? "Retrying..." : "Missing (Retry)" }}
                </button>
                <span v-else class="pdf-state-label" :class="{ 'bg-surface-accent-muted border-accent-300 text-accent-700': item.pdf_status === 'running' || item.pdf_status === 'queued' }">
                  <svg v-if="item.pdf_status === 'running'" class="mr-1 h-3 w-3 animate-spin text-accent-600" viewBox="0 0 24 24" fill="none">
                    <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
                    <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                  </svg>
                  {{ pdfPendingLabel(item) }}
                </span>
              </td>
              <td class="whitespace-nowrap">{{ item.year ?? "n/a" }}</td>
              <td class="whitespace-nowrap">{{ item.citation_count }}</td>
              <td>
                <div class="status-badges-row">
                  <AppBadge :tone="item.is_new_in_latest_run ? 'info' : 'neutral'">
                    {{ item.is_new_in_latest_run ? "New" : "Seen" }}
                  </AppBadge>
                  <AppBadge :tone="item.is_read ? 'success' : 'warning'">
                    {{ item.is_read ? "Read" : "Unread" }}
                  </AppBadge>
                </div>
              </td>
              <td class="whitespace-nowrap">{{ formatDate(item.first_seen_at) }}</td>
            </tr>
          </tbody>
        </AppTable>
      </AsyncStateGate>

      <div class="flex flex-wrap items-center justify-between gap-2 border-t border-stroke-default pt-2 text-xs text-secondary">
        <span>
          total {{ totalCount }} · visible {{ visibleCount }} · unread {{ visibleUnreadCount }} · favorites {{ visibleFavoriteCount }}
          · selected {{ selectedCount }}
        </span>
        <div class="flex items-center gap-2">
          <span>page {{ currentPage }} / {{ totalPages }}</span>
          <AppButton variant="ghost" class="h-8 min-h-8 px-2" :disabled="!hasPrevPage || loading" @click="onPrevPage">
            Prev
          </AppButton>
          <AppButton variant="ghost" class="h-8 min-h-8 px-2" :disabled="!hasNextPage || loading" @click="onNextPage">
            Next
          </AppButton>
        </div>
      </div>
    </AppCard>
    </div>
  </AppPage>
</template>

<style scoped>
.table-sort {
  @apply flex w-full items-center justify-between gap-2 text-left font-semibold text-ink-primary transition hover:text-ink-link focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-focus-offset;
}

.sort-marker {
  @apply text-[11px] leading-none text-ink-muted;
}

.favorite-filter-button {
  @apply inline-flex min-h-10 h-10 w-10 items-center justify-center rounded-full border text-base leading-none transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-focus-offset disabled:cursor-not-allowed disabled:opacity-50;
}

.favorite-filter-on {
  @apply border-warning-300 bg-warning-100 text-warning-700 hover:bg-warning-200;
}

.favorite-filter-off {
  @apply border-stroke-default bg-surface-card-muted text-ink-muted hover:border-stroke-interactive hover:text-ink-secondary;
}

.favorite-star-button {
  @apply inline-flex h-7 w-7 items-center justify-center rounded-full border text-sm leading-none transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-focus-offset disabled:cursor-not-allowed disabled:opacity-50;
}

.favorite-star-on {
  @apply border-warning-300 bg-warning-100 text-warning-700 hover:bg-warning-200;
}

.favorite-star-off {
  @apply border-stroke-default bg-surface-card-muted text-ink-muted hover:border-stroke-interactive hover:text-ink-secondary;
}

.pdf-link-button {
  @apply inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium border-state-success-border bg-state-success-bg text-state-success-text shadow-sm transition hover:brightness-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-focus-offset;
}

.pdf-retry-button {
  @apply inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium border-state-warning-border bg-state-warning-bg text-state-warning-text transition hover:brightness-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-focus-offset disabled:cursor-not-allowed disabled:opacity-50;
}

.pdf-state-label {
  @apply inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium border-stroke-default bg-surface-card-muted text-secondary;
}

.status-badges-row {
  @apply inline-flex items-center gap-1 whitespace-nowrap;
}
</style>
