"use client";

import * as React from "react";
import Link from "next/link";
import { useQuery } from "convex/react";
import { AnimatePresence, LayoutGroup, motion, useReducedMotion, type PanInfo } from "motion/react";
import { CalendarOff, CalendarPlus, ChevronLeft, ChevronRight, LogIn, LogOut, Users, X } from "lucide-react";
import { api } from "../../../convex/_generated/api";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { StatusBadge } from "@/components/ui/badge";
import { AnimatedNumber } from "@/components/motion/animated-number";
import { useReservationDrawer } from "@/components/reservations/reservation-drawer";
import { addDays, addMonths, dayKey, fmtDate, fmtRange, parseDay, startOfMonth } from "@/lib/dates";
import { CalendarAgenda } from "./calendar-mobile";
import type { CalApt, CalRes, CalBlock, CalPerms } from "./calendar-view";

type Mode = "month" | "timeline" | "year" | "agenda";
const MODES: { key: Mode; label: string }[] = [
  { key: "month", label: "Month" },
  { key: "timeline", label: "Timeline" },
  { key: "year", label: "Year" },
  { key: "agenda", label: "Agenda" },
];
const ACTIVE = ["CONFIRMED", "PENDING", "CHECKED_IN"];
const visualEnd = (r: CalRes) => (r.actualCheckOut && r.actualCheckOut < r.checkOut ? r.actualCheckOut : r.checkOut);
const STATUS_BAR: Record<string, string> = { CONFIRMED: "bg-positive-500 text-white", PENDING: "bg-warning-500 text-white", CHECKED_IN: "bg-info-500 text-white", CHECKED_OUT: "bg-stone-400 text-white", INQUIRY: "border border-dashed border-accent-500 bg-accent-500/15 text-accent-700 dark:text-accent-500" };
const monthTitle = (d: Date) => new Intl.DateTimeFormat("en", { month: "long", year: "numeric", timeZone: "UTC" }).format(d);
const spring = { type: "spring", stiffness: 520, damping: 40 } as const;
const ease = [0.16, 1, 0.3, 1] as const;

type Heat = { total: number; days: { k: string; booked: number; arrivals: number; departures: number }[] };

/**
 * Phone calendar. One live subscription per visible window feeds four ways of
 * looking at the same inventory: a swipeable month with occupancy heat and a
 * day panel, a per-apartment timeline, a whole-year heat map, and the agenda.
 */
export function CalendarPhone({ apartments, reservations: initialRes, blocks: initialBlocks, today, perms, currency, weekendDays, onBlock }: { apartments: CalApt[]; reservations: CalRes[]; blocks: CalBlock[]; today: string; loadedFrom: string; loadedTo: string; perms: CalPerms; currency: string; weekendDays: number[]; onBlock: (apartmentId: string | undefined, start: string, end: string) => void }) {
  const reduced = useReducedMotion();
  const [mode, setMode] = React.useState<Mode>("month");
  const [month, setMonth] = React.useState(() => startOfMonth(parseDay(today)));
  const [anchor, setAnchor] = React.useState(() => addDays(parseDay(today), -1));
  const [year, setYear] = React.useState(() => parseDay(today).getUTCFullYear());
  const [day, setDay] = React.useState<string>(today);
  const [apt, setApt] = React.useState<string>("");
  const [dir, setDir] = React.useState(1);

  // Live window around what is on screen.
  const winFrom = dayKey(addDays(mode === "timeline" ? anchor : month, -7));
  const winTo = dayKey(addDays(mode === "timeline" ? anchor : month, mode === "timeline" ? 30 : 45));
  const live = useQuery(api.reservations.calendar, { from: winFrom, to: winTo }) as { reservations: CalRes[]; blocks: CalBlock[] } | undefined;
  const reservations = live?.reservations ?? initialRes;
  const blocks = live?.blocks ?? initialBlocks;
  const heatFrom = mode === "year" ? `${year}-01-01` : dayKey(month);
  const heatTo = mode === "year" ? `${year + 1}-01-01` : dayKey(addMonths(month, 1));
  const heat = useQuery(api.reservations.calendarHeat, { from: heatFrom, to: heatTo }) as Heat | undefined;
  const heatMap = React.useMemo(() => new Map((heat?.days ?? []).map((d) => [d.k, d])), [heat]);
  const total = apt ? 1 : heat?.total ?? apartments.length;

  const visibleApts = apt ? apartments.filter((a) => a.id === apt) : apartments;
  const aptIds = React.useMemo(() => new Set(visibleApts.map((a) => a.id)), [visibleApts]);
  const res = React.useMemo(() => reservations.filter((r) => aptIds.has(r.apartmentId)), [reservations, aptIds]);
  const blk = React.useMemo(() => blocks.filter((b) => aptIds.has(b.apartmentId)), [blocks, aptIds]);

  const stayOn = (k: string) => res.filter((r) => ACTIVE.includes(r.status) && r.checkIn <= k && visualEnd(r) > k);
  const blockOn = (k: string) => blk.filter((b) => b.startDate <= k && b.endDate > k && b.type !== "HOLD");
  const bookedOn = (k: string) => (apt ? stayOn(k).length + blockOn(k).length : (heatMap.get(k)?.booked ?? stayOn(k).length + blockOn(k).length));

  // Month summary
  const monthDays = React.useMemo(() => {
    const first = startOfMonth(month);
    const n = new Date(Date.UTC(first.getUTCFullYear(), first.getUTCMonth() + 1, 0)).getUTCDate();
    return Array.from({ length: n }, (_, i) => dayKey(addDays(first, i)));
  }, [month]);
  const summary = React.useMemo(() => {
    const nights = monthDays.reduce((s, k) => s + bookedOn(k), 0);
    const cap = monthDays.length * Math.max(1, total);
    const arrivals = res.filter((r) => r.status !== "INQUIRY" && r.checkIn >= monthDays[0] && r.checkIn <= monthDays[monthDays.length - 1]).length;
    const departures = res.filter((r) => r.status !== "INQUIRY" && visualEnd(r) >= monthDays[0] && visualEnd(r) <= monthDays[monthDays.length - 1]).length;
    return { nights, occupancy: Math.round((nights / cap) * 100), arrivals, departures, free: cap - nights };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [monthDays, res, blk, heatMap, total]);

  const shiftMonth = (n: number) => {
    setDir(n);
    setMonth((m) => addMonths(m, n));
  };
  const onSwipe = (_: unknown, info: PanInfo) => {
    if (Math.abs(info.offset.x) < 60) return;
    if (mode === "month") shiftMonth(info.offset.x < 0 ? 1 : -1);
    else if (mode === "timeline") {
      setDir(info.offset.x < 0 ? 1 : -1);
      setAnchor((a) => addDays(a, info.offset.x < 0 ? 14 : -14));
    } else if (mode === "year") {
      setDir(info.offset.x < 0 ? 1 : -1);
      setYear((y) => y + (info.offset.x < 0 ? 1 : -1));
    }
  };
  const goToday = () => {
    setDir(1);
    setMonth(startOfMonth(parseDay(today)));
    setAnchor(addDays(parseDay(today), -1));
    setYear(parseDay(today).getUTCFullYear());
    setDay(today);
  };

  const title = mode === "year" ? String(year) : mode === "timeline" ? fmtRange(anchor, addDays(anchor, 13)) : monthTitle(month);
  const prev = () => (mode === "year" ? (setDir(-1), setYear((y) => y - 1)) : mode === "timeline" ? (setDir(-1), setAnchor((a) => addDays(a, -14))) : shiftMonth(-1));
  const next = () => (mode === "year" ? (setDir(1), setYear((y) => y + 1)) : mode === "timeline" ? (setDir(1), setAnchor((a) => addDays(a, 14))) : shiftMonth(1));
  const slide = { initial: reduced ? false : { opacity: 0, x: dir * 40 }, animate: { opacity: 1, x: 0 }, exit: reduced ? undefined : { opacity: 0, x: dir * -40 }, transition: { duration: 0.32, ease } };

  return (
    <div className="space-y-3">
      {/* Mode switch */}
      <LayoutGroup id="cal-mode">
        <div className="grid grid-cols-4 gap-0.5 rounded-xl bg-surface-2 p-1">
          {MODES.map((m) => (
            <button key={m.key} type="button" onClick={() => setMode(m.key)} className={cn("relative rounded-lg py-2 text-xs font-medium transition-colors", mode === m.key ? "text-fg" : "text-fg-muted")}>
              {mode === m.key ? <motion.span layoutId="cal-mode-pill" transition={spring} className="absolute inset-0 rounded-lg bg-surface shadow-xs hairline-top" aria-hidden /> : null}
              <span className="relative">{m.label}</span>
            </button>
          ))}
        </div>
      </LayoutGroup>

      {/* Period header */}
      {mode !== "agenda" ? (
        <div className="flex items-center gap-1">
          <button type="button" onClick={prev} className="flex size-9 items-center justify-center rounded-full border border-border bg-surface text-fg-muted transition active:scale-90" aria-label="Previous">
            <ChevronLeft className="size-4" />
          </button>
          <div className="min-w-0 flex-1 overflow-hidden text-center">
            <AnimatePresence mode="wait" initial={false}>
              <motion.p key={title} {...slide} className="truncate font-display text-lg font-medium">
                {title}
              </motion.p>
            </AnimatePresence>
          </div>
          <button type="button" onClick={next} className="flex size-9 items-center justify-center rounded-full border border-border bg-surface text-fg-muted transition active:scale-90" aria-label="Next">
            <ChevronRight className="size-4" />
          </button>
          <button type="button" onClick={goToday} className="ml-1 rounded-full border border-primary/40 bg-primary/10 px-3 py-1.5 text-xs font-semibold text-primary transition active:scale-95">
            Today
          </button>
        </div>
      ) : null}

      {/* Apartment chips */}
      <div className="-mx-3 flex gap-1.5 overflow-x-auto px-3 pb-0.5 scrollbar-none">
        <Chip active={!apt} onClick={() => setApt("")}>
          All · {apartments.length}
        </Chip>
        {apartments.map((a) => (
          <Chip key={a.id} active={apt === a.id} onClick={() => setApt(apt === a.id ? "" : a.id)}>
            <span className="font-mono">{a.code}</span>
          </Chip>
        ))}
      </div>

      {/* Smart summary */}
      {mode === "month" ? (
        <div className="grid grid-cols-4 gap-2">
          <Stat label="Occupancy" value={summary.occupancy} suffix="%" tone={summary.occupancy >= 70 ? "positive" : summary.occupancy >= 40 ? "primary" : "warning"} />
          <Stat label="Booked nights" value={summary.nights} />
          <Stat label="Arrivals" value={summary.arrivals} tone="positive" />
          <Stat label="Departures" value={summary.departures} tone="warning" />
        </div>
      ) : null}

      <div className="relative overflow-hidden">
        <AnimatePresence mode="wait" initial={false}>
          {mode === "month" ? (
            <motion.div key={`m-${dayKey(month)}-${apt}`} {...slide} drag={reduced ? false : "x"} dragConstraints={{ left: 0, right: 0 }} dragElastic={0.12} onDragEnd={onSwipe} className="touch-pan-y">
              <MonthGrid days={monthDays} today={today} selected={day} weekendDays={weekendDays} total={total} bookedOn={bookedOn} heatMap={heatMap} apt={apt} res={res} onPick={setDay} />
            </motion.div>
          ) : mode === "timeline" ? (
            <motion.div key={`t-${dayKey(anchor)}-${apt}`} {...slide} drag={reduced ? false : "x"} dragConstraints={{ left: 0, right: 0 }} dragElastic={0.12} onDragEnd={onSwipe} className="touch-pan-y">
              <Timeline apartments={visibleApts} res={res} blk={blk} anchor={anchor} today={today} weekendDays={weekendDays} perms={perms} onBlock={onBlock} />
            </motion.div>
          ) : mode === "year" ? (
            <motion.div key={`y-${year}-${apt}`} {...slide} drag={reduced ? false : "x"} dragConstraints={{ left: 0, right: 0 }} dragElastic={0.12} onDragEnd={onSwipe} className="touch-pan-y">
              <YearGrid year={year} today={today} total={total} bookedOn={bookedOn} loading={!heat} onPickMonth={(m) => { setDir(1); setMonth(m); setMode("month"); }} />
            </motion.div>
          ) : (
            <motion.div key="agenda" {...slide}>
              <CalendarAgenda apartments={visibleApts} reservations={res} blocks={blk} start={parseDay(today)} today={today} days={14} perms={perms} />
            </motion.div>
          )}
        </AnimatePresence>
      </div>

      {mode === "month" ? <DayPanel day={day} today={today} apartments={visibleApts} res={res} blk={blk} perms={perms} currency={currency} onBlock={onBlock} /> : null}

      <p className="flex flex-wrap items-center gap-x-3 gap-y-1 text-2xs text-fg-subtle">
        <span className="flex items-center gap-1"><span className="size-2 rounded-sm bg-positive-500" /> Confirmed</span>
        <span className="flex items-center gap-1"><span className="size-2 rounded-sm bg-info-500" /> In house</span>
        <span className="flex items-center gap-1"><span className="size-2 rounded-sm bg-warning-500" /> Pending</span>
        <span className="flex items-center gap-1"><span className="size-2 rounded-sm bg-[repeating-linear-gradient(45deg,var(--color-negative-500)_0_2px,transparent_2px_5px)]" /> Blocked</span>
        <span className="ml-auto flex items-center gap-1"><span className="live-dot" /> live</span>
      </p>
    </div>
  );
}

function Chip({ active, onClick, children }: { active: boolean; onClick: () => void; children: React.ReactNode }) {
  return (
    <button type="button" onClick={onClick} className={cn("shrink-0 rounded-full border px-3 py-1.5 text-xs font-medium transition-all active:scale-95", active ? "border-primary bg-primary/10 text-primary" : "border-border bg-surface text-fg-muted")}>
      {children}
    </button>
  );
}

function Stat({ label, value, suffix, tone }: { label: string; value: number; suffix?: string; tone?: "positive" | "warning" | "primary" }) {
  return (
    <div className="rounded-xl border border-border bg-surface p-2.5 shadow-xs hairline-top">
      <p className="text-[9px] font-semibold uppercase tracking-wider text-fg-subtle">{label}</p>
      <p className={cn("mt-0.5 text-lg font-semibold tabular leading-none", tone === "positive" && "text-positive-600", tone === "warning" && "text-warning-600", tone === "primary" && "text-primary")}>
        <AnimatedNumber value={value} />
        {suffix}
      </p>
    </div>
  );
}

/* ── Month ─────────────────────────────────────────────────── */
function MonthGrid({ days, today, selected, weekendDays, total, bookedOn, heatMap, apt, res, onPick }: { days: string[]; today: string; selected: string; weekendDays: number[]; total: number; bookedOn: (k: string) => number; heatMap: Map<string, { arrivals: number; departures: number }>; apt: string; res: CalRes[]; onPick: (k: string) => void }) {
  const lead = (parseDay(days[0]).getUTCDay() + 6) % 7;
  return (
    <div className="rounded-2xl border border-border bg-surface p-2 shadow-xs hairline-top">
      <div className="grid grid-cols-7 text-center text-[10px] font-semibold uppercase tracking-wider text-fg-subtle">
        {["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"].map((d) => (
          <span key={d} className="py-1">{d}</span>
        ))}
      </div>
      <div className="grid grid-cols-7 gap-1">
        {Array.from({ length: lead }).map((_, i) => (
          <span key={`l${i}`} />
        ))}
        {days.map((k, i) => {
          const booked = bookedOn(k);
          const ratio = total ? booked / total : 0;
          const weekend = weekendDays.includes(parseDay(k).getUTCDay());
          const arr = apt ? res.filter((r) => r.checkIn === k && r.status !== "INQUIRY").length : (heatMap.get(k)?.arrivals ?? 0);
          const dep = apt ? res.filter((r) => visualEnd(r) === k && r.status !== "INQUIRY").length : (heatMap.get(k)?.departures ?? 0);
          const past = k < today;
          return (
            <motion.button
              key={k}
              type="button"
              onClick={() => onPick(k)}
              initial={{ opacity: 0, scale: 0.8 }}
              animate={{ opacity: 1, scale: 1 }}
              transition={{ delay: Math.min(i, 30) * 0.012, ...spring }}
              whileTap={{ scale: 0.92 }}
              className={cn("relative flex aspect-[1/1.15] flex-col items-center justify-between rounded-lg px-0.5 pb-1 pt-1 text-xs transition-colors", selected === k ? "ring-2 ring-primary" : "", k === today && selected !== k && "ring-1 ring-primary/50", past && "opacity-60")}
              style={{ background: ratio > 0 ? `color-mix(in oklab, var(--color-primary) ${Math.round(12 + ratio * 55)}%, var(--color-surface-2))` : "var(--color-surface-2)" }}
              aria-label={`${k}: ${booked} of ${total} booked`}
            >
              <span className={cn("font-semibold tabular", ratio > 0.55 ? "text-white" : weekend ? "text-fg" : "text-fg", k === today && "text-primary")}>{Number(k.slice(-2))}</span>
              <span className={cn("text-[9px] tabular leading-none", ratio > 0.55 ? "text-white/85" : "text-fg-muted")}>{apt ? (booked ? "busy" : "free") : `${booked}/${total}`}</span>
              <span className="flex h-1.5 items-center gap-0.5">
                {arr ? <span className="size-1.5 rounded-full bg-positive-500 ring-1 ring-surface" /> : null}
                {dep ? <span className="size-1.5 rounded-full bg-warning-500 ring-1 ring-surface" /> : null}
              </span>
            </motion.button>
          );
        })}
      </div>
    </div>
  );
}

/* ── Day panel ─────────────────────────────────────────────── */
function DayPanel({ day, today, apartments, res, blk, perms, currency, onBlock }: { day: string; today: string; apartments: CalApt[]; res: CalRes[]; blk: CalBlock[]; perms: CalPerms; currency: string; onBlock: (apartmentId: string | undefined, start: string, end: string) => void }) {
  const { open } = useReservationDrawer();
  const d = parseDay(day);
  const next = dayKey(addDays(d, 1));
  const stays = res.filter((r) => ACTIVE.includes(r.status) && r.checkIn <= day && visualEnd(r) > day);
  const arrivals = res.filter((r) => r.checkIn === day && r.status !== "INQUIRY");
  const departures = res.filter((r) => visualEnd(r) === day && r.status !== "INQUIRY");
  const blocked = blk.filter((b) => b.startDate <= day && b.endDate > day);
  const busy = new Set([...stays.map((r) => r.apartmentId), ...blocked.map((b) => b.apartmentId)]);
  const free = apartments.filter((a) => !busy.has(a.id));
  const rows = [
    ...arrivals.map((r) => ({ r, kind: "in" as const })),
    ...stays.filter((r) => r.checkIn !== day).map((r) => ({ r, kind: "stay" as const })),
    ...departures.map((r) => ({ r, kind: "out" as const })),
  ];
  void currency;
  return (
    <AnimatePresence mode="wait" initial={false}>
      <motion.section key={day} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -6 }} transition={{ duration: 0.28, ease }} className="rounded-2xl border border-border bg-surface shadow-xs hairline-top">
        <header className="flex items-center justify-between gap-2 px-4 pt-3.5 pb-2">
          <div>
            <p className="text-sm font-semibold">
              {fmtDate(d, { style: "weekday" })}
              {day === today ? <span className="ml-2 rounded-full bg-primary/10 px-2 py-0.5 text-2xs text-primary">Today</span> : null}
            </p>
            <p className="text-2xs text-fg-muted">
              {stays.length + blocked.length}/{apartments.length} occupied · {arrivals.length} in · {departures.length} out · {free.length} free
            </p>
          </div>
          <div className="flex gap-1">
            {perms.block ? (
              <Button variant="secondary" size="iconSm" onClick={() => onBlock(undefined, day, next)} aria-label="Block dates">
                <CalendarOff />
              </Button>
            ) : null}
            {perms.create ? (
              <Button size="iconSm" asChild aria-label="New reservation">
                <Link href={`/reservations/new?checkIn=${day}&checkOut=${next}`}>
                  <CalendarPlus />
                </Link>
              </Button>
            ) : null}
          </div>
        </header>
        <ul className="stagger-fast divide-y divide-border">
          {rows.map(({ r, kind }) => {
            const a = apartments.find((x) => x.id === r.apartmentId);
            const left = Math.max(0, Math.round((parseDay(visualEnd(r)).getTime() - d.getTime()) / 86_400_000));
            return (
              <li key={`${kind}-${r.id}`}>
                <button type="button" onClick={() => open(r.id)} className="flex w-full items-center gap-3 px-4 py-2.5 text-left transition active:bg-surface-2">
                  <span className={cn("flex size-9 shrink-0 items-center justify-center rounded-lg font-mono text-xs font-bold", kind === "in" ? "bg-positive-500/12 text-positive-600" : kind === "out" ? "bg-warning-500/12 text-warning-600" : "bg-info-500/12 text-info-600")}>{a?.code}</span>
                  <span className="min-w-0 flex-1">
                    <span className="block truncate text-sm font-medium">{r.customer.fullName}</span>
                    <span className="flex items-center gap-1 text-2xs text-fg-muted">
                      {kind === "in" ? <LogIn className="size-3 text-positive-600" /> : kind === "out" ? <LogOut className="size-3 text-warning-600" /> : <Users className="size-3" />}
                      {kind === "in" ? `Arrives · ${r.nights} night${r.nights > 1 ? "s" : ""}` : kind === "out" ? "Leaves today" : `${left} night${left > 1 ? "s" : ""} left`} · {r.adults + r.children} guests
                    </span>
                  </span>
                  <StatusBadge status={r.status} size="sm" />
                </button>
              </li>
            );
          })}
          {blocked.map((b) => {
            const a = apartments.find((x) => x.id === b.apartmentId);
            return (
              <li key={b.id} className="flex items-center gap-3 px-4 py-2.5">
                <span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-negative-500/12 font-mono text-xs font-bold text-negative-600">{a?.code}</span>
                <span className="min-w-0 flex-1">
                  <span className="block truncate text-sm font-medium">{b.guestName ?? b.reason ?? "Blocked"}</span>
                  <span className="text-2xs text-fg-muted">{b.type.toLowerCase()} · {b.source.toLowerCase()} · until {fmtDate(parseDay(b.endDate), { style: "short" })}</span>
                </span>
              </li>
            );
          })}
          {rows.length === 0 && blocked.length === 0 ? <li className="px-4 py-4 text-center text-xs text-fg-muted">Quiet night — no stays, arrivals or departures.</li> : null}
        </ul>
        {free.length ? (
          <div className="border-t border-border px-4 py-3">
            <p className="mb-1.5 text-2xs font-semibold uppercase tracking-wider text-fg-subtle">Free tonight · tap to reserve</p>
            <div className="flex flex-wrap gap-1.5">
              {free.map((a) => (
                <Link key={a.id} href={perms.create ? `/reservations/new?apartment=${a.id}&checkIn=${day}&checkOut=${next}` : `/apartments/${a.id}`} className="rounded-lg border border-positive-500/30 bg-positive-50 px-2.5 py-1 font-mono text-xs font-semibold text-positive-700 transition active:scale-95 dark:bg-positive-500/10">
                  {a.code}
                </Link>
              ))}
            </div>
          </div>
        ) : null}
      </motion.section>
    </AnimatePresence>
  );
}

/* ── Timeline ───────────────────────────────────────────────── */
const COL = 44;
function Timeline({ apartments, res, blk, anchor, today, weekendDays, perms, onBlock }: { apartments: CalApt[]; res: CalRes[]; blk: CalBlock[]; anchor: Date; today: string; weekendDays: number[]; perms: CalPerms; onBlock: (apartmentId: string | undefined, start: string, end: string) => void }) {
  const { open } = useReservationDrawer();
  const days = Array.from({ length: 14 }, (_, i) => addDays(anchor, i));
  const keys = days.map(dayKey);
  const [pick, setPick] = React.useState<{ apartmentId: string; day: string } | null>(null);
  const idx = (k: string) => Math.round((parseDay(k).getTime() - anchor.getTime()) / 86_400_000);
  return (
    <div className="rounded-2xl border border-border bg-surface shadow-xs hairline-top">
      <div className="overflow-x-auto scrollbar-none">
        <div style={{ width: 64 + COL * 14 }}>
          <div className="flex border-b border-border">
            <div className="sticky left-0 z-10 w-16 shrink-0 bg-surface" />
            {days.map((d, i) => (
              <div key={keys[i]} style={{ width: COL }} className={cn("py-1.5 text-center", keys[i] === today && "rounded-t-md bg-primary/10", weekendDays.includes(d.getUTCDay()) && "bg-surface-2/60")}>
                <span className="block text-[9px] uppercase text-fg-subtle">{new Intl.DateTimeFormat("en", { weekday: "short", timeZone: "UTC" }).format(d).slice(0, 2)}</span>
                <span className={cn("block text-xs font-semibold tabular", keys[i] === today && "text-primary")}>{d.getUTCDate()}</span>
              </div>
            ))}
          </div>
          {apartments.map((a) => {
            const rows = res.filter((r) => r.apartmentId === a.id && r.status !== "CHECKED_OUT" && visualEnd(r) > keys[0] && r.checkIn < keys[13]);
            const bs = blk.filter((b) => b.apartmentId === a.id && b.endDate > keys[0] && b.startDate < keys[13]);
            return (
              <div key={a.id} className="relative flex h-12 border-b border-border last:border-0">
                <Link href={`/apartments/${a.id}`} className="sticky left-0 z-10 flex w-16 shrink-0 items-center border-r border-border bg-surface px-2 font-mono text-xs font-bold">
                  {a.code}
                </Link>
                <div className="relative" style={{ width: COL * 14 }}>
                  {keys.map((k, i) => (
                    <button key={k} type="button" onClick={() => setPick({ apartmentId: a.id, day: k })} className={cn("absolute inset-y-0 border-r border-border/60", k === today && "bg-primary/5", weekendDays.includes(days[i].getUTCDay()) && "bg-surface-2/40")} style={{ left: i * COL, width: COL }} aria-label={`${a.code} ${k}`} />
                  ))}
                  {bs.map((b) => {
                    const s = Math.max(0, idx(b.startDate));
                    const e = Math.min(14, idx(b.endDate));
                    return <span key={b.id} className={cn("absolute inset-y-2 rounded-md", b.type === "HOLD" ? "bg-warning-500/30" : "bg-[repeating-linear-gradient(45deg,color-mix(in_oklab,var(--color-negative-500)_60%,transparent)_0_3px,transparent_3px_7px)]")} style={{ left: s * COL + 2, width: Math.max(8, (e - s) * COL - 4) }} title={b.guestName ?? b.reason ?? "Blocked"} />;
                  })}
                  {rows.map((r) => {
                    const s = Math.max(0, idx(r.checkIn));
                    const e = Math.min(14, idx(visualEnd(r)));
                    return (
                      <motion.button key={r.id} type="button" onClick={() => open(r.id)} initial={{ scaleX: 0.6, opacity: 0 }} animate={{ scaleX: 1, opacity: 1 }} transition={spring} className={cn("absolute inset-y-2 flex items-center overflow-hidden rounded-md px-2 text-left text-[11px] font-medium shadow-sm origin-left", STATUS_BAR[r.status] ?? "bg-stone-400 text-white")} style={{ left: s * COL + 2, width: Math.max(COL - 4, (e - s) * COL - 4) }}>
                        <span className="truncate">{r.customer.fullName.split(" ")[0]}{e - s > 1 ? ` · ${r.nights}n` : ""}</span>
                      </motion.button>
                    );
                  })}
                </div>
              </div>
            );
          })}
        </div>
      </div>
      <AnimatePresence>
        {pick ? (
          <motion.div initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 8 }} className="flex items-center gap-2 border-t border-border p-2.5">
            <span className="min-w-0 flex-1 text-xs text-fg-muted">
              <b className="font-mono text-fg">{apartments.find((a) => a.id === pick.apartmentId)?.code}</b> · {fmtDate(parseDay(pick.day), { style: "weekday" })}
            </span>
            {perms.create ? (
              <Button size="sm" asChild>
                <Link href={`/reservations/new?apartment=${pick.apartmentId}&checkIn=${pick.day}&checkOut=${dayKey(addDays(parseDay(pick.day), 1))}`}>
                  <CalendarPlus /> Reserve
                </Link>
              </Button>
            ) : null}
            {perms.block ? (
              <Button size="sm" variant="secondary" onClick={() => { onBlock(pick.apartmentId, pick.day, dayKey(addDays(parseDay(pick.day), 1))); setPick(null); }}>
                <CalendarOff /> Block
              </Button>
            ) : null}
            <button type="button" onClick={() => setPick(null)} className="rounded-md p-1.5 text-fg-subtle" aria-label="Dismiss">
              <X className="size-4" />
            </button>
          </motion.div>
        ) : null}
      </AnimatePresence>
    </div>
  );
}

/* ── Year ───────────────────────────────────────────────────── */
function YearGrid({ year, today, total, bookedOn, loading, onPickMonth }: { year: number; today: string; total: number; bookedOn: (k: string) => number; loading: boolean; onPickMonth: (m: Date) => void }) {
  const months = Array.from({ length: 12 }, (_, i) => new Date(Date.UTC(year, i, 1)));
  return (
    <div className={cn("grid grid-cols-2 gap-2 transition-opacity", loading && "opacity-60")}>
      {months.map((m, mi) => {
        const n = new Date(Date.UTC(year, mi + 1, 0)).getUTCDate();
        const lead = (m.getUTCDay() + 6) % 7;
        const keys = Array.from({ length: n }, (_, i) => dayKey(addDays(m, i)));
        const nights = keys.reduce((s, k) => s + bookedOn(k), 0);
        const occ = total ? Math.round((nights / (n * total)) * 100) : 0;
        const current = today.startsWith(`${year}-${String(mi + 1).padStart(2, "0")}`);
        return (
          <motion.button key={mi} type="button" onClick={() => onPickMonth(m)} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: mi * 0.03, ...spring }} whileTap={{ scale: 0.97 }} className={cn("rounded-xl border bg-surface p-2.5 text-left shadow-xs hairline-top", current ? "border-primary/50" : "border-border")}>
            <div className="mb-1.5 flex items-center justify-between">
              <span className="text-xs font-semibold">{new Intl.DateTimeFormat("en", { month: "short", timeZone: "UTC" }).format(m)}</span>
              <span className={cn("text-2xs font-semibold tabular", occ >= 70 ? "text-positive-600" : occ >= 40 ? "text-primary" : "text-fg-muted")}>{occ}%</span>
            </div>
            <div className="grid grid-cols-7 gap-[3px]">
              {Array.from({ length: lead }).map((_, i) => (
                <span key={`l${i}`} />
              ))}
              {keys.map((k) => {
                const ratio = total ? bookedOn(k) / total : 0;
                return <span key={k} className={cn("aspect-square rounded-[2px]", k === today && "ring-1 ring-primary")} style={{ background: ratio > 0 ? `color-mix(in oklab, var(--color-primary) ${Math.round(15 + ratio * 70)}%, var(--color-surface-2))` : "var(--color-surface-3)" }} />;
              })}
            </div>
            <div className="mt-1.5 h-1 overflow-hidden rounded-full bg-surface-3">
              <motion.div className="h-full rounded-full bg-primary" initial={{ width: 0 }} animate={{ width: `${occ}%` }} transition={{ duration: 0.8, ease, delay: 0.1 + mi * 0.03 }} />
            </div>
          </motion.button>
        );
      })}
    </div>
  );
}
