<script setup lang="ts">
import { formatCurrencyAmount, type LedgerEntry } from "@cents/domain";
import {
  BookOpen,
  ChevronLeft,
  ChevronDown,
  ChevronRight,
  ChevronUp,
  CircleUserRound,
  BarChart3,
  CloudOff,
  Copy,
  LoaderCircle,
  Clock3,
  Filter,
  MoreHorizontal,
  RefreshCw,
  RotateCcw,
  Settings,
  Share2,
  X,
} from "@lucide/vue";
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import QuickEntryHost from "../components/QuickEntryHost.vue";
import { apiRequest, ApiError } from "../data/api";
import { categories, entryTypes, findCategory, findCategoryPath } from "../data/categories";
import { ledgerIconComponent } from "../data/ledger-icons";
import { ledgerTheme } from "../data/ledgers";
import {
  isReimbursement,
  netBaseAmount,
  netOriginalAmount,
  reimbursedBaseAmount,
  reimbursedOriginalAmount,
} from "../data/reimbursements";
import { useEntryStore } from "../stores/entries";
import { useLedgerStore } from "../stores/ledgers";

const ENTRY_PAGE_SIZE = 50;
const store = useEntryStore();
const ledgerStore = useLedgerStore();
const route = useRoute();
const router = useRouter();
const moreMenuOpen = ref(false);
const showBaseCurrency = computed(() => ledgerStore.currentLedger?.amountDisplay !== "original");
const participantOpen = ref(false);
const shareOpen = ref(false);
const shareFeedback = ref("");
const invitationLink = ref("");
const generatingInvitation = ref(false);
const converting = ref(false);
const visibleCount = ref(ENTRY_PAGE_SIZE);
const ledgerContent = ref<HTMLElement | null>(null);
const loadMoreTrigger = ref<HTMLElement | null>(null);
const selectedMonth = ref(monthQueryValue(route.query.month) ?? toMonthValue(new Date()));
const monthPickerOpen = ref(false);
const monthPickerMode = ref<"months" | "years">("months");
const monthPickerYear = ref(new Date().getFullYear());
const monthSwipeStart = ref<{ x: number; y: number } | null>(null);
const monthSwipeDelta = ref(0);
const monthSwipeDragging = ref(false);
const pullStartY = ref<number | null>(null);
const pullDistance = ref(0);
const pullEdge = ref<"top" | "bottom" | null>(null);
const pullLoading = ref(false);
const summaryCurrencyIndex = ref(0);
const filterOpen = ref(false);
const filterKeyword = ref(queryString(route.query.q));
const selectedFilterCategories = ref(parseFilterCategories(route.query.cat));
let loadMoreObserver: IntersectionObserver | null = null;

type FilterCategoryOption = {
  key: string;
  type: LedgerEntry["type"];
  label: string;
  parentLabel?: string;
};

function queryString(value: unknown) {
  return typeof value === "string" ? value : "";
}

function parseFilterCategories(value: unknown) {
  return [...new Set(queryString(value).split(",").filter(Boolean))];
}

const filterCategoryOptions = computed<FilterCategoryOption[]>(() =>
  entryTypes.flatMap((type) => [
    { key: `${type.id}:*`, type: type.id, label: type.label },
    ...categories[type.id].flatMap((parent) => [
      { key: `${type.id}:${parent.id}`, type: type.id, label: parent.label },
      ...(parent.children ?? []).map((child) => ({ key: `${type.id}:${child.id}`, type: type.id, label: child.label, parentLabel: parent.label })),
    ]),
  ]),
);

function isFilterCategorySelected(key: string) {
  return selectedFilterCategories.value.includes(key);
}

function toggleFilterCategory(key: string) {
  selectedFilterCategories.value = isFilterCategorySelected(key)
    ? selectedFilterCategories.value.filter((item) => item !== key)
    : [...selectedFilterCategories.value, key];
}

function matchesFilterCategory(entry: LedgerEntry, key: string) {
  const separator = key.indexOf(":");
  const type = key.slice(0, separator);
  const categoryId = key.slice(separator + 1);
  if (entry.type !== type) return false;
  if (categoryId === "*") return true;
  return findCategoryPath(entry.type, entry.categoryId).some((category) => category.id === categoryId);
}

const selectedMonthParts = computed(() => {
  const match = /^(\d{4})-(\d{2})$/.exec(selectedMonth.value);
  if (!match) return null;
  return { year: Number(match[1]), month: Number(match[2]) - 1 };
});
const monthEntries = computed(() => {
  const selected = selectedMonthParts.value;
  if (!selected) return [];
  return store.entries.filter((entry) => {
    const occurredAt = new Date(entry.occurredAt);
    return entry.ledgerIds.includes(ledgerStore.currentLedgerId)
      && occurredAt.getFullYear() === selected.year
      && occurredAt.getMonth() === selected.month;
  });
});

const monthlyEntries = computed(() => {
  const keyword = filterKeyword.value.trim().toLocaleLowerCase();
  const categoryKeys = selectedFilterCategories.value;
  if (!keyword && !categoryKeys.length) return monthEntries.value;
  return monthEntries.value.filter((entry) => {
    const matchesKeyword = !keyword || entry.note.toLocaleLowerCase().includes(keyword);
    const matchesCategory = !categoryKeys.length || categoryKeys.some((key) => matchesFilterCategory(entry, key));
    return matchesKeyword && matchesCategory;
  });
});

const activeFilterCount = computed(() => selectedFilterCategories.value.length + (filterKeyword.value.trim() ? 1 : 0));

const totals = computed(() => summarizeEntries(monthlyEntries.value));

const visibleMonthlyEntries = computed(() => monthlyEntries.value.slice(0, visibleCount.value));
const hasMoreEntries = computed(() => visibleCount.value < monthlyEntries.value.length);
const pendingEntryCount = computed(() => store.pendingEntryCount(ledgerStore.currentLedgerId));
const pendingConversionCount = computed(() =>
  showBaseCurrency.value
    ? monthlyEntries.value.filter((entry) => entry.currency !== "CNY" && entry.baseAmount === null).length
    : 0,
);
const currentTheme = computed(() => ledgerTheme(ledgerStore.currentLedger?.theme));

const dailyTotals = computed(() => {
  const summaries = new Map<string, { income: number; expense: number }>();
  for (const entry of monthlyEntries.value) {
    const key = localDateKey(entry.occurredAt);
    const summary = summaries.get(key) ?? { income: 0, expense: 0 };
    if (isReimbursement(entry) || entry.baseAmount === null) continue;
    if (entry.type === "income") summary.income += entry.baseAmount;
    else summary.expense += netBaseAmount(entry, store.entries) ?? 0;
    summaries.set(key, summary);
  }
  return summaries;
});

const groupedEntries = computed(() => {
  const groups = new Map<string, LedgerEntry[]>();
  for (const entry of visibleMonthlyEntries.value) {
    const key = localDateKey(entry.occurredAt);
    const list = groups.get(key) ?? [];
    list.push(entry);
    groups.set(key, list);
  }
  return Array.from(groups, ([date, entries]) => ({
    date,
    entries,
    income: dailyTotals.value.get(date)?.income ?? 0,
    expense: dailyTotals.value.get(date)?.expense ?? 0,
  }));
});

const ledgerEntries = computed(() => store.entries.filter((entry) => entry.ledgerIds.includes(ledgerStore.currentLedgerId)));

const headerEntries = computed(() => {
  if (ledgerStore.currentLedger?.summaryRange === "ledger") return ledgerEntries.value;
  const selected = selectedMonthParts.value;
  if (!selected) return [];
  return ledgerEntries.value.filter((entry) => new Date(entry.occurredAt).getFullYear() === selected.year);
});
const headerSummaryLabel = computed(() => ledgerStore.currentLedger?.summaryRange === "ledger" ? "账本结余" : "当年结余");

const summaryCurrencySlides = computed(() => {
  const summaries = new Map<LedgerEntry["currency"], {
    income: number;
    grossExpense: number;
    reimbursement: number;
  }>();
  for (const entry of headerEntries.value) {
    if (isReimbursement(entry)) continue;
    const currency = showBaseCurrency.value ? "CNY" : entry.currency;
    const amount = showBaseCurrency.value ? entry.baseAmount : entry.amount;
    if (amount === null) continue;
    const summary = summaries.get(currency) ?? { income: 0, grossExpense: 0, reimbursement: 0 };
    if (entry.type === "income") summary.income += amount;
    else {
      summary.grossExpense += amount;
      summary.reimbursement += showBaseCurrency.value
        ? reimbursedBaseAmount(entry, store.entries) ?? 0
        : reimbursedOriginalAmount(entry, store.entries);
    }
    summaries.set(currency, summary);
  }
  if (!summaries.size) summaries.set("CNY", { income: 0, grossExpense: 0, reimbursement: 0 });
  return [...summaries.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([currency, summary]) => {
    const expense = Math.max(0, summary.grossExpense - summary.reimbursement);
    return {
      currency,
      balance: formatCurrencyAmount(summary.income - expense, currency),
      income: formatCurrencyAmount(summary.income, currency),
      expense: formatCurrencyAmount(expense, currency),
      reimbursement: formatCurrencyAmount(summary.reimbursement, currency, false),
      hasReimbursement: summary.reimbursement > 0,
    };
  });
});

watch(summaryCurrencySlides, (slides) => {
  summaryCurrencyIndex.value = Math.min(summaryCurrencyIndex.value, Math.max(0, slides.length - 1));
});

function summarizeEntries(entries: LedgerEntry[]) {
  let income = 0;
  let expense = 0;
  const incomeByCurrency = new Map<LedgerEntry["currency"], number>();
  const expenseByCurrency = new Map<LedgerEntry["currency"], number>();
  const balanceByCurrency = new Map<LedgerEntry["currency"], number>();

  for (const entry of entries) {
    if (isReimbursement(entry)) continue;
    if (showBaseCurrency.value) {
      if (entry.baseAmount === null) continue;
      if (entry.type === "income") income += entry.baseAmount;
      else expense += netBaseAmount(entry, store.entries) ?? 0;
      continue;
    }
    const totalsByCurrency = entry.type === "income" ? incomeByCurrency : expenseByCurrency;
    const amount = entry.type === "expense" ? netOriginalAmount(entry, store.entries) : entry.amount;
    totalsByCurrency.set(entry.currency, (totalsByCurrency.get(entry.currency) ?? 0) + amount);
    balanceByCurrency.set(entry.currency, (balanceByCurrency.get(entry.currency) ?? 0) + (entry.type === "income" ? amount : -amount));
  }

  return {
    income,
    expense,
    count: entries.length,
    incomeParts: showBaseCurrency.value ? [`¥${formatMoney(income)}`] : formatCurrencyParts(incomeByCurrency),
    expenseParts: showBaseCurrency.value ? [`¥${formatMoney(expense)}`] : formatCurrencyParts(expenseByCurrency),
    balanceParts: showBaseCurrency.value ? [`¥${formatMoney(income - expense)}`] : formatCurrencyParts(balanceByCurrency),
    incomeLabel: showBaseCurrency.value ? `¥${formatMoney(income)}` : formatCurrencySummary(incomeByCurrency),
    expenseLabel: showBaseCurrency.value ? `¥${formatMoney(expense)}` : formatCurrencySummary(expenseByCurrency),
    balanceLabel: showBaseCurrency.value ? `¥${formatMoney(income - expense)}` : formatCurrencySummary(balanceByCurrency),
  };
}

const availableYears = computed(() => {
  const currentYear = new Date().getFullYear();
  const entryYears = ledgerEntries.value.map((entry) => new Date(entry.occurredAt).getFullYear());
  const firstYear = Math.min(currentYear, ...entryYears);
  return Array.from({ length: currentYear - firstYear + 1 }, (_, index) => currentYear - index);
});

const yearSummaries = computed(() => availableYears.value.map((year) => ({
  year,
  ...summarizeEntries(ledgerEntries.value.filter((entry) => new Date(entry.occurredAt).getFullYear() === year)),
})));

const monthSummaries = computed(() => Array.from({ length: 12 }, (_, month) => ({
  month,
  ...summarizeEntries(ledgerEntries.value.filter((entry) => {
    const date = new Date(entry.occurredAt);
    return date.getFullYear() === monthPickerYear.value && date.getMonth() === month;
  })),
}))); 

const pickerYearChoices = computed(() => [
  monthPickerYear.value - 1,
  monthPickerYear.value,
  monthPickerYear.value + 1,
].map((year) => yearSummaries.value.find((summary) => summary.year === year) ?? {
  year,
  income: 0,
  expense: 0,
  count: 0,
  incomeParts: ["0"],
  expenseParts: ["0"],
  balanceParts: ["0"],
  incomeLabel: "0",
  expenseLabel: "0",
  balanceLabel: "0",
}));

const pickerYearSummary = computed(() => yearSummaries.value.find((summary) => summary.year === monthPickerYear.value) ?? {
  year: monthPickerYear.value,
  income: 0,
  expense: 0,
  count: 0,
  incomeParts: ["0"],
  expenseParts: ["0"],
  balanceParts: ["0"],
  incomeLabel: "0",
  expenseLabel: "0",
  balanceLabel: "0",
});

const monthTitle = computed(() => {
  const selected = selectedMonthParts.value;
  return selected ? `${selected.year}年${selected.month + 1}月` : "选择月份";
});

watch([() => ledgerStore.currentLedgerId, ledgerEntries], ([ledgerId, entries]) => {
  if (!ledgerId || !entries.length || monthEntries.value.length) return;
  const latestEntry = [...entries].sort((left, right) => right.occurredAt.localeCompare(left.occurredAt))[0];
  if (latestEntry) selectedMonth.value = toMonthValue(new Date(latestEntry.occurredAt));
}, { immediate: true });

onMounted(async () => {
  await Promise.all([store.loadEntries(), ledgerStore.loadLedgers()]);
});

watch(selectedMonth, (value) => {
  if (route.name !== "ledger" || monthQueryValue(route.query.month) === value) return;
  void router.replace({ query: { ...route.query, month: value } });
});

watch(() => route.query, () => {
  filterKeyword.value = queryString(route.query.q);
  selectedFilterCategories.value = parseFilterCategories(route.query.cat);
}, { deep: true });

watch([filterKeyword, selectedFilterCategories], () => {
  if (route.name !== "ledger") return;
  const query = { ...route.query };
  const keyword = filterKeyword.value.trim();
  if (keyword) query.q = keyword;
  else delete query.q;
  if (selectedFilterCategories.value.length) query.cat = selectedFilterCategories.value.join(",");
  else delete query.cat;
  const currentKeyword = queryString(route.query.q);
  const currentCategories = parseFilterCategories(route.query.cat).join(",");
  if (currentKeyword === keyword && currentCategories === selectedFilterCategories.value.join(",")) return;
  void router.replace({ query });
});

watch(monthlyEntries, () => {
  visibleCount.value = ENTRY_PAGE_SIZE;
});

watch([ledgerContent, loadMoreTrigger], ([root, trigger]) => {
  loadMoreObserver?.disconnect();
  loadMoreObserver = null;
  if (!root || !trigger) return;
  loadMoreObserver = new IntersectionObserver(
    ([entry]) => {
      if (entry?.isIntersecting) {
        visibleCount.value = Math.min(visibleCount.value + ENTRY_PAGE_SIZE, monthlyEntries.value.length);
      }
    },
    { root, rootMargin: "160px 0px" },
  );
  loadMoreObserver.observe(trigger);
});

onBeforeUnmount(() => loadMoreObserver?.disconnect());

const pullAction = computed(() => {
  if (pullEdge.value === "bottom") {
    if (pullDistance.value < 72) return null;
    return adjacentMonthValue(1) ? "next" : "end";
  }
  if (pullEdge.value !== "top") return null;
  if (pullDistance.value >= 88) return adjacentMonthValue(-1) ? "previous" : "end";
  if (pullDistance.value >= 44) return "refresh";
  return null;
});

const pullLabel = computed(() => {
  if (pullAction.value === "previous") return `加载 ${formatMonthLabel(adjacentMonthValue(-1))} 流水`;
  if (pullAction.value === "refresh") return "刷新流水";
  if (pullAction.value === "end") return "没有更多数据";
  if (pullEdge.value === "bottom") return `继续上拉加载 ${formatMonthLabel(adjacentMonthValue(1))} 流水`;
  return "继续下拉刷新流水";
});

const pullProgress = computed(() => {
  const threshold = pullEdge.value === "bottom" ? 72 : pullDistance.value < 44 ? 44 : 88;
  return Math.min(1, pullDistance.value / threshold);
});

function localDateKey(value: string) {
  const date = new Date(value);
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
}

function toMonthValue(date: Date) {
  return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
}

function monthQueryValue(value: unknown) {
  return typeof value === "string" && /^\d{4}-(0[1-9]|1[0-2])$/.test(value) ? value : null;
}

function openMonthPicker() {
  monthPickerYear.value = selectedMonthParts.value?.year ?? new Date().getFullYear();
  monthPickerMode.value = "months";
  monthPickerOpen.value = true;
}

function closeMonthPicker() {
  monthPickerOpen.value = false;
}

function selectPickerYear(year: number) {
  if (!yearSummaries.value.find((summary) => summary.year === year)?.count) return;
  monthPickerYear.value = year;
  monthPickerMode.value = "months";
}

function openYearPicker() {
  monthPickerMode.value = "years";
}

function selectPickerMonth(month: number) {
  if (!monthSummaries.value[month]?.count) return;
  selectedMonth.value = `${monthPickerYear.value}-${String(month + 1).padStart(2, "0")}`;
  closeMonthPicker();
}

async function convertPendingEntries() {
  if (converting.value) return;
  const entryIds = monthlyEntries.value
    .filter((entry) => entry.currency !== "CNY" && entry.baseAmount === null)
    .map((entry) => entry.id);
  if (!entryIds.length) return;
  converting.value = true;
  try {
    await Promise.all(entryIds.map((entryId) => store.resolveLocalConversion(entryId)));
  } finally {
    converting.value = false;
  }
}

watch([showBaseCurrency, monthlyEntries], ([show]) => {
  if (show && pendingConversionCount.value) void convertPendingEntries();
});

function shiftPickerYear(offset: number) {
  const target = monthPickerYear.value + offset;
  if (!yearSummaries.value.find((summary) => summary.year === target)?.count) return;
  monthPickerYear.value = target;
}

function startMonthSwipe(event: TouchEvent) {
  const touch = event.touches[0];
  if (touch) {
    monthSwipeStart.value = { x: touch.clientX, y: touch.clientY };
    monthSwipeDelta.value = 0;
    monthSwipeDragging.value = false;
  }
}

function moveMonthSwipe(event: TouchEvent) {
  const start = monthSwipeStart.value;
  const touch = event.touches[0];
  if (!start || !touch) return;
  const deltaX = touch.clientX - start.x;
  const deltaY = touch.clientY - start.y;
  if (Math.abs(deltaY) > Math.abs(deltaX) && Math.abs(deltaY) > 8) {
    monthSwipeDelta.value = 0;
    monthSwipeDragging.value = false;
    return;
  }
  monthSwipeDelta.value = Math.max(-96, Math.min(96, deltaX));
  monthSwipeDragging.value = Math.abs(deltaX) > 8;
}

function finishMonthSwipe(event: TouchEvent) {
  const start = monthSwipeStart.value;
  const touch = event.changedTouches[0];
  monthSwipeStart.value = null;
  if (!start || !touch) return;
  const deltaX = touch.clientX - start.x;
  const deltaY = touch.clientY - start.y;
  if (Math.abs(deltaX) < 48 || Math.abs(deltaX) <= Math.abs(deltaY)) {
    monthSwipeDelta.value = 0;
    monthSwipeDragging.value = false;
    return;
  }
  shiftMonth(deltaX < 0 ? 1 : -1);
  monthSwipeDelta.value = 0;
  monthSwipeDragging.value = false;
}

function cancelMonthSwipe() {
  monthSwipeStart.value = null;
  monthSwipeDelta.value = 0;
  monthSwipeDragging.value = false;
}

function startLedgerPull(event: TouchEvent) {
  if (pullLoading.value) return;
  const content = ledgerContent.value;
  const touch = event.touches[0];
  if (!content || !touch) return;
  const atTop = content.scrollTop <= 0;
  const isShortList = content.scrollHeight <= content.clientHeight + 4;
  const atBottom = isShortList || content.scrollTop + content.clientHeight >= content.scrollHeight - 2;
  pullEdge.value = atTop ? "top" : atBottom ? "bottom" : null;
  pullStartY.value = pullEdge.value ? touch.clientY : null;
  pullDistance.value = 0;
}

function moveLedgerPull(event: TouchEvent) {
  const startY = pullStartY.value;
  const edge = pullEdge.value;
  const touch = event.touches[0];
  const content = ledgerContent.value;
  if (startY === null || !edge || !touch || !content) return;
  const isShortList = content.scrollHeight <= content.clientHeight + 4;
  if (isShortList && edge === "top" && touch.clientY < startY) {
    pullEdge.value = "bottom";
  } else if (isShortList && edge === "bottom" && touch.clientY > startY) {
    pullEdge.value = "top";
  }
  const activeEdge = pullEdge.value;
  if (!activeEdge) return;
  const delta = activeEdge === "top" ? touch.clientY - startY : startY - touch.clientY;
  if (delta <= 0) {
    pullDistance.value = 0;
    return;
  }
  pullDistance.value = Math.min(144, delta * 0.55);
  if (activeEdge === "bottom") {
    requestAnimationFrame(() => {
      const current = ledgerContent.value;
      if (current && pullEdge.value === "bottom") {
        current.scrollTop = current.scrollHeight - current.clientHeight;
      }
    });
  }
}

async function finishLedgerPull() {
  const action = pullAction.value;
  pullStartY.value = null;
  if (!action || pullLoading.value) {
    pullEdge.value = null;
    pullDistance.value = 0;
    return;
  }
  pullLoading.value = true;
  try {
    if (action === "refresh" || action === "previous" || action === "next") await store.syncEntries();
    if (action === "previous") shiftMonth(-1);
    if (action === "next") shiftMonth(1);
  } finally {
    pullLoading.value = false;
    pullEdge.value = null;
    pullDistance.value = 0;
  }
}

function cancelLedgerPull() {
  pullStartY.value = null;
  pullEdge.value = null;
  pullDistance.value = 0;
}

function shiftMonth(offset: -1 | 1) {
  const target = adjacentMonthValue(offset);
  if (target) selectedMonth.value = target;
}

function adjacentMonthValue(offset: -1 | 1) {
  const selected = selectedMonthParts.value;
  if (!selected) return null;
  const current = selected.year * 12 + selected.month;
  const months = [...new Set(ledgerEntries.value.map((entry) => {
    const date = new Date(entry.occurredAt);
    return date.getFullYear() * 12 + date.getMonth();
  }))];
  const candidates = months.filter((month) => offset < 0 ? month < current : month > current);
  if (!candidates.length) return null;
  const target = offset < 0 ? Math.max(...candidates) : Math.min(...candidates);
  return toMonthValue(new Date(Math.floor(target / 12), target % 12, 1));
}

function formatMonthLabel(value: string | null) {
  if (!value) return "下一月";
  const match = /^(\d{4})-(\d{2})$/.exec(value);
  return match ? `${match[1]}年${Number(match[2])}月` : "下一月";
}

const monthSwipePreview = computed(() => {
  const selected = selectedMonthParts.value;
  if (!selected || !monthSwipeDelta.value) return "";
  const date = new Date(selected.year, selected.month + (monthSwipeDelta.value < 0 ? 1 : -1), 1);
  return `${date.getFullYear()}年${date.getMonth() + 1}月`;
});

function dateHeading(value: string) {
  const date = new Date(`${value}T00:00:00`);
  const today = new Date();
  const yesterday = new Date(today);
  yesterday.setDate(today.getDate() - 1);
  const label = localDateKey(today.toISOString()) === value
    ? "今天"
    : localDateKey(yesterday.toISOString()) === value
      ? "昨天"
      : `${date.getMonth() + 1}月${date.getDate()}日`;
  const weekday = new Intl.DateTimeFormat("zh-CN", { weekday: "short" }).format(date);
  return `${label} · ${weekday}`;
}

function formatMoney(value: number) {
  return (value / 100).toLocaleString("zh-CN", {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2,
  });
}

function formatCompactMoney(value: number) {
  const amount = value / 100;
  if (amount >= 10000) return `${(amount / 10000).toFixed(1)}万`;
  if (amount >= 1000) return `${(amount / 1000).toFixed(1)}k`;
  return amount.toLocaleString("zh-CN", { maximumFractionDigits: 0 });
}

function formatCurrencySummary(values: Map<LedgerEntry["currency"], number>) {
  return formatCurrencyParts(values).join(" · ");
}

function formatCurrencyParts(values: Map<LedgerEntry["currency"], number>) {
  if (!values.size) return ["0"];
  return [...values.entries()]
    .sort(([left], [right]) => left.localeCompare(right))
    .map(([currency, amount]) => formatCurrencyAmount(amount, currency))
}

function selectSummaryCurrency(index: number) {
  summaryCurrencyIndex.value = index;
}

function formatTime(value: string) {
  return new Intl.DateTimeFormat("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false }).format(new Date(value));
}

function categoryLabel(entry: LedgerEntry) {
  if (isReimbursement(entry)) {
    const source = store.entries.find((item) => item.id === entry.reimbursementOfEntryId);
    const sourcePath = source ? findCategoryPath("expense", source.categoryId) : [];
    return `报销${sourcePath.length ? ` · ${sourcePath.at(-1)!.label}` : ""}`;
  }
  const path = findCategoryPath(entry.type, entry.categoryId);
  return path.map((item) => item.label).join(" · ") || (entry.type === "expense" ? "支出" : "收入");
}

function categoryIcon(entry: LedgerEntry) {
  if (isReimbursement(entry)) return RotateCcw;
  return findCategory(entry.categoryId)?.icon ?? entryTypes.find((type) => type.id === entry.type)?.icon ?? BookOpen;
}

function entryHasReimbursement(entry: LedgerEntry) {
  return entry.type === "expense" && reimbursedOriginalAmount(entry, store.entries) > 0;
}

function entryDisplayAmount(entry: LedgerEntry, net = false) {
  if (showBaseCurrency.value && entry.baseAmount !== null) {
    const amount = net && entry.type === "expense"
      ? netBaseAmount(entry, store.entries) ?? entry.baseAmount
      : entry.baseAmount;
    return `¥${formatMoney(amount)}`;
  }
  const amount = net && entry.type === "expense"
    ? netOriginalAmount(entry, store.entries)
    : entry.amount;
  return formatCurrencyAmount(amount, entry.currency);
}

function openParticipantSheet() {
  participantOpen.value = true;
  void ledgerStore.loadCurrentMembers();
}

async function openShareSheet() {
  moreMenuOpen.value = false;
  shareFeedback.value = "正在生成邀请链接";
  shareOpen.value = true;
  await ensureInvitation();
}

function openLedgerSettings() {
  moreMenuOpen.value = false;
  router.push("/ledger/settings");
}

function openLedgerStats() {
  moreMenuOpen.value = false;
  void router.push({ name: "stats", query: { range: "month", month: selectedMonth.value } });
}

async function ensureInvitation() {
  if (invitationLink.value || generatingInvitation.value || !ledgerStore.currentLedgerId) return invitationLink.value;
  generatingInvitation.value = true;
  try {
    const result = await apiRequest<{ invitation: { url: string; expiresAt: string } }>(
      `/api/ledgers/${encodeURIComponent(ledgerStore.currentLedgerId)}/invitations`,
      { method: "POST" },
    );
    invitationLink.value = result.invitation.url;
    shareFeedback.value = "邀请链接将在 7 天后失效";
  } catch (error) {
    shareFeedback.value = error instanceof ApiError ? error.message : "邀请链接生成失败";
  } finally {
    generatingInvitation.value = false;
  }
  return invitationLink.value;
}

async function copyInvitation() {
  const link = await ensureInvitation();
  if (!link) return;
  if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(link);
  else {
    const input = document.createElement("textarea");
    input.value = link;
    document.body.append(input);
    input.select();
    document.execCommand("copy");
    input.remove();
  }
  shareFeedback.value = "邀请链接已复制";
}

async function shareLedger() {
  const url = await ensureInvitation();
  if (!url) return;
  const data = {
    title: ledgerStore.currentLedger?.name ?? "账本",
    text: `邀请你加入我的${ledgerStore.currentLedger?.name ?? "账本"}`,
    url,
  };
  if (!navigator.share) {
    await copyInvitation();
    return;
  }
  try {
    await navigator.share(data);
    shareFeedback.value = "已打开系统分享";
  } catch (error) {
    if (error instanceof DOMException && error.name === "AbortError") return;
    shareFeedback.value = "分享失败，请复制邀请链接";
  }
}

</script>

<template>
  <main class="app-shell">
    <header class="ledger-header" :style="{ '--ledger-gradient': currentTheme.gradient, '--ledger-accent': currentTheme.accent }">
      <div class="top-actions">
        <button v-if="!ledgerStore.currentLedger?.isPersonal" class="participant-avatars" type="button" aria-label="查看账本参与者" title="账本参与者" @click="openParticipantSheet">
          <span v-for="member in ledgerStore.currentMembers.slice(0, 3)" :key="member.id" class="participant-avatar" :class="member.role">
            {{ member.name.slice(0, 1) }}
          </span>
        </button>
        <span v-else aria-hidden="true"></span>
        <button class="ledger-switcher" type="button" @click="router.push('/ledgers')">
          <span class="ledger-header-icon" :style="{ background: currentTheme.gradient }"><component :is="ledgerIconComponent(ledgerStore.currentLedger?.icon)" :size="16" /></span>
          <span>{{ ledgerStore.currentLedger?.name ?? "选择账本" }}</span>
          <ChevronDown :size="16" />
        </button>
        <div class="ledger-header-actions">
          <button class="icon-button header-icon" type="button" aria-label="更多" title="更多" @click.stop="moreMenuOpen = true"><MoreHorizontal :size="21" /></button>
        </div>
      </div>

      <Transition name="more-menu">
        <div v-if="moreMenuOpen" class="ledger-more-layer">
          <button class="ledger-more-scrim" type="button" aria-label="关闭更多菜单" @click="moreMenuOpen = false"></button>
          <div class="ledger-more-menu" role="menu">
            <button type="button" role="menuitem" @click="openLedgerStats"><BarChart3 :size="18" />统计</button>
            <button type="button" role="menuitem" @click="openLedgerSettings"><Settings :size="18" />设置</button>
            <button v-if="!ledgerStore.currentLedger?.isPersonal" type="button" role="menuitem" @click="openShareSheet"><Share2 :size="18" />分享</button>
          </div>
        </div>
      </Transition>

      <section
        class="monthly-summary"
        aria-label="月度收支"
      >
        <div class="summary-values">
          <div class="summary-main">
            <div class="summary-balance-label">
              <span>{{ headerSummaryLabel }}</span>
              <div class="summary-currency-picker" role="tablist" aria-label="选择币种">
                <button v-for="(slide, index) in summaryCurrencySlides" :key="slide.currency" type="button" role="tab" :aria-selected="summaryCurrencyIndex === index" :class="{ active: summaryCurrencyIndex === index }" @click="selectSummaryCurrency(index)">{{ slide.currency }}</button>
              </div>
            </div>
            <div class="summary-value-window">
              <div class="summary-value-track" :style="{ '--summary-slides': summaryCurrencySlides.length, transform: `translate3d(-${summaryCurrencyIndex * (100 / summaryCurrencySlides.length)}%, 0, 0)` }">
                <strong v-for="slide in summaryCurrencySlides" :key="slide.currency" class="summary-value-slide" :class="{ compact: slide.balance.length > 12 }">{{ slide.balance }}</strong>
              </div>
            </div>
          </div>
          <div class="summary-secondary">
            <div class="summary-stat income-stat">
              <span>收入</span>
              <div class="summary-value-window">
                <div class="summary-value-track" :style="{ '--summary-slides': summaryCurrencySlides.length, transform: `translate3d(-${summaryCurrencyIndex * (100 / summaryCurrencySlides.length)}%, 0, 0)` }">
                  <strong v-for="slide in summaryCurrencySlides" :key="slide.currency" class="summary-value-slide" :class="{ compact: slide.income.length > 10 }">{{ slide.income }}</strong>
                </div>
              </div>
            </div>
            <div class="summary-stat expense-stat">
              <span>支出</span>
              <div class="summary-value-window">
                <div class="summary-value-track" :style="{ '--summary-slides': summaryCurrencySlides.length, transform: `translate3d(-${summaryCurrencyIndex * (100 / summaryCurrencySlides.length)}%, 0, 0)` }">
                  <div v-for="slide in summaryCurrencySlides" :key="slide.currency" class="summary-value-slide summary-expense-slide">
                    <strong :class="{ compact: slide.expense.length > 10 }">{{ slide.expense }}</strong>
                    <small v-if="slide.hasReimbursement">已减去报销金额 {{ slide.reimbursement }}</small>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </section>
    </header>

    <Transition name="month-picker">
      <div v-if="monthPickerOpen" class="month-picker-layer">
        <button class="month-picker-scrim" type="button" aria-label="关闭月份选择" @click="closeMonthPicker"></button>
        <section class="month-picker-modal" role="dialog" aria-modal="true" aria-label="选择月份">
          <header>
            <button v-if="monthPickerMode === 'years'" type="button" aria-label="返回月份选择" title="返回月份选择" @click="monthPickerMode = 'months'"><ChevronLeft :size="20" /></button>
            <span v-else></span>
            <strong>{{ monthPickerMode === "months" ? "选择月份" : "选择年份" }}</strong>
            <button type="button" aria-label="关闭" title="关闭" @click="closeMonthPicker"><X :size="20" /></button>
          </header>

          <template v-if="monthPickerMode === 'months'">
            <div class="month-picker-year-nav">
              <button type="button" aria-label="上一年" title="上一年" :disabled="!yearSummaries.find((summary) => summary.year === monthPickerYear - 1)?.count" @click="shiftPickerYear(-1)"><ChevronLeft :size="18" /></button>
              <button v-for="summary in pickerYearChoices" :key="summary.year" type="button" :disabled="summary.count === 0" :class="{ selected: summary.year === monthPickerYear }" @click="summary.year === monthPickerYear ? openYearPicker() : selectPickerYear(summary.year)">{{ summary.year }}</button>
              <button type="button" aria-label="下一年" title="下一年" :disabled="!yearSummaries.find((summary) => summary.year === monthPickerYear + 1)?.count" @click="shiftPickerYear(1)"><ChevronRight :size="18" /></button>
            </div>
            <div class="month-picker-total">
              <span class="income">+ {{ pickerYearSummary.incomeLabel }}</span>
              <span class="expense">− {{ pickerYearSummary.expenseLabel }}</span>
            </div>
            <div class="month-picker-grid">
              <button v-for="summary in monthSummaries" :key="summary.month" type="button" :disabled="summary.count === 0" :class="{ selected: selectedMonth === `${monthPickerYear}-${String(summary.month + 1).padStart(2, '0')}` }" @click="selectPickerMonth(summary.month)">
                <strong>{{ summary.month + 1 }}月</strong>
                <small class="income">+{{ summary.incomeLabel }}</small>
                <small class="expense">−{{ summary.expenseLabel }}</small>
              </button>
            </div>
          </template>

          <div v-else class="year-picker-list">
            <button v-for="summary in yearSummaries" :key="summary.year" type="button" :disabled="summary.count === 0" :class="{ selected: summary.year === monthPickerYear }" @click="selectPickerYear(summary.year)">
              <strong>{{ summary.year }}年</strong>
              <span><i class="income">+{{ summary.incomeLabel }}</i><i class="expense">−{{ summary.expenseLabel }}</i></span>
            </button>
          </div>
        </section>
      </div>
    </Transition>

    <Transition name="month-picker">
      <div v-if="filterOpen" class="month-picker-layer filter-layer">
        <button class="month-picker-scrim" type="button" aria-label="关闭筛选" @click="filterOpen = false"></button>
        <section class="month-picker-modal filter-modal" role="dialog" aria-modal="true" aria-label="筛选流水">
          <header>
            <span></span>
            <strong>筛选流水</strong>
            <button type="button" aria-label="关闭" title="关闭" @click="filterOpen = false"><X :size="20" /></button>
          </header>
          <label class="filter-keyword-field">
            <span>备注关键词</span>
            <input v-model="filterKeyword" type="search" placeholder="模糊匹配备注" autocomplete="off" />
          </label>
          <div class="filter-category-heading"><strong>分类</strong><button v-if="selectedFilterCategories.length" type="button" @click="selectedFilterCategories = []">清除分类</button></div>
          <div class="filter-category-list">
            <button v-for="option in filterCategoryOptions" :key="option.key" type="button" class="filter-category-option" :class="{ selected: isFilterCategorySelected(option.key), child: option.parentLabel }" @click="toggleFilterCategory(option.key)">
              <span><small v-if="option.parentLabel">{{ option.parentLabel }} · </small>{{ option.label }}</span>
              <Check v-if="isFilterCategorySelected(option.key)" :size="16" />
            </button>
          </div>
          <button class="filter-done" type="button" @click="filterOpen = false">完成</button>
        </section>
      </div>
    </Transition>

    <section
      ref="ledgerContent"
      class="ledger-content"
      aria-label="账本流水"
      @touchstart.passive="startLedgerPull"
      @touchmove.passive="moveLedgerPull"
      @touchend="finishLedgerPull"
      @touchcancel="cancelLedgerPull"
    >
      <div class="ledger-notice-slot" aria-live="polite">
        <div v-if="pendingEntryCount" class="sync-notice" role="status">
          <span><CloudOff :size="16" />{{ pendingEntryCount }} 条账目未同步云端</span>
          <button type="button" :disabled="store.syncing" @click="store.syncEntries">
            <RefreshCw :size="15" :class="{ spinning: store.syncing }" />{{ store.syncing ? "同步中" : "立即同步" }}
          </button>
        </div>
        <div v-else-if="pendingConversionCount" class="conversion-notice" role="status">
          <span><Clock3 :size="15" />{{ pendingConversionCount }} 条外币账目等待换算</span>
          <button type="button" :disabled="converting" @click="convertPendingEntries">
            <RefreshCw :size="14" :class="{ spinning: converting }" />{{ converting ? "换算中" : "立即换算" }}
          </button>
        </div>
      </div>
      <div class="list-toolbar" @touchstart.passive="startMonthSwipe" @touchmove.passive="moveMonthSwipe" @touchend="finishMonthSwipe" @touchcancel="cancelMonthSwipe">
        <div class="list-toolbar-content" :class="{ dragging: monthSwipeDragging }" :style="{ '--month-swipe-offset': `${monthSwipeDelta * 0.22}px` }">
          <div class="list-toolbar-copy">
            <div class="list-toolbar-heading">
              <button class="list-month-switcher" type="button" @click="openMonthPicker">
                <strong>{{ monthTitle }}</strong>
                <ChevronDown :size="14" />
              </button>
              <span>{{ monthlyEntries.length }} 笔</span>
            </div>
            <div class="list-toolbar-totals">
              <span class="income">收入 {{ formatMoney(totals.income) }}</span>
              <span class="expense">支出 {{ formatMoney(totals.expense) }}</span>
            </div>
          </div>
          <div class="list-filter-action">
            <button class="icon-button list-action" type="button" :class="{ active: activeFilterCount }" aria-label="筛选流水" title="筛选流水" @click="filterOpen = true">
              <Filter :size="19" />
            </button>
            <span v-if="activeFilterCount" class="active-filter-count">{{ activeFilterCount }} 项筛选</span>
          </div>
        </div>
        <div v-if="monthSwipeDragging" class="month-swipe-hint" :class="{ next: monthSwipeDelta < 0 }" aria-hidden="true">
          <ChevronRight v-if="monthSwipeDelta < 0" :size="14" />
          <ChevronLeft v-else :size="14" />
          <span>{{ monthSwipePreview }}</span>
        </div>
      </div>

      <div
        v-if="pullEdge === 'top' && pullDistance > 8"
        :class="['ledger-pull-space', 'top', pullAction]"
        :style="{ height: `${pullDistance}px`, '--pull-progress': pullProgress }"
        role="status"
      >
        <RefreshCw v-if="pullAction !== 'previous'" :size="15" :class="{ spinning: pullLoading }" />
        <ChevronDown v-else :size="15" />
        <span>{{ pullLoading ? "正在加载" : pullLabel }}</span>
      </div>

      <section v-for="group in groupedEntries" :key="group.date" class="day-group">
        <div class="day-group-heading">
          <h2>{{ dateHeading(group.date) }}</h2>
          <div class="day-summary">
            <span class="income">收 {{ formatMoney(group.income) }}</span>
            <span class="expense">支 {{ formatMoney(group.expense) }}</span>
          </div>
        </div>
        <RouterLink v-for="entry in group.entries" :key="entry.id" class="entry-row" :to="`/entries/${entry.id}`">
          <div
            class="entry-category-icon"
            :style="{
              color: isReimbursement(entry) ? '#287c70' : findCategory(entry.categoryId)?.color ?? (entry.type === 'expense' ? '#ef5b3f' : '#16966f'),
              background: isReimbursement(entry) ? '#e8f6f2' : findCategory(entry.categoryId)?.tint ?? (entry.type === 'expense' ? '#fff0ec' : '#e9faf4'),
            }"
          >
            <component :is="categoryIcon(entry)" :size="21" />
          </div>
          <div class="entry-copy">
            <strong>{{ entry.note || categoryLabel(entry) }}</strong>
            <div class="entry-subline">
              <span class="entry-recorder"><CircleUserRound :size="12" />{{ ledgerStore.memberName(entry.createdBy) }}</span>
              <span class="entry-category-label">{{ categoryLabel(entry) }}</span>
              <span v-if="store.isEntryPending(entry.id)" class="entry-sync-pending" role="img" aria-label="未同步云端" title="未同步云端"><CloudOff :size="13" /></span>
            </div>
          </div>
          <div class="entry-value">
            <span v-if="entryHasReimbursement(entry)" class="entry-original-amount">−{{ entryDisplayAmount(entry) }}</span>
            <div v-if="isReimbursement(entry)" class="entry-reimbursement-value">
              <span class="entry-reimbursement-tag">不计入</span>
              <strong class="reimbursement">{{ entryDisplayAmount(entry, true) }}</strong>
            </div>
            <strong v-else :class="entry.type">{{ entry.type === "expense" ? "−" : "+" }}{{ entryDisplayAmount(entry, true) }}</strong>
            <span>{{ formatTime(entry.occurredAt) }}</span>
          </div>
        </RouterLink>
      </section>

      <div v-if="hasMoreEntries" ref="loadMoreTrigger" class="load-more-sentinel" aria-live="polite">
        <LoaderCircle :size="17" />
        正在加载更多流水
      </div>

      <div v-if="!monthlyEntries.length" class="empty-state">
        <BookOpen :size="26" />
        <strong>还没有流水</strong>
        <span>从第一笔开始记录</span>
      </div>
      <div
        v-if="pullEdge === 'bottom' && pullDistance > 8"
        :class="['ledger-pull-space', 'bottom', pullAction]"
        :style="{ height: `${pullDistance}px`, '--pull-progress': pullProgress }"
        role="status"
      >
        <ChevronUp :size="15" />
        <span>{{ pullLoading ? "正在加载" : pullLabel }}</span>
      </div>
    </section>

    <Transition name="share-sheet">
      <div v-if="participantOpen" class="share-sheet-layer">
        <button class="share-sheet-scrim" type="button" aria-label="关闭参与者列表" @click="participantOpen = false"></button>
        <section class="ledger-share-sheet participant-sheet" aria-label="账本参与者">
          <div class="share-sheet-handle"></div>
          <header>
            <div>
              <strong>{{ ledgerStore.currentLedger?.name ?? "账本" }}</strong>
              <span>{{ ledgerStore.currentMembers.length }} 位参与者</span>
            </div>
            <button type="button" aria-label="关闭" title="关闭" @click="participantOpen = false"><X :size="20" /></button>
          </header>

          <div class="share-member-list" aria-label="当前参与者">
            <div v-for="member in ledgerStore.currentMembers" :key="member.id" class="share-member-row">
              <span class="share-member-avatar" :class="member.role">{{ member.name.slice(0, 1) }}</span>
              <span><strong>{{ member.name }}</strong><small>{{ member.role === "owner" ? "拥有者" : "成员" }}</small></span>
            </div>
          </div>

        </section>
      </div>
    </Transition>

    <Transition name="share-sheet">
      <div v-if="shareOpen" class="share-sheet-layer">
        <button class="share-sheet-scrim" type="button" aria-label="关闭账本分享" @click="shareOpen = false"></button>
        <section class="ledger-share-sheet compact-share-sheet" aria-label="账本分享">
          <div class="share-sheet-handle"></div>
          <header>
            <div>
              <strong>分享 {{ ledgerStore.currentLedger?.name ?? "账本" }}</strong>
              <span>邀请成员加入当前账本</span>
            </div>
            <button type="button" aria-label="关闭" title="关闭" @click="shareOpen = false"><X :size="20" /></button>
          </header>
          <div class="share-actions">
            <button type="button" class="copy-invite-action" :disabled="generatingInvitation" @click="copyInvitation"><Copy :size="18" />复制链接</button>
            <button type="button" class="system-share-action" :disabled="generatingInvitation" @click="shareLedger"><Share2 :size="18" />分享账本</button>
          </div>
          <p class="share-feedback" role="status">{{ shareFeedback || "通过邀请链接加入此账本" }}</p>
        </section>
      </div>
    </Transition>

    <QuickEntryHost />
  </main>
</template>
