/**
 * Reno System – Balance Sheet Module (v5)
 * -----------------------------------------------------------------------
 * What changed from v4, and why:
 *
 * 1. EQUAL-HEIGHT KPI CARDS.
 *    The three summary cards (Income / Expense / Net for Period) could end
 *    up different heights because their caption text wraps differently.
 *    Fixed by making each Grid item + Card stretch to fill the row height
 *    (display: "flex", width: "100%") and pushing each caption to the
 *    bottom with `mt: "auto"` so the top content always lines up.
 *
 * 2. MONTH NAVIGATION.
 *    Added "‹ Previous month / Next month ›" arrows next to the quick
 *    range chips, with a label showing the month currently in view.
 *    Clicking them shifts the date range by exactly one calendar month
 *    (1st to last day), so hopping from June to July is one click instead
 *    of re-typing both date fields.
 *
 * 3. NICER ATTACHMENT ICON.
 *    The small paperclip icon/chip that opens the attachment viewer now
 *    has a soft rounded background, a subtle hover state, and a cleaner
 *    "N" badge for multiple attachments — same click behavior as before,
 *    just tidier. The dialog itself is unchanged from v4.
 *
 * 4. RUNNING BALANCE FIX (this patch).
 *    The per-row "Balance" column used to be computed from the currently
 *    FILTERED ledger (search box / Income-Expense KPI toggle), and its
 *    chronological sort had no tiebreaker for same-day transactions. Both
 *    of those meant the on-screen order of rows and the order the balance
 *    math actually walked them in could disagree, so the Balance column
 *    could visibly go DOWN on an income row (or up on an expense row).
 *    Fixed by:
 *      a) computing the running balance from the full, UNFILTERED ledger
 *         for the period (so a search/type filter never changes what the
 *         balance numbers themselves are — it only changes which rows are
 *         shown), and
 *      b) giving every sort (main ledger, running-balance walk, and PDF
 *         export) the same deterministic tiebreaker for same-date rows —
 *         numeric id ascending within each kind, expenses before incomes —
 *         so ties are always resolved identically everywhere.
 *
 * 5. CASH BASIS (this patch).
 *    The Balance Sheet now only counts expenses with status === "Paid" and
 *    income with status === "verified" — everywhere: period totals, the
 *    opening/carry-forward balance for prior periods, the ledger rows, and
 *    both exports. Unpaid expenses and pending/unverified income no longer
 *    affect the Balance. This matches the "Bank Balance" KPI on the project
 *    dashboard (Index.tsx), which already used the same Paid/verified
 *    filter, so the two pages now agree on what counts as real cash moved.
 *
 * 6. PDF EXPORT TEXT FIX (this patch).
 *    The PDF export was prefixing attachment counts onto the description
 *    with a paperclip emoji (📎). jsPDF's built-in fonts only support the
 *    WinAnsi character set and have no glyph for emoji, so instead of
 *    erroring it silently wrote garbled bytes into the PDF (e.g. a "rent"
 *    row with one attachment printed as "Ø=ÜÎ rent"). Replaced with a
 *    plain-ASCII marker ("[Attachment] " / "[N Attachments] ") that always
 *    renders correctly.
 *
 * Everything else below is unchanged from v4.
 *
 * =========================================================================
 * WIRING THIS UP
 * =========================================================================
 *
 * 1. Route (App.tsx / your router file):
 *
 *    import BalanceSheet from "./pages/BalanceSheet"; // adjust path
 *    <Route path="/balance-sheet/:projectId" element={<BalanceSheet />} />
 *
 * 2. Card on the Expense dashboard (Index.tsx), next to the "Income" card:
 *
 *    <Grid item xs={12} sm={3} md={3}>
 *      <Card elevation={3} onClick={() => navigate(`/balance-sheet/${projectId}`)}
 *            sx={{ cursor: 'pointer' }}>
 *        <CardContent>
 *          <Grid container direction="column" alignItems="center">
 *            <FaChartPie size={40} color="#795548" />
 *            <Typography variant="h5" mt={2}>Balance Sheet</Typography>
 *          </Grid>
 *        </CardContent>
 *      </Card>
 *    </Grid>
 *
 * 3. Put your logo file at `public/logo.png` (any size, PNG works best
 *    with jsPDF). If your logo lives elsewhere, change COMPANY.logoUrl
 *    below. If you'd rather ship the logo without a network request,
 *    replace the loadImageAsDataUrl() call with a hardcoded base64
 *    string and skip the fetch entirely.
 *
 * No new backend endpoints are required — this reuses:
 *   GET  {API_URL}/view-expenses/{projectId}
 *   GET  {API_URL}/view-expense-category/{projectId}
 *   GET  {API_URL}/property-incomes/{projectId}
 *   GET  {API_URL}/income-sources
 */

import React, { useEffect, useMemo, useState } from "react";
import {
  Grid,
  Card,
  CardContent,
  Typography,
  Box,
  Button,
  TextField,
  MenuItem,
  InputAdornment,
  Table,
  TableBody,
  TableCell,
  TableContainer,
  TableHead,
  TableFooter,
  TableRow,
  Chip,
  CircularProgress,
  Paper,
  IconButton,
  Tooltip,
  useMediaQuery,
  Dialog,
  DialogTitle,
  DialogContent,
  DialogActions,
} from "@mui/material";
import { useTheme } from "@mui/material/styles";
import { useNavigate, useParams } from "react-router-dom";
import axios from "axios";
import { CiSearch } from "react-icons/ci";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import FileDownloadIcon from "@mui/icons-material/FileDownload";
import PictureAsPdfRoundedIcon from "@mui/icons-material/PictureAsPdfRounded";
import TrendingUpIcon from "@mui/icons-material/TrendingUp";
import TrendingDownIcon from "@mui/icons-material/TrendingDown";
import AccountBalanceWalletIcon from "@mui/icons-material/AccountBalanceWallet";
import SwapVertIcon from "@mui/icons-material/SwapVert";
import AttachFileIcon from "@mui/icons-material/AttachFile";
import CloseIcon from "@mui/icons-material/Close";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import * as XLSX from "xlsx";
import jsPDF from "jspdf";
import autoTable from "jspdf-autotable";
import { useNotifier } from "../../Core/Notifier"; // adjust path to match your project structure
import LogoMain from "../../Images/logo_main_black.webp";
import LinearProgress from "@mui/material/LinearProgress";

// ---------------------------------------------------------------------------
// Company / branding — used for the PDF header
// ---------------------------------------------------------------------------

const COMPANY = {
  name: "Reno System",
  logoUrl: LogoMain, // put your logo file in the `public` folder
};

// ---------------------------------------------------------------------------
// A single timezone used everywhere a "today" or date-boundary is computed,
// so quick-range filters (This Month / Last Month) and the opening-balance
// cutoff always agree, regardless of the browser's local timezone.
// ---------------------------------------------------------------------------

const APP_TIMEZONE = "America/Toronto";

// ---------------------------------------------------------------------------
// Palette — one place to tweak, everything below reads from these
// ---------------------------------------------------------------------------

const COLOR = {
  green: "#4CAF50",
  greenDark: "#2e7d32",
  greenBg: "#e8f5e9",
  red: "#e53935",
  redDark: "#c62828",
  redBg: "#fdecea",
  blue: "#3f51b5",
  blueBg: "#eef0fb",
  headerBg: "#f5f6f8",
  neutralBg: "#fafafa",
};

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

interface ExpenseRow {
  id: number;
  date: string;
  description: string;
  category: number;
  type: string;
  amount: number;
  hst: number;
  total: number;
  status: number | string;
  installment: number;
  return_amount?: number;
}

interface IncomeRow {
  id: number;
  income_source_id: number;
  amount: number;
  income_date: string;
  payment_method: string;
  description?: string;
  status: "created" | "verified" | "cancelled";
  // Matches PropertyIncome in the Income module: legacy single-file field
  // kept for older records, plus the newer multi-file array.
  attachment?: string;
  attachments?: string[];
}

interface Category {
  id: number;
  category_name: string;
}

interface IncomeSource {
  id: number;
  name: string;
}

// A single row in the combined ledger. Only one of expenseAmount /
// incomeAmount is ever set — that's what gives us the "two columns in one
// table" layout.
interface LedgerEntry {
  id: string; // prefixed so income & expense ids never collide, e.g. "inc-12" / "exp-45"
  sourceId: number; // raw numeric id, used as a deterministic same-date tiebreaker
  date: string;
  kind: "Income" | "Expense";
  label: string; // category name (expense) or source name (income)
  description: string;
  expenseAmount: number | null;
  incomeAmount: number | null;
  attachments: string[]; // raw storage paths, same as PropertyIncome.attachments
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

// fix: handle sign INSIDE fmt so nothing else needs to prepend "-"
const fmt = (n: number) => {
  const abs = Math.abs(n);
  const val = `$${abs.toLocaleString("en-CA", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
  return n < 0 ? `-${val}` : val;
};

// Normalize an income record's attachments into a single string[] regardless
// of whether the backend returns the legacy single `attachment` field or the
// newer `attachments` array — same logic as the Income module.
const getAttachmentList = (income: { attachment?: string; attachments?: string[] }): string[] => {
  if (income.attachments && income.attachments.length) return income.attachments;
  if (income.attachment) return [income.attachment];
  return [];
};

// A small, neat, reusable attachment trigger — a soft rounded pill with a
// paperclip icon, and a badge count when there's more than one file. Used
// both in the desktop Description column and the mobile compact row so the
// two stay visually consistent. This only styles the trigger; the viewer
// dialog it opens is unchanged.
const AttachmentButton: React.FC<{
  count: number;
  onClick: () => void;
  size?: "small" | "medium";
}> = ({ count, onClick, size = "small" }) => {
  if (count === 0) return null;
  const dim = size === "small" ? 22 : 26;
  const iconSize = size === "small" ? 13 : 15;
  return (
    <Tooltip title={count > 1 ? `View ${count} attachments` : "View attachment"}>
      <Box
        onClick={(e) => {
          e.stopPropagation();
          onClick();
        }}
        sx={{
          position: "relative",
          display: "inline-flex",
          alignItems: "center",
          justifyContent: "center",
          width: dim,
          height: dim,
          borderRadius: "50%",
          backgroundColor: COLOR.blueBg,
          color: COLOR.blue,
          cursor: "pointer",
          flexShrink: 0,
          transition: "background-color 0.15s, transform 0.15s",
          "&:hover": {
            backgroundColor: COLOR.blue,
            color: "#fff",
            transform: "scale(1.08)",
          },
        }}
      >
        <AttachFileIcon sx={{ fontSize: iconSize }} />
        {count > 1 && (
          <Box
            sx={{
              position: "absolute",
              top: -4,
              right: -4,
              minWidth: 15,
              height: 15,
              px: "3px",
              borderRadius: "8px",
              backgroundColor: COLOR.blue,
              color: "#fff",
              fontSize: "0.6rem",
              fontWeight: 700,
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              border: "1.5px solid #fff",
              lineHeight: 1,
            }}
          >
            {count}
          </Box>
        )}
      </Box>
    </Tooltip>
  );
};

const BreakdownList: React.FC<{
  data: [string, number][];
  total: number;
  color: string;
  colorDark: string;
  emptyLabel: string;
}> = ({ data, total, color, colorDark, emptyLabel }) => {
  if (data.length === 0) {
    return (
      <Box sx={{ py: 4, textAlign: "center", color: "text.secondary" }}>
        <Typography variant="body2">{emptyLabel}</Typography>
      </Box>
    );
  }

  return (
    <Box>
      {data.map(([name, amt], idx) => {
        const pct = total > 0 ? (amt / total) * 100 : 0;
        return (
          <Box key={name}>
            <Box
              sx={{
                display: "flex",
                justifyContent: "space-between",
                alignItems: "baseline",
                mb: 0.5,
              }}
            >
              <Typography variant="body2" fontWeight={600}>
                {name}
              </Typography>
              <Box sx={{ display: "flex", alignItems: "baseline", gap: 1 }}>
                <Typography variant="caption" color="text.secondary">
                  {pct.toFixed(0)}%
                </Typography>
                <Typography variant="body2" fontWeight={700} sx={{ color: colorDark }}>
                  {fmt(amt)}
                </Typography>
              </Box>
            </Box>
            <LinearProgress
              variant="determinate"
              value={pct}
              sx={{
                height: 6,
                borderRadius: 3,
                backgroundColor: "#f0f0f0",
                "& .MuiLinearProgress-bar": { backgroundColor: color, borderRadius: 3 },
              }}
            />
            {idx < data.length - 1 && (
              <Box sx={{ borderBottom: "1px solid #f0f0f0", my: 1.5 }} />
            )}
          </Box>
        );
      })}
    </Box>
  );
};

// -----------------------------------------------------------------------
// Timezone-safe date math.
//
// PREVIOUS BUG: building "today" via `new Date()` (browser-local time),
// then re-reading it in APP_TIMEZONE with toLocaleDateString bounced the
// moment through two different clocks. If the visitor's local timezone is
// far enough ahead of APP_TIMEZONE, local midnight on the 1st can land on
// the *previous day* once re-read in APP_TIMEZONE — which is exactly why
// "This Month" (and the month-nav arrows, which re-parsed "YYYY-MM-DD"
// strings with `new Date(str)` — itself parsed as UTC midnight) could show
// July when it was already August locally.
//
// FIX: read "today" directly out of APP_TIMEZONE exactly once, as plain
// {year, month, day} integers — never round-tripping through a Date
// object's local getters/setters again. All month arithmetic below is
// then pure integer math (no timezone conversion involved), so it can't
// drift by a day.
// -----------------------------------------------------------------------

const MONTH_NAMES = [
  "January", "February", "March", "April", "May", "June",
  "July", "August", "September", "October", "November", "December",
];

const pad2 = (n: number) => String(n).padStart(2, "0");

// "YYYY-MM-DD" from plain integer parts — no Date object involved.
const ymd = (year: number, month0: number, day: number) => `${year}-${pad2(month0 + 1)}-${pad2(day)}`;

// How many days are in a given (year, 0-indexed month). Pure calendar
// arithmetic on local components only — no timezone lookup, so it's safe.
const daysInMonth = (year: number, month0: number) => new Date(year, month0 + 1, 0).getDate();

// Reads "today" as seen in APP_TIMEZONE, once, directly as integers.
const getTodayInTZ = (): { year: number; month0: number; day: number } => {
  const parts = new Intl.DateTimeFormat("en-CA", {
    timeZone: APP_TIMEZONE,
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
  }).formatToParts(new Date());
  const map: Record<string, string> = {};
  parts.forEach((p) => {
    if (p.type !== "literal") map[p.type] = p.value;
  });
  return { year: Number(map.year), month0: Number(map.month) - 1, day: Number(map.day) };
};

// Parses a "YYYY-MM-DD" string we generated ourselves back into integer
// parts, via string split — never via `new Date(str)`, which parses
// date-only strings as UTC midnight and can shift by a day once read back
// in local time.
const parseYMD = (s: string): { year: number; month0: number; day: number } => {
  const [y, m, d] = s.split("-").map(Number);
  return { year: y, month0: m - 1, day: d };
};

const firstDayOfMonth = () => {
  const { year, month0 } = getTodayInTZ();
  return ymd(year, month0, 1);
};

const today = () => {
  const { year, month0, day } = getTodayInTZ();
  return ymd(year, month0, day);
};

type QuickRange = "thisMonth" | "lastMonth" | "thisYear" | "allTime" | "custom";

const quickRanges: { key: QuickRange; label: string }[] = [
  { key: "thisMonth", label: "This Month" },
  { key: "lastMonth", label: "Last Month" },
  { key: "thisYear", label: "This Year" },
  { key: "allTime", label: "All Time" },
];

// Loads an image and returns it as a base64 data URL, for embedding into the
// PDF. Resolves to null (instead of throwing) if the image can't be loaded,
// so a missing/misconfigured logo never blocks the export.
const loadImageAsDataUrl = (url: string): Promise<string | null> =>
  new Promise((resolve) => {
    const img = new Image();
    img.crossOrigin = "anonymous";
    img.onload = () => {
      try {
        const canvas = document.createElement("canvas");
        canvas.width = img.naturalWidth;
        canvas.height = img.naturalHeight;
        const ctx = canvas.getContext("2d");
        if (!ctx) return resolve(null);
        ctx.drawImage(img, 0, 0);
        resolve(canvas.toDataURL("image/png"));
      } catch {
        resolve(null);
      }
    };
    img.onerror = () => resolve(null);
    img.src = url;
  });

// -----------------------------------------------------------------------
// Deterministic ledger ordering.
//
// PREVIOUS BUG: every sort in this file compared only `new Date(a.date)`
// vs `new Date(b.date)`. Two rows sharing the same date compare EQUAL, so
// Array.prototype.sort is free to leave them in whatever order it likes.
// V8 uses a stable sort, so in practice this usually preserved insertion
// order (expenses first, then incomes, in whatever order the API returned
// them) — but that insertion order has nothing to do with the actual
// order the transactions happened in. It also meant the on-screen row
// order and the order the running-balance walk processed rows in could
// silently disagree (see compareLedgerEntries usage in
// runningBalanceById below), which is what let the Balance column
// decrease on an income row.
//
// FIX: give every same-date comparison a deterministic tiebreaker —
// expenses before incomes, then ascending numeric id within each kind —
// and use this ONE comparator everywhere the ledger is ordered (main
// table, running-balance walk, PDF export) so they can never disagree.
// -----------------------------------------------------------------------
const compareLedgerEntries = (a: LedgerEntry, b: LedgerEntry, asc: boolean) => {
  const dateDiff = new Date(a.date).getTime() - new Date(b.date).getTime();
  if (dateDiff !== 0) return asc ? dateDiff : -dateDiff;
  if (a.kind !== b.kind) return a.kind === "Expense" ? -1 : 1;
  return a.sourceId - b.sourceId;
};

// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------

const BalanceSheet: React.FC = () => {
  const { projectId } = useParams<{ projectId: string }>();
  const navigate = useNavigate();
  const API_URL = process.env.REACT_APP_API_URL;
  const LARAVEL_API_URL = process.env.REACT_APP_LARAVEL_API;
  const { showErrorMessage: showError } = useNotifier();
  const theme = useTheme();
  const isMobile = useMediaQuery(theme.breakpoints.down("sm"));

  const [expenses, setExpenses] = useState<ExpenseRow[]>([]);
  const [incomes, setIncomes] = useState<IncomeRow[]>([]);
  const [categories, setCategories] = useState<Category[]>([]);
  const [incomeSources, setIncomeSources] = useState<IncomeSource[]>([]);
  const [loading, setLoading] = useState(false);
  const [exportingPdf, setExportingPdf] = useState(false);

  const [searchQuery, setSearchQuery] = useState("");
  const [kindFilter, setKindFilter] = useState<"All" | "Income" | "Expense">("All");
  const [startDate, setStartDate] = useState(firstDayOfMonth());
  const [endDate, setEndDate] = useState(today());
  const [activeRange, setActiveRange] = useState<QuickRange>("thisMonth");
  // Default to oldest-first: a running Balance column only reads naturally
  // top-to-bottom when the balance is climbing/falling in date order. Banks
  // show statements this way for the same reason.
  const [sortAsc, setSortAsc] = useState(true);

  // File viewer — same paging pattern as the Income module, so clicking an
  // attachment here behaves consistently with clicking one there.
  const [fileViewerOpen, setFileViewerOpen] = useState(false);
  const [fileViewerList, setFileViewerList] = useState<string[]>([]);
  const [fileViewerIndex, setFileViewerIndex] = useState(0);

  const openFileViewer = (paths: string[], startIndex = 0) => {
    if (!paths.length) return;
    setFileViewerList(paths);
    setFileViewerIndex(startIndex);
    setFileViewerOpen(true);
  };

  const goToFileViewerIndex = (index: number) => {
    if (index < 0 || index >= fileViewerList.length) return;
    setFileViewerIndex(index);
  };

  const fileViewerUrl = fileViewerList.length
    ? `${LARAVEL_API_URL}/${fileViewerList[fileViewerIndex]}`
    : "";

  // -------------------------------------------------------------------------
  // Fetch
  // -------------------------------------------------------------------------

  const fetchAll = async () => {
    setLoading(true);
    try {
      const [expRes, catRes, incRes, srcRes] = await Promise.all([
        axios.get(`${API_URL}/view-expenses/${projectId}`),
        axios.get(`${API_URL}/view-expense-category/${projectId}`),
        axios.get(`${API_URL}/property-incomes/${projectId}`, {
          headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
        }),
        axios.get(`${API_URL}/income-sources`, {
          headers: { Authorization: `Bearer ${localStorage.getItem("token")}` },
        }),
      ]);
      setExpenses(expRes.data || []);
      setCategories(catRes.data || []);
      setIncomes(incRes.data || []);
      setIncomeSources(srcRes.data || []);
    } catch (error) {
      console.error("Failed to load balance sheet data", error);
      showError("Failed to load balance sheet data.");
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchAll();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [API_URL, projectId]);

  // -------------------------------------------------------------------------
  // Quick range handler
  // -------------------------------------------------------------------------

  const isFiltered =
    !!searchQuery || kindFilter !== "All" || activeRange !== "thisMonth";

  const resetFilters = () => {
    setSearchQuery("");
    setKindFilter("All");
    applyQuickRange("thisMonth");
  };

  // Clicking a KPI card filters the ledger to match it — clicking the same
  // one again clears the type filter. This ties the summary numbers
  // directly to the table below them instead of leaving them as static,
  // disconnected figures.
  const toggleKindFilter = (kind: "Income" | "Expense") => {
    setKindFilter((prev) => (prev === kind ? "All" : kind));
  };

  // All quick-range math below uses getTodayInTZ() (read once, as plain
  // integers) instead of `new Date()` + a timezone re-read — see the note
  // above daysInMonth/getTodayInTZ for why that combination used to be
  // able to land on the wrong month.
  const applyQuickRange = (key: QuickRange) => {
    setActiveRange(key);
    const { year, month0 } = getTodayInTZ();
    if (key === "thisMonth") {
      setStartDate(ymd(year, month0, 1));
      setEndDate(today());
    } else if (key === "lastMonth") {
      const prevMonth0 = month0 - 1 < 0 ? 11 : month0 - 1;
      const prevYear = month0 - 1 < 0 ? year - 1 : year;
      setStartDate(ymd(prevYear, prevMonth0, 1));
      setEndDate(ymd(prevYear, prevMonth0, daysInMonth(prevYear, prevMonth0)));
    } else if (key === "thisYear") {
      setStartDate(ymd(year, 0, 1));
      setEndDate(today());
    } else if (key === "allTime") {
      setStartDate("");
      setEndDate("");
    }
  };

  // Shifts the current view by one whole calendar month (1st -> last day),
  // anchored off the current startDate. This is what powers the "‹ / ›"
  // month-navigation arrows: from a June 1–30 view, "›" jumps straight to
  // July 1–31, no manual re-typing of both date fields. Parses startDate
  // via parseYMD (a plain string split) rather than `new Date(startDate)`,
  // which parses date-only strings as UTC midnight and can shift by a day
  // once read back through local getters.
  const shiftMonth = (dir: 1 | -1) => {
    const anchor = startDate ? parseYMD(startDate) : getTodayInTZ();
    let month0 = anchor.month0 + dir;
    let year = anchor.year;
    if (month0 < 0) {
      month0 = 11;
      year -= 1;
    } else if (month0 > 11) {
      month0 = 0;
      year += 1;
    }
    setStartDate(ymd(year, month0, 1));
    setEndDate(ymd(year, month0, daysInMonth(year, month0)));
    setActiveRange("custom");
  };

  // Human-readable label for whatever month is currently anchoring the
  // filter (e.g. "June 2026"), shown between the nav arrows. Falls back to
  // "All Time" when there's no lower bound. Built from parseYMD's plain
  // integers, not from `new Date(startDate)`.
  const currentMonthLabel = useMemo(() => {
    if (!startDate) return "All Time";
    const { year, month0 } = parseYMD(startDate);
    return `${MONTH_NAMES[month0]} ${year}`;
  }, [startDate]);

  // -------------------------------------------------------------------------
  // Lookups
  // -------------------------------------------------------------------------

  const categoryMap = useMemo(
    () =>
      categories.reduce((map, c) => {
        map[c.id] = c.category_name;
        return map;
      }, {} as Record<number, string>),
    [categories]
  );

  const sourceMap = useMemo(
    () =>
      incomeSources.reduce((map, s) => {
        map[s.id] = s.name;
        return map;
      }, {} as Record<number, string>),
    [incomeSources]
  );

  // -------------------------------------------------------------------------
  // Date-filtered base sets
  // -------------------------------------------------------------------------

  const inRange = (dateStr: string) => {
    if (!dateStr) return false;
    const d = new Date(dateStr).getTime();
    const s = startDate ? new Date(startDate).getTime() : -Infinity;
    const e = endDate ? new Date(endDate).getTime() : Infinity;
    return d >= s && d <= e;
  };

  // CASH BASIS: the Balance Sheet reflects actual cash movement, not every
  // recorded transaction. An expense only counts once it's actually been
  // paid (status === "Paid"); income only counts once it's been confirmed
  // (status === "verified"). Unpaid expenses and pending/unverified income
  // never touch the totals, the running Balance, or either export — they
  // haven't happened yet from a cash standpoint. This mirrors the same
  // status filter used for the "Bank Balance" KPI on the project dashboard
  // (Index.tsx's calculatePaidExpenses / verifiedIncome), so the two pages
  // now agree on what counts as real money moved.
  const rangedExpenses = useMemo(
    () =>
      expenses.filter(
        (e) =>
        //  e.installment !== 1 && // exclude installment placeholder rows, matches Expense Index.tsx convention
          String(e.status) === "Paid" &&
          inRange(e.date)
      ),
    [expenses, startDate, endDate]
  );

  const rangedIncomes = useMemo(
    () => incomes.filter((i) => i.status === "verified" && inRange(i.income_date)),
    [incomes, startDate, endDate]
  );

  // -------------------------------------------------------------------------
  // Totals
  // -------------------------------------------------------------------------

  const totalExpense = useMemo(
    () =>
      rangedExpenses.reduce((sum, e) => {
        const returned = Number(e.return_amount) || 0;
        return sum + (Number(e.total) || 0) - returned;
      }, 0),
    [rangedExpenses]
  );

  const totalIncome = useMemo(
    () => rangedIncomes.reduce((sum, i) => sum + (Number(i.amount) || 0), 0),
    [rangedIncomes]
  );

  const netForPeriod = totalIncome - totalExpense;
  const isPositive = netForPeriod >= 0;

  // -------------------------------------------------------------------------
  // Carry-forward balance
  // -------------------------------------------------------------------------
  // openingBalance = net of every transaction strictly BEFORE startDate,
  // computed against the FULL (unfiltered) expense/income arrays — not the
  // already-ranged ones — so it reflects the true running balance walking
  // into this period, not just $0.
  const openingBalance = useMemo(() => {
    if (!startDate) return 0; // "All Time" / no lower bound -> nothing to carry forward

    const startTime = new Date(startDate).getTime();

    const priorExpenseTotal = expenses.reduce((sum, e) => {
      if (e.installment === 1) return sum; // same placeholder-row exclusion as rangedExpenses
      if (String(e.status) !== "Paid") return sum; // same cash-basis filter as rangedExpenses
      if (!e.date) return sum;
      if (new Date(e.date).getTime() >= startTime) return sum;
      const returned = Number(e.return_amount) || 0;
      return sum + (Number(e.total) || 0) - returned;
    }, 0);

    const priorIncomeTotal = incomes.reduce((sum, i) => {
      if (i.status !== "verified") return sum; // same cash-basis filter as rangedIncomes
      if (!i.income_date) return sum;
      if (new Date(i.income_date).getTime() >= startTime) return sum;
      return sum + (Number(i.amount) || 0);
    }, 0);

    return priorIncomeTotal - priorExpenseTotal;
  }, [expenses, incomes, startDate]);

  // closingBalance = opening balance + this period's net. This is the ONE
  // "Balance" figure shown anywhere in the ledger, its footer, and both
  // exports — there is no second, differently-scoped number also called
  // "Balance" anywhere on screen (see netForPeriod / "Net for Period" KPI,
  // which is intentionally named differently).
  const closingBalance = openingBalance + netForPeriod;
  const isClosingPositive = closingBalance >= 0;

  // -------------------------------------------------------------------------
  // Breakdowns
  // -------------------------------------------------------------------------

  const expenseByCategory = useMemo(() => {
    const map: Record<string, number> = {};
    rangedExpenses.forEach((e) => {
      const name = categoryMap[e.category] || "Unknown";
      const returned = Number(e.return_amount) || 0;
      map[name] = (map[name] || 0) + (Number(e.total) || 0) - returned;
    });
    return Object.entries(map).sort(([, a], [, b]) => b - a);
  }, [rangedExpenses, categoryMap]);

  const incomeBySource = useMemo(() => {
    const map: Record<string, number> = {};
    rangedIncomes.forEach((i) => {
      const name = sourceMap[i.income_source_id] || "Unknown";
      map[name] = (map[name] || 0) + (Number(i.amount) || 0);
    });
    return Object.entries(map).sort(([, a], [, b]) => b - a);
  }, [rangedIncomes, sourceMap]);

  // -------------------------------------------------------------------------
  // ONE combined ledger, two amount columns
  // -------------------------------------------------------------------------

  const ledger: LedgerEntry[] = useMemo(() => {
    const expEntries: LedgerEntry[] = rangedExpenses.map((e) => {
      const returned = Number(e.return_amount) || 0;
      return {
        id: `exp-${e.id}`,
        sourceId: e.id,
        date: e.date,
        kind: "Expense",
        label: categoryMap[e.category] || "Unknown",
        description: e.description,
        expenseAmount: (Number(e.total) || 0) - returned,
        incomeAmount: null,
        attachments: [],
      };
    });

    const incEntries: LedgerEntry[] = rangedIncomes.map((i) => ({
      id: `inc-${i.id}`,
      sourceId: i.id,
      date: i.income_date,
      kind: "Income",
      label: sourceMap[i.income_source_id] || "Unknown",
      description: i.description || i.payment_method || "",
      expenseAmount: null,
      incomeAmount: Number(i.amount) || 0,
      attachments: getAttachmentList(i),
    }));

    return [...expEntries, ...incEntries].sort((a, b) => compareLedgerEntries(a, b, sortAsc));
  }, [rangedExpenses, rangedIncomes, categoryMap, sourceMap, sortAsc]);

  const filteredLedger = useMemo(() => {
    const q = searchQuery.toLowerCase();
    return ledger.filter((row) => {
      const matchesKind = kindFilter === "All" || row.kind === kindFilter;
      const matchesSearch =
        !q || row.label.toLowerCase().includes(q) || row.description.toLowerCase().includes(q);
      return matchesKind && matchesSearch;
    });
  }, [ledger, searchQuery, kindFilter]);

  // Running balance is computed from the FULL period ledger — never from
  // filteredLedger — so a search term or the Income/Expense KPI-card
  // toggle only changes which rows are DISPLAYED, never what the balance
  // numbers themselves are. It's always walked oldest -> newest using the
  // same deterministic comparator as everything else (compareLedgerEntries),
  // then mapped back onto every row by id. Seeded with openingBalance so it
  // carries forward instead of starting at 0.
  const runningBalanceById = useMemo(() => {
    const chronological = [...ledger].sort((a, b) => compareLedgerEntries(a, b, true));
    let running = openingBalance;
    const map: Record<string, number> = {};
    chronological.forEach((row) => {
      running += row.incomeAmount ?? -(row.expenseAmount ?? 0);
      map[row.id] = running;
    });
    return map;
  }, [ledger, openingBalance]);

  // -------------------------------------------------------------------------
  // Export — Excel
  // -------------------------------------------------------------------------

  const exportExcel = () => {
    const data: any[] = [];

    const balanceForwardRow = startDate
      ? {
          Date: startDate,
          "Category / Source": "",
          Description: "Balance Forward",
          Expense: "",
          Income: "",
          Balance: openingBalance,
          // Attachments: "",
        }
      : null;

    // Match the on-screen order: Balance Forward leads when reading
    // oldest-first, and trails (as the running starting point already baked
    // into the total) when reading newest-first — never pinned to the top
    // regardless of sort direction.
    if (balanceForwardRow && sortAsc) data.push(balanceForwardRow);

    filteredLedger.forEach((row) => {
      data.push({
        Date: row.date,
        "Category / Source": row.label,
        Description: row.description,
        Expense: row.expenseAmount ?? "",
        Income: row.incomeAmount ?? "",
        Balance: runningBalanceById[row.id],
        // Attachments: row.attachments.map((p) => `${LARAVEL_API_URL}/${p}`).join(", "),
      });
    });

    if (balanceForwardRow && !sortAsc) data.push(balanceForwardRow);

    data.push({
      Date: "",
      "Category / Source": "",
      Description: "TOTAL",
      Expense: totalExpense,
      Income: totalIncome,
      Balance: closingBalance,
      // Attachments: "",
    });

    const worksheet = XLSX.utils.json_to_sheet(data);
    worksheet["!cols"] = [
      { wch: 12 },
      { wch: 20 },
      { wch: 30 },
      { wch: 14 },
      { wch: 14 },
      { wch: 14 },
      // { wch: 30 },
    ];
    const workbook = XLSX.utils.book_new();
    XLSX.utils.book_append_sheet(workbook, worksheet, "Balance Sheet");
    XLSX.writeFile(workbook, `Balance_Sheet_${startDate || "all"}_to_${endDate || "date"}.xlsx`);
  };

  // -------------------------------------------------------------------------
  // Export — PDF (with logo)
  // -------------------------------------------------------------------------

  const exportPDF = async () => {
    setExportingPdf(true);
    try {
      const doc = new jsPDF("p", "mm", "a4");
      const pageWidth = doc.internal.pageSize.getWidth();
      const marginX = 14;
      let cursorY = 20;

      // --- Logo + title (no company name text) --------------------------
      const logoDataUrl = await loadImageAsDataUrl(COMPANY.logoUrl);
      if (logoDataUrl) {
        try {
          const props = doc.getImageProperties(logoDataUrl);
          const logoHeight = 12;
          const logoWidth = (props.width / props.height) * logoHeight;
          doc.addImage(logoDataUrl, "PNG", marginX, cursorY - 9, logoWidth, logoHeight);
        } catch {
          // skip silently
        }
      }

      doc.setFontSize(16);
      doc.setTextColor(33, 33, 33);
      doc.text("Balance Sheet", pageWidth / 2, cursorY, { align: "center" });
      cursorY += 6;

      doc.setFontSize(9.5);
      doc.setTextColor(130, 130, 130);
      doc.text(`Period: ${startDate || "Inception"} to ${endDate || "Present"}`, pageWidth / 2, cursorY, {
        align: "center",
      });
      cursorY += 6;

      // thin divider under header
      doc.setDrawColor(230, 230, 230);
      doc.line(marginX, cursorY, pageWidth - marginX, cursorY);
      cursorY += 10;

      // --- Summary: 3 equal columns, label above value -------------------
      const colWidth = (pageWidth - marginX * 2) / 3;
      const colX = [marginX, marginX + colWidth, marginX + colWidth * 2];
      const colCenters = colX.map((x) => x + colWidth / 2);

      doc.setFontSize(8.5);
      doc.setTextColor(140, 140, 140);
      doc.text("TOTAL INCOME", colCenters[0], cursorY, { align: "center" });
      doc.text("TOTAL EXPENSE", colCenters[1], cursorY, { align: "center" });
      doc.text("BALANCE (CARRIED FORWARD)", colCenters[2], cursorY, { align: "center" });
      cursorY += 6;

      doc.setFontSize(14);
      doc.setTextColor(46, 125, 50);
      doc.text(fmt(totalIncome), colCenters[0], cursorY, { align: "center" });
      doc.setTextColor(198, 40, 40);
      doc.text(fmt(totalExpense), colCenters[1], cursorY, { align: "center" });
      doc.setTextColor(isClosingPositive ? 46 : 198, isClosingPositive ? 125 : 40, isClosingPositive ? 50 : 40);
      doc.text(fmt(closingBalance), colCenters[2], cursorY, { align: "center" });
      cursorY += 5;

      if (startDate) {
        doc.setFontSize(8);
        doc.setTextColor(140, 140, 140);
        doc.text(
          `Balance forward from ${startDate}: ${fmt(openingBalance)}`,
          colCenters[2],
          cursorY,
          { align: "center" }
        );
        cursorY += 5;
      }

      cursorY += 3;

      // --- Ledger table ----------------------------------------------------
      const rows: any[] = [];

      const balanceForwardPdfRow = startDate
        ? [
            startDate,
            "",
            { content: "Balance Forward", styles: { fontStyle: "italic" } },
            "",
            "",
            { content: fmt(openingBalance), styles: { fontStyle: "italic" } },
          ]
        : null;

      if (balanceForwardPdfRow && sortAsc) rows.push(balanceForwardPdfRow);

      // Sort matches whatever the user has toggled on screen (sortAsc),
      // using the SAME deterministic comparator as the main table and the
      // running-balance walk, so the Balance figures printed here always
      // agree with what's on screen.
      [...filteredLedger]
        .sort((a, b) => compareLedgerEntries(a, b, sortAsc))
        .forEach((row) => {
          // NOTE: jsPDF's built-in fonts (Helvetica/Times/Courier) only cover
          // the WinAnsi character set — there's no glyph for emoji like 📎.
          // Feeding one through doesn't error, it silently renders as
          // garbled bytes (e.g. "Ø=ÜÎ") instead of a paperclip. Use a plain
          // ASCII marker here so attachment counts always render correctly
          // in the exported PDF.
          const descText = row.description || "—";
          // const attachmentMark =
          //   row.attachments.length === 1
          //     ? "[Attachment] "
          //     : row.attachments.length > 1
          //     ? `[${row.attachments.length} Attachments] `
          //     : "";
          rows.push([
            row.date,
            row.label,
            // `${attachmentMark}
             `${descText}`,
            row.expenseAmount != null ? fmt(row.expenseAmount) : "",
            row.incomeAmount != null ? fmt(row.incomeAmount) : "",
            fmt(runningBalanceById[row.id]),
          ]);
        });

      if (balanceForwardPdfRow && !sortAsc) rows.push(balanceForwardPdfRow);

      autoTable(doc, {
        startY: cursorY + 4,
        head: [["Date", "Category / Source", "Description", "Expense", "Income", "Balance"]],
        body: rows,
        foot: [
          [
            "",
            "",
            { content: "Total", styles: { halign: "right", fontStyle: "bold" } },
            { content: fmt(totalExpense), styles: { halign: "right", fontStyle: "bold", textColor: [198, 40, 40] } },
            { content: fmt(totalIncome), styles: { halign: "right", fontStyle: "bold", textColor: [46, 125, 50] } },
            { content: fmt(closingBalance), styles: { halign: "right", fontStyle: "bold" } },
          ],
        ],
        styles: { fontSize: 8.5, cellPadding: 2.2 },
        headStyles: { fillColor: [63, 81, 181], textColor: 255, fontSize: 9.5 },
        footStyles: { fillColor: [245, 246, 248], textColor: 33, fontStyle: "bold" },
        alternateRowStyles: { fillColor: [250, 250, 250] },
        columnStyles: {
          3: { halign: "right", textColor: [198, 40, 40] },
          4: { halign: "right", textColor: [46, 125, 50] },
          5: { halign: "right", fontStyle: "bold" },
        },
        didDrawPage: () => {
          const pageCount = doc.getNumberOfPages();
          doc.setFontSize(8);
          doc.setTextColor(150, 150, 150);
          doc.text(
            `Page ${doc.getCurrentPageInfo().pageNumber} of ${pageCount}`,
            pageWidth - marginX,
            doc.internal.pageSize.getHeight() - 8,
            { align: "right" }
          );
        },
      });

      doc.save(`Balance_Sheet_${startDate || "all"}_to_${endDate || "date"}.pdf`);
    } catch (error) {
      console.error("Failed to export PDF", error);
      showError("Failed to export PDF.");
    } finally {
      setExportingPdf(false);
    }
  };

  // -------------------------------------------------------------------------
  // Render
  // -------------------------------------------------------------------------

  // Balance Forward row, rendered as a real <TableRow>. Extracted into a
  // constant so it can be placed either above or below the ledger rows
  // depending on sort direction, instead of being hardcoded above the map.
  const balanceForwardRowEl = startDate ? (
    <TableRow key="balance-forward" sx={{ backgroundColor: COLOR.neutralBg }}>
      <TableCell sx={{ whiteSpace: "nowrap", color: "text.secondary" }}>
        {startDate}
      </TableCell>
      <TableCell colSpan={isMobile ? 3 : 1}>
        <Typography variant="body2" fontStyle="italic" color="text.secondary">
          Balance Forward
        </Typography>
      </TableCell>
      <TableCell sx={{ display: { xs: "none", md: "table-cell" } }} />
      <TableCell align="right" />
      <TableCell align="right" />
      <TableCell
        align="right"
        sx={{
          display: { xs: "none", sm: "table-cell" },
          fontWeight: 700,
          fontStyle: "italic",
          whiteSpace: "nowrap",
        }}
      >
        {fmt(openingBalance)}
      </TableCell>
    </TableRow>
  ) : null;

  return (
    <>
      {/* Header */}
      <Grid container spacing={2} alignItems="center" sx={{ mt: 1, mb: 2 }}>
        <Grid item>
          <IconButton onClick={() => navigate(-1)}>
            <ArrowBackIcon />
          </IconButton>
        </Grid>
        <Grid item xs>
          <Typography variant="h5" fontWeight={700}>
            Balance Sheet
          </Typography>
          <Typography variant="body2" color="text.secondary">
            Income and expenses.
          </Typography>
        </Grid>
        <Grid item>
          <Button
            variant="outlined"
            size="small"
            startIcon={<FileDownloadIcon />}
            onClick={exportExcel}
            sx={{ mr: 1 }}
          >
            Excel
          </Button>
          <Button
            variant="outlined"
            size="small"
            color="error"
            startIcon={<PictureAsPdfRoundedIcon />}
            onClick={exportPDF}
            disabled={exportingPdf}
          >
            {exportingPdf ? "Preparing…" : "PDF"}
          </Button>
        </Grid>
      </Grid>

      {/* Filter bar — back to the original chip-based look. Only change
          from the original: this now wraps onto a second line on narrow
          screens instead of being forced onto one non-wrapping row, so the
          month-nav arrows are never pushed off-screen behind a scrollbar. */}
      <Card elevation={2} sx={{ mb: 3, borderRadius: 2 }}>
        <CardContent>
          <Grid container spacing={2} alignItems="center">
            <Grid item xs={12} md="auto">
              <Box display="flex" gap={1} flexWrap="wrap" alignItems="center">
                {quickRanges.map((r) => (
                  <Chip
                    key={r.key}
                    label={r.label}
                    clickable
                    onClick={() => applyQuickRange(r.key)}
                    color={activeRange === r.key ? "primary" : "default"}
                    variant={activeRange === r.key ? "filled" : "outlined"}
                    size="small"
                  />
                ))}
                {isFiltered && (
                  <Chip
                    label="Clear filters"
                    size="small"
                    onClick={resetFilters}
                    onDelete={resetFilters}
                    variant="outlined"
                    sx={{ borderStyle: "dashed" }}
                  />
                )}

                {/* Month navigation — shifts the current view by one whole
                    calendar month in either direction. */}
                <Box
                  display="flex"
                  alignItems="center"
                  gap={0.25}
                  sx={{
                    ml: 1,
                    pl: 1,
                    borderLeft: "1px solid #e0e0e0",
                  }}
                >
                  <Tooltip title="Previous month">
                    <IconButton size="small" onClick={() => shiftMonth(-1)}>
                      <ChevronLeftIcon fontSize="small" />
                    </IconButton>
                  </Tooltip>
                  <Chip
                    label={currentMonthLabel}
                    size="small"
                    variant="outlined"
                    sx={{ minWidth: 128, justifyContent: "center", fontWeight: 600 }}
                  />
                  <Tooltip title="Next month">
                    <IconButton size="small" onClick={() => shiftMonth(1)}>
                      <ChevronRightIcon fontSize="small" />
                    </IconButton>
                  </Tooltip>
                </Box>
              </Box>
            </Grid>

            <Grid item xs={12} md sx={{ display: "flex", justifyContent: { md: "flex-end" } }}>
              <Box display="flex" gap={1.5} alignItems="center" flexWrap="wrap">
                <TextField
                  size="small"
                  label="Start Date"
                  type="date"
                  InputLabelProps={{ shrink: true }}
                  value={startDate}
                  onChange={(e) => {
                    setStartDate(e.target.value);
                    setActiveRange("custom");
                  }}
                  sx={{ width: 155 }}
                />
                <TextField
                  size="small"
                  label="End Date"
                  type="date"
                  InputLabelProps={{ shrink: true }}
                  value={endDate}
                  onChange={(e) => {
                    setEndDate(e.target.value);
                    setActiveRange("custom");
                  }}
                  sx={{ width: 155 }}
                />
                <TextField
                  select
                  size="small"
                  label="Type"
                  value={kindFilter}
                  onChange={(e) => setKindFilter(e.target.value as any)}
                  sx={{ width: 120 }}
                >
                  <MenuItem value="All">All</MenuItem>
                  <MenuItem value="Income">Income</MenuItem>
                  <MenuItem value="Expense">Expense</MenuItem>
                </TextField>
                <TextField
                  size="small"
                  placeholder="Search…"
                  value={searchQuery}
                  onChange={(e) => setSearchQuery(e.target.value)}
                  InputProps={{
                    startAdornment: (
                      <InputAdornment position="start">
                        <CiSearch />
                      </InputAdornment>
                    ),
                  }}
                  sx={{ width: 180 }}
                />
              </Box>
            </Grid>
          </Grid>
        </CardContent>
      </Card>

      {loading ? (
        <Box display="flex" justifyContent="center" py={6}>
          <CircularProgress />
        </Box>
      ) : (
        <>
          {/* KPI cards — Grid items stretch (display:flex) and each Card is
              width:100% with a flex-column CardContent, so all three cards
              in the row always match height regardless of caption length. */}
          <Grid container spacing={3} sx={{ mb: 3, alignItems: "stretch" }}>
            <Grid item xs={12} sm={4} sx={{ display: "flex" }}>
              <Card
                elevation={3}
                onClick={() => toggleKindFilter("Income")}
                sx={{
                  borderRadius: 2,
                  cursor: "pointer",
                  width: "100%",
                  display: "flex",
                  flexDirection: "column",
                  transition: "box-shadow 0.15s, transform 0.15s",
                  border: kindFilter === "Income" ? `2px solid ${COLOR.green}` : "2px solid transparent",
                  "&:hover": { boxShadow: 6, transform: "translateY(-1px)" },
                }}
              >
                <CardContent sx={{ flexGrow: 1, display: "flex", flexDirection: "column" }}>
                  <Box display="flex" alignItems="center" gap={1} mb={1}>
                    <TrendingUpIcon sx={{ color: COLOR.green }} />
                    <Typography variant="body2" color="textSecondary">
                      Total Income (Verified)
                    </Typography>
                  </Box>
                  <Typography variant="h4" fontWeight={700} sx={{ color: COLOR.green }}>
                    {fmt(totalIncome)}
                  </Typography>
                  <Typography variant="caption" color="text.secondary" sx={{ mt: "auto", pt: 1 }}>
                    {kindFilter === "Income" ? "Showing income only — click to clear" : "Click to show income only"}
                  </Typography>
                </CardContent>
              </Card>
            </Grid>
            <Grid item xs={12} sm={4} sx={{ display: "flex" }}>
              <Card
                elevation={3}
                onClick={() => toggleKindFilter("Expense")}
                sx={{
                  borderRadius: 2,
                  cursor: "pointer",
                  width: "100%",
                  display: "flex",
                  flexDirection: "column",
                  transition: "box-shadow 0.15s, transform 0.15s",
                  border: kindFilter === "Expense" ? `2px solid ${COLOR.red}` : "2px solid transparent",
                  "&:hover": { boxShadow: 6, transform: "translateY(-1px)" },
                }}
              >
                <CardContent sx={{ flexGrow: 1, display: "flex", flexDirection: "column" }}>
                  <Box display="flex" alignItems="center" gap={1} mb={1}>
                    <TrendingDownIcon sx={{ color: COLOR.red }} />
                    <Typography variant="body2" color="textSecondary">
                      Total Expense (Paid)
                    </Typography>
                  </Box>
                  <Typography variant="h4" fontWeight={700} sx={{ color: COLOR.red }}>
                    {fmt(totalExpense)}
                  </Typography>
                  <Typography variant="caption" color="text.secondary" sx={{ mt: "auto", pt: 1 }}>
                    {kindFilter === "Expense" ? "Showing expenses only — click to clear" : "Click to show expenses only"}
                  </Typography>
                </CardContent>
              </Card>
            </Grid>
            <Grid item xs={12} sm={4} sx={{ display: "flex" }}>
              <Card
                elevation={3}
                sx={{
                  borderRadius: 2,
                  width: "100%",
                  display: "flex",
                  flexDirection: "column",
                  background: isPositive
                    ? `linear-gradient(135deg, ${COLOR.greenBg} 0%, #ffffff 100%)`
                    : `linear-gradient(135deg, ${COLOR.redBg} 0%, #ffffff 100%)`,
                }}
              >
                <CardContent sx={{ flexGrow: 1, display: "flex", flexDirection: "column" }}>
                  <Box display="flex" alignItems="center" gap={1} mb={1}>
                    <AccountBalanceWalletIcon sx={{ color: isPositive ? COLOR.green : COLOR.red }} />
                    <Typography variant="body2" color="textSecondary">
                      Net for Period
                    </Typography>
                  </Box>
                  <Typography
                    variant="h4"
                    fontWeight={700}
                    sx={{ color: isPositive ? COLOR.greenDark : COLOR.redDark }}
                  >
                    {fmt(netForPeriod)}
                  </Typography>
                  <Typography variant="caption" color="text.secondary" sx={{ mt: "auto", pt: 1 }}>
                    Income minus expense for this period only — see Balance below for the running total
                  </Typography>
                </CardContent>
              </Card>
            </Grid>
          </Grid>

          {/* THE combined ledger — one table, Expense + Income columns side by side */}
          <Card elevation={3} sx={{ borderRadius: 2, marginBottom: 3  }}>
            <Box
              sx={{
                px: 2,
                py: 1.5,
                display: "flex",
                alignItems: "center",
                justifyContent: "space-between",
                flexWrap: "wrap",
                gap: 1,
                borderBottom: "1px solid #eee",
              }}
            >
              <Typography variant="subtitle1" fontWeight={700}>
                Transactions
                <Typography component="span" variant="body2" color="text.secondary" sx={{ ml: 1 }}>
                  {startDate || "Inception"} – {endDate || "Present"}
                </Typography>
              </Typography>
              <Box display="flex" alignItems="center" gap={1}>
                <Chip
                  size="small"
                  label={`${filteredLedger.length} entries`}
                  sx={{ backgroundColor: COLOR.headerBg, fontWeight: 600 }}
                />
                <Tooltip title={sortAsc ? "Showing oldest first" : "Showing newest first"}>
                  <IconButton size="small" onClick={() => setSortAsc((s) => !s)}>
                    <SwapVertIcon fontSize="small" />
                  </IconButton>
                </Tooltip>
              </Box>
            </Box>

            <TableContainer sx={{ maxHeight: 560 }}>
              <Table size="small" stickyHeader>
                <TableHead>
                  <TableRow>
                    <TableCell sx={{ fontWeight: 700, backgroundColor: COLOR.headerBg }}>Date</TableCell>
                    <TableCell sx={{ fontWeight: 700, backgroundColor: COLOR.headerBg }}>
                      Category / Source
                    </TableCell>
                    <TableCell
                      sx={{
                        fontWeight: 700,
                        backgroundColor: COLOR.headerBg,
                        display: { xs: "none", md: "table-cell" },
                      }}
                    >
                      Description
                    </TableCell>
                    <TableCell
                      align="right"
                      sx={{ fontWeight: 700, backgroundColor: COLOR.redBg, color: COLOR.redDark }}
                    >
                      Expense
                    </TableCell>
                    <TableCell
                      align="right"
                      sx={{ fontWeight: 700, backgroundColor: COLOR.greenBg, color: COLOR.greenDark }}
                    >
                      Income
                    </TableCell>
                    <TableCell
                      align="right"
                      sx={{
                        fontWeight: 700,
                        backgroundColor: COLOR.headerBg,
                        display: { xs: "none", sm: "table-cell" },
                      }}
                    >
                      Balance
                    </TableCell>
                  </TableRow>
                </TableHead>
                <TableBody>
                  {/* Balance Forward row — carries the pre-period balance
                      into this table. Placed at the top when reading
                      oldest-first, and at the bottom when reading
                      newest-first, so it always sits next to the
                      chronological point it represents and the running
                      Balance column never appears to "jump" or reset. */}
                  {sortAsc && balanceForwardRowEl}

                  {filteredLedger.length === 0 ? (
                    <TableRow>
                      <TableCell colSpan={6} align="center" sx={{ color: "text.secondary", py: 5 }}>
                        No transactions found for this period.
                      </TableCell>
                    </TableRow>
                  ) : (
                    filteredLedger.map((row) => (
                      <TableRow key={row.id} hover  sx={{ "& td": { borderBottom: "1px solid #eee" } }}>
                        <TableCell sx={{ whiteSpace: "nowrap" }}>{row.date}</TableCell>
                        <TableCell>
                          <Chip label={row.label} size="small" variant="outlined" sx={{ fontSize: "0.7rem" }} />
                          {isMobile && (row.description || row.attachments.length > 0) ? (
                            <Box sx={{ display: "flex", alignItems: "center", gap: 0.75, mt: 0.35, flexWrap: "wrap" }}>
                              {row.description && (
                                <Typography variant="caption" color="text.secondary">
                                  {row.description}
                                </Typography>
                              )}
                              <AttachmentButton
                                count={row.attachments.length}
                                onClick={() => openFileViewer(row.attachments, 0)}
                                size="small"
                              />
                            </Box>
                          ) : null}
                        </TableCell>
                        <TableCell
                          title={row.description || undefined}
                          sx={{
                            display: { xs: "none", md: "table-cell" },
                            maxWidth: 220,
                            overflow: "hidden",
                            textOverflow: "ellipsis",
                            whiteSpace: "nowrap",
                            color: "text.secondary",
                          }}
                        >
                          <Box component="span" sx={{ display: "inline-flex", alignItems: "center", gap: 0.75, maxWidth: "100%" }}>
                            <Box
                              component="span"
                              sx={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
                            >
                              {row.description || "—"}
                            </Box>
                            <AttachmentButton
                              count={row.attachments.length}
                              onClick={() => openFileViewer(row.attachments, 0)}
                              size="small"
                            />
                          </Box>
                        </TableCell>
                        <TableCell align="right" sx={{ color: COLOR.redDark, fontWeight: 600, whiteSpace: "nowrap" }}>
                          {row.expenseAmount != null ? `-${fmt(row.expenseAmount)}` : ""}
                        </TableCell>
                        <TableCell align="right" sx={{ color: COLOR.greenDark, fontWeight: 600, whiteSpace: "nowrap" }}>
                          {row.incomeAmount != null ? `+${fmt(row.incomeAmount)}` : ""}
                        </TableCell>
                        <TableCell
                          align="right"
                          sx={{ display: { xs: "none", sm: "table-cell" }, fontWeight: 600, whiteSpace: "nowrap" }}
                        >
                          {fmt(runningBalanceById[row.id])}
                        </TableCell>
                      </TableRow>
                    ))
                  )}

                  {!sortAsc && balanceForwardRowEl}
                </TableBody>
                {/* Totals row — lines up under the exact same columns as the
                    data rows above, so "Total Expense" sits under Expense,
                    "Total Income" sits under Income, and the carried-forward
                    closing balance sits under Balance. */}
                <TableFooter>
                  <TableRow sx={{ "& th, & td": { borderBottom: "2px solid #e0e0e0" } }}>
                    <TableCell sx={{ fontWeight: 700, backgroundColor: COLOR.neutralBg }} />
                    <TableCell sx={{ fontWeight: 700, backgroundColor: COLOR.neutralBg }}>
                      Total
                    </TableCell>
                    <TableCell
                      sx={{
                        display: { xs: "none", md: "table-cell" },
                        backgroundColor: COLOR.neutralBg,
                      }}
                    />
                    <TableCell
                      align="right"
                      sx={{ fontWeight: 700, color: COLOR.redDark, backgroundColor: COLOR.neutralBg, whiteSpace: "nowrap" }}
                    >
                      -{fmt(totalExpense)}
                    </TableCell>
                    <TableCell
                      align="right"
                      sx={{ fontWeight: 700, color: COLOR.greenDark, backgroundColor: COLOR.neutralBg, whiteSpace: "nowrap" }}
                    >
                      +{fmt(totalIncome)}
                    </TableCell>
                    <TableCell
                      align="right"
                      sx={{
                        display: { xs: "none", sm: "table-cell" },
                        fontWeight: 700,
                        color: isClosingPositive ? COLOR.greenDark : COLOR.redDark,
                        backgroundColor: COLOR.neutralBg,
                        whiteSpace: "nowrap",
                      }}
                    >
                      {fmt(closingBalance)}
                    </TableCell>
                  </TableRow>
                </TableFooter>
              </Table>
            </TableContainer>

            {/* Balance summary is also restated plainly below the table for
                anyone who has the Balance column hidden on a small screen. */}
            <Box
              sx={{
                display: { xs: "flex", sm: "none" },
                flexDirection: "column",
                alignItems: "flex-end",
                px: 2,
                py: 1.25,
                borderTop: "1px solid #eee",
                gap: 0.25,
              }}
            >
              {startDate && (
                <Typography variant="caption" color="text.secondary" fontStyle="italic">
                  Balance Forward: {fmt(openingBalance)}
                </Typography>
              )}
              <Typography variant="body2" fontWeight={700}>
                Balance:{" "}
                <Box component="span" sx={{ color: isClosingPositive ? COLOR.greenDark : COLOR.redDark }}>
                  {fmt(closingBalance)}
                </Box>
              </Typography>
            </Box>
          </Card>

          {/* Breakdown tables */}
          <Grid container spacing={3} sx={{ mb: 3 }}>
            <Grid item xs={12} md={6}>
              <Card elevation={2} sx={{ borderRadius: 2 }}>
                <CardContent>
                  <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", mb: 2 }}>
                    <Typography variant="subtitle1" fontWeight={700}>
                      Income by Source
                    </Typography>
                    <Typography variant="body2" fontWeight={700} sx={{ color: COLOR.greenDark }}>
                      {fmt(totalIncome)}
                    </Typography>
                  </Box>
                  <BreakdownList
                    data={incomeBySource}
                    total={totalIncome}
                    color={COLOR.green}
                    colorDark={COLOR.greenDark}
                    emptyLabel="No income in this period."
                  />
                </CardContent>
              </Card>
            </Grid>

            <Grid item xs={12} md={6}>
              <Card elevation={2} sx={{ borderRadius: 2 }}>
                <CardContent>
                  <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", mb: 2 }}>
                    <Typography variant="subtitle1" fontWeight={700}>
                      Expense by Category
                    </Typography>
                    <Typography variant="body2" fontWeight={700} sx={{ color: COLOR.redDark }}>
                      {fmt(totalExpense)}
                    </Typography>
                  </Box>
                  <BreakdownList
                    data={expenseByCategory}
                    total={totalExpense}
                    color={COLOR.red}
                    colorDark={COLOR.redDark}
                    emptyLabel="No expenses in this period."
                  />
                </CardContent>
              </Card>
            </Grid>
          </Grid>
        </>
      )}

      {/* ────────────────────────────────────────────────────────────────── */}
      {/* Attachment viewer — unchanged from v4, same paging pattern as the  */}
      {/* Income module.                                                    */}
      {/* ────────────────────────────────────────────────────────────────── */}
      <Dialog open={fileViewerOpen} onClose={() => setFileViewerOpen(false)} maxWidth="md" fullWidth>
        <DialogTitle>
          <Box display="flex" alignItems="center" gap={1}>
            <span>Attachment</span>
            {fileViewerList.length > 1 && (
              <Chip
                size="small"
                label={`${fileViewerIndex + 1} of ${fileViewerList.length}`}
                sx={{ fontSize: "0.7rem" }}
              />
            )}
          </Box>
          <IconButton onClick={() => setFileViewerOpen(false)} sx={{ position: "absolute", right: 8, top: 8 }}>
            <CloseIcon />
          </IconButton>
        </DialogTitle>
        <DialogContent>
          <Box sx={{ position: "relative" }}>
            {fileViewerList.length > 1 && (
              <IconButton
                onClick={() => goToFileViewerIndex(fileViewerIndex - 1)}
                disabled={fileViewerIndex === 0}
                sx={{
                  position: "absolute", left: 4, top: "50%", transform: "translateY(-50%)",
                  zIndex: 1, backgroundColor: "rgba(255,255,255,0.85)",
                  "&:hover": { backgroundColor: "rgba(255,255,255,1)" },
                }}
                title="Previous attachment"
              >
                ‹
              </IconButton>
            )}

            {fileViewerUrl.toLowerCase().endsWith(".pdf") ? (
              <iframe src={fileViewerUrl} title="PDF" style={{ width: "100%", height: 500, border: "none" }} />
            ) : (
              <img src={fileViewerUrl} alt="Attachment" style={{ width: "100%", maxHeight: 500, objectFit: "contain" }} />
            )}

            {fileViewerList.length > 1 && (
              <IconButton
                onClick={() => goToFileViewerIndex(fileViewerIndex + 1)}
                disabled={fileViewerIndex === fileViewerList.length - 1}
                sx={{
                  position: "absolute", right: 4, top: "50%", transform: "translateY(-50%)",
                  zIndex: 1, backgroundColor: "rgba(255,255,255,0.85)",
                  "&:hover": { backgroundColor: "rgba(255,255,255,1)" },
                }}
                title="Next attachment"
              >
                ›
              </IconButton>
            )}
          </Box>

          {fileViewerList.length > 1 && (
            <Box display="flex" gap={1} mt={1.5} sx={{ overflowX: "auto", pb: 0.5 }}>
              {fileViewerList.map((path, idx) => (
                <Chip
                  key={path}
                  size="small"
                  variant={idx === fileViewerIndex ? "filled" : "outlined"}
                  color={idx === fileViewerIndex ? "primary" : "default"}
                  icon={<AttachFileIcon fontSize="small" />}
                  label={path.split("/").pop()}
                  onClick={() => goToFileViewerIndex(idx)}
                  sx={{ fontSize: "0.65rem", flexShrink: 0, maxWidth: 200 }}
                />
              ))}
            </Box>
          )}
        </DialogContent>
        <DialogActions sx={{ px: 3, py: 2 }}>
          <Button variant="outlined" onClick={() => setFileViewerOpen(false)}>Close</Button>
        </DialogActions>
      </Dialog>
    </>
  );
};

export default BalanceSheet;