import { createFileRoute } from "@tanstack/react-router";
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { supabase } from "@/integrations/supabase/client";
import { RequireRole } from "@/hooks/use-roles";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Download, Save, CalendarDays, TrendingUp, TrendingDown, Coins, BadgeDollarSign, Printer, FileText, Image as ImageIcon, Search, RefreshCw } from "lucide-react";
import { toast } from "sonner";
import { jsPDF } from "jspdf";
import html2canvas from "html2canvas-pro";

export const Route = createFileRoute("/_authenticated/salary")({
  component: () => (
    <RequireRole any={["super_admin", "admin", "company_admin", "ops_manager"]}>
      <SalaryPage />
    </RequireRole>
  ),
});

type RateRow = { basic: number; ot: number; deduction: number; dutyHours: number };
type Rates = Record<string, RateRow>;
type PostOption = { id: string; name: string; client_name: string | null };
type SalaryAttendanceRow = {
  guard_id: string;
  status: string;
  shift: string | null;
  check_in_at: string;
  check_out_at: string | null;
  duty_post_id: string | null;
  duty_posts: { name: string; client_name: string | null } | null;
};

const ALL_POSTS = "all";
const TZ = "Asia/Dhaka";

const ymdInDhaka = (value: string | Date) => {
  const d = typeof value === "string" ? new Date(value) : value;
  const parts = new Intl.DateTimeFormat("en-CA", {
    timeZone: TZ,
    year: "numeric",
    month: "2-digit",
    day: "2-digit",
  }).formatToParts(d);
  const get = (type: string) => parts.find((p) => p.type === type)?.value ?? "";
  return `${get("year")}-${get("month")}-${get("day")}`;
};

const dayInDhaka = (value: string) => Number(ymdInDhaka(value).slice(-2));

function monthRange(ym: string) {
  const [y, m] = ym.split("-").map(Number);
  const start = new Date(y, m - 1, 1);
  const end = new Date(y, m, 0);
  const pad = (n: number) => String(n).padStart(2, "0");
  const fmt = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
  return { from: fmt(start), to: fmt(end), days: end.getDate() };
}

const dhakaDayStartIso = (ymd: string) => `${ymd}T00:00:00+06:00`;
const dhakaDayEndIso = (ymd: string) => `${ymd}T23:59:59.999+06:00`;

function toCSV(rows: Record<string, unknown>[]) {
  if (!rows.length) return "";
  const headers = Object.keys(rows[0]);
  const esc = (v: unknown) => {
    const s = v == null ? "" : String(v);
    return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
  };
  return [headers.join(","), ...rows.map((r) => headers.map((h) => esc(r[h])).join(","))].join("\n");
}

function SalaryPage() {
  const now = new Date();
  const defaultMonth = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
  const defaultDay = `${defaultMonth}-${String(now.getDate()).padStart(2, "0")}`;
  const [month, setMonth] = useState(defaultMonth);
  const [mode, setMode] = useState<"monthly" | "daily">("monthly");
  const [day, setDay] = useState(defaultDay);
  const qc = useQueryClient();
  const [rates, setRates] = useState<Rates>({});
  const [dirty, setDirty] = useState<Set<string>>(new Set());
  const [search, setSearch] = useState("");
  const [postId, setPostId] = useState(ALL_POSTS);
  const sheetRef = useRef<HTMLDivElement | null>(null);
  const headerRef = useRef<HTMLDivElement | null>(null);
  const [exportingPdf, setExportingPdf] = useState(false);
  const [exportingImg, setExportingImg] = useState(false);
  const [pdfFormat, setPdfFormat] = useState<"a4" | "legal">("a4");
  const [pdfOrientation, setPdfOrientation] = useState<"p" | "l">("l");
  const [pdfMargin, setPdfMargin] = useState(5); // mm
  const [pdfScale, setPdfScale] = useState(100); // %

  const { from, to, days } = useMemo(() => {
    if (mode === "daily") return { from: day, to: day, days: 1 };
    return monthRange(month);
  }, [month, mode, day]);

  useEffect(() => {
    if (mode === "daily") setMonth(day.slice(0, 7));
  }, [day, mode]);

  const bn = (n: number | string) =>
    String(n).split("").map((c) => "০১২৩৪৫৬৭৮৯"[Number(c)] ?? c).join("");

  const guardsQ = useQuery({
    queryKey: ["salary-guards"],
    queryFn: async () => {
      const { data, error } = await supabase
        .from("security_guards")
        .select("id, full_name, employee_id, status")
        .order("full_name");
      if (error) throw error;
      return data ?? [];
    },
  });

  const postsQ = useQuery({
    queryKey: ["salary-duty-posts"],
    queryFn: async () => {
      const { data, error } = await supabase
        .from("duty_posts")
        .select("id, name, client_name")
        .order("name");
      if (error) throw error;
      return (data ?? []) as PostOption[];
    },
  });

  const attQ = useQuery({
    queryKey: ["salary-attendance", from, to, postId],
    queryFn: async () => {
      const pageSize = 1000;
      let page = 0;
      const allRows: SalaryAttendanceRow[] = [];

      while (true) {
        const { data, error } = await supabase
          .from("attendance")
          .select("guard_id, status, shift, check_in_at, check_out_at, duty_post_id, duty_posts(name, client_name)")
          .gte("check_in_at", dhakaDayStartIso(from))
          .lte("check_in_at", dhakaDayEndIso(to))
          .order("check_in_at", { ascending: true })
          .range(page * pageSize, (page + 1) * pageSize - 1);
        if (error) throw error;
        const rows = data ?? [];
        allRows.push(...rows);
        if (rows.length < pageSize) break;
        page += 1;
      }

      return allRows;
    },
  });

  const rosterQ = useQuery({
    queryKey: ["salary-rosters", from, to, postId],
    queryFn: async () => {
      let q = supabase
        .from("duty_rosters")
        .select("guard_id, duty_post_id")
        .gte("shift_date", from)
        .lte("shift_date", to);
      if (postId !== ALL_POSTS) q = q.eq("duty_post_id", postId);
      const { data, error } = await q;
      if (error) throw error;
      return data ?? [];
    },
  });

  const sheetQ = useQuery({
    queryKey: ["salary-sheets", month],
    queryFn: async () => {
      const { data, error } = await supabase
        .from("salary_sheets")
        .select("guard_id, basic, ot, deduction, duty_hours")
        .eq("month", month);
      if (error) throw error;
      return data ?? [];
    },
  });

  useEffect(() => {
    const map: Rates = {};
    for (const s of sheetQ.data ?? []) {
      map[s.guard_id] = {
        basic: Number(s.basic) || 0,
        ot: Number(s.ot) || 0,
        deduction: Number(s.deduction) || 0,
        dutyHours: Number((s as { duty_hours?: number }).duty_hours) || 8,
      };
    }
    setRates(map);
    setDirty(new Set());
  }, [sheetQ.data, month]);

  const upsertMut = useMutation({
    mutationFn: async (guardIds: string[]) => {
      const payload = guardIds.map((id) => ({
        guard_id: id, month,
        basic: rates[id]?.basic ?? 0,
        ot: rates[id]?.ot ?? 0,
        deduction: rates[id]?.deduction ?? 0,
        duty_hours: rates[id]?.dutyHours ?? 8,
      }));
      const { error } = await supabase.from("salary_sheets").upsert(payload, { onConflict: "guard_id,month" });
      if (error) throw error;
    },
    onSuccess: (_d, ids) => {
      setDirty((s) => { const n = new Set(s); ids.forEach((i) => n.delete(i)); return n; });
      qc.invalidateQueries({ queryKey: ["salary-sheets", month] });
    },
    onError: (e: unknown) => toast.error(e instanceof Error ? e.message : "Save failed"),
  });

  const saveAll = () => {
    const ids = Array.from(dirty);
    if (!ids.length) { toast.message("Nothing to save"); return; }
    upsertMut.mutate(ids, { onSuccess: () => toast.success(`Saved ${ids.length} row(s)`) });
  };
  const saveOne = (id: string) => upsertMut.mutate([id]);

  const rows = useMemo(() => {
    type Stat = { hours: number; late: number; byDay: Map<number, string[]> };
    const byGuard = new Map<string, Stat>();
    const rosterGuardIds = new Set((rosterQ.data ?? []).map((r) => r.guard_id));
    // Guard -> primary duty post (roster first, then most-frequent attendance post)
    const postNameById = new Map<string, string>();
    for (const p of postsQ.data ?? []) postNameById.set(p.id, p.name);
    const guardPost = new Map<string, string>(); // guard_id -> post_id
    for (const r of rosterQ.data ?? []) {
      if (r.duty_post_id && !guardPost.has(r.guard_id)) guardPost.set(r.guard_id, r.duty_post_id);
    }
    const attPostCount = new Map<string, Map<string, number>>();
    for (const a of attQ.data ?? []) {
      if (!a.duty_post_id) continue;
      const inner = attPostCount.get(a.guard_id) ?? new Map<string, number>();
      inner.set(a.duty_post_id, (inner.get(a.duty_post_id) ?? 0) + 1);
      attPostCount.set(a.guard_id, inner);
    }
    for (const [gid, counts] of attPostCount) {
      if (guardPost.has(gid)) continue;
      let best: string | null = null; let bestN = 0;
      for (const [pid, n] of counts) if (n > bestN) { best = pid; bestN = n; }
      if (best) guardPost.set(gid, best);
    }
    // For a specific post: only count attendance rows that belong to that
    // post OR have no post assigned but the guard is on this post's roster
    // (so mobile check-ins without a duty_post_id are still visible).
    const attRows = (attQ.data ?? []).filter((a) => {
      if (postId === ALL_POSTS) return true;
      if (a.duty_post_id === postId) return true;
      if (a.duty_post_id == null && rosterGuardIds.has(a.guard_id)) return true;
      return false;
    });
    const attendanceGuardIds = new Set(attRows.map((a) => a.guard_id));
    // Sort by check-in so the first per day is the regular duty
    const seenSalaryDuties = new Set<string>();
    const sorted = attRows.filter((a) => a.check_in_at)
      .sort((a, b) => new Date(a.check_in_at as string).getTime() - new Date(b.check_in_at as string).getTime());
    for (const a of sorted) {
      const cur = byGuard.get(a.guard_id) ?? { hours: 0, late: 0, byDay: new Map<number, string[]>() };
      const d = dayInDhaka(a.check_in_at as string);
      const arr = cur.byDay.get(d) ?? [];
      const shift = ((a as { shift?: string | null }).shift || "").toUpperCase();
      const letter = shift === "A" || shift === "B" || shift === "C" ? shift : "P";
      const salaryDutyKey = `${a.guard_id}|${ymdInDhaka(a.check_in_at as string)}|${letter}`;
      if (seenSalaryDuties.has(salaryDutyKey)) continue;
      seenSalaryDuties.add(salaryDutyKey);

      if (a.check_in_at && a.check_out_at) {
        const ms = new Date(a.check_out_at as string).getTime() - new Date(a.check_in_at as string).getTime();
        if (ms > 0) cur.hours += ms / 3600000;
      }
      // Count one salary duty per guard + day + shift. Different shifts on
      // the same day still count as OT, but duplicate same-shift check-ins do
      // not inflate the salary total.
      arr.push(letter);
      cur.byDay.set(d, arr);
      if (a.status === "flagged") cur.late += 1;
      byGuard.set(a.guard_id, cur);
    }
    const q = search.trim().toLowerCase();
    return (guardsQ.data ?? [])
      .filter((g) => g.status !== "inactive")
      .filter((g) => postId === ALL_POSTS || rosterGuardIds.has(g.id) || attendanceGuardIds.has(g.id))
      .filter((g) => {
        if (!q) return true;
        return `${g.full_name ?? ""} ${g.employee_id ?? ""}`.toLowerCase().includes(q);
      })
      .map((g) => {
      const r = rates[g.id] ?? { basic: 0, ot: 0, deduction: 0, dutyHours: 8 };
      const stats = byGuard.get(g.id) ?? { hours: 0, late: 0, byDay: new Map<number, string[]>() };
      const dh = r.dutyHours > 0 ? r.dutyHours : 8;
      let dutyDays = 0;
      let otDays = 0;
      for (const arr of stats.byDay.values()) {
        if (arr.length > 0) dutyDays += 1;
        if (arr.length > 1) otDays += arr.length - 1;
      }
      const absentDays = Math.max(0, days - dutyDays);
      const perDay = days > 0 ? r.basic / days : 0;
      const earned = perDay * dutyDays;
      const otAmount = r.ot > 0 ? r.ot : Math.round(perDay * otDays);
      const net = earned + otAmount - r.deduction;
      return {
        id: g.id,
        employee_id: g.employee_id,
        name: g.full_name,
        postId: guardPost.get(g.id) ?? null,
        postName: (guardPost.get(g.id) && postNameById.get(guardPost.get(g.id) as string)) || "",
        presentDays: dutyDays,
        absentDays,
        late: stats.late,
        hours: Math.round(stats.hours * 10) / 10,
        dutyHours: dh,
        basic: r.basic,
        ot: otAmount,
        otDays,
        deduction: r.deduction,
        earned: Math.round(earned),
        net: Math.round(net),
        presentSet: new Set(stats.byDay.keys()),
        dayMarks: stats.byDay,
      };
    })
    .sort((a, b) => {
      const na = Number(String(a.employee_id ?? "").replace(/\D/g, ""));
      const nb = Number(String(b.employee_id ?? "").replace(/\D/g, ""));
      if (Number.isFinite(na) && Number.isFinite(nb) && na !== nb) return na - nb;
      return String(a.employee_id ?? "").localeCompare(String(b.employee_id ?? ""));
    });
  }, [guardsQ.data, attQ.data, rosterQ.data, postsQ.data, rates, days, search, postId]);

  // Group rows by duty post so each sheet is calculated separately.
  const groups = useMemo(() => {
    const map = new Map<string, { postName: string; rows: typeof rows }>();
    for (const r of rows) {
      const key = r.postName;
      const g = map.get(key) ?? { postName: key, rows: [] as typeof rows };
      g.rows.push(r);
      map.set(key, g);
    }
    return Array.from(map.values()).sort((a, b) => a.postName.localeCompare(b.postName));
  }, [rows]);

  const totals = rows.reduce(
    (acc, r) => ({
      basic: acc.basic + r.basic,
      earned: acc.earned + r.earned,
      ot: acc.ot + r.ot,
      deduction: acc.deduction + r.deduction,
      net: acc.net + r.net,
    }),
    { basic: 0, earned: 0, ot: 0, deduction: 0, net: 0 }
  );

  const updateRate = (id: string, key: "basic" | "ot" | "deduction" | "dutyHours", val: number) => {
    setRates((s) => ({ ...s, [id]: { ...(s[id] ?? { basic: 0, ot: 0, deduction: 0, dutyHours: 8 }), [key]: val } }));
    setDirty((s) => new Set(s).add(id));
  };

  const exportCsv = () => {
    const csv = toCSV(rows.map((r) => ({
      EmployeeID: r.employee_id, Name: r.name,
      DutyHours: r.dutyHours, WorkedHours: r.hours,
      Present: r.presentDays, Absent: r.absentDays, Late: r.late,
      Basic: r.basic, OTDuties: r.otDays, OTAmount: r.ot, Deduction: r.deduction,
      Earned: r.earned, TotalDuties: r.presentDays + r.otDays, Net: r.net,
    })));
    const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url; a.download = `salary-${month}.csv`; a.click();
    URL.revokeObjectURL(url);
    toast.success("Salary sheet exported");
  };

  const selectedPost = postsQ.data?.find((p) => p.id === postId);
  const sheetTitle = selectedPost ? selectedPost.name : "All Duty Posts";
  const isLoading = guardsQ.isLoading || attQ.isLoading || sheetQ.isLoading || rosterQ.isLoading || postsQ.isLoading;
  const isError = guardsQ.isError || attQ.isError || sheetQ.isError || rosterQ.isError || postsQ.isError;
  const errorMessage =
    guardsQ.error?.message || attQ.error?.message || sheetQ.error?.message || rosterQ.error?.message || postsQ.error?.message || "Salary sheet failed to load";

  const exportPdf = async () => {
    setExportingPdf(true);
    try {
      const { default: autoTable } = await import("jspdf-autotable");
      // Use explicit mm dimensions so "Legal" reliably yields 8.5" x 14"
      // (215.9 x 355.6 mm) regardless of jsPDF's named-format handling.
      const formatDims: [number, number] =
        pdfFormat === "legal" ? [215.9, 355.6] : [210, 297];
      const pageSize: [number, number] =
        pdfOrientation === "l" ? [formatDims[1], formatDims[0]] : formatDims;
      const pdf = new jsPDF({ orientation: pdfOrientation, unit: "mm", format: pageSize });
      const pageW = pdf.internal.pageSize.getWidth();
      const pageH = pdf.internal.pageSize.getHeight();
      const margin = Math.max(0, Math.min(pdfMargin, Math.min(pageW, pageH) / 2 - 5));
      const scale = Math.max(0.1, pdfScale / 100);

      // 1) Bengali header rendered as a small image (jsPDF fonts can't
      //    shape Bengali text); small captures are reliable.
      let startY = margin;
      if (headerRef.current) {
        const hc = await html2canvas(headerRef.current, {
          scale: 3,
          useCORS: true,
          backgroundColor: "#ffffff",
          logging: false,
        });
        const hw = pageW - margin * 2;
        const hh = (hc.height * hw) / hc.width;
        pdf.addImage(hc.toDataURL("image/png"), "PNG", margin, margin, hw, hh);
        startY = margin + hh + 2;
      }

      // 2) Table drawn natively — one per duty post so each sheet's
      //    calculation is separate.
      let cursorY = startY;
      for (let gi = 0; gi < groups.length; gi++) {
        const g = groups[gi];
        if (gi > 0) {
          pdf.addPage();
          cursorY = margin;
        }
        if (g.postName) {
          pdf.setFontSize(9);
          pdf.setFont("helvetica", "bold");
          pdf.text(`Duty Post: ${g.postName}  (${g.rows.length})`, margin, cursorY + 3);
          cursorY += 5;
        }

        const head = [[
          "SL", "ID No", "Name",
          ...Array.from({ length: days }, (_, i) => String(i + 1)),
          "Duty", "OT", "Total",
        ]];
        const otCells = new Set<string>();
        const body = g.rows.map((r, idx) => {
          const dayCells = Array.from({ length: days }, (_, i) => {
            const arr = r.dayMarks.get(i + 1) ?? [];
            if (arr.length > 1) otCells.add(`${idx}:${3 + i}`);
            return Array.from(new Set(arr)).join("\n");
          });
          return [
            String(idx + 1), String(r.employee_id ?? ""), String(r.name ?? ""),
            ...dayCells,
            r.presentSet.size ? String(r.presentSet.size) : "",
            r.otDays ? String(r.otDays) : "",
            (r.presentDays + r.otDays) ? String(r.presentDays + r.otDays) : "",
          ];
        });
        const foot = [[
          { content: g.postName ? `Subtotal — ${g.postName}` : "Subtotal", colSpan: 3 + days, styles: { halign: "center" as const, fontStyle: "bold" as const } },
          String(g.rows.reduce((s, r) => s + r.presentSet.size, 0)),
          String(g.rows.reduce((s, r) => s + r.otDays, 0)),
          String(g.rows.reduce((s, r) => s + r.presentDays + r.otDays, 0)),
        ]];

        autoTable(pdf, {
          head,
          body,
          foot,
          showFoot: "lastPage",
          startY: cursorY,
          margin: { left: margin, right: margin, top: margin, bottom: margin },
          theme: "grid",
          styles: {
            fontSize: 12 * scale,
            cellPadding: 1,
            halign: "center",
            valign: "middle",
            lineColor: [0, 0, 0],
            lineWidth: 0.12,
            textColor: [0, 0, 0],
            overflow: "linebreak",
          },
          headStyles: { fillColor: [255, 255, 255], textColor: [0, 0, 0], fontStyle: "bold" },
          footStyles: { fillColor: [240, 240, 240], textColor: [0, 0, 0], fontStyle: "bold" },
          columnStyles: { 2: { halign: "left", cellWidth: 34 } },
          didParseCell: (d) => {
            if (d.section === "body" && otCells.has(`${d.row.index}:${d.column.index}`)) {
              d.cell.styles.fillColor = [253, 230, 138];
            }
          },
        });
        cursorY = (pdf as unknown as { lastAutoTable: { finalY: number } }).lastAutoTable.finalY;
      }

      // Grand total across all duty posts (matches on-screen table)
      if (groups.length > 1 && rows.length > 0) {
        const gHead = [[
          "SL", "ID No", "Name",
          ...Array.from({ length: days }, (_, i) => String(i + 1)),
          "Duty", "OT", "Total",
        ]];
        const gFoot = [[
          { content: "Grand Total", colSpan: 3 + days, styles: { halign: "center" as const, fontStyle: "bold" as const } },
          String(rows.reduce((s, r) => s + r.presentSet.size, 0)),
          String(rows.reduce((s, r) => s + r.otDays, 0)),
          String(rows.reduce((s, r) => s + r.presentDays + r.otDays, 0)),
        ]];
        autoTable(pdf, {
          head: gHead,
          body: [],
          foot: gFoot,
          showHead: "never",
          startY: cursorY + 2,
          margin: { left: margin, right: margin, top: margin, bottom: margin },
          theme: "grid",
          styles: {
            fontSize: 12 * scale,
            cellPadding: 1,
            halign: "center",
            valign: "middle",
            lineColor: [0, 0, 0],
            lineWidth: 0.12,
            textColor: [0, 0, 0],
          },
          footStyles: { fillColor: [220, 220, 220], textColor: [0, 0, 0], fontStyle: "bold" },
          columnStyles: { 2: { halign: "left", cellWidth: 34 } },
        });
        cursorY = (pdf as unknown as { lastAutoTable: { finalY: number } }).lastAutoTable.finalY;
      }

      // 3) Signature footer
      const finalY = (pdf as unknown as { lastAutoTable: { finalY: number } }).lastAutoTable.finalY;
      let sy = finalY + 22;
      if (sy > pageH - margin - 8) {
        pdf.addPage();
        sy = margin + 30;
      }
      const labels = ["Post Supervisor", "Security Officer (Round)", "Senior Manager (Ops)"];
      const colW = (pageW - margin * 2) / 3;
      pdf.setFontSize(8);
      pdf.setDrawColor(0);
      labels.forEach((label, i) => {
        const cx = margin + colW * i + colW / 2;
        pdf.line(cx - colW * 0.32, sy, cx + colW * 0.32, sy);
        pdf.text(label, cx, sy + 4, { align: "center" });
      });

      pdf.save(`salary-${month}.pdf`);
      toast.success("PDF exported");
    } catch (e) {
      toast.error(e instanceof Error ? e.message : "PDF export failed");
    } finally {
      setExportingPdf(false);
    }
  };

  const exportImage = async () => {
    if (!sheetRef.current) return;
    setExportingImg(true);
    const outer = sheetRef.current;
    const target = (outer.firstElementChild as HTMLElement) ?? outer;
    const prevOverflow = outer.style.overflow;
    outer.style.overflow = "visible";
    try {
      const fullWidth = Math.max(target.scrollWidth, 1200);
      const rectH = Math.ceil(target.getBoundingClientRect().height);
      const fullHeight = Math.max(target.scrollHeight, target.offsetHeight, rectH) + 80;
      const canvas = await html2canvas(target, {
        scale: 2,
        useCORS: true,
        backgroundColor: "#ffffff",
        logging: false,
        width: fullWidth,
        height: fullHeight,
        windowWidth: fullWidth,
        windowHeight: fullHeight,
      });
      const dataUrl = canvas.toDataURL("image/png");
      const a = document.createElement("a");
      a.href = dataUrl;
      a.download = `salary-${month}.png`;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      toast.success("Image exported");
    } catch (e) {
      toast.error(e instanceof Error ? e.message : "Image export failed");
    } finally {
      outer.style.overflow = prevOverflow;
      setExportingImg(false);
    }
  };

  return (
    <div className="space-y-6">
      {/* Toolbar (hidden on print) */}
      <div className="flex flex-wrap items-end justify-between gap-3 rounded-xl border bg-card p-3 shadow-sm print:hidden">
        <div className="flex flex-wrap items-end gap-3">
          <div className="space-y-1">
            <Label className="text-xs uppercase tracking-wide text-muted-foreground">Period</Label>
            <Select value={mode} onValueChange={(v) => setMode(v as "monthly" | "daily")}>
              <SelectTrigger className="w-32"><SelectValue /></SelectTrigger>
              <SelectContent>
                <SelectItem value="monthly">Monthly</SelectItem>
                <SelectItem value="daily">Daily</SelectItem>
              </SelectContent>
            </Select>
          </div>
          <div className="space-y-1">
            {mode === "monthly" ? (
              <>
                <Label htmlFor="month" className="text-xs uppercase tracking-wide text-muted-foreground">মাস / Month</Label>
                <Input id="month" type="month" value={month} onChange={(e) => setMonth(e.target.value)} className="w-44" />
              </>
            ) : (
              <>
                <Label htmlFor="day" className="text-xs uppercase tracking-wide text-muted-foreground">দৈনিক / Day</Label>
                <Input id="day" type="date" value={day} onChange={(e) => setDay(e.target.value)} className="w-44" />
              </>
            )}
          </div>
          <div className="space-y-1">
            <Label className="text-xs uppercase tracking-wide text-muted-foreground">Duty Post</Label>
            <Select value={postId} onValueChange={setPostId}>
              <SelectTrigger className="w-64">
                <SelectValue placeholder="Select duty post" />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value={ALL_POSTS}>All Duty Posts</SelectItem>
                {(postsQ.data ?? []).map((p) => (
                  <SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>
                ))}
              </SelectContent>
            </Select>
          </div>
          <div className="space-y-1">
            <Label htmlFor="salary-search" className="text-xs uppercase tracking-wide text-muted-foreground">Search</Label>
            <div className="relative">
              <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
              <Input
                id="salary-search"
                value={search}
                onChange={(e) => setSearch(e.target.value)}
                placeholder="Name or ID"
                className="w-56 pl-9"
              />
            </div>
          </div>
        </div>
        <div className="flex flex-wrap gap-2">
          <Button onClick={() => { qc.invalidateQueries({ queryKey: ["salary-guards"] }); qc.invalidateQueries({ queryKey: ["salary-attendance"] }); qc.invalidateQueries({ queryKey: ["salary-rosters"] }); qc.invalidateQueries({ queryKey: ["salary-sheets"] }); }} variant="outline" className="gap-1.5">
            <RefreshCw className="h-4 w-4" /> Refresh
          </Button>
          <Button onClick={() => window.print()} variant="outline" className="gap-1.5">
            <Printer className="h-4 w-4" /> Print
          </Button>
          <Button onClick={exportPdf} disabled={exportingPdf} variant="outline" className="gap-1.5">
            <FileText className="h-4 w-4" /> {exportingPdf ? "Rendering…" : "Export PDF"}
          </Button>
          <div className="flex flex-wrap items-end gap-2 rounded-md border bg-muted/30 px-2 py-1.5">
            <div className="space-y-1">
              <Label className="text-[10px] uppercase tracking-wide text-muted-foreground">Size</Label>
              <Select value={pdfFormat} onValueChange={(v) => setPdfFormat(v as "a4" | "legal")}>
                <SelectTrigger className="h-8 w-24"><SelectValue /></SelectTrigger>
                <SelectContent>
                  <SelectItem value="a4">A4</SelectItem>
                  <SelectItem value="legal">Legal</SelectItem>
                </SelectContent>
              </Select>
            </div>
            <div className="space-y-1">
              <Label className="text-[10px] uppercase tracking-wide text-muted-foreground">Orient.</Label>
              <Select value={pdfOrientation} onValueChange={(v) => setPdfOrientation(v as "p" | "l")}>
                <SelectTrigger className="h-8 w-28"><SelectValue /></SelectTrigger>
                <SelectContent>
                  <SelectItem value="p">Portrait</SelectItem>
                  <SelectItem value="l">Landscape</SelectItem>
                </SelectContent>
              </Select>
            </div>
            <div className="space-y-1">
              <Label className="text-[10px] uppercase tracking-wide text-muted-foreground">Margin mm</Label>
              <Input type="number" min={0} max={50} value={pdfMargin}
                onChange={(e) => setPdfMargin(Number(e.target.value) || 0)}
                className="h-8 w-20" />
            </div>
            <div className="space-y-1">
              <Label className="text-[10px] uppercase tracking-wide text-muted-foreground">Scale %</Label>
              <Input type="number" min={10} max={400} value={pdfScale}
                onChange={(e) => setPdfScale(Number(e.target.value) || 100)}
                className="h-8 w-20" />
            </div>
          </div>
          <Button onClick={exportImage} disabled={exportingImg} variant="outline" className="gap-1.5">
            <ImageIcon className="h-4 w-4" /> {exportingImg ? "Rendering…" : "Export IMG"}
          </Button>
          <Button onClick={saveAll} disabled={!dirty.size || upsertMut.isPending} variant="secondary" className="gap-1.5">
            <Save className="h-4 w-4" />
            {upsertMut.isPending ? "Saving…" : `Save${dirty.size ? ` (${dirty.size})` : ""}`}
          </Button>
          <Button onClick={exportCsv} className="gap-1.5">
            <Download className="h-4 w-4" /> Export CSV
          </Button>
        </div>
      </div>

      {/* Paper roster — CU style */}
      <div ref={sheetRef} className="mx-auto w-full overflow-x-auto rounded-xl border bg-white p-4 text-black shadow-elegant sm:p-6 print:border-0 print:p-0 print:shadow-none">
        <div className="min-w-[1100px]">
          {/* Header */}
          <div ref={headerRef} className="bg-white pb-1">
            <div className="text-center">
              <h1 className="text-2xl font-bold tracking-wide sm:text-3xl">ইন্টিগ্রেটেড সিকিউরিটি সার্ভিসেস লি.</h1>
              <p className="mt-1 text-sm sm:text-base">
                বাড়ী#৫৭০, রোড#০৪, জাকির হোসেন সোসাইটি, দক্ষিণ খুলশী চট্টগ্রাম।
              </p>
              <p className="text-sm sm:text-base">মোবাইল : ০১৮১৮-৮৯৯০১৪ (ট্রেনিং সেন্টার-মনছুরবাদ)</p>
            </div>
            <div className="mt-3 flex flex-wrap items-end justify-between gap-2 text-sm font-semibold">
              <div>Month: {new Date(`${month}-01`).toLocaleString("en-US", { month: "long", year: "numeric" })}</div>
              <div>Duty Post: {sheetTitle}</div>
            </div>
          </div>

          {isError && (
            <div className="mt-3 border border-red-600 bg-red-50 px-3 py-2 text-left text-xs font-semibold text-red-700">
              {errorMessage}
            </div>
          )}

          {/* Attendance grid — one section per duty post */}
          <table className="mt-2 w-full border-collapse border border-black text-center text-[11px]">
            <thead>
              <tr className="bg-white">
                <th className="border border-black px-1 py-1 font-semibold">SL</th>
                <th className="border border-black px-1 py-1 font-semibold">ID No</th>
                <th className="border border-black px-2 py-1 text-left font-semibold">Name</th>
                {Array.from({ length: days }, (_, i) => (
                  <th key={i} className="border border-black px-1 py-1 font-semibold">{i + 1}</th>
                ))}
                <th className="border border-black px-1 py-1 font-semibold">Duty</th>
                <th className="border border-black px-1 py-1 font-semibold">OT</th>
                <th className="border border-black px-1 py-1 font-semibold">Total</th>
              </tr>
            </thead>
            <tbody>
              {isLoading ? (
                <tr><td colSpan={6 + days} className="border border-black py-6">Loading…</td></tr>
              ) : rows.length === 0 ? (
                <tr><td colSpan={6 + days} className="border border-black py-6">No guards found for this salary sheet.</td></tr>
              ) : groups.map((g) => (
                <Fragment key={g.postName}>
                  {g.postName && (
                    <tr className="bg-gray-200 text-left">
                      <td colSpan={6 + days} className="border border-black px-2 py-1 font-bold uppercase tracking-wide">
                        Duty Post: {g.postName} ({g.rows.length})
                      </td>
                    </tr>
                  )}
                  {g.rows.map((r, idx) => (
                    <tr key={r.id} className="align-middle">
                      <td className="border border-black px-1 py-1 tabular-nums">{idx + 1}</td>
                      <td className="border border-black px-1 py-1 tabular-nums">{r.employee_id}</td>
                      <td className="border border-black px-2 py-1 text-left whitespace-nowrap">{r.name}</td>
                      {Array.from({ length: days }, (_, i) => {
                        const arr = r.dayMarks.get(i + 1) ?? [];
                        return (
                          <td key={i} className={`border border-black px-1 py-1 font-semibold leading-tight ${arr.length > 1 ? "bg-amber-100" : ""}`}>
                            <div className="flex flex-col items-center">
                              {Array.from(new Set(arr)).map((s, i2) => (
                                <span key={i2}>{s}</span>
                              ))}
                            </div>
                          </td>
                        );
                      })}
                      <td className="border border-black px-1 py-1 tabular-nums">{r.presentSet.size || ""}</td>
                      <td className="border border-black px-1 py-1 font-semibold tabular-nums">{r.otDays || ""}</td>
                      <td className="border border-black px-1 py-1 font-semibold tabular-nums">{(r.presentDays + r.otDays) || ""}</td>
                    </tr>
                  ))}
                  <tr className="bg-gray-100 font-bold">
                    <td className="border border-black px-1 py-1" colSpan={3 + days}>{g.postName ? `Subtotal — ${g.postName}` : "Subtotal"}</td>
                    <td className="border border-black px-1 py-1 tabular-nums">{g.rows.reduce((s, r) => s + r.presentSet.size, 0)}</td>
                    <td className="border border-black px-1 py-1 tabular-nums">{g.rows.reduce((s, r) => s + r.otDays, 0)}</td>
                    <td className="border border-black px-1 py-1 tabular-nums">{g.rows.reduce((s, r) => s + r.presentDays + r.otDays, 0)}</td>
                  </tr>
                </Fragment>
              ))}
              {rows.length > 0 && groups.length > 1 && (
                <tr className="bg-gray-300 font-bold">
                  <td className="border border-black px-1 py-1" colSpan={3 + days}>Grand Total</td>
                  <td className="border border-black px-1 py-1 tabular-nums">{rows.reduce((s, r) => s + r.presentSet.size, 0)}</td>
                  <td className="border border-black px-1 py-1 tabular-nums">{rows.reduce((s, r) => s + r.otDays, 0)}</td>
                  <td className="border border-black px-1 py-1 tabular-nums">{rows.reduce((s, r) => s + r.presentDays + r.otDays, 0)}</td>
                </tr>
              )}
            </tbody>
          </table>

          {/* Signature footer */}
          <div className="mt-20 grid grid-cols-3 gap-6 text-center text-xs">
            {["Post Supervisor", "Security Officer (Round)", "Senior Manager (Ops)"].map((label) => (
              <div key={label}>
                <div className="mx-auto mb-1 w-3/4 border-t border-black" />
                <div className="font-medium">{label}</div>
              </div>
            ))}
          </div>
        </div>
      </div>

      {/* Totals (hidden on print) */}
      <div className="grid grid-cols-2 gap-3 print:hidden sm:grid-cols-3 lg:grid-cols-5">
        {[
          { l: "Total Basic", v: totals.basic, Icon: Coins, tint: "text-sky-600 bg-sky-500/10" },
          { l: "Earned", v: totals.earned, Icon: TrendingUp, tint: "text-emerald-600 bg-emerald-500/10" },
          { l: "OT", v: totals.ot, Icon: CalendarDays, tint: "text-violet-600 bg-violet-500/10" },
          { l: "Deductions", v: totals.deduction, Icon: TrendingDown, tint: "text-rose-600 bg-rose-500/10" },
          { l: "Net Payable", v: totals.net, Icon: BadgeDollarSign, tint: "text-amber-600 bg-amber-500/10", highlight: true },
        ].map(({ l, v, Icon, tint, highlight }) => (
          <Card key={l} className={highlight ? "border-primary/40 shadow-glow" : undefined}>
            <CardHeader className="flex flex-row items-center justify-between space-y-0 p-3 pb-1 sm:p-4 sm:pb-2">
              <CardDescription className="truncate text-[10px] font-medium uppercase tracking-wide sm:text-xs">{l}</CardDescription>
              <div className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-lg sm:h-8 sm:w-8 ${tint}`}>
                <Icon className="h-3.5 w-3.5 sm:h-4 sm:w-4" />
              </div>
            </CardHeader>
            <CardContent className="p-3 pt-0 sm:p-4 sm:pt-0">
              <div className={`truncate text-lg font-bold tracking-tight sm:text-2xl ${highlight ? "text-gradient-primary" : ""}`}>
                {Math.round(v).toLocaleString()}
              </div>
            </CardContent>
          </Card>
        ))}
      </div>

      {/* Rates editor (hidden on print) */}
      <Card className="overflow-hidden print:hidden">
        <CardHeader className="border-b bg-muted/30">
          <div className="flex flex-wrap items-center justify-between gap-2">
            <div>
              <CardTitle className="flex items-center gap-2">
                <CalendarDays className="h-5 w-5 text-primary" />
                Payroll — {month}
              </CardTitle>
              <CardDescription>Edit Basic / OT / Deduction per guard. Auto-saves on blur.</CardDescription>
            </div>
            {dirty.size > 0 && (
              <span className="inline-flex items-center gap-1.5 rounded-full bg-amber-500/10 px-3 py-1 text-xs font-medium text-amber-700 dark:text-amber-400">
                <span className="h-1.5 w-1.5 animate-pulse-dot rounded-full bg-amber-500" />
                {dirty.size} unsaved
              </span>
            )}
          </div>
        </CardHeader>
        {/* Mobile card list */}
        <div className="space-y-3 p-3 md:hidden">
          {isLoading ? (
            <div className="py-10 text-center text-sm text-muted-foreground">Loading payroll…</div>
          ) : rows.length === 0 ? (
            <div className="py-10 text-center text-sm text-muted-foreground">No guards found for this salary sheet.</div>
          ) : rows.map((r) => (
            <div
              key={r.id}
              className={`rounded-xl border p-3 transition-colors ${dirty.has(r.id) ? "border-amber-500/40 bg-amber-500/5" : "bg-card"}`}
            >
              <div className="flex items-center gap-3">
                <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-gradient-primary text-xs font-semibold text-primary-foreground">
                  {r.name.split(" ").map((p) => p[0]).slice(0, 2).join("").toUpperCase()}
                </div>
                <div className="min-w-0 flex-1">
                  <div className="truncate font-medium leading-tight">{r.name}</div>
                  <div className="truncate text-xs text-muted-foreground">{r.employee_id}</div>
                </div>
                <div className="text-right">
                  <div className="text-[10px] uppercase tracking-wide text-muted-foreground">Net</div>
                  <div className="text-base font-semibold tabular-nums">{r.net.toLocaleString()}</div>
                </div>
              </div>
              <div className="mt-3 flex gap-2 text-xs">
                <span className="rounded-md bg-emerald-500/10 px-2 py-0.5 font-medium text-emerald-700 dark:text-emerald-400">P {r.presentDays}</span>
                <span className="rounded-md bg-rose-500/10 px-2 py-0.5 font-medium text-rose-700 dark:text-rose-400">A {r.absentDays}</span>
                <span className="rounded-md bg-sky-500/10 px-2 py-0.5 font-medium text-sky-700 dark:text-sky-400">Hrs {r.hours}</span>
                <span className="rounded-md bg-muted px-2 py-0.5 font-medium text-muted-foreground">Earned {r.earned.toLocaleString()}</span>
              </div>
              <div className="mt-3 grid grid-cols-2 gap-2 sm:grid-cols-4">
                <div className="space-y-1">
                  <Label className="text-[10px] uppercase tracking-wide text-muted-foreground">Duty Hrs</Label>
                  <Input type="number" min={1} step="0.5" value={r.dutyHours} onChange={(e) => updateRate(r.id, "dutyHours", Number(e.target.value) || 0)} onBlur={() => dirty.has(r.id) && saveOne(r.id)} className="h-9" />
                </div>
                <div className="space-y-1">
                  <Label className="text-[10px] uppercase tracking-wide text-muted-foreground">Basic</Label>
                  <Input type="number" min={0} value={r.basic} onChange={(e) => updateRate(r.id, "basic", Number(e.target.value) || 0)} onBlur={() => dirty.has(r.id) && saveOne(r.id)} className="h-9" />
                </div>
                <div className="space-y-1">
                  <Label className="text-[10px] uppercase tracking-wide text-muted-foreground">OT</Label>
                  <Input type="number" min={0} value={r.ot} onChange={(e) => updateRate(r.id, "ot", Number(e.target.value) || 0)} onBlur={() => dirty.has(r.id) && saveOne(r.id)} className="h-9" />
                </div>
                <div className="space-y-1">
                  <Label className="text-[10px] uppercase tracking-wide text-muted-foreground">Deduction</Label>
                  <Input type="number" min={0} value={r.deduction} onChange={(e) => updateRate(r.id, "deduction", Number(e.target.value) || 0)} onBlur={() => dirty.has(r.id) && saveOne(r.id)} className="h-9" />
                </div>
              </div>
            </div>
          ))}
        </div>

        {/* Desktop / tablet table */}
        <CardContent className="hidden overflow-x-auto p-0 md:block">
          <Table>
            <TableHeader>
              <TableRow className="bg-muted/20 hover:bg-muted/20">
                <TableHead className="pl-6">Guard</TableHead>
                <TableHead className="w-24">Duty Hrs</TableHead>
                <TableHead className="text-right">Worked</TableHead>
                <TableHead className="text-right">Present</TableHead>
                <TableHead className="text-right">Absent</TableHead>
                <TableHead className="w-28">Basic</TableHead>
                <TableHead className="w-24">OT</TableHead>
                <TableHead className="w-28">Deduction</TableHead>
                <TableHead className="text-right">Earned</TableHead>
                <TableHead className="pr-6 text-right font-semibold">Net</TableHead>
              </TableRow>
            </TableHeader>
            <TableBody>
              {isLoading ? (
                <TableRow><TableCell colSpan={10} className="text-center text-muted-foreground py-10">Loading payroll…</TableCell></TableRow>
              ) : rows.length === 0 ? (
                <TableRow><TableCell colSpan={10} className="text-center text-muted-foreground py-10">No guards found for this salary sheet.</TableCell></TableRow>
              ) : rows.map((r) => (
                <TableRow key={r.id} className={`transition-colors ${dirty.has(r.id) ? "bg-amber-500/5" : "hover:bg-muted/30"}`}>
                  <TableCell className="pl-6">
                    <div className="flex items-center gap-3">
                      <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-gradient-primary text-xs font-semibold text-primary-foreground">
                        {r.name.split(" ").map((p) => p[0]).slice(0, 2).join("").toUpperCase()}
                      </div>
                      <div className="min-w-0">
                        <div className="font-medium leading-tight">{r.name}</div>
                        <div className="text-xs text-muted-foreground">{r.employee_id}</div>
                      </div>
                    </div>
                  </TableCell>
                  <TableCell><Input type="number" min={1} step="0.5" value={r.dutyHours} onChange={(e) => updateRate(r.id, "dutyHours", Number(e.target.value) || 0)} onBlur={() => dirty.has(r.id) && saveOne(r.id)} className="h-9" /></TableCell>
                  <TableCell className="text-right tabular-nums text-muted-foreground">{r.hours}</TableCell>
                  <TableCell className="text-right">
                    <span className="inline-flex min-w-8 justify-center rounded-md bg-emerald-500/10 px-2 py-0.5 text-xs font-medium text-emerald-700 dark:text-emerald-400">
                      {r.presentDays}
                    </span>
                  </TableCell>
                  <TableCell className="text-right">
                    <span className="inline-flex min-w-8 justify-center rounded-md bg-rose-500/10 px-2 py-0.5 text-xs font-medium text-rose-700 dark:text-rose-400">
                      {r.absentDays}
                    </span>
                  </TableCell>
                  <TableCell><Input type="number" min={0} value={r.basic} onChange={(e) => updateRate(r.id, "basic", Number(e.target.value) || 0)} onBlur={() => dirty.has(r.id) && saveOne(r.id)} className="h-9" /></TableCell>
                  <TableCell><Input type="number" min={0} value={r.ot} onChange={(e) => updateRate(r.id, "ot", Number(e.target.value) || 0)} onBlur={() => dirty.has(r.id) && saveOne(r.id)} className="h-9" /></TableCell>
                  <TableCell><Input type="number" min={0} value={r.deduction} onChange={(e) => updateRate(r.id, "deduction", Number(e.target.value) || 0)} onBlur={() => dirty.has(r.id) && saveOne(r.id)} className="h-9" /></TableCell>
                  <TableCell className="text-right tabular-nums text-muted-foreground">{r.earned.toLocaleString()}</TableCell>
                  <TableCell className="pr-6 text-right font-semibold tabular-nums">{r.net.toLocaleString()}</TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </CardContent>
      </Card>
    </div>
  );
}