/* eslint-disable @typescript-eslint/no-explicit-any -- Convex table-generic helpers; runtime validators enforce the shapes */
import { v } from "convex/values";
import { query, type QueryCtx } from "./_generated/server";
import type { Doc, Id } from "./_generated/dataModel";
import { assertPermission, can, requireActor } from "./lib/access";
import { getSettings } from "./lib/settings";
import { addDaysKey, addMonthsKey, nightsIn, parseKey, startOfMonthKey, startOfWeekKey, startOfYearKey, todayKey, nightsBetweenKeys } from "./lib/days";
import { effectiveCheckOut } from "./lib/inventory";
import { withId, loader, isRevenue, pct, delta } from "./lib/shape";
import { RESERVATION_SOURCE_META, RESERVATION_STATUS_META } from "../src/lib/domain";
import { fmtMoney, fmtPercent } from "../src/lib/format";

type Res = Doc<"reservations">;
const monthLabel = (k: string, year = false) => parseKey(k).toLocaleString("en", { month: "short", ...(year ? { year: "2-digit" } : {}), timeZone: "UTC" });
const dayLabel = (k: string) => `${parseKey(k).getUTCDate()}/${parseKey(k).getUTCMonth() + 1}`;
const revenueIn = (rs: Res[], from: string, to: string) => rs.filter((r) => isRevenue(r.status) && r.checkIn >= from && r.checkIn < to).reduce((s, r) => s + r.totalAmount, 0);
const countIn = (rs: Res[], from: string, to: string, pred: (r: Res) => boolean = () => true) => rs.filter((r) => pred(r) && r.checkIn >= from && r.checkIn < to).length;
const occupiedNights = (rs: Res[], from: string, to: string) => rs.reduce((n, r) => n + (isRevenue(r.status) ? nightsIn(r.checkIn, effectiveCheckOut(r), from, to) : 0), 0);
const days = (a: string, b: string) => Math.max(0, nightsBetweenKeys(a, b));

async function windowReservations(ctx: QueryCtx, from: string, to: string) {
  const byIn = await ctx.db.query("reservations").withIndex("by_checkIn", (q) => q.gte("checkIn", addDaysKey(from, -120)).lt("checkIn", to)).collect();
  const inHouse = await ctx.db.query("reservations").withIndex("by_status_checkIn", (q) => q.eq("status", "CHECKED_IN")).collect();
  const map = new Map<string, Res>();
  for (const r of [...byIn, ...inHouse]) if (r.checkOut >= from || r.checkIn >= from || r.status === "CHECKED_IN") map.set(r._id, r);
  return [...map.values()];
}

export const admin = query({
  args: {},
  returns: v.any(),
  handler: async (ctx) => {
    const actor = await assertPermission(ctx, "financials.view_revenue");
    const settings = await getSettings(ctx);
    const today = todayKey(settings.timezone);
    const tomorrow = addDaysKey(today, 1);
    const yesterday = addDaysKey(today, -1);
    const weekStart = startOfWeekKey(today);
    const prevWeekStart = addDaysKey(weekStart, -7);
    const monthStart = startOfMonthKey(today);
    const prevMonthStart = addMonthsKey(monthStart, -1);
    const yearStart = startOfYearKey(today);
    const prevYearStart = addMonthsKey(yearStart, -12);
    const windowStart = addMonthsKey(monthStart, -12);
    const windowEnd = addDaysKey(today, 60);
    const rs = await windowReservations(ctx, windowStart, windowEnd);
    const expenses = (await ctx.db.query("expenses").withIndex("by_date", (q) => q.gte("date", windowStart).lt("date", windowEnd)).collect()).filter((e) => !e.deletedAt);
    const cats = await ctx.db.query("expenseCategories").collect();
    const apartments = (await ctx.db.query("apartments").withIndex("by_active", (q) => q.eq("isActive", true)).collect()).filter((a) => !a.deletedAt);
    const roles = await ctx.db.query("roles").collect();
    const adminRole = roles.find((r) => r.key === "ADMIN");
    const allUsers = (await ctx.db.query("users").collect()).filter((u) => !u.deletedAt && u.roleId && u.roleId !== adminRole?._id);
    const customersCount = (await ctx.db.query("customers").collect()).filter((c) => !c.deletedAt).length;
    const commissions = await ctx.db.query("commissions").collect();
    const activity = (await ctx.db.query("auditLog").withIndex("by_at").order("desc").take(60)).filter((a) => !["LOGIN", "LOGOUT", "LOGIN_FAILED"].includes(a.action)).slice(0, 14);
    const tasksToday = (await ctx.db.query("tasks").withIndex("by_dueDate", (q) => q.eq("dueDate", today)).collect()).filter((t) => t.status === "TODO" || t.status === "IN_PROGRESS");
    const cleaningToday = (await ctx.db.query("cleaningTasks").collect()).filter((c) => c.status === "NEEDS_CLEANING" || c.status === "IN_PROGRESS");
    const maintenanceOpen = (await ctx.db.query("maintenanceTickets").collect()).filter((m) => m.status !== "COMPLETED").sort((a, b) => b.createdAt - a.createdAt);
    const apts = loader(ctx, "apartments");
    const users = loader(ctx, "users");
    const customers = loader(ctx, "customers");
    const expIn = (from: string, to: string) => expenses.filter((e) => e.date >= from && e.date < to).reduce((s, e) => s + e.amount, 0);
    const aptCount = apartments.length;

    const revToday = revenueIn(rs, today, tomorrow);
    const revYesterday = revenueIn(rs, yesterday, today);
    const revWeek = revenueIn(rs, weekStart, tomorrow);
    const revPrevWeek = revenueIn(rs, prevWeekStart, addDaysKey(prevWeekStart, days(weekStart, tomorrow)));
    const revMonth = revenueIn(rs, monthStart, tomorrow);
    const revPrevMonth = revenueIn(rs, prevMonthStart, addDaysKey(prevMonthStart, days(monthStart, tomorrow)));
    const revYear = revenueIn(rs, yearStart, tomorrow);
    const revPrevYear = revenueIn(rs, prevYearStart, addDaysKey(prevYearStart, days(yearStart, tomorrow)));
    const expMonth = expIn(monthStart, tomorrow);
    const expPrevMonth = expIn(prevMonthStart, monthStart);
    const monthStartMs = Date.parse(monthStart + "T00:00:00Z");
    const comMonth = commissions.filter((c) => !["CANCELLED", "REVERSED"].includes(c.status) && c.createdAt >= monthStartMs).reduce((s, c) => s + c.amount, 0);
    const profitMonth = revMonth - expMonth - comMonth;
    const profitPrevMonth = revPrevMonth - expPrevMonth;
    const occMonth = pct(occupiedNights(rs, monthStart, tomorrow), aptCount * days(monthStart, tomorrow));
    const occPrevMonth = pct(occupiedNights(rs, prevMonthStart, monthStart), aptCount * days(prevMonthStart, monthStart));
    const checkInsToday = rs.filter((r) => r.checkIn === today && ["CONFIRMED", "PENDING", "CHECKED_IN"].includes(r.status));
    const checkOutsToday = rs.filter((r) => r.checkOut === today && ["CHECKED_IN", "CHECKED_OUT"].includes(r.status));
    const active = rs.filter((r) => r.status === "CHECKED_IN");
    const upcoming = rs.filter((r) => ["CONFIRMED", "PENDING"].includes(r.status) && r.checkIn > today);
    const cancelledMonth = rs.filter((r) => r.status === "CANCELLED" && (r.cancelledAt ?? 0) >= monthStartMs);
    const cancelledPrevMonth = rs.filter((r) => r.status === "CANCELLED" && (r.cancelledAt ?? 0) >= Date.parse(prevMonthStart + "T00:00:00Z") && (r.cancelledAt ?? 0) < monthStartMs);
    const outstanding = rs.filter((r) => ["CONFIRMED", "CHECKED_IN"].includes(r.status)).reduce((s, r) => s + Math.max(0, r.totalAmount - r.amountPaid), 0);
    const pendingCom = commissions.filter((c) => c.status === "PENDING" || c.status === "APPROVED");
    const spark = Array.from({ length: 14 }, (_, i) => revenueIn(rs, addDaysKey(today, i - 13), addDaysKey(today, i - 12)));
    const sparkMonths = Array.from({ length: 6 }, (_, i) => { const s = addMonthsKey(monthStart, i - 5); return revenueIn(rs, s, addMonthsKey(s, 1)); });
    const daily = Array.from({ length: 30 }, (_, i) => { const d = addDaysKey(today, i - 29); const n = addDaysKey(d, 1); const rev = revenueIn(rs, d, n); const exp = expIn(d, n); return { key: d, label: dayLabel(d), revenue: rev, expenses: exp, profit: rev - exp, reservations: countIn(rs, d, n, (r) => isRevenue(r.status)), occupancy: pct(occupiedNights(rs, d, n), aptCount) }; });
    const weekly = Array.from({ length: 12 }, (_, i) => { const s = addDaysKey(weekStart, (i - 11) * 7); const e = addDaysKey(s, 7); const rev = revenueIn(rs, s, e); const exp = expIn(s, e); return { key: s, label: `W${Math.ceil((nightsBetweenKeys(startOfYearKey(s), s) + 1) / 7)}`, revenue: rev, expenses: exp, profit: rev - exp, reservations: countIn(rs, s, e, (r) => isRevenue(r.status)), occupancy: pct(occupiedNights(rs, s, e), aptCount * 7) }; });
    const monthly = Array.from({ length: 12 }, (_, i) => { const s = addMonthsKey(monthStart, i - 11); const e = addMonthsKey(s, 1); const rev = revenueIn(rs, s, e); const exp = expIn(s, e); return { key: s, label: monthLabel(s), revenue: rev, expenses: exp, profit: rev - exp, reservations: countIn(rs, s, e, (r) => isRevenue(r.status)), occupancy: pct(occupiedNights(rs, s, e), aptCount * days(s, e)) }; });
    const yearRs = rs.filter((r) => r.checkIn >= yearStart && r.checkIn < tomorrow);
    const bySource = Object.entries(RESERVATION_SOURCE_META).map(([k, m]) => ({ name: m.label, color: m.color, value: yearRs.filter((r) => r.source === k && isRevenue(r.status)).length, revenue: yearRs.filter((r) => r.source === k && isRevenue(r.status)).reduce((s, r) => s + r.totalAmount, 0) })).filter((x) => x.value > 0).sort((a, b) => b.value - a.value);
    const byStatus = Object.entries(RESERVATION_STATUS_META).map(([k, m]) => ({ name: m.label, value: rs.filter((r) => r.status === k && r.checkIn >= monthStart).length, tone: m.tone })).filter((x) => x.value > 0);
    const daysYtd = days(yearStart, tomorrow);
    const apartmentPerf = apartments.map((a) => { const ars = yearRs.filter((r) => r.apartmentId === a._id && isRevenue(r.status)); const revenue = ars.reduce((s, r) => s + r.totalAmount, 0); const nights = occupiedNights(rs.filter((r) => r.apartmentId === a._id), yearStart, tomorrow); const exp = expenses.filter((e) => e.apartmentId === a._id && e.date >= yearStart).reduce((s, e) => s + e.amount, 0); return { id: a._id, code: a.code, name: a.name, status: a.status, revenue, expenses: exp, profit: revenue - exp, reservations: ars.length, occupancy: pct(nights, daysYtd), adr: nights ? Math.round(revenue / nights) : 0, nights }; }).sort((a, b) => b.revenue - a.revenue);
    const workerPerf = allUsers.map((u) => { const created = yearRs.filter((r) => r.createdById === u._id); const confirmed = created.filter((r) => isRevenue(r.status)); const cancelled = created.filter((r) => r.status === "CANCELLED" || r.status === "NO_SHOW"); const com = commissions.filter((c) => c.workerId === u._id); const sum = (st: string[]) => com.filter((c) => st.includes(c.status)).reduce((s, c) => s + c.amount, 0); return { id: u._id, name: u.fullName ?? "", role: roles.find((r) => r._id === u.roleId)?.name ?? "", lastSeenAt: u.lastSeenAt ?? null, created: created.length, confirmed: confirmed.length, cancelled: cancelled.length, revenue: confirmed.reduce((s, r) => s + r.totalAmount, 0), conversion: pct(confirmed.length, created.length), commissionPending: sum(["PENDING", "APPROVED"]), commissionPaid: sum(["PAID"]) }; }).sort((a, b) => b.confirmed - a.confirmed);
    const todayRes = await Promise.all([...checkInsToday, ...checkOutsToday].map(async (r) => ({ r, customer: await customers(r.customerId), apartment: await apts(r.apartmentId) })));
    const timeline = [
      ...(await Promise.all(cleaningToday.map(async (c) => { const a = await apts(c.apartmentId); return { id: `c-${c._id}`, time: c.scheduledFor ? "08:00" : "09:00", kind: "cleaning" as const, title: `Cleaning ${a?.code ?? ""}`, subtitle: a?.name ?? "", status: c.status, href: `/cleaning` }; }))),
      ...todayRes.filter(({ r }) => r.checkOut === today).map(({ r, customer, apartment }) => ({ id: `out-${r._id}`, time: settings.checkOutTime, kind: "checkout" as const, title: `Check-out · ${r.code}`, subtitle: `${customer?.fullName ?? ""} · ${apartment?.code ?? ""}`, status: r.status, href: `/reservations/${r._id}` })),
      ...todayRes.filter(({ r }) => r.checkIn === today).map(({ r, customer, apartment }) => ({ id: `in-${r._id}`, time: settings.checkInTime, kind: "checkin" as const, title: `Check-in · ${r.code}`, subtitle: `${customer?.fullName ?? ""} · ${apartment?.code ?? ""}`, status: r.status, href: `/reservations/${r._id}` })),
      ...(await Promise.all(tasksToday.filter((t) => !["CHECK_IN", "CHECK_OUT", "CLEANING"].includes(t.type)).map(async (t) => ({ id: `t-${t._id}`, time: t.dueTime ?? "12:00", kind: "task" as const, title: t.title, subtitle: [(await apts(t.apartmentId))?.code, (await users(t.assigneeId))?.fullName].filter(Boolean).join(" · "), status: t.status, href: `/tasks` })))),
      ...(await Promise.all(maintenanceOpen.filter((m) => m.priority === "HIGH" || m.priority === "URGENT").slice(0, 3).map(async (m) => ({ id: `m-${m._id}`, time: "16:00", kind: "maintenance" as const, title: m.title, subtitle: `${(await apts(m.apartmentId))?.code ?? ""} · ${m.code}`, status: m.status, href: `/maintenance` })))),
    ].sort((a, b) => a.time.localeCompare(b.time));
    void actor;
    return {
      today,
      kpis: { revToday, revTodayDelta: delta(revToday, revYesterday), revWeek, revWeekDelta: delta(revWeek, revPrevWeek), revMonth, revMonthDelta: delta(revMonth, revPrevMonth), revYear, revYearDelta: delta(revYear, revPrevYear), profitMonth, profitDelta: delta(profitMonth, profitPrevMonth), expMonth, expDelta: delta(expMonth, expPrevMonth), comMonth, occMonth, occDelta: delta(occMonth, occPrevMonth), availableNow: apartments.filter((a) => a.status === "AVAILABLE").length, occupiedNow: apartments.filter((a) => a.status === "OCCUPIED").length, aptCount, checkInsToday: checkInsToday.length, checkOutsToday: checkOutsToday.length, active: active.length, upcoming: upcoming.length, cancelledMonth: cancelledMonth.length, cancelledDelta: delta(cancelledMonth.length, cancelledPrevMonth.length), customers: customersCount, workers: allUsers.filter((u) => u.status === "ACTIVE").length, outstanding, commissionsPending: pendingCom.reduce((s, c) => s + c.amount, 0), commissionsPendingCount: pendingCom.length, spark, sparkMonths },
      trends: { daily, weekly, monthly },
      bySource,
      byStatus,
      apartmentPerf,
      workerPerf,
      activity: activity.map((a) => ({ ...withId(a), createdAt: a.at })),
      timeline,
      cleaningPending: cleaningToday.length,
      maintenanceOpen: maintenanceOpen.length,
      apartments: apartments.sort((a, b) => a.code.localeCompare(b.code)).map((a) => ({ id: a._id, code: a.code, name: a.name, status: a.status, cleaningStatus: a.cleaningStatus })),
      expenseCategories: cats.map((c) => c.name),
    };
  },
});

export const worker = query({
  args: {},
  returns: v.any(),
  handler: async (ctx) => {
    const actor = await requireActor(ctx);
    const settings = await getSettings(ctx);
    const today = todayKey(settings.timezone);
    const monthStart = startOfMonthKey(today);
    const prevMonthStart = addMonthsKey(monthStart, -1);
    const monthStartMs = Date.parse(monthStart + "T00:00:00Z");
    const customers = loader(ctx, "customers");
    const apts = loader(ctx, "apartments");
    const decorate = async (r: Res) => { const [c, a] = await Promise.all([customers(r.customerId), apts(r.apartmentId)]); return { ...withId(r), customer: { id: r.customerId, fullName: c?.fullName ?? "", phone: c?.phone ?? "", avatarIndex: c?.avatarIndex ?? null }, apartment: { id: r.apartmentId, code: a?.code ?? "", name: a?.name ?? "", coverImageId: a?.coverImageId ?? null } }; };
    const [insRaw, outsRaw, activeRaw, upRaw, mine, tasksRaw, cleaningRaw, commissions, apartments, notificationsRaw] = await Promise.all([
      ctx.db.query("reservations").withIndex("by_checkIn", (q) => q.eq("checkIn", today)).collect(),
      ctx.db.query("reservations").withIndex("by_checkOut", (q) => q.eq("checkOut", today)).collect(),
      ctx.db.query("reservations").withIndex("by_status_checkIn", (q) => q.eq("status", "CHECKED_IN")).collect(),
      ctx.db.query("reservations").withIndex("by_checkIn", (q) => q.gt("checkIn", today).lte("checkIn", addDaysKey(today, 7))).collect(),
      ctx.db.query("reservations").withIndex("by_createdBy_createdAt", (q) => q.eq("createdById", actor.id).gte("createdAt", Date.parse(prevMonthStart + "T00:00:00Z"))).collect(),
      ctx.db.query("tasks").withIndex("by_assignee_status", (q) => q.eq("assigneeId", actor.id).eq("status", "TODO")).collect().then(async (t) => t.concat(await ctx.db.query("tasks").withIndex("by_assignee_status", (q) => q.eq("assigneeId", actor.id).eq("status", "IN_PROGRESS")).collect())),
      ctx.db.query("cleaningTasks").collect(),
      ctx.db.query("commissions").withIndex("by_worker", (q) => q.eq("workerId", actor.id)).collect(),
      ctx.db.query("apartments").withIndex("by_active", (q) => q.eq("isActive", true)).collect(),
      ctx.db.query("notifications").withIndex("by_user_read", (q) => q.eq("userId", actor.id).eq("readAt", undefined)).order("desc").take(5),
    ]);
    const checkIns = await Promise.all(insRaw.filter((r) => ["CONFIRMED", "PENDING", "CHECKED_IN"].includes(r.status)).map(decorate));
    const checkOuts = await Promise.all(outsRaw.filter((r) => ["CHECKED_IN", "CHECKED_OUT"].includes(r.status)).map(decorate));
    const upcoming = (await Promise.all(upRaw.filter((r) => r.status === "CONFIRMED" || r.status === "PENDING").sort((a, b) => a.checkIn.localeCompare(b.checkIn)).slice(0, 8).map(decorate)));
    const tasks = await Promise.all(tasksRaw.filter((t) => !t.dueDate || t.dueDate <= today).sort((a, b) => ["URGENT", "HIGH", "MEDIUM", "LOW"].indexOf(a.priority) - ["URGENT", "HIGH", "MEDIUM", "LOW"].indexOf(b.priority) || (a.dueTime ?? "").localeCompare(b.dueTime ?? "")).map(async (t) => ({ ...withId(t), apartment: t.apartmentId ? { code: (await apts(t.apartmentId))?.code ?? "" } : null })));
    const cleaning = await Promise.all(cleaningRaw.filter((c) => c.status === "NEEDS_CLEANING" || c.status === "IN_PROGRESS").map(async (c) => ({ ...withId(c), apartment: { code: (await apts(c.apartmentId))?.code ?? "", name: (await apts(c.apartmentId))?.name ?? "" } })));
    const thisMonth = mine.filter((r) => r.createdAt >= monthStartMs);
    const prevMonth = mine.filter((r) => r.createdAt < monthStartMs);
    const confirmedM = thisMonth.filter((r) => isRevenue(r.status)).length;
    const sum = (st: string[], from?: number) => commissions.filter((c) => st.includes(c.status) && (!from || c.createdAt >= from)).reduce((s, c) => s + c.amount, 0);
    const timeline = [
      ...cleaning.map((c) => ({ id: `c-${c.id}`, time: "08:00", kind: "cleaning" as const, title: `Cleaning ${c.apartment.code}`, subtitle: c.apartment.name, status: c.status, href: "/cleaning" })),
      ...checkOuts.map((r) => ({ id: `out-${r.id}`, time: settings.checkOutTime, kind: "checkout" as const, title: `Check-out · ${r.code}`, subtitle: `${r.customer.fullName} · ${r.apartment.code}`, status: r.status, href: `/reservations/${r.id}` })),
      ...checkIns.map((r) => ({ id: `in-${r.id}`, time: settings.checkInTime, kind: "checkin" as const, title: `Check-in · ${r.code}`, subtitle: `${r.customer.fullName} · ${r.apartment.code}`, status: r.status, href: `/reservations/${r.id}` })),
      ...tasks.filter((t) => !["CHECK_IN", "CHECK_OUT", "CLEANING"].includes(t.type)).map((t) => ({ id: `t-${t.id}`, time: t.dueTime ?? "12:00", kind: "task" as const, title: t.title, subtitle: t.apartment?.code ?? "", status: t.status, href: "/tasks" })),
    ].sort((a, b) => a.time.localeCompare(b.time));
    const aptRows = apartments.filter((a) => !a.deletedAt).sort((a, b) => a.code.localeCompare(b.code)).map((a) => ({ id: a._id, code: a.code, name: a.name, status: a.status, cleaningStatus: a.cleaningStatus }));
    return { today, checkIns, checkOuts, active: activeRaw.length, upcoming, tasks, cleaning, apartments: aptRows, notifications: notificationsRaw.map(withId), availableToday: aptRows.filter((a) => a.status === "AVAILABLE").length, perf: { createdM: thisMonth.length, createdPrev: prevMonth.length, confirmedM, cancelledM: thisMonth.filter((r) => r.status === "CANCELLED").length, noShowM: thisMonth.filter((r) => r.status === "NO_SHOW").length, commissionMonth: sum(["PENDING", "APPROVED", "PAID"], monthStartMs), commissionPending: sum(["PENDING", "APPROVED"]), commissionPaid: sum(["PAID"]), conversion: pct(confirmedM, thisMonth.length) }, timeline };
  },
});

// ── Command center ───────────────────────────────────────────
export type CCRange = "today" | "7d" | "30d" | "month" | "lastMonth" | "quarter" | "year" | "lastYear" | "custom";
export function resolveRange(range: string, tz: string, from?: string, to?: string) {
  const today = todayKey(tz);
  const tomorrow = addDaysKey(today, 1);
  let start = startOfMonthKey(today);
  let end = tomorrow;
  let label = "This month";
  const q = Math.floor(Number(today.slice(5, 7)) / 3.01) * 3 + 1;
  switch (range) {
    case "today": (start = today), (label = "Today"); break;
    case "yesterday": (start = addDaysKey(today, -1)), (end = today), (label = "Yesterday"); break;
    case "7d": (start = addDaysKey(today, -6)), (label = "Last 7 days"); break;
    case "30d": (start = addDaysKey(today, -29)), (label = "Last 30 days"); break;
    case "week": (start = startOfWeekKey(today)), (label = "This week"); break;
    case "lastMonth": (start = addMonthsKey(startOfMonthKey(today), -1)), (end = startOfMonthKey(today)), (label = "Last month"); break;
    case "quarter": (start = `${today.slice(0, 4)}-${String(q).padStart(2, "0")}-01`), (label = "This quarter"); break;
    case "year": (start = startOfYearKey(today)), (label = "This year"); break;
    case "lastYear": (start = addMonthsKey(startOfYearKey(today), -12)), (end = startOfYearKey(today)), (label = "Last year"); break;
    case "custom": (start = from && /^\d{4}-\d{2}-\d{2}$/.test(from) ? from : startOfMonthKey(today)), (end = to && /^\d{4}-\d{2}-\d{2}$/.test(to) ? addDaysKey(to, 1) : tomorrow), (label = `${start} → ${addDaysKey(end, -1)}`); break;
  }
  const n = Math.max(1, nightsBetweenKeys(start, end));
  return { today, tomorrow, start, end, days: n, prevStart: addDaysKey(start, -n), prevEnd: start, label };
}

export const commandCenter = query({
  args: { range: v.optional(v.string()), from: v.optional(v.string()), to: v.optional(v.string()) },
  returns: v.any(),
  handler: async (ctx, args) => {
    const actor = await assertPermission(ctx, "financials.view_revenue");
    const settings = await getSettings(ctx);
    const { today, tomorrow, start, end, days: nDays, prevStart, prevEnd, label } = resolveRange(args.range ?? "month", settings.timezone, args.from, args.to);
    const horizon = addDaysKey(today, 31);
    const apts = loader(ctx, "apartments");
    const customers = loader(ctx, "customers");
    const usersL = loader(ctx, "users");
    const roles = await ctx.db.query("roles").collect();
    const adminRole = roles.find((r) => r.key === "ADMIN");
    const apartments = (await ctx.db.query("apartments").withIndex("by_active", (q) => q.eq("isActive", true)).collect()).filter((a) => !a.deletedAt).sort((a, b) => a.code.localeCompare(b.code));
    const aptCount = apartments.length || 1;
    const rangeRs = await ctx.db.query("reservations").withIndex("by_checkIn", (q) => q.gte("checkIn", start).lt("checkIn", end)).collect();
    const prevRs = await ctx.db.query("reservations").withIndex("by_checkIn", (q) => q.gte("checkIn", prevStart).lt("checkIn", prevEnd)).collect();
    const overlapRaw = (await windowReservations(ctx, addDaysKey(start, -1), horizon)).filter((r) => r.checkIn < horizon && r.checkOut > addDaysKey(start, -1) && ["PENDING", "CONFIRMED", "CHECKED_IN", "CHECKED_OUT", "CANCELLED"].includes(r.status));
    const overlapRs = await Promise.all(overlapRaw.map(async (r) => { const [c, a, by] = await Promise.all([customers(r.customerId), apts(r.apartmentId), usersL(r.createdById)]); const contracts = await ctx.db.query("contracts").withIndex("by_reservation", (q) => q.eq("reservationId", r._id)).collect(); return { ...r, customer: { id: r.customerId, fullName: c?.fullName ?? "", phone: c?.phone ?? "" }, apartment: { id: r.apartmentId, code: a?.code ?? "", name: a?.name ?? "", coverImageId: a?.coverImageId ?? null }, contract: contracts.sort((x, y) => y.version - x.version)[0]?.status ?? null, createdByName: by?.fullName ?? "" }; }));
    const expenses = (await ctx.db.query("expenses").withIndex("by_date", (q) => q.gte("date", start).lt("date", end)).collect()).filter((e) => !e.deletedAt);
    const cats = await ctx.db.query("expenseCategories").collect();
    const prevExp = (await ctx.db.query("expenses").withIndex("by_date", (q) => q.gte("date", prevStart).lt("date", prevEnd)).collect()).filter((e) => !e.deletedAt).reduce((s, e) => s + e.amount, 0);
    const commissionsRaw = (await ctx.db.query("commissions").collect()).filter((c) => !["CANCELLED", "REVERSED"].includes(c.status));
    const commissions = await Promise.all(commissionsRaw.map(async (c) => ({ ...c, checkIn: (await ctx.db.get(c.reservationId))?.checkIn ?? "" })));
    const users = (await ctx.db.query("users").collect()).filter((u) => !u.deletedAt && u.roleId && u.roleId !== adminRole?._id);
    const auditRows = (await ctx.db.query("auditLog").withIndex("by_at").order("desc").take(80)).filter((a) => !["LOGIN", "LOGOUT", "LOGIN_FAILED"].includes(a.action)).slice(0, 25);
    const d30ms = Date.now() - 30 * 86_400_000;
    const releasedEventsRaw = (await ctx.db.query("inventoryEvents").withIndex("by_at", (q) => q.gte("at", d30ms)).order("desc").take(300)).filter((e) => ["RELEASED", "HOLD_RELEASED", "REBOOKED", "EARLY_CHECKOUT", "HOLD_CREATED"].includes(e.action)).slice(0, 40);
    const releasedEvents = await Promise.all(releasedEventsRaw.map(async (e) => { const r = e.reservationId ? await ctx.db.get(e.reservationId) : null; return { ...e, apartmentCode: (await apts(e.apartmentId))?.code ?? "", reservationCode: r?.code ?? null, guest: r ? (await customers(r.customerId))?.fullName ?? null : null }; }));
    const holdsRaw = await ctx.db.query("apartmentBlocks").withIndex("by_type", (q) => q.eq("type", "HOLD")).collect();
    const holds = await Promise.all(holdsRaw.map(async (h) => ({ ...h, apartment: { code: (await apts(h.apartmentId))?.code ?? "", basePrice: (await apts(h.apartmentId))?.basePrice ?? 0 }, reservationCode: h.reservationId ? (await ctx.db.get(h.reservationId))?.code ?? null : null })));
    const cleaning = await Promise.all((await ctx.db.query("cleaningTasks").collect()).filter((c) => c.status === "NEEDS_CLEANING" || c.status === "IN_PROGRESS").map(async (c) => ({ ...c, apartmentCode: (await apts(c.apartmentId))?.code ?? "" })));
    const maintenance = await Promise.all((await ctx.db.query("maintenanceTickets").collect()).filter((m) => m.status !== "COMPLETED").map(async (m) => ({ ...m, apartmentCode: (await apts(m.apartmentId))?.code ?? "" })));
    const unpaidChecked = overlapRs.filter((r) => r.status === "CHECKED_IN");
    const blocks = (await ctx.db.query("apartmentBlocks").withIndex("by_start", (q) => q.gte("startDate", addDaysKey(today, -400)).lt("startDate", horizon)).collect()).filter((b) => b.endDate > today);
    const recentRs = (await ctx.db.query("reservations").withIndex("by_checkOut", (q) => q.gte("checkOut", addDaysKey(today, -60))).collect()).filter((r) => ["PENDING", "CONFIRMED", "CHECKED_IN", "CHECKED_OUT"].includes(r.status));
    const allCustomers = (await ctx.db.query("customers").collect()).filter((c) => !c.deletedAt);
    const soonIn = new Set((await ctx.db.query("reservations").withIndex("by_checkIn", (q) => q.gte("checkIn", addDaysKey(today, -400)).lte("checkIn", addDaysKey(today, 3))).collect()).filter((r) => r.status === "CONFIRMED" || r.status === "CHECKED_IN").map((r) => r.customerId));
    const missingIds: { id: Id<"customers">; fullName: string }[] = [];
    for (const c of allCustomers) if (soonIn.has(c._id)) { const docs = (await ctx.db.query("documents").withIndex("by_customer", (q) => q.eq("customerId", c._id)).collect()).filter((d) => !d.deletedAt && (d.category === "ID_FRONT" || d.category === "ID_BACK")); if (!docs.length) missingIds.push({ id: c._id, fullName: c.fullName }); }
    let unsignedContracts = 0;
    for (const r of unpaidChecked) { const cs = await ctx.db.query("contracts").withIndex("by_reservation", (q) => q.eq("reservationId", r._id)).collect(); if (cs.some((c) => c.status === "GENERATED") && !cs.some((c) => c.status === "SIGNED")) unsignedContracts++; }
    const pendingApprovalHolds = holds.filter((h) => h.pendingApproval).length;

    const rev = rangeRs.filter((r) => isRevenue(r.status));
    const prevRev = prevRs.filter((r) => isRevenue(r.status));
    const gross = rev.reduce((s, r) => s + r.totalAmount, 0);
    const prevGross = prevRev.reduce((s, r) => s + r.totalAmount, 0);
    const paid = rev.reduce((s, r) => s + r.amountPaid, 0);
    const deposits = rev.reduce((s, r) => s + r.deposit, 0);
    const realized = rev.filter((r) => r.status !== "CONFIRMED").reduce((s, r) => s + r.totalAmount, 0);
    const expected = rev.filter((r) => r.status === "CONFIRMED").reduce((s, r) => s + r.totalAmount, 0);
    const outstanding = gross - paid;
    const expensesTotal = expenses.reduce((s, e) => s + e.amount, 0);
    const com = commissions.filter((c) => c.checkIn >= start && c.checkIn < end).reduce((s, c) => s + c.amount, 0);
    const prevCom = commissions.filter((c) => c.checkIn >= prevStart && c.checkIn < prevEnd).reduce((s, c) => s + c.amount, 0);
    const net = gross - com;
    const profit = gross - expensesTotal - com;
    const prevProfit = prevGross - prevExp - prevCom;
    const nights = overlapRs.filter((r) => isRevenue(r.status)).reduce((s, r) => s + nightsIn(r.checkIn, effectiveCheckOut(r), start, end), 0);
    const prevNights = prevRev.reduce((s, r) => s + nightsIn(r.checkIn, effectiveCheckOut(r), prevStart, prevEnd), 0);
    const occupancy = pct(nights, aptCount * nDays);
    const adr = nights ? gross / nights : 0;
    const step: "day" | "week" | "month" = nDays <= 45 ? "day" : nDays <= 200 ? "week" : "month";
    const buckets: { s: string; e: string; label: string }[] = [];
    if (step === "day") for (let d = start; d < end; d = addDaysKey(d, 1)) buckets.push({ s: d, e: addDaysKey(d, 1), label: `${parseKey(d).getUTCDate()} ${monthLabel(d)}` });
    else if (step === "week") for (let d = startOfWeekKey(start); d < end; d = addDaysKey(d, 7)) buckets.push({ s: d, e: addDaysKey(d, 7), label: `${parseKey(d).getUTCDate()} ${monthLabel(d)}` });
    else for (let d = startOfMonthKey(start); d < end; d = addMonthsKey(d, 1)) buckets.push({ s: d, e: addMonthsKey(d, 1), label: monthLabel(d, true) });
    const trend = buckets.map((b) => { const inB = (d: string) => d >= b.s && d < b.e; const r = rev.filter((x) => inB(x.checkIn)).reduce((s, x) => s + x.totalAmount, 0); const ps = addDaysKey(b.s, -nDays); const pe = addDaysKey(b.e, -nDays); const pr = prevRev.filter((x) => x.checkIn >= ps && x.checkIn < pe).reduce((s, x) => s + x.totalAmount, 0); const e = expenses.filter((x) => inB(x.date)).reduce((s, x) => s + x.amount, 0); const occ = pct(overlapRs.filter((x) => isRevenue(x.status)).reduce((s, x) => s + nightsIn(x.checkIn, effectiveCheckOut(x), b.s, b.e), 0), aptCount * nightsBetweenKeys(b.s, b.e)); return { label: b.label, key: b.s, revenue: r, previous: pr, expenses: e, profit: r - e, reservations: rev.filter((x) => inB(x.checkIn)).length, occupancy: occ, checkIns: overlapRs.filter((x) => inB(x.checkIn) && x.status !== "CANCELLED").length, checkOuts: overlapRs.filter((x) => inB(effectiveCheckOut(x)) && x.status !== "CANCELLED").length }; });
    const dto = (r: (typeof overlapRs)[number]) => ({ id: r._id, code: r.code, status: r.status, source: r.source, checkIn: r.checkIn, checkOut: r.checkOut, actualCheckOut: r.actualCheckOut ?? null, nights: r.nights, guests: r.adults + r.children, totalAmount: r.totalAmount, amountPaid: r.amountPaid, balance: Math.max(0, r.totalAmount - r.amountPaid), apartment: r.apartment, customer: r.customer, contract: r.contract, createdBy: r.createdByName, cancelReason: r.cancelReason ?? null, cancelledAt: r.cancelledAt ? new Date(r.cancelledAt).toISOString() : null, earlyCheckout: r.earlyCheckout, releasedNights: r.releasedNights, recoveredNights: r.recoveredNights });
    const inHouse = overlapRs.filter((r) => r.status === "CHECKED_IN").sort((a, b) => a.checkOut.localeCompare(b.checkOut)).map(dto);
    const arriving = overlapRs.filter((r) => r.checkIn === today && ["CONFIRMED", "PENDING", "CHECKED_IN"].includes(r.status)).map(dto);
    const departing = overlapRs.filter((r) => effectiveCheckOut(r) === today && ["CHECKED_IN", "CHECKED_OUT"].includes(r.status)).map(dto);
    const upcoming = overlapRs.filter((r) => ["CONFIRMED", "PENDING"].includes(r.status) && r.checkIn > today && r.checkIn <= addDaysKey(today, 7)).sort((a, b) => a.checkIn.localeCompare(b.checkIn)).map(dto);
    const cancelled = overlapRs.filter((r) => r.status === "CANCELLED" && (r.cancelledAt ?? 0) >= Date.now() - 7 * 86_400_000).sort((a, b) => (b.cancelledAt ?? 0) - (a.cancelledAt ?? 0)).map(dto);
    const released = releasedEvents.filter((e) => e.action === "RELEASED" || e.action === "HOLD_RELEASED").map((e) => ({ id: e._id, apartment: e.apartmentCode, start: e.startDate, end: e.endDate, nights: e.nights, value: e.estimatedValue ?? 0, reason: e.previousState ?? null, reservation: e.reservationCode, guest: e.guest, at: new Date(e.at).toISOString(), rebooked: recentRs.some((x) => x._id !== e.reservationId && x.apartmentId === e.apartmentId && x.checkIn < e.endDate && x.checkOut > e.startDate) }));
    const d30 = addDaysKey(today, -30);
    const portfolio = apartments.map((a) => { const current = overlapRs.find((r) => r.apartmentId === a._id && r.status === "CHECKED_IN"); const next = overlapRs.filter((r) => r.apartmentId === a._id && ["CONFIRMED", "PENDING"].includes(r.status) && r.checkIn >= today).sort((x, y) => x.checkIn.localeCompare(y.checkIn))[0]; const occ30 = pct(overlapRs.filter((r) => r.apartmentId === a._id && isRevenue(r.status)).reduce((s, r) => s + nightsIn(r.checkIn, effectiveCheckOut(r), d30, today), 0), 30); const blk = blocks.find((b) => b.apartmentId === a._id && b.startDate <= today && b.endDate > today); return { id: a._id, code: a.code, name: a.name, coverImageId: a.coverImageId ?? null, status: a.status, cleaningStatus: a.cleaningStatus, maintenanceStatus: a.maintenanceStatus, basePrice: a.basePrice, maxGuests: a.maxGuests, building: a.building ?? null, current: current ? { id: current._id, guest: current.customer.fullName, checkOut: effectiveCheckOut(current) } : null, next: next ? { id: next._id, guest: next.customer.fullName, checkIn: next.checkIn, source: next.source } : null, occupancy30: occ30, block: blk ? { type: blk.type, source: blk.source, guest: blk.guestName ?? null } : null, openMaintenance: maintenance.filter((m) => m.apartmentId === a._id).length }; });
    const statusCounts = portfolio.reduce<Record<string, number>>((m, a) => ((m[a.status] = (m[a.status] ?? 0) + 1), m), {});
    const stateOn = (aptId: Id<"apartments">, d: string): "occupied" | "reserved" | "blocked" | "available" => { if (overlapRs.some((r) => r.apartmentId === aptId && r.status === "CHECKED_IN" && r.checkIn <= d && effectiveCheckOut(r) > d)) return "occupied"; if (overlapRs.some((r) => r.apartmentId === aptId && ["CONFIRMED", "PENDING"].includes(r.status) && r.checkIn <= d && r.checkOut > d)) return "reserved"; if (blocks.some((b) => b.apartmentId === aptId && b.startDate <= d && b.endDate > d)) return "blocked"; return "available"; };
    const forecastFor = (n: number) => { const counts = { occupied: 0, reserved: 0, blocked: 0, available: 0, total: aptCount * n }; for (let i = 0; i < n; i++) { const d = addDaysKey(today, i); for (const a of apartments) counts[stateOn(a._id, d)]++; } return { ...counts, occupancy: pct(counts.occupied + counts.reserved, counts.total) }; };
    const forecast = { today: forecastFor(1), tomorrow: (() => { const c = { occupied: 0, reserved: 0, blocked: 0, available: 0, total: aptCount }; for (const a of apartments) c[stateOn(a._id, tomorrow)]++; return { ...c, occupancy: pct(c.occupied + c.reserved, c.total) }; })(), d7: forecastFor(7), d14: forecastFor(14), d30: forecastFor(30) };
    const heatDays = Array.from({ length: 14 }, (_, i) => addDaysKey(today, i));
    const heatmap = apartments.map((a) => ({ id: a._id, code: a.code, cells: heatDays.map((d) => stateOn(a._id, d)) }));
    const d30ago = Date.now() - 30 * 86_400_000;
    const workers = await Promise.all(users.filter((u) => u.status === "ACTIVE").map(async (u) => { const created = await ctx.db.query("reservations").withIndex("by_createdBy_createdAt", (q) => q.eq("createdById", u._id).gte("createdAt", d30ago)).collect(); const confirmed = created.filter((r) => isRevenue(r.status)); const cancelledN = created.filter((r) => r.status === "CANCELLED" || r.status === "NO_SHOW").length; const spark = Array.from({ length: 7 }, (_, i) => { const dk = addDaysKey(today, i - 6); return created.filter((r) => new Date(r.createdAt).toISOString().slice(0, 10) === dk).length; }); const todayN = created.filter((r) => r.createdAt >= Date.parse(today + "T00:00:00Z")).length; const d7 = created.filter((r) => r.createdAt >= Date.parse(addDaysKey(today, -6) + "T00:00:00Z")).length; const comAll = commissions.filter((c) => c.workerId === u._id); const ins = [...(await ctx.db.query("reservations").withIndex("by_status_checkIn", (q) => q.eq("status", "CHECKED_IN")).collect()), ...(await ctx.db.query("reservations").withIndex("by_status_checkIn", (q) => q.eq("status", "CHECKED_OUT")).collect())]; const checkIns = ins.filter((r) => r.checkedInById === u._id).length; const checkOuts = ins.filter((r) => r.checkedOutById === u._id).length; const raw = confirmed.length * 3 + (checkIns + checkOuts) * 0.4 - cancelledN * 2; const role = roles.find((r) => r._id === u.roleId); return { raw, id: u._id, name: u.fullName ?? "", role: role?.name ?? "", roleKey: role?.key ?? "", online: !!u.lastSeenAt && Date.now() - u.lastSeenAt < 3 * 60_000, lastSeenAt: u.lastSeenAt ? new Date(u.lastSeenAt).toISOString() : null, lastLoginAt: u.lastLoginAt ? new Date(u.lastLoginAt).toISOString() : null, created30: created.length, confirmed30: confirmed.length, cancelled30: cancelledN, today: todayN, d7, checkIns, checkOuts, revenue30: confirmed.reduce((s, r) => s + r.totalAmount, 0), commissionPending: comAll.filter((c) => c.status === "PENDING" || c.status === "APPROVED").reduce((s, c) => s + c.amount, 0), commissionPaid: comAll.filter((c) => c.status === "PAID").reduce((s, c) => s + c.amount, 0), spark, score: 0, recent: auditRows.filter((a) => a.userId === u._id).slice(0, 5).map((a) => ({ id: a._id, action: a.action, label: a.entityLabel ?? null, at: new Date(a.at).toISOString() })) }; }));
    workers.sort((a, b) => b.confirmed30 - a.confirmed30);
    const bestRaw = Math.max(1, ...workers.map((w) => w.raw));
    workers.forEach((w) => (w.score = Math.max(0, Math.round((w.raw / bestRaw) * 100))));
    const attention: { id: string; severity: "critical" | "important" | "normal"; title: string; detail: string; href: string; count: number }[] = [];
    const unpaid = unpaidChecked.filter((r) => r.totalAmount - r.amountPaid > 0.5);
    if (unpaid.length) attention.push({ id: "unpaid", severity: "critical", title: `${unpaid.length} in-house reservation${unpaid.length > 1 ? "s" : ""} unpaid`, detail: `${fmtMoney(unpaid.reduce((s, r) => s + r.totalAmount - r.amountPaid, 0), settings.currency)} still to collect from guests currently staying.`, href: "/payments?filter=outstanding", count: unpaid.length });
    const arrivingSoon = arriving.filter((r) => r.status !== "CHECKED_IN");
    if (arrivingSoon.length) attention.push({ id: "arrivals", severity: "important", title: `${arrivingSoon.length} check-in${arrivingSoon.length > 1 ? "s" : ""} still to do today`, detail: arrivingSoon.map((r) => `${r.apartment.code} · ${r.customer.fullName}`).slice(0, 3).join(" · "), href: "/reservations?arriving=today", count: arrivingSoon.length });
    const notReady = portfolio.filter((a) => arriving.some((r) => r.apartment.id === a.id) && a.cleaningStatus !== "CLEAN" && a.cleaningStatus !== "READY");
    if (notReady.length) attention.push({ id: "notready", severity: "critical", title: `${notReady.length} apartment${notReady.length > 1 ? "s" : ""} not ready for today's arrival`, detail: notReady.map((a) => a.code).join(", "), href: "/cleaning", count: notReady.length });
    if (cleaning.length) attention.push({ id: "cleaning", severity: "normal", title: `${cleaning.length} apartment${cleaning.length > 1 ? "s" : ""} awaiting cleaning`, detail: cleaning.map((c) => c.apartmentCode).join(", "), href: "/cleaning", count: cleaning.length });
    if (missingIds.length) attention.push({ id: "ids", severity: "important", title: `${missingIds.length} upcoming guest${missingIds.length > 1 ? "s" : ""} missing ID documents`, detail: missingIds.map((c) => c.fullName).slice(0, 3).join(", "), href: "/customers?segment=missing_id", count: missingIds.length });
    if (unsignedContracts) attention.push({ id: "contracts", severity: "normal", title: `${unsignedContracts} contract${unsignedContracts > 1 ? "s" : ""} unsigned for in-house guests`, detail: "Capture signatures from the contract page.", href: "/documents", count: unsignedContracts });
    if (pendingApprovalHolds) attention.push({ id: "holds", severity: "important", title: `${pendingApprovalHolds} inventory release${pendingApprovalHolds > 1 ? "s" : ""} awaiting your approval`, detail: `${holds.filter((h) => h.pendingApproval).reduce((s, h) => s + nightsBetweenKeys(h.startDate, h.endDate), 0)} nights blocked after cancellations.`, href: "/calendar?holds=1", count: pendingApprovalHolds });
    const urgentMnt = maintenance.filter((m) => m.priority === "URGENT" || m.priority === "HIGH");
    if (urgentMnt.length) attention.push({ id: "maintenance", severity: urgentMnt.some((m) => m.priority === "URGENT") ? "critical" : "important", title: `${urgentMnt.length} high-priority maintenance issue${urgentMnt.length > 1 ? "s" : ""}`, detail: urgentMnt.map((m) => `${m.apartmentCode} · ${m.title}`).slice(0, 2).join(" · "), href: "/maintenance", count: urgentMnt.length });
    const commissionsPending = commissions.filter((c) => c.status === "PENDING").length;
    if (commissionsPending >= 5) attention.push({ id: "commissions", severity: "normal", title: `${commissionsPending} commissions pending approval`, detail: "Approve or pay from the commissions page.", href: "/commissions?status=PENDING", count: commissionsPending });
    const sevRank = { critical: 0, important: 1, normal: 2 };
    attention.sort((a, b) => sevRank[a.severity] - sevRank[b.severity]);
    const freeTonight = apartments.filter((a) => stateOn(a._id, today) === "available");
    const freeTomorrow = apartments.filter((a) => stateOn(a._id, tomorrow) === "available");
    const releasedNightsRange = rangeRs.reduce((s, r) => s + r.releasedNights, 0);
    const recoveredNightsRange = rev.reduce((s, r) => s + r.recoveredNights, 0);
    const recoveredRevenue = rev.reduce((s, r) => s + r.recoveredNights * r.nightlyPrice, 0);
    const lostRevenue = rangeRs.filter((r) => r.status === "CHECKED_OUT" && r.earlyCheckout).reduce((s, r) => s + r.releasedNights * r.nightlyPrice, 0);
    const reopened30 = releasedEvents.filter((e) => e.action === "RELEASED" || e.action === "HOLD_RELEASED").reduce((s, e) => ({ nights: s.nights + e.nights, value: s.value + (e.estimatedValue ?? 0) }), { nights: 0, value: 0 });
    const opportunities: { id: string; title: string; detail: string; value?: number; href: string; tone: "positive" | "warning" | "info" }[] = [];
    if (freeTonight.length) opportunities.push({ id: "tonight", title: `${freeTonight.length} apartment${freeTonight.length > 1 ? "s" : ""} available tonight`, detail: freeTonight.map((a) => a.code).join(", "), value: freeTonight.reduce((s, a) => s + a.basePrice, 0), href: `/calendar?view=day`, tone: "warning" });
    if (freeTomorrow.length) opportunities.push({ id: "tomorrow", title: `${freeTomorrow.length} apartment${freeTomorrow.length > 1 ? "s" : ""} unoccupied tomorrow`, detail: freeTomorrow.map((a) => a.code).join(", "), value: freeTomorrow.reduce((s, a) => s + a.basePrice, 0), href: `/calendar?view=day&start=${tomorrow}`, tone: "info" });
    for (const e of released.filter((x) => !x.rebooked && x.end > today).slice(0, 4)) opportunities.push({ id: e.id, title: `${e.nights} night${e.nights > 1 ? "s" : ""} reopened on ${e.apartment}`, detail: `${e.start} → ${e.end} · ${e.reason === "EARLY_CHECKOUT" ? "early check-out" : e.reason === "CANCELLED" ? "cancellation" : "released"}${e.guest ? ` (${e.guest})` : ""}`, value: e.value, href: `/calendar?start=${e.start}`, tone: "positive" });
    for (const h of holds.filter((x) => x.pendingApproval).slice(0, 3)) { const n = nightsBetweenKeys(h.startDate, h.endDate); opportunities.push({ id: h._id, title: `Approve release of ${n} nights on ${h.apartment.code}`, detail: `${h.startDate} → ${h.endDate} · ${h.reservationCode ?? "hold"}`, value: n * h.apartment.basePrice, href: `/apartments/${h.apartmentId}?tab=timeline`, tone: "warning" }); }
    const insights: string[] = [];
    const occDelta = delta(occupancy, pct(prevNights, aptCount * nDays));
    if (occDelta != null && Math.abs(occDelta) >= 3 && prevNights > 0) insights.push(`Occupancy is ${Math.abs(occDelta).toFixed(0)}% ${occDelta > 0 ? "higher" : "lower"} than the previous period (${fmtPercent(occupancy, 0)}).`);
    const bySourceRaw = Object.keys(RESERVATION_SOURCE_META).map((k) => ({ k, n: rev.filter((r) => r.source === k).length, p: prevRev.filter((r) => r.source === k).length }));
    const direct = bySourceRaw.find((s) => s.k === "DIRECT");
    if (direct && direct.p > 0 && delta(direct.n, direct.p) != null && Math.abs(delta(direct.n, direct.p)!) >= 10) insights.push(`Direct bookings ${delta(direct.n, direct.p)! > 0 ? "increased" : "decreased"} by ${Math.abs(delta(direct.n, direct.p)!).toFixed(0)}% vs the previous period.`);
    const topApt = [...apartments].map((a) => ({ a, rev: rev.filter((r) => r.apartmentId === a._id).reduce((s, r) => s + r.totalAmount, 0) })).sort((x, y) => y.rev - x.rev)[0];
    if (topApt && topApt.rev > 0) insights.push(`${topApt.a.code} (${topApt.a.name}) is your highest-revenue apartment for ${label.toLowerCase()} with ${fmtMoney(topApt.rev, settings.currency, { compact: true })}.`);
    const directConf = rev.filter((r) => r.source === "DIRECT");
    if (directConf.length >= 4) { const byW = new Map<string, number>(); directConf.forEach((r) => byW.set(r.createdById, (byW.get(r.createdById) ?? 0) + 1)); const [wid, n] = [...byW.entries()].sort((a, b) => b[1] - a[1])[0]; const w = users.find((u) => u._id === wid); if (w) insights.push(`${(w.fullName ?? "").split(" ")[0]} created ${pct(n, directConf.length).toFixed(0)}% of confirmed direct reservations in this period.`); }
    if (reopened30.nights > 0) insights.push(`${reopened30.nights} night${reopened30.nights > 1 ? "s" : ""} were handed back to inventory in the last 30 days (worth about ${fmtMoney(reopened30.value, settings.currency, { compact: true })}); ${released.filter((r) => r.rebooked).length} of ${released.length} releases were re-sold.`);
    if (recoveredNightsRange > 0) insights.push(`${recoveredNightsRange} recovered night${recoveredNightsRange > 1 ? "s" : ""} generated ${fmtMoney(recoveredRevenue, settings.currency, { compact: true })} in ${label.toLowerCase()}.`);
    const cancelRate = pct(rangeRs.filter((r) => r.status === "CANCELLED" || r.status === "NO_SHOW").length, rangeRs.length);
    if (rangeRs.length >= 10 && cancelRate >= 15) insights.push(`Cancellation rate is ${cancelRate.toFixed(0)}% — consider deposits on pending reservations.`);
    const showProfit = can(actor, "financials.view_profit");
    return {
      generatedAt: new Date().toISOString(),
      period: { range: args.range ?? "month", label, start, end: addDaysKey(end, -1), days: nDays, today },
      hero: { gross, grossDelta: delta(gross, prevGross), net, expenses: expensesTotal, expensesDelta: delta(expensesTotal, prevExp), profit: showProfit ? profit : 0, profitDelta: showProfit ? delta(profit, prevProfit) : null, margin: showProfit ? pct(profit, gross) : 0, realized, expected, outstanding, deposits, paid, pending: outstanding, commissions: com, avgBooking: rev.length ? gross / rev.length : 0, adr, revpaa: gross / aptCount, occupancy, occupancyDelta: occDelta, nights, reservations: rev.length, reservationsDelta: delta(rev.length, prevRev.length), cancelled: rangeRs.filter((r) => r.status === "CANCELLED" || r.status === "NO_SHOW").length },
      trend: showProfit ? trend : trend.map((t) => ({ ...t, profit: 0 })),
      ops: { inHouse, arriving, departing, upcoming, cancelled, released },
      portfolio,
      statusCounts,
      forecast,
      heatmap: { days: heatDays.map((d) => ({ key: d, label: `${parseKey(d).toLocaleString("en", { weekday: "short", timeZone: "UTC" }).slice(0, 2)} ${parseKey(d).getUTCDate()}` })), rows: heatmap },
      workers,
      activity: auditRows.map((a) => ({ ...withId(a), createdAt: new Date(a.at).toISOString() })),
      attention,
      opportunities,
      insights,
      recovery: { releasedNights: releasedNightsRange, recoveredNights: recoveredNightsRange, recoveredRevenue, lostRevenue, reopened30, rebookRate: pct(released.filter((r) => r.rebooked).length, released.length) },
      bySource: bySourceRaw.filter((s) => s.n > 0).map((s) => ({ name: RESERVATION_SOURCE_META[s.k as keyof typeof RESERVATION_SOURCE_META].label, color: RESERVATION_SOURCE_META[s.k as keyof typeof RESERVATION_SOURCE_META].color, value: s.n, revenue: rev.filter((r) => r.source === s.k).reduce((x, r) => x + r.totalAmount, 0) })).sort((a, b) => b.revenue - a.revenue),
      expensesByCategory: Object.values(expenses.reduce<Record<string, { name: string; value: number }>>((m, e) => { const name = cats.find((c) => c._id === e.categoryId)?.name ?? "Other"; m[name] = m[name] ?? { name, value: 0 }; m[name].value += e.amount; return m; }, {})).sort((a, b) => b.value - a.value).slice(0, 6),
    };
  },
});
