/* eslint-disable @typescript-eslint/no-explicit-any -- Convex table-generic helpers; runtime validators enforce the shapes */
/* eslint-disable no-console */
import { v } from "convex/values";
import { internalAction, internalMutation, internalQuery } from "./_generated/server";
import { internal } from "./_generated/api";
import type { Id } from "./_generated/dataModel";
import { createAccount } from "@convex-dev/auth/server";
import { PERMISSION_KEYS, ROLE_DEFAULTS, SYSTEM_ROLES } from "../src/lib/permissions";
import { DEFAULT_SETTINGS } from "../src/lib/settings-defaults";
import { NOTIFICATION_EVENTS } from "../src/lib/notification-events";
import { renderContractHtml } from "../src/lib/contract-template";
import { placeholderSvg } from "../src/lib/media-placeholder";
import { addDaysKey, eachNightKey, nightsBetweenKeys, parseKey, todayKey, weekdayOf } from "./lib/days";

/**
 * Demo dataset — run with `npx convex run seed:run` (local or cloud).
 * Deterministic PRNG so the demo is reproducible. Split into phases so
 * every mutation stays well under Convex transaction limits.
 */
const TABLES = ["auditLog", "notifications", "notificationRules", "commissions", "payments", "contracts", "documents", "tasks", "cleaningTasks", "inventoryEvents", "apartmentBlocks", "maintenanceTickets", "reservationHistory", "reservationGuests", "reservations", "customerNotes", "customerIncidents", "customers", "expenses", "expenseCategories", "apartmentImages", "apartments", "sessionMeta", "loginHistory", "authRefreshTokens", "authSessions", "authAccounts", "authVerificationCodes", "authVerifiers", "authRateLimits", "users", "roles", "settings", "sequences"] as const;

let seedState = 20260910;
function resetRng(s = 20260910) { seedState = s; }
function rnd() { seedState = (seedState * 1664525 + 1013904223) % 4294967296; return seedState / 4294967296; }
const pick = <T,>(arr: readonly T[]): T => arr[Math.floor(rnd() * arr.length)];
const between = (a: number, b: number) => a + Math.floor(rnd() * (b - a + 1));
const chance = (p: number) => rnd() < p;
const ms = (day: string, hours = 0, minutes = 0) => Date.parse(day + "T00:00:00Z") + hours * 3_600_000 + minutes * 60_000;
const START = "2026-01-05";
const END = "2026-11-20";

export const wipe = internalMutation({
  args: { table: v.string() },
  returns: v.number(),
  handler: async (ctx, { table }) => {
    const rows = await (ctx.db.query(table as "users") as any).take(1500);
    for (const r of rows) await ctx.db.delete(r._id);
    return rows.length;
  },
});

export const storeImage = internalMutation({
  args: { apartmentId: v.id("apartments"), storageId: v.id("_storage"), category: v.string(), index: v.number(), size: v.number(), caption: v.optional(v.string()), uploadedById: v.id("users") },
  returns: v.null(),
  handler: async (ctx, a) => {
    const id = await ctx.db.insert("apartmentImages", { apartmentId: a.apartmentId, storageId: a.storageId, mimeType: "image/svg+xml", width: 1600, height: 1000, size: a.size, category: a.category, caption: a.caption, isCover: a.index === 0, sortOrder: a.index, uploadedById: a.uploadedById });
    if (a.index === 0) await ctx.db.patch(a.apartmentId, { coverImageId: id });
    return null;
  },
});

export const storeDocument = internalMutation({
  args: { customerId: v.id("customers"), storageId: v.id("_storage"), category: v.string(), code: v.string(), fileName: v.string(), size: v.number(), uploadedById: v.id("users"), at: v.number() },
  returns: v.null(),
  handler: async (ctx, a) => {
    await ctx.db.insert("documents", { code: a.code, category: a.category, fileName: a.fileName, storageId: a.storageId, mimeType: "image/svg+xml", size: a.size, customerId: a.customerId, uploadedById: a.uploadedById, isSensitive: true, at: a.at });
    return null;
  },
});

/** Phase 1: roles, settings, notification rules, users, expense categories, apartments, customers. */
export const phase1 = internalMutation({
  args: {},
  returns: v.object({ users: v.any(), apartments: v.array(v.id("apartments")), customers: v.array(v.id("customers")) }),
  handler: async (ctx) => {
    resetRng();
    const roles: Record<string, Id<"roles">> = {};
    for (const r of SYSTEM_ROLES) roles[r.key] = await ctx.db.insert("roles", { key: r.key, name: r.name, description: r.description, isSystem: true, permissions: r.key === "ADMIN" ? PERMISSION_KEYS : ROLE_DEFAULTS[r.key] });
    const now = Date.now();
    for (const [key, value] of Object.entries(DEFAULT_SETTINGS)) await ctx.db.insert("settings", { key, value, group: "general", updatedAt: now });
    for (const e of NOTIFICATION_EVENTS) await ctx.db.insert("notificationRules", { eventType: e.type, label: e.label, enabled: true, inApp: true, email: e.defaultPriority === "CRITICAL", push: false, priority: e.defaultPriority, recipientRoles: e.defaultRole ? [e.defaultRole] : [], recipientUserIds: [], notifyAssigned: e.notifyAssigned, notifyActor: false });
    let wrk = 0;
    const mk = async (d: { email: string; username: string; fullName: string; phone: string; role: string; hire: string; lastLogin: number; status?: string }) =>
      ctx.db.insert("users", { code: `WRK-${String(++wrk).padStart(4, "0")}`, email: d.email, name: d.fullName, fullName: d.fullName, username: d.username, phone: d.phone, roleId: roles[d.role], status: d.status ?? "ACTIVE", hireDate: d.hire, locale: "en", timezone: "Africa/Casablanca", twoFactorEnabled: false, permissionOverrides: [], lastLoginAt: now - d.lastLogin * 60_000, lastSeenAt: now - d.lastLogin * 60_000, passwordChangedAt: now - 40 * 86_400_000 });
    const admin = await mk({ email: "admin@locajour.ma", username: "admin", fullName: "Mohammed El Amrani", phone: "+212661000001", role: "ADMIN", hire: "2024-01-15", lastLogin: 1 });
    const salma = await mk({ email: "salma@locajour.ma", username: "salma", fullName: "Salma Benjelloun", phone: "+212661000002", role: "MANAGER", hire: "2024-03-01", lastLogin: 2 });
    const youssef = await mk({ email: "youssef@locajour.ma", username: "youssef", fullName: "Youssef Tazi", phone: "+212661000003", role: "RECEPTION", hire: "2024-06-10", lastLogin: 35 });
    const ahmed = await mk({ email: "ahmed@locajour.ma", username: "ahmed", fullName: "Ahmed Berrada", phone: "+212661000004", role: "RECEPTION", hire: "2025-02-01", lastLogin: 190 });
    const fatima = await mk({ email: "fatima@locajour.ma", username: "fatima", fullName: "Fatima Zahra Idrissi", phone: "+212661000005", role: "CLEANER", hire: "2025-04-15", lastLogin: 600 });
    await mk({ email: "karim@locajour.ma", username: "karim", fullName: "Karim Ouazzani", phone: "+212661000006", role: "RECEPTION", hire: "2024-09-01", lastLogin: 100_000, status: "INACTIVE" });
    await ctx.db.patch(ahmed, { permissionOverrides: [{ key: "reservations.apply_discount", granted: false }] });
    const staff = [admin, salma, youssef, ahmed, fatima];
    for (const u of staff) {
      const doc = (await ctx.db.get(u))!;
      for (let i = 0; i < 12; i++) await ctx.db.insert("loginHistory", { userId: u, email: doc.email!, success: !chance(0.08), ipAddress: `196.200.${between(10, 99)}.${between(2, 250)}`, device: pick(["Desktop · macOS", "Desktop · Windows", "Mobile · Android", "Mobile · iOS", "Tablet · iOS"]), browser: pick(["Chrome", "Safari", "Firefox", "Edge"]), at: now - between(1, 40) * 86_400_000 - between(0, 86_400_000) });
    }
    const catDefs = [["ELECTRICITY", "Electricity", "zap"], ["WATER", "Water", "droplets"], ["INTERNET", "Internet", "wifi"], ["CLEANING", "Cleaning", "sparkles"], ["LAUNDRY", "Laundry", "shirt"], ["MAINTENANCE", "Maintenance", "wrench"], ["REPAIRS", "Repairs", "hammer"], ["FURNITURE", "Furniture", "armchair"], ["SUPPLIES", "Supplies", "package"], ["STAFF", "Staff", "users"], ["RENT", "Rent", "building"], ["BUILDING_FEES", "Building fees", "landmark"], ["TAXES", "Taxes", "receipt"], ["TRANSPORT", "Transport", "car"], ["OTHER", "Other", "circle-ellipsis"]];
    for (let i = 0; i < catDefs.length; i++) await ctx.db.insert("expenseCategories", { key: catDefs[i][0], name: catDefs[i][1], icon: catDefs[i][2], sortOrder: i, isActive: true });
    const aptDefs = [
      { code: "A01", name: "Anfa Sky Studio", building: "Résidence Anfa Sky", floor: "3", address: "18 Rue Ibnou Sina, Anfa", city: "Casablanca", bedrooms: 0, beds: 1, bathrooms: 1, maxGuests: 2, basePrice: 450, weekendPrice: 520, amenities: ["Wi-Fi", "Air conditioning", "Kitchen", "TV", "Elevator"] },
      { code: "A02", name: "Anfa Sky One-Bed", building: "Résidence Anfa Sky", floor: "5", address: "18 Rue Ibnou Sina, Anfa", city: "Casablanca", bedrooms: 1, beds: 2, bathrooms: 1, maxGuests: 3, basePrice: 600, weekendPrice: 690, amenities: ["Wi-Fi", "Air conditioning", "Kitchen", "Washing machine", "TV", "Balcony", "Elevator"] },
      { code: "A03", name: "Anfa Sky Family", building: "Résidence Anfa Sky", floor: "7", address: "18 Rue Ibnou Sina, Anfa", city: "Casablanca", bedrooms: 2, beds: 3, bathrooms: 2, maxGuests: 5, basePrice: 850, weekendPrice: 980, amenities: ["Wi-Fi", "Air conditioning", "Kitchen", "Washing machine", "TV", "Balcony", "Elevator", "Parking"] },
      { code: "B01", name: "Corniche Ocean View", building: "Le Corniche", floor: "9", address: "44 Boulevard de la Corniche, Ain Diab", city: "Casablanca", bedrooms: 2, beds: 2, bathrooms: 2, maxGuests: 4, basePrice: 1100, weekendPrice: 1300, amenities: ["Wi-Fi", "Air conditioning", "Kitchen", "TV", "Sea view", "Pool", "Parking", "Elevator"] },
      { code: "B02", name: "Corniche Penthouse", building: "Le Corniche", floor: "12", address: "44 Boulevard de la Corniche, Ain Diab", city: "Casablanca", bedrooms: 3, beds: 4, bathrooms: 2, maxGuests: 6, basePrice: 1600, weekendPrice: 1900, amenities: ["Wi-Fi", "Air conditioning", "Kitchen", "Washing machine", "TV", "Sea view", "Pool", "Parking", "Elevator", "Balcony"] },
      { code: "B03", name: "Corniche Garden Flat", building: "Le Corniche", floor: "1", address: "44 Boulevard de la Corniche, Ain Diab", city: "Casablanca", bedrooms: 1, beds: 1, bathrooms: 1, maxGuests: 2, basePrice: 700, weekendPrice: 800, amenities: ["Wi-Fi", "Air conditioning", "Kitchen", "TV", "Pool", "Parking"] },
      { code: "C01", name: "Maarif Loft", building: "Twin Center Residence", floor: "4", address: "7 Rue Al Moutanabi, Maarif", city: "Casablanca", bedrooms: 1, beds: 1, bathrooms: 1, maxGuests: 2, basePrice: 550, weekendPrice: 620, amenities: ["Wi-Fi", "Air conditioning", "Kitchen", "TV", "Workspace", "Elevator"] },
      { code: "C02", name: "Maarif Duplex", building: "Twin Center Residence", floor: "8", address: "7 Rue Al Moutanabi, Maarif", city: "Casablanca", bedrooms: 2, beds: 3, bathrooms: 2, maxGuests: 5, basePrice: 900, weekendPrice: 1000, amenities: ["Wi-Fi", "Air conditioning", "Kitchen", "Washing machine", "TV", "Workspace", "Elevator", "Parking"] },
      { code: "D01", name: "Gauthier Classic", building: "Immeuble Gauthier", floor: "2", address: "23 Rue Jean Jaurès, Gauthier", city: "Casablanca", bedrooms: 1, beds: 2, bathrooms: 1, maxGuests: 3, basePrice: 500, weekendPrice: 560, amenities: ["Wi-Fi", "Heating", "Kitchen", "TV", "Balcony"] },
      { code: "D02", name: "Gauthier Terrace", building: "Immeuble Gauthier", floor: "6", address: "23 Rue Jean Jaurès, Gauthier", city: "Casablanca", bedrooms: 2, beds: 2, bathrooms: 1, maxGuests: 4, basePrice: 750, weekendPrice: 850, amenities: ["Wi-Fi", "Air conditioning", "Kitchen", "Washing machine", "TV", "Balcony", "Elevator"] },
      { code: "E01", name: "Agdal Riverside", building: "Résidence Les Orangers", floor: "3", address: "5 Avenue de France, Agdal", city: "Rabat", bedrooms: 2, beds: 2, bathrooms: 1, maxGuests: 4, basePrice: 650, weekendPrice: 720, amenities: ["Wi-Fi", "Air conditioning", "Kitchen", "TV", "Parking", "Elevator"] },
      { code: "E02", name: "Hay Riad Executive", building: "Prestigia Tower", floor: "10", address: "Mahaj Riad, Hay Riad", city: "Rabat", bedrooms: 1, beds: 1, bathrooms: 1, maxGuests: 2, basePrice: 800, weekendPrice: 900, amenities: ["Wi-Fi", "Air conditioning", "Kitchen", "TV", "Workspace", "Pool", "Parking", "Elevator"] },
    ];
    const apartments: Id<"apartments">[] = [];
    for (const a of aptDefs) apartments.push(await ctx.db.insert("apartments", { ...a, status: "AVAILABLE", cleaningStatus: "CLEAN", maintenanceStatus: "OK", isActive: true, updatedAt: now }));
    const firstNames = ["Omar", "Yasmine", "Hamza", "Nadia", "Rachid", "Imane", "Mehdi", "Sofia", "Khalid", "Leila", "Anas", "Houda", "Bilal", "Samira", "Tariq", "Amina", "Reda", "Zineb", "Ismail", "Meryem", "Julien", "Claire", "Marco", "Elena", "Lucas", "Sara", "Adam", "Nour", "Ayoub", "Kenza", "Hicham", "Rim", "Othmane", "Ghita", "Walid", "Salwa", "Nabil", "Hind", "Driss", "Laila", "Thomas", "Emma", "Carlos", "Aisha", "Ibrahim"];
    const lastNames = ["Alaoui", "Bennani", "Chraibi", "Daoudi", "El Fassi", "Filali", "Guessous", "Haddad", "Idrissi", "Jabri", "Kettani", "Lahlou", "Mansouri", "Naciri", "Oufkir", "Qadiri", "Rami", "Sebti", "Tahiri", "Ziani", "Dupont", "Martin", "Rossi", "García", "Bernard", "Kabbaj", "Slaoui", "Berrada", "Tazi", "Cherkaoui", "Benkirane", "Bouazza", "Amrani", "Lamrani", "Hajji", "Skalli", "Benslimane", "Mekouar", "El Kadiri", "Zouiten", "Müller", "Dubois", "Fernández", "Khan", "Yilmaz"];
    const nationalities = ["Moroccan", "Moroccan", "Moroccan", "Moroccan", "French", "Spanish", "Italian", "German", "British", "Emirati"];
    const normName = (s: string) => s.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z\s]/g, "").split(/\s+/).filter(Boolean).sort().join(" ");
    const customers: Id<"customers">[] = [];
    for (let i = 0; i < 220; i++) {
      const firstName = firstNames[(i * 7 + Math.floor(i / firstNames.length)) % firstNames.length];
      const lastName = lastNames[(i * 11 + Math.floor(i / lastNames.length) * 3) % lastNames.length];
      const nat = nationalities[i % nationalities.length];
      const idType = nat === "Moroccan" ? "CIN" : "PASSPORT";
      const phone = `+2126${between(10000000, 99999999)}`;
      const fullName = `${firstName} ${lastName}`;
      customers.push(await ctx.db.insert("customers", { code: `CUS-${String(i + 1).padStart(4, "0")}`, firstName, lastName, fullName, nameKey: normName(fullName), phone, phoneKey: phone.replace(/[^\d]/g, "").slice(-8), email: chance(0.8) ? `${firstName.toLowerCase()}.${lastName.toLowerCase().replace(/[^a-z]/g, "")}${i >= 45 ? i : ""}@${pick(["gmail.com", "outlook.com", "yahoo.fr", "icloud.com"])}` : undefined, nationality: nat, dateOfBirth: `${between(1965, 2002)}-${String(between(1, 12)).padStart(2, "0")}-${String(between(1, 28)).padStart(2, "0")}`, idType, idNumber: idType === "CIN" ? `${pick(["BE", "BK", "BJ", "AB", "K", "EE"])}${between(100000, 999999)}` : `${pick(["P", "X", "N"])}${between(10000000, 99999999)}`, idExpiration: `${between(2027, 2033)}-${String(between(1, 12)).padStart(2, "0")}-15`, address: chance(0.6) ? `${between(2, 120)} ${pick(["Rue", "Avenue", "Boulevard"])} ${pick(["Hassan II", "Mohammed V", "Zerktouni", "Al Massira", "des FAR", "Victor Hugo"])}, ${pick(["Casablanca", "Rabat", "Marrakech", "Fès", "Tanger", "Paris", "Madrid", "Lyon"])}` : undefined, preferredLanguage: nat === "Moroccan" ? pick(["ar", "fr"]) : nat === "French" ? "fr" : "en", isBlacklisted: false, riskLevel: "NORMAL", verificationStatus: "UNVERIFIED", updatedAt: ms(addDaysKey(START, -between(10, 200))) }));
    }
    await ctx.db.insert("sequences", { name: "customer", value: customers.length });
    await ctx.db.insert("sequences", { name: "worker", value: wrk });
    return { users: { admin, salma, youssef, ahmed, fatima }, apartments, customers };
  },
});

/** Phase 2: reservations, payments, commissions, contracts, cleaning expenses for one apartment. */
export const phase2 = internalMutation({
  args: { apartmentId: v.id("apartments"), aptIndex: v.number(), users: v.any(), customers: v.array(v.id("customers")), seqs: v.object({ res: v.number(), pay: v.number(), com: v.number(), ctr: v.number() }) },
  returns: v.object({ seqs: v.object({ res: v.number(), pay: v.number(), com: v.number(), ctr: v.number() }), needDocs: v.array(v.object({ customerId: v.id("customers"), uploadedById: v.id("users"), at: v.number() })) }),
  handler: async (ctx, { apartmentId, aptIndex, users, customers, seqs }) => {
    resetRng(20260910 + aptIndex * 7919);
    const TODAY = todayKey("Africa/Casablanca");
    const apt = (await ctx.db.get(apartmentId))!;
    const { admin, salma, youssef, ahmed } = users as Record<string, Id<"users">>;
    const sources = ["AIRBNB", "AIRBNB", "AIRBNB", "BOOKING", "BOOKING", "DIRECT", "DIRECT", "PHONE", "WHATSAPP", "WHATSAPP", "WALKIN", "RETURNING", "OTHER"] as const;
    const cleaningCat = (await ctx.db.query("expenseCategories").withIndex("by_key", (q) => q.eq("key", "CLEANING")).unique())!;
    const needDocs: { customerId: Id<"customers">; uploadedById: Id<"users">; at: number }[] = [];
    const visits = new Map<string, number>();
    let cursor = addDaysKey(START, between(0, 6));
    let { res: resSeq, pay: paySeq, com: comSeq, ctr: ctrSeq } = seqs;
    while (cursor < END) {
      const nights = chance(0.5) ? between(1, 3) : chance(0.7) ? between(3, 6) : between(7, 14);
      const checkIn = cursor;
      const checkOut = addDaysKey(checkIn, nights);
      if (checkOut > END) break;
      const month = parseKey(checkIn).getUTCMonth();
      if (chance(month >= 5 && month <= 8 ? 0.15 : 0.35)) { cursor = addDaysKey(cursor, between(1, 4)); continue; }
      const customerId = chance(0.3) ? customers[between(0, 24)] : pick(customers);
      const nVisits = (visits.get(customerId) ?? 0) + 1;
      visits.set(customerId, nVisits);
      const source = nVisits > 1 && chance(0.12) ? "RETURNING" : pick(sources);
      const creator = source === "AIRBNB" || source === "BOOKING" ? pick([admin, salma, youssef]) : pick([youssef, youssef, ahmed, salma, admin]);
      const createdAt = Math.min(Date.now() - 3_600_000, ms(addDaysKey(checkIn, -between(1, 30)), between(8, 21)));
      const priced = eachNightKey(checkIn, checkOut).map((d) => ((weekdayOf(d) === 5 || weekdayOf(d) === 6) && apt.weekendPrice ? apt.weekendPrice : apt.basePrice));
      const subtotal = priced.reduce((a, b) => a + b, 0);
      const discount = chance(0.15) ? Math.round((subtotal * between(5, 10)) / 100 / 10) * 10 : 0;
      const totalAmount = subtotal - discount;
      const nightlyPrice = Math.round((subtotal / nights) * 100) / 100;
      const deposit = chance(0.7) ? pick([500, 1000, 1500]) : 0;
      let status: string;
      if (checkOut <= TODAY) status = chance(0.86) ? "CHECKED_OUT" : chance(0.7) ? "CANCELLED" : "NO_SHOW";
      else if (checkIn <= TODAY) status = "CHECKED_IN";
      else if (checkIn <= addDaysKey(TODAY, 3)) status = chance(0.9) ? "CONFIRMED" : "PENDING";
      else status = chance(0.65) ? "CONFIRMED" : chance(0.7) ? "PENDING" : chance(0.6) ? "INQUIRY" : "CANCELLED";
      const code = `RES-${++resSeq}`;
      const paidFraction = status === "CHECKED_OUT" ? 1 : status === "CHECKED_IN" ? (chance(0.75) ? 1 : 0.5) : status === "CONFIRMED" ? (chance(0.6) ? 0.3 : chance(0.5) ? 1 : 0) : 0;
      const amountPaid = Math.round(totalAmount * paidFraction);
      const adults = Math.min(apt.maxGuests, between(1, Math.max(1, apt.maxGuests - 1)));
      const children = apt.maxGuests - adults > 0 && chance(0.3) ? between(0, apt.maxGuests - adults) : 0;
      const stayIn = status === "CHECKED_IN" || status === "CHECKED_OUT";
      const checkedInAt = stayIn ? ms(checkIn, 14, between(0, 240)) : undefined;
      const checkedInById = stayIn ? pick([youssef, ahmed]) : undefined;
      const paymentMethod = amountPaid > 0 ? pick(["CASH", "CASH", "BANK_TRANSFER", "CARD"]) : undefined;
      const rid = await ctx.db.insert("reservations", { code, customerId, apartmentId, checkIn, checkOut, originalCheckIn: checkIn, originalCheckOut: checkOut, nights, adults, children, source, status, nightlyPrice, discount, totalAmount, deposit, amountPaid, paymentMethod, createdById: creator, assignedToId: chance(0.7) ? pick([youssef, ahmed]) : undefined, externalRef: source === "AIRBNB" ? `HM${between(10000000, 99999999)}` : source === "BOOKING" ? `${between(1000000000, 4000000000)}` : undefined, internalNotes: chance(0.25) ? pick(["Arriving late, after 22:00.", "Repeat guest — prefers high floor.", "Business trip, needs invoice.", "Asked for extra towels.", "Baby cot requested."]) : undefined, customerRequests: chance(0.2) ? pick(["Early check-in if possible", "Airport transfer", "Quiet apartment", "Late check-out"]) : undefined, createdAt, updatedAt: createdAt, cancelledAt: status === "CANCELLED" ? ms(addDaysKey(checkIn, -between(1, 5)), 10) : undefined, cancelReason: status === "CANCELLED" ? pick(["Change of plans", "Found cheaper option", "Trip cancelled", "No deposit received"]) : undefined, checkedInAt, checkedInById, checkedOutAt: status === "CHECKED_OUT" ? ms(checkOut, 10, between(0, 90)) : undefined, checkedOutById: status === "CHECKED_OUT" ? pick([youssef, ahmed]) : undefined, earlyCheckout: false, releasedNights: 0, recoveredNights: 0, segmentIndex: 0 });
      await ctx.db.insert("reservationHistory", { reservationId: rid, type: "CREATED", newValue: JSON.stringify({ status: "PENDING", apartment: apt.code, checkIn, checkOut, total: totalAmount }), performedById: creator, at: createdAt });
      if (status !== "PENDING" && status !== "INQUIRY") await ctx.db.insert("reservationHistory", { reservationId: rid, type: "STATUS_CHANGED", previousValue: JSON.stringify("PENDING"), newValue: JSON.stringify(status === "CANCELLED" || status === "NO_SHOW" || stayIn ? "CONFIRMED" : status), performedById: creator, at: createdAt + 3_600_000 });
      if (amountPaid > 0) {
        const parts = amountPaid === totalAmount && chance(0.4) ? [Math.round(totalAmount * 0.3), totalAmount - Math.round(totalAmount * 0.3)] : [amountPaid];
        let when = createdAt + 2 * 3_600_000;
        for (const amt of parts) {
          await ctx.db.insert("payments", { code: `PAY-${String(++paySeq).padStart(4, "0")}`, reservationId: rid, customerId, amount: amt, type: parts.length > 1 && amt === parts[0] ? "DEPOSIT" : "PAYMENT", method: paymentMethod ?? "CASH", paidAt: when, recordedById: creator });
          when = checkedInAt ?? ms(checkIn, 14);
        }
      }
      if (creator !== admin && ["CONFIRMED", "CHECKED_IN", "CHECKED_OUT", "CANCELLED"].includes(status)) {
        const age = (ms(TODAY) - createdAt) / 86_400_000;
        const cstatus = status === "CANCELLED" ? "CANCELLED" : age > 45 ? "PAID" : age > 14 ? "APPROVED" : "PENDING";
        const approved = cstatus === "APPROVED" || cstatus === "PAID";
        await ctx.db.insert("commissions", { code: `COM-${String(++comSeq).padStart(4, "0")}`, workerId: creator, reservationId: rid, amount: DEFAULT_SETTINGS.commissionAmount, triggerEvent: "RESERVATION_CONFIRMED", status: cstatus, approvedAt: approved ? createdAt + 7 * 86_400_000 : undefined, approvedById: approved ? admin : undefined, paidAt: cstatus === "PAID" ? createdAt + 35 * 86_400_000 : undefined, adminNotes: cstatus === "CANCELLED" ? "Reservation cancelled before check-in" : undefined, history: [{ from: "NONE", to: "PENDING", by: "System", at: createdAt + 3_600_000 }, ...(approved ? [{ from: "PENDING", to: "APPROVED", by: "Mohammed El Amrani", at: createdAt + 7 * 86_400_000 }] : []), ...(cstatus === "PAID" ? [{ from: "APPROVED", to: "PAID", by: "Mohammed El Amrani", at: createdAt + 35 * 86_400_000 }] : []), ...(cstatus === "CANCELLED" ? [{ from: "PENDING", to: "CANCELLED", by: "System", at: createdAt + 2 * 86_400_000, note: "Reservation cancelled" }] : [])], createdAt: createdAt + 3_600_000 });
      }
      if (stayIn) {
        const customer = (await ctx.db.get(customerId))!;
        const ccode = `CTR-${String(++ctrSeq).padStart(4, "0")}`;
        const at = ms(checkIn, 13);
        await ctx.db.insert("contracts", { code: ccode, reservationId: rid, customerId, version: 1, status: chance(0.85) ? "SIGNED" : "GENERATED", terms: DEFAULT_SETTINGS.contractTerms, generatedById: checkedInById ?? creator, signedAt: checkedInAt, at, contentHtml: renderContractHtml({ code: ccode, version: 1, businessName: DEFAULT_SETTINGS.businessName, businessAddress: DEFAULT_SETTINGS.address, businessPhone: DEFAULT_SETTINGS.contactPhone, businessEmail: DEFAULT_SETTINGS.contactEmail, customerName: customer.fullName, customerIdType: customer.idType ?? null, customerIdNumber: customer.idNumber ?? null, customerPhone: customer.phone, customerNationality: customer.nationality ?? null, customerAddress: customer.address ?? null, apartmentName: apt.name, apartmentCode: apt.code, apartmentAddress: `${apt.address}, ${apt.city}`, checkIn: parseKey(checkIn), checkOut: parseKey(checkOut), checkInTime: DEFAULT_SETTINGS.checkInTime, checkOutTime: DEFAULT_SETTINGS.checkOutTime, nights, nightlyPrice, total: totalAmount, deposit, guests: adults + children, reservationCode: code, terms: DEFAULT_SETTINGS.contractTerms, generatedAt: new Date(at), currency: DEFAULT_SETTINGS.currency }) });
        needDocs.push({ customerId, uploadedById: creator, at: createdAt });
      }
      if (status === "CHECKED_OUT") await ctx.db.insert("expenses", { code: `EXP-TMP-${code}`, date: checkOut, categoryId: cleaningCat._id, apartmentId, description: `Turnover cleaning after ${code}`, amount: apt.bedrooms >= 2 ? 150 : 100, paymentMethod: "CASH", isRecurring: false, addedById: salma, updatedAt: ms(checkOut, 12) });
      cursor = addDaysKey(checkOut, chance(0.4) ? 0 : between(1, 3));
    }
    return { seqs: { res: resSeq, pay: paySeq, com: comSeq, ctr: ctrSeq }, needDocs };
  },
});

/** Phase 3: live statuses, cleaning, inventory ledger, early check-outs, holds, external blocks, maintenance. */
export const phase3 = internalMutation({
  args: { users: v.any(), apartments: v.array(v.id("apartments")), customers: v.array(v.id("customers")), seqs: v.object({ res: v.number(), pay: v.number(), com: v.number(), ctr: v.number() }) },
  returns: v.object({ res: v.number(), pay: v.number() }),
  handler: async (ctx, { users, apartments, customers, seqs }) => {
    resetRng(777);
    const TODAY = todayKey("Africa/Casablanca");
    const { admin, salma, youssef, ahmed, fatima } = users as Record<string, Id<"users">>;
    const staffDocs = new Map<string, string>();
    for (const u of [admin, salma, youssef, ahmed, fatima]) staffDocs.set(u, (await ctx.db.get(u))?.fullName ?? "System");
    const nameOf = (id: Id<"users">) => staffDocs.get(id) ?? "System";
    const reservations = await ctx.db.query("reservations").collect();
    const aptDocs = await Promise.all(apartments.map((a) => ctx.db.get(a)));
    let resSeq = seqs.res;
    let paySeq = seqs.pay;
    for (const apt of aptDocs) {
      if (!apt) continue;
      const current = reservations.find((r) => r.apartmentId === apt._id && r.status === "CHECKED_IN");
      const outToday = reservations.find((r) => r.apartmentId === apt._id && r.status === "CHECKED_OUT" && r.checkOut === TODAY);
      const reservedToday = reservations.find((r) => r.apartmentId === apt._id && r.status === "CONFIRMED" && r.checkIn === TODAY);
      let status = "AVAILABLE";
      let cleaning = "CLEAN";
      if (current) status = "OCCUPIED";
      else if (outToday) { status = "CLEANING"; cleaning = chance(0.5) ? "NEEDS_CLEANING" : "IN_PROGRESS"; }
      else if (reservedToday) status = "RESERVED";
      await ctx.db.patch(apt._id, { status, cleaningStatus: cleaning });
      if (status === "CLEANING") await ctx.db.insert("cleaningTasks", { apartmentId: apt._id, reservationId: outToday!._id, assigneeId: fatima, status: cleaning, scheduledFor: TODAY, startedAt: cleaning === "IN_PROGRESS" ? Date.now() - 40 * 60_000 : undefined, notes: "Turnover cleaning", createdAt: Date.now() - 60 * 60_000 });
    }
    for (let i = 1; i <= 20; i++) {
      const day = addDaysKey(TODAY, -i);
      for (const r of reservations.filter((r) => r.status === "CHECKED_OUT" && r.checkOut === day)) await ctx.db.insert("cleaningTasks", { apartmentId: r.apartmentId, reservationId: r._id, assigneeId: fatima, status: "READY", scheduledFor: day, startedAt: ms(day, 11, 30), completedAt: ms(day, 13, 30), completedById: fatima, createdAt: ms(day, 11) });
    }
    const ev = async (e: { apartmentId: Id<"apartments">; action: string; startDate: string; endDate: string; nights: number; previousState?: string; newState?: string; reservationId?: Id<"reservations">; blockId?: Id<"apartmentBlocks">; source?: string | null; userId: Id<"users">; estimatedValue?: number; reason?: string; at: number }) => ctx.db.insert("inventoryEvents", { apartmentId: e.apartmentId, action: e.action, startDate: e.startDate, endDate: e.endDate, nights: e.nights, previousState: e.previousState, newState: e.newState, reservationId: e.reservationId, blockId: e.blockId, source: e.source ?? undefined, userId: e.userId, userName: nameOf(e.userId), estimatedValue: e.estimatedValue, reason: e.reason, at: e.at });
    for (const r of reservations) {
      if (r.status === "INQUIRY") continue;
      await ev({ apartmentId: r.apartmentId, action: "RESERVATION_CREATED", startDate: r.checkIn, endDate: r.checkOut, nights: r.nights, previousState: "AVAILABLE", newState: "RESERVED", reservationId: r._id, source: r.source, userId: r.createdById, estimatedValue: r.totalAmount, at: r.createdAt });
      if (r.status === "CHECKED_IN" || r.status === "CHECKED_OUT") await ev({ apartmentId: r.apartmentId, action: "CHECKIN", startDate: r.checkIn, endDate: r.checkOut, nights: r.nights, previousState: "RESERVED", newState: "OCCUPIED", reservationId: r._id, userId: r.createdById, at: ms(r.checkIn, 14) });
      if (r.status === "CHECKED_OUT") await ev({ apartmentId: r.apartmentId, action: "CHECKOUT", startDate: r.checkIn, endDate: r.checkOut, nights: r.nights, previousState: "OCCUPIED", newState: "CLEANING", reservationId: r._id, userId: r.createdById, at: ms(r.checkOut, 10, 30) });
      if (r.status === "CANCELLED") {
        const ca = ms(addDaysKey(r.checkIn, -between(1, 5)), 10);
        await ev({ apartmentId: r.apartmentId, action: "RESERVATION_CANCELLED", startDate: r.checkIn, endDate: r.checkOut, nights: r.nights, previousState: "CONFIRMED", newState: "CANCELLED", reservationId: r._id, userId: r.createdById, at: ca });
        await ev({ apartmentId: r.apartmentId, action: "RELEASED", startDate: r.checkIn, endDate: r.checkOut, nights: r.nights, previousState: "CANCELLED", newState: "AVAILABLE", reservationId: r._id, userId: r.createdById, estimatedValue: r.totalAmount, at: ca + 60_000 });
      }
    }
    // Early check-outs + recoveries
    const earlyCandidates = reservations.filter((r) => r.status === "CHECKED_OUT" && r.nights >= 3 && r.checkOut < TODAY && r.checkOut > addDaysKey(TODAY, -120)).slice(0, 40);
    let early = 0;
    for (const r of earlyCandidates) {
      if (early >= 14) break;
      if (!chance(0.4)) continue;
      early++;
      const cut = between(1, Math.min(2, r.nights - 1));
      const actual = addDaysKey(r.checkOut, -cut);
      await ctx.db.patch(r._id, { actualCheckOut: actual, earlyCheckout: true, releasedNights: cut, checkedOutAt: ms(actual, 11) });
      await ctx.db.insert("reservationHistory", { reservationId: r._id, type: "CHECKED_OUT", previousValue: JSON.stringify({ plannedCheckOut: r.checkOut }), newValue: JSON.stringify({ actualCheckOut: actual, releasedNights: cut, policy: "AFTER_CLEANING" }), reason: `Early check-out · ${cut} night${cut > 1 ? "s" : ""} released`, performedById: pick([youssef, ahmed]), at: ms(actual, 11) });
      await ev({ apartmentId: r.apartmentId, action: "EARLY_CHECKOUT", startDate: r.checkIn, endDate: actual, nights: r.nights - cut, previousState: "OCCUPIED", newState: "CLEANING", reservationId: r._id, userId: youssef, at: ms(actual, 11) });
      await ev({ apartmentId: r.apartmentId, action: "HOLD_CREATED", startDate: actual, endDate: r.checkOut, nights: cut, previousState: "EARLY_CHECKOUT", newState: "HOLD:AFTER_CLEANING", reservationId: r._id, userId: youssef, estimatedValue: cut * r.nightlyPrice, at: ms(actual, 11) + 1000 });
      await ev({ apartmentId: r.apartmentId, action: "HOLD_RELEASED", startDate: actual, endDate: r.checkOut, nights: cut, previousState: "HOLD", newState: "AVAILABLE", reservationId: r._id, userId: fatima, estimatedValue: cut * r.nightlyPrice, at: ms(actual, 13, 30) });
      const taken = reservations.some((o) => o.apartmentId === r.apartmentId && o._id !== r._id && ["CONFIRMED", "CHECKED_IN", "CHECKED_OUT"].includes(o.status) && o.checkIn < r.checkOut && o.checkOut > actual);
      if (!taken && chance(0.6)) {
        const apt = aptDocs.find((a) => a?._id === r.apartmentId)!;
        const customerId = pick(customers);
        const creator = pick([youssef, ahmed, salma]);
        const createdAt = ms(actual, 15);
        const total = cut * apt.basePrice;
        const rb = await ctx.db.insert("reservations", { code: `RES-${++resSeq}`, customerId, apartmentId: apt._id, checkIn: actual, checkOut: r.checkOut, originalCheckIn: actual, originalCheckOut: r.checkOut, nights: cut, adults: Math.min(2, apt.maxGuests), children: 0, source: pick(["WHATSAPP", "DIRECT", "PHONE", "WALKIN"]), status: "CHECKED_OUT", nightlyPrice: apt.basePrice, discount: 0, totalAmount: total, deposit: 0, amountPaid: total, paymentMethod: "CASH", createdById: creator, recoveredNights: cut, recoveredFromReservationId: r._id, internalNotes: "Last-minute booking on released nights", createdAt, updatedAt: createdAt, checkedInAt: ms(actual, 16), checkedInById: creator, checkedOutAt: ms(r.checkOut, 10), checkedOutById: creator, earlyCheckout: false, releasedNights: 0, segmentIndex: 0 });
        await ctx.db.insert("reservationHistory", { reservationId: rb, type: "CREATED", newValue: JSON.stringify({ status: "CONFIRMED", apartment: apt.code, checkIn: actual, checkOut: r.checkOut, total, recoveredNights: cut }), performedById: creator, at: createdAt });
        await ctx.db.insert("payments", { code: `PAY-${String(++paySeq).padStart(4, "0")}`, reservationId: rb, customerId, amount: total, type: "PAYMENT", method: "CASH", paidAt: ms(actual, 16), recordedById: creator });
        await ev({ apartmentId: apt._id, action: "RESERVATION_CREATED", startDate: actual, endDate: r.checkOut, nights: cut, previousState: "AVAILABLE", newState: "RESERVED", reservationId: rb, userId: creator, estimatedValue: total, at: createdAt });
        await ev({ apartmentId: apt._id, action: "REBOOKED", startDate: actual, endDate: r.checkOut, nights: cut, previousState: "RELEASED", newState: "RESERVED", reservationId: rb, userId: creator, estimatedValue: total, reason: `${cut} released night${cut > 1 ? "s" : ""} re-sold`, at: createdAt + 1000 });
        await ev({ apartmentId: apt._id, action: "CHECKIN", startDate: actual, endDate: r.checkOut, nights: cut, previousState: "RESERVED", newState: "OCCUPIED", reservationId: rb, userId: creator, at: ms(actual, 16) });
        await ev({ apartmentId: apt._id, action: "CHECKOUT", startDate: actual, endDate: r.checkOut, nights: cut, previousState: "OCCUPIED", newState: "CLEANING", reservationId: rb, userId: creator, at: ms(r.checkOut, 10) });
      }
    }
    // Holds awaiting approval
    for (const r of reservations.filter((r) => r.status === "CANCELLED" && r.checkIn > TODAY && r.checkIn < addDaysKey(TODAY, 21)).slice(0, 2)) {
      const hold = await ctx.db.insert("apartmentBlocks", { apartmentId: r.apartmentId, startDate: r.checkIn, endDate: r.checkOut, type: "HOLD", source: "HOLD", reason: "Awaiting admin approval", reservationId: r._id, pendingApproval: true, releaseOnCleaning: false, createdById: youssef, notes: `Cancellation of ${r.code}`, createdAt: ms(addDaysKey(TODAY, -1), 9) });
      await ev({ apartmentId: r.apartmentId, action: "HOLD_CREATED", startDate: r.checkIn, endDate: r.checkOut, nights: r.nights, previousState: "CANCELLED", newState: "HOLD:APPROVAL", reservationId: r._id, blockId: hold, userId: youssef, estimatedValue: r.totalAmount, at: ms(addDaysKey(TODAY, -1), 9) });
    }
    const cancelOverlap = async (apartmentId: Id<"apartments">, start: string, end: string, reason: string, daysAgo: number) => {
      for (const r of reservations.filter((r) => r.apartmentId === apartmentId && r.checkIn < end && r.checkOut > start && ["CONFIRMED", "PENDING", "INQUIRY"].includes(r.status))) { await ctx.db.patch(r._id, { status: "CANCELLED", cancelReason: reason, cancelledAt: ms(addDaysKey(TODAY, -daysAgo), 10) }); r.status = "CANCELLED"; }
    };
    const byCode = (code: string) => aptDocs.find((a) => a?.code === code)!;
    const extDefs = [
      { code: "A02", source: "AIRBNB", start: 6, nights: 4, guest: "Laura Bianchi", ref: "HMA8K2QT9", amount: 2760 },
      { code: "D01", source: "BOOKING", start: 9, nights: 3, guest: "Peter Schmidt", ref: "3921047788", amount: 1500 },
      { code: "B02", source: "EXPEDIA", start: 15, nights: 5, guest: "Ana Pereira", ref: "EXP-7721A", amount: 8000 },
      { code: "C02", source: "AIRBNB", start: 27, nights: 2, guest: "Karim Haddadi", ref: "HMTQ4PLZ1", amount: 1800 },
      { code: "E01", source: "OWNER", start: 34, nights: 3, guest: "", ref: "", amount: 0 },
    ];
    for (const e of extDefs) {
      const apt = byCode(e.code);
      const start = addDaysKey(TODAY, e.start);
      const end = addDaysKey(start, e.nights);
      await cancelOverlap(apt._id, start, end, `Guest booked through ${e.source.toLowerCase()} instead`, 3);
      const reason = e.source === "OWNER" ? "Owner personal use" : `${e.source === "AIRBNB" ? "Airbnb" : e.source === "BOOKING" ? "Booking.com" : "Expedia"} reservation`;
      const createdAt = ms(addDaysKey(TODAY, -between(1, 6)), 12);
      const b = await ctx.db.insert("apartmentBlocks", { apartmentId: apt._id, startDate: start, endDate: end, type: e.source === "OWNER" ? "MANUAL" : "EXTERNAL", source: e.source, reason, externalRef: e.ref || undefined, guestName: e.guest || undefined, amount: e.amount || undefined, pendingApproval: false, releaseOnCleaning: false, createdById: admin, createdAt });
      await ev({ apartmentId: apt._id, action: e.source === "OWNER" ? "BLOCKED" : "EXTERNAL_BLOCKED", startDate: start, endDate: end, nights: e.nights, previousState: "AVAILABLE", newState: `BLOCKED:${e.source}`, source: e.source, blockId: b, userId: admin, estimatedValue: e.amount || e.nights * apt.basePrice, reason, at: createdAt });
    }
    // Maintenance
    const e02 = byCode("E02");
    const now = Date.now();
    const mnt1 = await ctx.db.insert("maintenanceTickets", { code: "MNT-0001", apartmentId: e02._id, title: "Water heater replacement", category: "PLUMBING", priority: "HIGH", description: "Water heater leaking; replacement ordered.", assigneeId: fatima, reportedById: youssef, cost: 3200, status: "IN_PROGRESS", blocksApartment: true, startDate: addDaysKey(TODAY, 12), createdAt: now - 2 * 86_400_000, updatedAt: now - 2 * 86_400_000 });
    const mb = await ctx.db.insert("apartmentBlocks", { apartmentId: e02._id, startDate: addDaysKey(TODAY, 12), endDate: addDaysKey(TODAY, 15), reason: "Water heater replacement", type: "MAINTENANCE", source: "MAINTENANCE", maintenanceId: mnt1, pendingApproval: false, releaseOnCleaning: false, createdById: youssef, createdAt: now - 2 * 86_400_000 });
    await ctx.db.patch(mnt1, { blockId: mb });
    await ctx.db.patch(e02._id, { maintenanceStatus: "ISSUE" });
    await cancelOverlap(e02._id, addDaysKey(TODAY, 12), addDaysKey(TODAY, 15), "Apartment blocked for maintenance", 2);
    const b01 = byCode("B01"); const c02 = byCode("C02"); const a03 = byCode("A03"); const c01 = byCode("C01");
    await ctx.db.patch(b01._id, { maintenanceStatus: "ISSUE" });
    const mnts = [
      { code: "MNT-0002", apartmentId: b01._id, title: "AC remote not working", category: "HVAC", priority: "MEDIUM", description: "Guest reported remote unresponsive; batteries replaced, still faulty.", assigneeId: fatima, reportedById: ahmed, cost: 150, status: "REPORTED", daysAgo: 1 },
      { code: "MNT-0003", apartmentId: c02._id, title: "Balcony door lock", category: "STRUCTURAL", priority: "LOW", description: "Lock sticks, needs lubrication or replacement.", assigneeId: undefined, reportedById: youssef, cost: 0, status: "WAITING", daysAgo: 6 },
      { code: "MNT-0004", apartmentId: a03._id, title: "Washing machine drainage", category: "APPLIANCE", priority: "HIGH", description: "Water not draining fully.", assigneeId: salma, reportedById: fatima, cost: 450, status: "COMPLETED", startDate: addDaysKey(TODAY, -9), completionDate: addDaysKey(TODAY, -7), daysAgo: 10 },
      { code: "MNT-0005", apartmentId: c01._id, title: "Replace kitchen faucet", category: "PLUMBING", priority: "MEDIUM", description: "Dripping faucet.", assigneeId: fatima, reportedById: salma, cost: 380, status: "COMPLETED", startDate: addDaysKey(TODAY, -20), completionDate: addDaysKey(TODAY, -19), daysAgo: 21 },
    ];
    for (const m of mnts) { const { daysAgo, ...rest } = m; await ctx.db.insert("maintenanceTickets", { ...rest, blocksApartment: false, createdAt: now - daysAgo * 86_400_000, updatedAt: now - daysAgo * 86_400_000 }); }
    await ctx.db.insert("sequences", { name: "maintenance", value: 5 });
    await ctx.db.insert("apartmentBlocks", { apartmentId: c01._id, startDate: addDaysKey(TODAY, 20), endDate: addDaysKey(TODAY, 24), reason: "Owner personal use", type: "MANUAL", source: "OWNER", pendingApproval: false, releaseOnCleaning: false, createdById: admin, createdAt: now - 86_400_000 });
    await cancelOverlap(c01._id, addDaysKey(TODAY, 20), addDaysKey(TODAY, 24), "Owner blocked dates", 1);
    console.log(`inventory: early check-outs=${early}`);
    return { res: resSeq, pay: paySeq };
  },
});

/** Phase 4: expenses, tasks, notes, risk & incidents, merge, sequences. */
export const phase4 = internalMutation({
  args: { users: v.any(), apartments: v.array(v.id("apartments")), customers: v.array(v.id("customers")), seqs: v.object({ res: v.number(), pay: v.number(), com: v.number(), ctr: v.number() }) },
  returns: v.null(),
  handler: async (ctx, { users, apartments, customers, seqs }) => {
    resetRng(4242);
    const TODAY = todayKey("Africa/Casablanca");
    const { admin, salma, youssef, ahmed, fatima } = users as Record<string, Id<"users">>;
    const staff = [admin, salma, youssef, ahmed, fatima];
    const aptDocs = (await Promise.all(apartments.map((a) => ctx.db.get(a)))).filter((a): a is NonNullable<typeof a> => !!a);
    const cats: Record<string, Id<"expenseCategories">> = {};
    for (const c of await ctx.db.query("expenseCategories").collect()) cats[c.key] = c._id;
    let expSeq = 0;
    const exp = async (d: { date: string; categoryId: Id<"expenseCategories">; apartmentId?: Id<"apartments">; description: string; amount: number; paymentMethod: string; vendor?: string; isRecurring?: boolean; addedById: Id<"users"> }) => ctx.db.insert("expenses", { code: `EXP-${String(++expSeq).padStart(4, "0")}`, date: d.date, categoryId: d.categoryId, apartmentId: d.apartmentId, description: d.description, amount: d.amount, paymentMethod: d.paymentMethod, vendor: d.vendor, isRecurring: !!d.isRecurring, recurrence: d.isRecurring ? "MONTHLY" : undefined, addedById: d.addedById, updatedAt: ms(d.date, 9) });
    for (let m = 0; m <= 9; m++) {
      const mStart = `2026-${String(m + 1).padStart(2, "0")}-01`;
      if (mStart > TODAY) break;
      for (const apt of aptDocs) {
        await exp({ date: addDaysKey(mStart, 4), categoryId: cats.ELECTRICITY, apartmentId: apt._id, description: `Electricity — ${apt.code}`, amount: between(180, 520), paymentMethod: "BANK_TRANSFER", vendor: "Lydec", isRecurring: true, addedById: admin });
        await exp({ date: addDaysKey(mStart, 5), categoryId: cats.WATER, apartmentId: apt._id, description: `Water — ${apt.code}`, amount: between(60, 160), paymentMethod: "BANK_TRANSFER", vendor: "Lydec", isRecurring: true, addedById: admin });
        await exp({ date: addDaysKey(mStart, 2), categoryId: cats.INTERNET, apartmentId: apt._id, description: `Fibre internet — ${apt.code}`, amount: 249, paymentMethod: "BANK_TRANSFER", vendor: "Orange", isRecurring: true, addedById: admin });
        await exp({ date: addDaysKey(mStart, 1), categoryId: cats.BUILDING_FEES, apartmentId: apt._id, description: `Syndic fees — ${apt.code}`, amount: (apt.building ?? "").includes("Corniche") ? 600 : 350, paymentMethod: "BANK_TRANSFER", isRecurring: true, addedById: admin });
      }
      await exp({ date: addDaysKey(mStart, 27), categoryId: cats.STAFF, description: "Salaries — reception & housekeeping", amount: 14500, paymentMethod: "BANK_TRANSFER", isRecurring: true, addedById: admin });
      await exp({ date: addDaysKey(mStart, between(6, 24)), categoryId: cats.LAUNDRY, description: "Linen & towel laundry", amount: between(900, 1900), paymentMethod: "CASH", vendor: "Pressing Al Amal", addedById: salma });
      await exp({ date: addDaysKey(mStart, between(6, 24)), categoryId: cats.SUPPLIES, description: "Toiletries, coffee, cleaning supplies", amount: between(400, 1200), paymentMethod: "CARD", vendor: "Marjane", addedById: salma });
      if (chance(0.5)) await exp({ date: addDaysKey(mStart, between(3, 26)), categoryId: pick([cats.REPAIRS, cats.FURNITURE, cats.MAINTENANCE, cats.TRANSPORT]), apartmentId: pick(aptDocs)._id, description: pick(["Replace bedside lamps", "Plumber call-out", "New mattress protector set", "Taxi for key handover", "Paint touch-up", "Replace shower head"]), amount: between(150, 2400), paymentMethod: pick(["CASH", "CARD"]), addedById: pick([admin, salma]) });
    }
    const tmp = (await ctx.db.query("expenses").collect()).filter((e) => e.code.startsWith("EXP-TMP-")).sort((a, b) => a.date.localeCompare(b.date));
    for (const e of tmp) await ctx.db.patch(e._id, { code: `EXP-${String(++expSeq).padStart(4, "0")}` });
    for (const [name, value] of [["reservation", seqs.res], ["payment", seqs.pay], ["commission", seqs.com], ["contract", seqs.ctr], ["expense", expSeq]] as const) await ctx.db.insert("sequences", { name, value });
    // Tasks
    const reservations = await ctx.db.query("reservations").collect();
    const todayIns = reservations.filter((r) => r.checkIn === TODAY && r.status !== "CANCELLED");
    const todayOuts = reservations.filter((r) => r.checkOut === TODAY && (r.status === "CHECKED_IN" || r.status === "CHECKED_OUT"));
    const now = Date.now();
    const task = async (t: { title: string; type: string; apartmentId?: Id<"apartments">; reservationId?: Id<"reservations">; customerId?: Id<"customers">; assigneeId?: Id<"users">; createdById: Id<"users">; priority: string; dueDate?: string; dueTime?: string; status: string; completedAt?: number }) => ctx.db.insert("tasks", { ...t, createdAt: now - between(1, 48) * 3_600_000, updatedAt: now - between(0, 60) * 60_000 });
    for (const r of todayIns.slice(0, 4)) await task({ title: `Check-in ${r.code}`, type: "CHECK_IN", apartmentId: r.apartmentId, reservationId: r._id, customerId: r.customerId, assigneeId: pick([youssef, ahmed]), createdById: salma, priority: "HIGH", dueDate: TODAY, dueTime: pick(["14:00", "15:00", "16:30", "19:00"]), status: "TODO" });
    for (const r of todayOuts.slice(0, 3)) await task({ title: `Check-out ${r.code}`, type: "CHECK_OUT", apartmentId: r.apartmentId, reservationId: r._id, customerId: r.customerId, assigneeId: pick([youssef, ahmed]), createdById: salma, priority: "MEDIUM", dueDate: TODAY, dueTime: "11:00", status: r.status === "CHECKED_OUT" ? "COMPLETED" : "TODO", completedAt: r.status === "CHECKED_OUT" ? now : undefined });
    const b01 = aptDocs.find((a) => a.code === "B01")!;
    await task({ title: "Deep clean B02 before VIP arrival", type: "CLEANING", apartmentId: aptDocs[4]._id, assigneeId: fatima, createdById: salma, priority: "HIGH", dueDate: addDaysKey(TODAY, 1), dueTime: "09:00", status: "TODO" });
    await task({ title: "Collect remaining balance", type: "PAYMENT_COLLECTION", reservationId: reservations.find((r) => r.status === "CHECKED_IN" && r.amountPaid < r.totalAmount)?._id, assigneeId: youssef, createdById: admin, priority: "HIGH", dueDate: TODAY, dueTime: "18:00", status: "IN_PROGRESS" });
    await task({ title: "Verify passport copy for upcoming guest", type: "DOCUMENT_VERIFICATION", customerId: customers[7], assigneeId: ahmed, createdById: salma, priority: "MEDIUM", dueDate: addDaysKey(TODAY, 1), status: "TODO" });
    await task({ title: "Monthly inspection — Corniche building", type: "INSPECTION", apartmentId: b01._id, assigneeId: salma, createdById: admin, priority: "LOW", dueDate: addDaysKey(TODAY, 4), status: "TODO" });
    await task({ title: "Buy replacement kettle for A01", type: "GENERAL", apartmentId: aptDocs[0]._id, assigneeId: salma, createdById: youssef, priority: "LOW", dueDate: addDaysKey(TODAY, -1), status: "COMPLETED", completedAt: now - 86_400_000 });
    await task({ title: "Guest asks for extra pillows — D02", type: "CUSTOMER_REQUEST", apartmentId: aptDocs[9]._id, assigneeId: fatima, createdById: ahmed, priority: "MEDIUM", dueDate: TODAY, dueTime: "16:00", status: "TODO" });
    await task({ title: "Fix AC remote B01", type: "MAINTENANCE", apartmentId: b01._id, assigneeId: fatima, createdById: ahmed, priority: "MEDIUM", dueDate: addDaysKey(TODAY, 2), status: "TODO" });
    await task({ title: "Prepare welcome kit for returning guest", type: "GENERAL", assigneeId: youssef, createdById: salma, priority: "LOW", dueDate: addDaysKey(TODAY, -3), status: "CANCELLED" });
    // Notes
    for (let i = 0; i < 12; i++) await ctx.db.insert("customerNotes", { customerId: customers[i * 3], authorId: pick(staff), body: pick(["Very tidy guest, left apartment in perfect condition.", "Prefers to pay cash at check-in.", "Asked about monthly rates for winter.", "Travels with a small dog — approved for B03 only.", "Speaks French and Spanish.", "VIP — corporate account, always invoice."]), at: now - between(3, 120) * 86_400_000 });
    // Verification for customers with ID docs
    const withDocs = new Set((await ctx.db.query("documents").collect()).filter((d) => d.customerId).map((d) => d.customerId!));
    for (const cid of withDocs) { const verified = chance(0.9); await ctx.db.patch(cid, { verificationStatus: verified ? "VERIFIED" : "PENDING", verifiedAt: verified ? now - between(5, 200) * 86_400_000 : undefined }); }
    // Risk & incidents
    let incSeq = 0;
    const riskDefs: { idx: number; level: string; reason: string; incidents: { type: string; severity: string; title: string; description: string; amount?: number; daysAgo: number; resolved?: boolean }[] }[] = [
      { idx: 3, level: "WATCHLIST", reason: "Two cancellations in the last three months.", incidents: [{ type: "REPEATED_CANCELLATION", severity: "LOW", title: "Cancelled twice within 3 months", description: "Both cancellations happened less than 48 h before arrival.", daysAgo: 22 }] },
      { idx: 9, level: "HIGH_ATTENTION", reason: "Left an unpaid balance in June; deposit required before check-in.", incidents: [{ type: "UNPAID_BALANCE", severity: "HIGH", title: "Unpaid balance of 600 MAD", description: "Guest left before settling the last night. Two reminders sent by WhatsApp.", amount: 600, daysAgo: 95 }] },
      { idx: 15, level: "RESTRICTED", reason: "Damage to furniture in B02 — deposit retained, repair cost exceeded deposit.", incidents: [{ type: "DAMAGE", severity: "HIGH", title: "Broken sofa and stained carpet", description: "Housekeeping reported damage at check-out. Repair invoice 1,850 MAD, deposit 1,000 MAD retained.", amount: 1850, daysAgo: 60 }, { type: "LATE_CHECKOUT", severity: "LOW", title: "Left 3 hours after check-out time", description: "Next guest delayed.", daysAgo: 61, resolved: true }] },
      { idx: 21, level: "BLOCKED", reason: "ID document did not match the guest; reservation refused by admin.", incidents: [{ type: "DOCUMENT_CONCERN", severity: "HIGH", title: "Passport number mismatch", description: "Presented passport belonged to a different person. Check-in refused.", daysAgo: 130 }] },
      { idx: 27, level: "WATCHLIST", reason: "Repeated late check-outs.", incidents: [{ type: "LATE_CHECKOUT", severity: "LOW", title: "Late check-out (2 h)", description: "Apologised, no dispute.", daysAgo: 40, resolved: true }, { type: "LATE_CHECKOUT", severity: "LOW", title: "Late check-out (1 h 30)", description: "", daysAgo: 12 }] },
      { idx: 33, level: "HIGH_ATTENTION", reason: "Serious noise complaint from the building syndic.", incidents: [{ type: "COMPLAINT", severity: "MEDIUM", title: "Noise complaint from neighbours", description: "Syndic of Le Corniche called at 01:30. Guest warned by phone.", daysAgo: 18 }] },
      { idx: 48, level: "NORMAL", reason: "", incidents: [{ type: "NO_SHOW", severity: "MEDIUM", title: "No-show without notice", description: "Deposit of 500 MAD kept as per policy.", amount: 500, daysAgo: 75, resolved: true }] },
    ];
    for (const rd of riskDefs) {
      const cid = customers[rd.idx];
      const lastRes = reservations.filter((r) => r.customerId === cid).sort((a, b) => b.checkIn.localeCompare(a.checkIn))[0];
      if (rd.level !== "NORMAL") await ctx.db.patch(cid, { riskLevel: rd.level, riskReason: rd.reason, riskSetAt: now - (Math.min(...rd.incidents.map((i) => i.daysAgo)) - 1) * 86_400_000, riskSetById: admin, isBlacklisted: rd.level === "BLOCKED" });
      for (const inc of rd.incidents) await ctx.db.insert("customerIncidents", { code: `INC-${String(++incSeq).padStart(4, "0")}`, customerId: cid, reservationId: lastRes?._id, type: inc.type, severity: inc.severity, title: inc.title, description: inc.description || undefined, amount: inc.amount, occurredAt: now - inc.daysAgo * 86_400_000, reportedById: pick([youssef, ahmed, salma]), resolvedAt: inc.resolved ? now - (inc.daysAgo - 2) * 86_400_000 : undefined, resolution: inc.resolved ? "Settled with the guest." : undefined });
    }
    await ctx.db.insert("sequences", { name: "incident", value: incSeq });
    await ctx.db.patch(customers[210], { mergedIntoId: customers[12], deletedAt: now - 9 * 86_400_000 });
    return null;
  },
});

/** Phase 5: audit narrative (EVT codes) and notifications. */
export const phase5 = internalMutation({
  args: { users: v.any(), apartments: v.array(v.id("apartments")) },
  returns: v.null(),
  handler: async (ctx, { users, apartments }) => {
    resetRng(99);
    const TODAY = todayKey("Africa/Casablanca");
    const { admin, salma, youssef, ahmed, fatima } = users as Record<string, Id<"users">>;
    const staff = new Map<string, { fullName: string; roleKey: string }>();
    for (const [id, role] of [[admin, "ADMIN"], [salma, "MANAGER"], [youssef, "RECEPTION"], [ahmed, "RECEPTION"], [fatima, "CLEANER"]] as const) staff.set(id, { fullName: (await ctx.db.get(id))?.fullName ?? "", roleKey: role });
    const apts = (await Promise.all(apartments.map((a) => ctx.db.get(a)))).filter((a): a is NonNullable<typeof a> => !!a);
    const aptCode = (id: Id<"apartments">) => apts.find((a) => a._id === id)?.code ?? "";
    const reservations = await ctx.db.query("reservations").collect();
    const now = Date.now();
    type A = { action: string; module: string; userId: Id<"users">; entityType?: string; entityId?: string; entityLabel?: string; previousValue?: unknown; newValue?: unknown; reason?: string; reservationId?: Id<"reservations">; apartmentId?: Id<"apartments">; customerId?: Id<"customers">; device?: string; browser?: string; ipAddress?: string; severity?: string; at: number };
    const aud: A[] = [];
    for (const r of reservations) {
      aud.push({ action: "RESERVATION_CREATED", module: "reservations", userId: r.createdById, entityType: "reservation", entityId: r._id, entityLabel: r.code, newValue: { apartment: aptCode(r.apartmentId), total: r.totalAmount }, reservationId: r._id, apartmentId: r.apartmentId, customerId: r.customerId, device: pick(["Desktop · macOS", "Mobile · Android"]), browser: "Chrome", ipAddress: "196.200.45.12", at: r.createdAt });
      if (r.status === "CANCELLED") aud.push({ action: "RESERVATION_CANCELLED", module: "reservations", userId: r.createdById, entityType: "reservation", entityId: r._id, entityLabel: r.code, previousValue: "CONFIRMED", newValue: "CANCELLED", reason: r.cancelReason ?? undefined, reservationId: r._id, apartmentId: r.apartmentId, customerId: r.customerId, severity: "WARNING", at: r.cancelledAt ?? ms(addDaysKey(r.checkIn, -2), 10) });
    }
    const moved = reservations.find((r) => r.status === "CHECKED_IN" && r.nights >= 3);
    if (moved) {
      const other = apts.find((a) => a._id !== moved.apartmentId && a.status === "AVAILABLE" && a.maxGuests >= 2);
      if (other) {
        await ctx.db.insert("reservationHistory", { reservationId: moved._id, type: "APARTMENT_CHANGED", fromApartmentId: other._id, toApartmentId: moved.apartmentId, previousValue: JSON.stringify({ apartment: other.code }), newValue: JSON.stringify({ apartment: aptCode(moved.apartmentId) }), priceDifference: 0, reason: "Guest requested higher floor", performedById: salma, at: now - 5 * 3_600_000 });
        aud.push({ action: "APARTMENT_CHANGED", module: "reservations", userId: salma, entityType: "reservation", entityId: moved._id, entityLabel: moved.code, previousValue: { apartment: other.code }, newValue: { apartment: aptCode(moved.apartmentId) }, reason: "Guest requested higher floor", reservationId: moved._id, apartmentId: moved.apartmentId, customerId: moved.customerId, device: "Desktop · macOS", browser: "Safari", at: now - 5 * 3_600_000 });
      }
    }
    const priced = reservations.find((r) => r.status === "CONFIRMED" && r.checkIn > TODAY);
    if (priced) aud.push({ action: "PRICE_CHANGED", module: "reservations", userId: ahmed, entityType: "reservation", entityId: priced._id, entityLabel: priced.code, previousValue: { nightlyPrice: priced.nightlyPrice + 50 }, newValue: { nightlyPrice: priced.nightlyPrice }, reason: "Negotiated with guest", reservationId: priced._id, apartmentId: priced.apartmentId, customerId: priced.customerId, device: "Mobile · Android", browser: "Chrome", severity: "WARNING", at: now - 2 * 3_600_000 });
    aud.push(
      { action: "LOGIN", module: "auth", userId: salma, entityType: "user", entityId: salma, entityLabel: staff.get(salma)!.fullName, device: "Desktop · macOS", browser: "Safari", ipAddress: "196.200.45.88", at: now - 2 * 60_000 },
      { action: "LOGIN", module: "auth", userId: youssef, entityType: "user", entityId: youssef, entityLabel: staff.get(youssef)!.fullName, device: "Mobile · Android", browser: "Chrome", ipAddress: "196.200.45.12", at: now - 35 * 60_000 },
      { action: "CLEANING_UPDATED", module: "cleaning", userId: fatima, entityType: "apartment", entityId: apts[2]._id, entityLabel: "A03", previousValue: "IN_PROGRESS", newValue: "READY", apartmentId: apts[2]._id, device: "Mobile · Android", browser: "Chrome", at: now - 50 * 60_000 },
      { action: "EXPENSE_ADDED", module: "expenses", userId: salma, entityType: "expense", entityLabel: "Linen & towel laundry", newValue: { amount: 1450 }, device: "Desktop · macOS", browser: "Safari", at: now - 3 * 3_600_000 },
      { action: "PERMISSIONS_CHANGED", module: "workers", userId: admin, entityType: "user", entityId: ahmed, entityLabel: staff.get(ahmed)!.fullName, previousValue: { "reservations.apply_discount": true }, newValue: { "reservations.apply_discount": false }, reason: "Discounts must go through manager", device: "Desktop · macOS", browser: "Chrome", severity: "WARNING", at: now - 4 * 86_400_000 }
    );
    aud.sort((a, b) => a.at - b.at);
    let evt = 0;
    for (const a of aud) { const s = staff.get(a.userId) ?? { fullName: "System", roleKey: "" }; await ctx.db.insert("auditLog", { code: `EVT-${String(++evt).padStart(6, "0")}`, action: a.action, module: a.module, severity: a.severity ?? "INFO", userId: a.userId, userName: s.fullName, roleKey: s.roleKey, entityType: a.entityType, entityId: a.entityId, entityLabel: a.entityLabel, previousValue: a.previousValue === undefined ? undefined : JSON.stringify(a.previousValue), newValue: a.newValue === undefined ? undefined : JSON.stringify(a.newValue), reason: a.reason, reservationId: a.reservationId, apartmentId: a.apartmentId, customerId: a.customerId, device: a.device, browser: a.browser, ipAddress: a.ipAddress, at: a.at }); }
    await ctx.db.insert("sequences", { name: "event", value: evt });
    const docCount = (await ctx.db.query("documents").collect()).length;
    await ctx.db.insert("sequences", { name: "document", value: docCount });
    // Notifications
    const todayIns = reservations.filter((r) => r.checkIn === TODAY && r.status !== "CANCELLED");
    const n = async (x: { userId: Id<"users">; type: string; title: string; body: string; priority: string; href?: string; entityType?: string; entityId?: string; at: number; readAt?: number }) => ctx.db.insert("notifications", x);
    for (const r of reservations.filter((r) => r.createdAt > now - 3 * 86_400_000).slice(0, 5)) await n({ userId: admin, type: "NEW_RESERVATION", title: "New reservation", body: `${staff.get(r.createdById)?.fullName ?? "Staff"} created ${r.code} for ${aptCode(r.apartmentId)}.`, priority: "NORMAL", entityType: "reservation", entityId: r._id, href: `/reservations/${r._id}`, at: r.createdAt });
    for (const r of todayIns.slice(0, 3)) for (const u of [admin, youssef]) await n({ userId: u, type: "UPCOMING_CHECKIN", title: "Check-in today", body: `${r.code} arrives today at ${aptCode(r.apartmentId)}.`, priority: "NORMAL", entityType: "reservation", entityId: r._id, href: `/reservations/${r._id}`, at: now - 6 * 3_600_000 });
    if (priced) await n({ userId: admin, type: "PRICE_CHANGED", title: "Price changed by worker", body: `Ahmed Berrada lowered the nightly price of ${priced.code} by 50 MAD.`, priority: "HIGH", entityType: "reservation", entityId: priced._id, href: `/reservations/${priced._id}`, at: now - 2 * 3_600_000 });
    await n({ userId: admin, type: "MAINTENANCE_ALERT", title: "Maintenance reported", body: "AC remote not working in B01 (MNT-0002).", priority: "HIGH", entityType: "maintenance", href: "/maintenance", at: now - 86_400_000 });
    await n({ userId: fatima, type: "APARTMENT_NEEDS_CLEANING", title: "Apartment needs cleaning", body: "Check-out completed — turnover cleaning required today.", priority: "HIGH", href: "/cleaning", at: now - 90 * 60_000 });
    await n({ userId: youssef, type: "COMMISSION_APPROVED", title: "Commission approved", body: "3 commissions were approved (150 MAD).", priority: "NORMAL", href: "/my-commission", at: now - 2 * 86_400_000, readAt: now - 86_400_000 });
    await n({ userId: admin, type: "SECURITY_ALERT", title: "Failed login attempts", body: "3 failed login attempts for ahmed@locajour.ma from a new device.", priority: "CRITICAL", href: "/security", at: now - 86_400_000, readAt: now - 86_400_000 });
    await n({ userId: admin, type: "NEW_EXPENSE", title: "New expense", body: "Salma added Linen & towel laundry — 1,450.00 MAD.", priority: "LOW", href: "/expenses", at: now - 3 * 3_600_000, readAt: now - 2 * 3_600_000 });
    return null;
  },
});

export const apartmentMeta = internalQuery({
  args: { ids: v.array(v.id("apartments")) },
  returns: v.array(v.object({ id: v.id("apartments"), code: v.string(), name: v.string(), city: v.string(), bedrooms: v.number() })),
  handler: async (ctx, { ids }) => (await Promise.all(ids.map((id) => ctx.db.get(id)))).filter((a): a is NonNullable<typeof a> => !!a).map((a) => ({ id: a._id, code: a.code, name: a.name, city: a.city, bedrooms: a.bedrooms })),
});

export const customerCode = internalQuery({
  args: { id: v.id("customers") },
  returns: v.string(),
  handler: async (ctx, { id }) => (await ctx.db.get(id))?.code ?? "CUS",
});

function idScanSvg(code: string, category: string) {
  const label = category.replace(/_/g, " ");
  return `<svg xmlns="http://www.w3.org/2000/svg" width="640" height="400" viewBox="0 0 640 400"><rect width="640" height="400" rx="16" fill="#e7e5e4"/><rect x="24" y="24" width="592" height="352" rx="12" fill="#faf9f7" stroke="#d6d3d1"/><rect x="48" y="60" width="140" height="180" rx="8" fill="#d6d3d1"/><rect x="212" y="70" width="300" height="18" rx="4" fill="#a8a29e"/><rect x="212" y="104" width="220" height="14" rx="4" fill="#d6d3d1"/><rect x="212" y="132" width="260" height="14" rx="4" fill="#d6d3d1"/><rect x="212" y="160" width="180" height="14" rx="4" fill="#d6d3d1"/><rect x="212" y="200" width="320" height="14" rx="4" fill="#d6d3d1"/><text x="320" y="330" text-anchor="middle" font-family="Inter,Arial" font-size="18" fill="#78716c">${label} — demo placeholder (${code})</text></svg>`;
}

/** Entry point: `npx convex run seed:run` */
export const run = internalAction({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    console.log("Wiping…");
    for (const t of TABLES) { let n = 0; do { n = await ctx.runMutation(internal.seed.wipe, { table: t }); } while (n > 0); }
    console.log("Phase 1: roles, users, apartments, customers");
    const p1 = await ctx.runMutation(internal.seed.phase1, {});
    const users = p1.users as Record<string, Id<"users">>;
    for (const email of ["admin@locajour.ma", "salma@locajour.ma", "youssef@locajour.ma", "ahmed@locajour.ma", "fatima@locajour.ma", "karim@locajour.ma"]) await createAccount(ctx, { provider: "password", account: { id: email, secret: "password123" }, profile: { email } });
    console.log("Phase 2: reservations");
    let seqs = { res: 1000, pay: 0, com: 0, ctr: 0 };
    const needDocs: { customerId: Id<"customers">; uploadedById: Id<"users">; at: number }[] = [];
    for (let i = 0; i < p1.apartments.length; i++) {
      const r = await ctx.runMutation(internal.seed.phase2, { apartmentId: p1.apartments[i], aptIndex: i, users, customers: p1.customers, seqs });
      seqs = r.seqs;
      needDocs.push(...r.needDocs);
    }
    console.log("Phase 3: inventory, statuses, maintenance");
    const p3 = await ctx.runMutation(internal.seed.phase3, { users, apartments: p1.apartments, customers: p1.customers, seqs });
    seqs = { ...seqs, res: p3.res, pay: p3.pay };
    console.log("ID documents");
    const seen = new Set<string>();
    let docSeq = 0;
    for (const d of needDocs) {
      if (seen.has(d.customerId)) continue;
      seen.add(d.customerId);
      const code = await ctx.runQuery(internal.seed.customerCode, { id: d.customerId });
      for (const cat of ["ID_FRONT", "ID_BACK"]) {
        const svg = idScanSvg(code, cat);
        const storageId = await ctx.storage.store(new Blob([svg], { type: "image/svg+xml" }));
        await ctx.runMutation(internal.seed.storeDocument, { customerId: d.customerId, storageId, category: cat, code: `DOC-${String(++docSeq).padStart(6, "0")}`, fileName: `${code}-${cat.toLowerCase()}.svg`, size: svg.length, uploadedById: d.uploadedById, at: d.at });
      }
    }
    console.log("Phase 4: expenses, tasks, risk");
    await ctx.runMutation(internal.seed.phase4, { users, apartments: p1.apartments, customers: p1.customers, seqs });
    console.log("Apartment media");
    const imgCats = ["LIVING_ROOM", "BEDROOM", "KITCHEN", "BATHROOM", "VIEW", "EXTERIOR"] as const;
    for (const apt of await ctx.runQuery(internal.seed.apartmentMeta, { ids: p1.apartments })) {
      const n = apt.bedrooms >= 2 ? 6 : 5;
      for (let i = 0; i < n; i++) {
        const category = imgCats[i % imgCats.length];
        const svg = placeholderSvg({ code: apt.code, name: apt.name, city: apt.city, category, index: i });
        const storageId = await ctx.storage.store(new Blob([svg], { type: "image/svg+xml" }));
        await ctx.runMutation(internal.seed.storeImage, { apartmentId: apt.id, storageId, category, index: i, size: svg.length, caption: i === 0 ? `${apt.name} — main living space` : undefined, uploadedById: users.admin });
      }
    }
    console.log("Phase 5: audit + notifications");
    await ctx.runMutation(internal.seed.phase5, { users, apartments: p1.apartments });
    console.log("Done. Login: admin@locajour.ma / password123 (also salma, youssef, ahmed, fatima @locajour.ma)");
    return null;
  },
});
