import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import type { Doc } from "./_generated/dataModel";
import { assertPermission, actorLite, AppError, requireActor, can } from "./lib/access";
import { audit } from "./lib/audit";
import { notify } from "./lib/notify";
import { getSettings } from "./lib/settings";
import { findConflicts, activeApartments } from "./lib/availability";
import { inventoryEvent, releaseCleaningHolds, syncApartmentStatus, effectiveCheckOut } from "./lib/inventory";
import { addDaysKey, todayKey } from "./lib/days";
import { APARTMENT_STATUSES, CLEANING_STATUSES } from "../src/lib/domain";

const apartmentArgs = {
  code: v.string(),
  name: v.string(),
  building: v.optional(v.union(v.string(), v.null())),
  floor: v.optional(v.union(v.string(), v.null())),
  address: v.string(),
  city: v.string(),
  bedrooms: v.number(),
  beds: v.number(),
  bathrooms: v.number(),
  maxGuests: v.number(),
  basePrice: v.number(),
  weekendPrice: v.optional(v.union(v.number(), v.null())),
  amenities: v.optional(v.array(v.string())),
  notes: v.optional(v.union(v.string(), v.null())),
  isActive: v.optional(v.boolean()),
};

function validate(data: { code: string; name: string; address: string; city: string; bedrooms: number; beds: number; bathrooms: number; maxGuests: number; basePrice: number }) {
  const fields: Record<string, string> = {};
  if (!/^[A-Za-z0-9-]{1,12}$/.test(data.code.trim())) fields.code = "Letters, digits and dashes only";
  if (data.name.trim().length < 2) fields.name = "Name is required";
  if (data.address.trim().length < 3) fields.address = "Address is required";
  if (data.city.trim().length < 2) fields.city = "City is required";
  if (data.maxGuests < 1) fields.maxGuests = "At least one guest";
  if (data.basePrice < 0) fields.basePrice = "Price must be positive";
  if (Object.keys(fields).length) throw new AppError(Object.values(fields)[0], "VALIDATION", { fields });
}

export const upsertCoverFromImages = async (ctx: Parameters<typeof mutation>[0] extends never ? never : import("./_generated/server").MutationCtx, apartmentId: Doc<"apartments">["_id"]) => {
  const imgs = await ctx.db.query("apartmentImages").withIndex("by_apartment", (q) => q.eq("apartmentId", apartmentId)).collect();
  const live = imgs.filter((i) => !i.archivedAt).sort((a, b) => a.sortOrder - b.sortOrder);
  const cover = live.find((i) => i.isCover) ?? live[0];
  await ctx.db.patch(apartmentId, { coverImageId: cover?._id, updatedAt: Date.now() });
};

// ── Queries ──────────────────────────────────────────────────
export const list = query({
  args: {},
  returns: v.array(v.any()),
  handler: async (ctx) => {
    const actor = await assertPermission(ctx, "apartments.view");
    const settings = await getSettings(ctx);
    const today = todayKey(settings.timezone);
    const tomorrow = addDaysKey(today, 1);
    const money = can(actor, "financials.view_revenue");
    const monthStart = `${today.slice(0, 7)}-01`;
    const horizon = addDaysKey(today, 7);
    const rows = (await ctx.db.query("apartments").collect()).filter((a) => !a.deletedAt).sort((a, b) => a.code.localeCompare(b.code));
    return Promise.all(
      rows.map(async (a) => {
        const rs = (await ctx.db.query("reservations").withIndex("by_apartment_checkIn", (q) => q.eq("apartmentId", a._id).gte("checkIn", addDaysKey(today, -60))).collect()).filter((r) => ["CONFIRMED", "CHECKED_IN", "CHECKED_OUT", "PENDING"].includes(r.status));
        const current = rs.find((r) => r.status === "CHECKED_IN");
        const upcoming = rs.filter((r) => ["CONFIRMED", "PENDING"].includes(r.status) && r.checkIn >= today).sort((x, y) => x.checkIn.localeCompare(y.checkIn));
        const next = upcoming[0];
        const [cur, nxt] = await Promise.all([current ? ctx.db.get(current.customerId) : null, next ? ctx.db.get(next.customerId) : null]);
        const mtd = rs.filter((r) => r.checkIn >= monthStart && r.checkIn <= today && r.status !== "PENDING");
        const maintenance = (await ctx.db.query("maintenanceTickets").withIndex("by_apartment_status", (q) => q.eq("apartmentId", a._id)).collect()).filter((m) => m.status !== "COMPLETED").sort((x, y) => y.createdAt - x.createdAt);
        const cleaningPending = (await ctx.db.query("cleaningTasks").withIndex("by_apartment_status", (q) => q.eq("apartmentId", a._id).eq("status", "NEEDS_CLEANING")).collect()).length;
        const blocks = (await ctx.db.query("apartmentBlocks").withIndex("by_apartment_start", (q) => q.eq("apartmentId", a._id).gte("startDate", addDaysKey(today, -400))).collect()).filter((b) => b.endDate > today && b.startDate < horizon);
        const images = (await ctx.db.query("apartmentImages").withIndex("by_apartment", (q) => q.eq("apartmentId", a._id)).collect()).filter((i) => !i.archivedAt).sort((x, y) => Number(y.isCover) - Number(x.isCover) || x.sortOrder - y.sortOrder).slice(0, 6);
        let nightsMtd = 0;
        for (const r of mtd) {
          const s = r.checkIn > monthStart ? r.checkIn : monthStart;
          const e = effectiveCheckOut(r) < tomorrow ? effectiveCheckOut(r) : tomorrow;
          if (e > s) nightsMtd += Math.round((Date.parse(e) - Date.parse(s)) / 86_400_000);
        }
        const daysMtd = Math.round((Date.parse(tomorrow) - Date.parse(monthStart)) / 86_400_000);
        // Next 7 nights, one state per night — drives the mini availability strip on every card.
        const week = Array.from({ length: 7 }, (_, i) => {
          const d = addDaysKey(today, i);
          if (rs.some((r) => r.status === "CHECKED_IN" && r.checkIn <= d && effectiveCheckOut(r) > d)) return "occupied";
          if (rs.some((r) => (r.status === "CONFIRMED" || r.status === "PENDING") && r.checkIn <= d && r.checkOut > d)) return "reserved";
          if (blocks.some((b) => b.startDate <= d && b.endDate > d)) return "blocked";
          return "free";
        });
        const freeNights7 = week.filter((w) => w === "free").length;
        return { id: a._id, code: a.code, name: a.name, building: a.building ?? null, city: a.city, floor: a.floor ?? null, bedrooms: a.bedrooms, beds: a.beds, bathrooms: a.bathrooms, maxGuests: a.maxGuests, basePrice: a.basePrice, weekendPrice: a.weekendPrice ?? null, status: a.status, cleaningStatus: a.cleaningStatus, maintenanceStatus: a.maintenanceStatus, isActive: a.isActive, coverImageId: a.coverImageId ?? null, imageIds: images.map((i) => i._id), amenities: a.amenities, openMaintenance: maintenance.length, maintenanceTitle: maintenance[0]?.title ?? null, cleaningPending, current: current ? { id: current._id, code: current.code, guest: cur?.fullName ?? "", checkOut: effectiveCheckOut(current), balance: Math.max(0, current.totalAmount - current.amountPaid), guests: current.adults + current.children } : null, next: next ? { id: next._id, code: next.code, guest: nxt?.fullName ?? "", checkIn: next.checkIn, nights: next.nights, source: next.source, status: next.status } : null, upcomingCount: upcoming.length, occupancyMtd: daysMtd ? Math.round((nightsMtd / daysMtd) * 100) : 0, nightsMtd, revenueMtd: money ? mtd.reduce((s, r) => s + r.totalAmount, 0) : 0, week, freeNights7, potential7: freeNights7 * a.basePrice, today };
      })
    );
  },
});

export const options = query({
  args: {},
  returns: v.array(v.object({ id: v.id("apartments"), code: v.string(), name: v.string(), maxGuests: v.number(), basePrice: v.number(), building: v.union(v.string(), v.null()), city: v.string() })),
  handler: async (ctx) => {
    await requireActor(ctx);
    return (await activeApartments(ctx)).map((a) => ({ id: a._id, code: a.code, name: a.name, maxGuests: a.maxGuests, basePrice: a.basePrice, building: a.building ?? null, city: a.city }));
  },
});

export const get = query({
  args: { id: v.id("apartments") },
  returns: v.union(v.null(), v.any()),
  handler: async (ctx, { id }) => {
    await assertPermission(ctx, "apartments.view");
    const a = await ctx.db.get(id);
    if (!a || a.deletedAt) return null;
    const images = (await ctx.db.query("apartmentImages").withIndex("by_apartment", (q) => q.eq("apartmentId", id)).collect()).sort((x, y) => Number(y.isCover) - Number(x.isCover) || x.sortOrder - y.sortOrder);
    return { ...a, images };
  },
});

// ── Mutations ────────────────────────────────────────────────
export const create = mutation({
  args: apartmentArgs,
  returns: v.object({ id: v.id("apartments") }),
  handler: async (ctx, data) => {
    const user = await assertPermission(ctx, "apartments.create");
    validate(data);
    const code = data.code.trim().toUpperCase();
    if (await ctx.db.query("apartments").withIndex("by_code", (q) => q.eq("code", code)).unique()) throw new AppError(`Code ${code} is already used.`, "VALIDATION", { fields: { code: "Already used" } });
    const id = await ctx.db.insert("apartments", { code, name: data.name.trim(), building: data.building || undefined, floor: data.floor || undefined, address: data.address.trim(), city: data.city.trim(), bedrooms: data.bedrooms, beds: data.beds, bathrooms: data.bathrooms, maxGuests: data.maxGuests, basePrice: data.basePrice, weekendPrice: data.weekendPrice || undefined, status: "AVAILABLE", cleaningStatus: "CLEAN", maintenanceStatus: "OK", amenities: data.amenities ?? [], notes: data.notes || undefined, isActive: data.isActive ?? true, updatedAt: Date.now() });
    await audit(ctx, actorLite(user), { action: "APARTMENT_CREATED", module: "apartments", entityType: "apartment", entityId: id, entityLabel: code, newValue: { name: data.name, basePrice: data.basePrice, maxGuests: data.maxGuests }, apartmentId: id });
    return { id };
  },
});

export const update = mutation({
  args: { id: v.id("apartments"), ...apartmentArgs },
  returns: v.null(),
  handler: async (ctx, { id, ...data }) => {
    const user = await assertPermission(ctx, "apartments.edit");
    validate(data);
    const existing = await ctx.db.get(id);
    if (!existing || existing.deletedAt) throw new AppError("Apartment not found", "NOT_FOUND");
    const code = data.code.trim().toUpperCase();
    if (code !== existing.code && (await ctx.db.query("apartments").withIndex("by_code", (q) => q.eq("code", code)).unique())) throw new AppError(`Code ${code} is already used.`, "VALIDATION");
    const next = { code, name: data.name.trim(), building: data.building || undefined, floor: data.floor || undefined, address: data.address.trim(), city: data.city.trim(), bedrooms: data.bedrooms, beds: data.beds, bathrooms: data.bathrooms, maxGuests: data.maxGuests, basePrice: data.basePrice, weekendPrice: data.weekendPrice || undefined, amenities: data.amenities ?? [], notes: data.notes || undefined, isActive: data.isActive ?? existing.isActive };
    const prev: Record<string, unknown> = {};
    const changed: Record<string, unknown> = {};
    for (const k of Object.keys(next) as (keyof typeof next)[]) if (JSON.stringify(existing[k] ?? null) !== JSON.stringify(next[k] ?? null)) {
      prev[k] = existing[k] ?? null;
      changed[k] = next[k] ?? null;
    }
    await ctx.db.patch(id, { ...next, updatedAt: Date.now() });
    if (Object.keys(changed).length) await audit(ctx, actorLite(user), { action: "APARTMENT_EDITED", module: "apartments", entityType: "apartment", entityId: id, entityLabel: code, previousValue: prev, newValue: changed, apartmentId: id });
    return null;
  },
});

export const setStatus = mutation({
  args: { id: v.id("apartments"), status: v.string(), reason: v.optional(v.string()) },
  returns: v.null(),
  handler: async (ctx, { id, status, reason }) => {
    const user = await assertPermission(ctx, "apartments.edit");
    if (!APARTMENT_STATUSES.includes(status as (typeof APARTMENT_STATUSES)[number])) throw new AppError("Invalid status", "VALIDATION");
    if (status === "OCCUPIED") throw new AppError("Occupied is set automatically by check-in.", "VALIDATION");
    const settings = await getSettings(ctx);
    const a = await ctx.db.get(id);
    if (!a) throw new AppError("Apartment not found", "NOT_FOUND");
    await ctx.db.patch(id, { status, maintenanceStatus: status === "MAINTENANCE" ? "BLOCKED" : a.maintenanceStatus === "BLOCKED" ? "OK" : a.maintenanceStatus, updatedAt: Date.now() });
    await audit(ctx, actorLite(user), { action: "APARTMENT_STATUS_CHANGED", module: "apartments", entityType: "apartment", entityId: id, entityLabel: a.code, previousValue: a.status, newValue: status, reason, apartmentId: id });
    if (status === "AVAILABLE") await syncApartmentStatus(ctx, id, settings.timezone);
    return null;
  },
});

export const setCleaningStatus = mutation({
  args: { id: v.id("apartments"), cleaningStatus: v.string(), notes: v.optional(v.string()) },
  returns: v.null(),
  handler: async (ctx, { id, cleaningStatus, notes }) => {
    const user = await assertPermission(ctx, "cleaning.manage");
    if (!CLEANING_STATUSES.includes(cleaningStatus as (typeof CLEANING_STATUSES)[number])) throw new AppError("Invalid status", "VALIDATION");
    const settings = await getSettings(ctx);
    const a = await ctx.db.get(id);
    if (!a) throw new AppError("Apartment not found", "NOT_FOUND");
    const now = Date.now();
    const clean = cleaningStatus === "READY" || cleaningStatus === "CLEAN";
    await ctx.db.patch(id, { cleaningStatus, ...(clean ? {} : { status: a.status === "AVAILABLE" || a.status === "CLEANING" ? "CLEANING" : a.status }), updatedAt: now });
    const openTasks = (await ctx.db.query("cleaningTasks").withIndex("by_apartment_status", (q) => q.eq("apartmentId", id)).collect()).filter((c) => c.status === "NEEDS_CLEANING" || c.status === "IN_PROGRESS").sort((x, y) => y.createdAt - x.createdAt);
    const open = openTasks[0];
    if (cleaningStatus === "NEEDS_CLEANING" && !open) await ctx.db.insert("cleaningTasks", { apartmentId: id, status: "NEEDS_CLEANING", assigneeId: user.roleKey === "CLEANER" ? user.id : undefined, notes, createdAt: now });
    else if (cleaningStatus === "IN_PROGRESS") {
      if (open) await ctx.db.patch(open._id, { status: "IN_PROGRESS", startedAt: now, assigneeId: open.assigneeId ?? user.id });
      else await ctx.db.insert("cleaningTasks", { apartmentId: id, status: "IN_PROGRESS", startedAt: now, assigneeId: user.id, notes, createdAt: now });
    } else if (clean) {
      for (const t of openTasks) await ctx.db.patch(t._id, { status: "READY", completedAt: now, completedById: user.id, notes: notes ?? t.notes });
      for (const t of await ctx.db.query("tasks").withIndex("by_apartment", (q) => q.eq("apartmentId", id)).collect()) if (t.type === "CLEANING" && (t.status === "TODO" || t.status === "IN_PROGRESS")) await ctx.db.patch(t._id, { status: "COMPLETED", completedAt: now, updatedAt: now });
    }
    const actor = actorLite(user);
    await audit(ctx, actor, { action: "CLEANING_UPDATED", module: "cleaning", entityType: "apartment", entityId: id, entityLabel: a.code, previousValue: a.cleaningStatus, newValue: cleaningStatus, reason: notes, apartmentId: id });
    if (clean) {
      const today = todayKey(settings.timezone);
      await inventoryEvent(ctx, actor, { apartmentId: id, action: "CLEANING_COMPLETED", startDate: today, endDate: today, previousState: a.cleaningStatus, newState: cleaningStatus, reason: notes });
      const released = await releaseCleaningHolds(ctx, actor, id);
      if (released) await notify(ctx, { type: "CLEANING_COMPLETED", title: `${a.code}: ${released} hold${released > 1 ? "s" : ""} released`, body: `Nights kept after an early check-out or shortening are back on sale now that ${a.name} is ready.`, href: `/apartments/${id}?tab=timeline`, actorId: user.id });
      await notify(ctx, { type: "CLEANING_COMPLETED", title: `${a.code} is ready`, body: `${user.fullName} marked ${a.name} ready after cleaning.`, href: `/apartments/${id}`, actorId: user.id });
    }
    await syncApartmentStatus(ctx, id, settings.timezone);
    return null;
  },
});

export const blockDates = mutation({
  args: { apartmentId: v.id("apartments"), startDate: v.string(), endDate: v.string(), reason: v.optional(v.string()) },
  returns: v.null(),
  handler: async (ctx, input) => {
    const user = await assertPermission(ctx, "apartments.block");
    if (input.endDate <= input.startDate) throw new AppError("End date must be after start date.", "VALIDATION");
    const a = await ctx.db.get(input.apartmentId);
    if (!a) throw new AppError("Apartment not found", "NOT_FOUND");
    const conflicts = await findConflicts(ctx, a._id, input.startDate, input.endDate);
    if (conflicts.length) throw new AppError(`Not available: ${conflicts.map((c) => c.label).join(", ")}`, "CONFLICT", { conflicts });
    const actor = actorLite(user);
    const id = await ctx.db.insert("apartmentBlocks", { apartmentId: a._id, startDate: input.startDate, endDate: input.endDate, reason: input.reason || undefined, type: "MANUAL", source: "OTHER", pendingApproval: false, releaseOnCleaning: false, createdById: user.id, createdAt: Date.now() });
    await inventoryEvent(ctx, actor, { apartmentId: a._id, action: "BLOCKED", startDate: input.startDate, endDate: input.endDate, previousState: "AVAILABLE", newState: "BLOCKED:OTHER", reason: input.reason, blockId: id });
    await audit(ctx, actor, { action: "APARTMENT_BLOCKED", module: "apartments", entityType: "apartment", entityId: a._id, entityLabel: a.code, newValue: { start: input.startDate, end: input.endDate }, reason: input.reason, apartmentId: a._id });
    return null;
  },
});

export const remove = mutation({
  args: { id: v.id("apartments"), reason: v.string() },
  returns: v.null(),
  handler: async (ctx, { id, reason }) => {
    const user = await assertPermission(ctx, "apartments.delete");
    const a = await ctx.db.get(id);
    if (!a) throw new AppError("Apartment not found", "NOT_FOUND");
    const active = (await ctx.db.query("reservations").withIndex("by_apartment_checkIn", (q) => q.eq("apartmentId", id)).collect()).some((r) => ["PENDING", "CONFIRMED", "CHECKED_IN"].includes(r.status));
    if (active) throw new AppError("This apartment has active reservations. Move or cancel them first.", "VALIDATION");
    await ctx.db.patch(id, { deletedAt: Date.now(), isActive: false, status: "BLOCKED", updatedAt: Date.now() });
    await audit(ctx, actorLite(user), { action: "APARTMENT_EDITED", module: "apartments", entityType: "apartment", entityId: id, entityLabel: a.code, newValue: "DELETED", reason, apartmentId: id, severity: "WARNING" });
    return null;
  },
});

// ── Detail page ──────────────────────────────────────────────
import { withId, loader } from "./lib/shape";

export const detail = query({
  args: { id: v.id("apartments") },
  returns: v.union(v.null(), v.any()),
  handler: async (ctx, { id }) => {
    await assertPermission(ctx, "apartments.view");
    const settings = await getSettings(ctx);
    const today = todayKey(settings.timezone);
    const a = await ctx.db.get(id);
    if (!a || a.deletedAt) return null;
    const users = loader(ctx, "users");
    const customers = loader(ctx, "customers");
    const [resRaw, blocksRaw, eventsRaw, mntRaw, cleaningRaw, expRaw, docsRaw, imagesRaw, activityRaw] = await Promise.all([
      ctx.db.query("reservations").withIndex("by_apartment_checkIn", (q) => q.eq("apartmentId", id)).order("desc").take(200),
      ctx.db.query("apartmentBlocks").withIndex("by_apartment_start", (q) => q.eq("apartmentId", id)).collect(),
      ctx.db.query("inventoryEvents").withIndex("by_apartment_at", (q) => q.eq("apartmentId", id)).order("desc").take(60),
      ctx.db.query("maintenanceTickets").withIndex("by_apartment_status", (q) => q.eq("apartmentId", id)).collect(),
      ctx.db.query("cleaningTasks").withIndex("by_apartment_status", (q) => q.eq("apartmentId", id)).collect(),
      ctx.db.query("expenses").withIndex("by_apartment", (q) => q.eq("apartmentId", id)).collect(),
      ctx.db.query("documents").withIndex("by_apartment", (q) => q.eq("apartmentId", id)).collect(),
      ctx.db.query("apartmentImages").withIndex("by_apartment", (q) => q.eq("apartmentId", id)).collect(),
      ctx.db.query("auditLog").withIndex("by_apartment", (q) => q.eq("apartmentId", id)).order("desc").take(40),
    ]);
    const cats = await ctx.db.query("expenseCategories").collect();
    const reservations = await Promise.all(resRaw.map(async (r) => { const [c, by] = await Promise.all([customers(r.customerId), users(r.createdById)]); return { ...withId(r), customer: { id: r.customerId, fullName: c?.fullName ?? "" }, createdBy: { fullName: by?.fullName ?? "" } }; }));
    const blocks = blocksRaw.filter((b) => b.endDate >= addDaysKey(today, -30)).sort((x, y) => x.startDate.localeCompare(y.startDate)).map(withId);
    const inventoryEvents = eventsRaw.map((e) => ({ ...withId(e), createdAt: e.at }));
    const maintenance = await Promise.all(mntRaw.sort((x, y) => y.createdAt - x.createdAt).map(async (m) => ({ ...withId(m), assignee: m.assigneeId ? { fullName: (await users(m.assigneeId))?.fullName ?? "" } : null, reportedBy: { fullName: (await users(m.reportedById))?.fullName ?? "" } })));
    const cleaningTasks = cleaningRaw.sort((x, y) => y.createdAt - x.createdAt).slice(0, 30).map(withId);
    const expenses = expRaw.filter((e) => !e.deletedAt).sort((x, y) => y.date.localeCompare(x.date)).slice(0, 100).map((e) => ({ ...withId(e), category: cats.find((c) => c._id === e.categoryId) ?? { name: "" } }));
    const documents = await Promise.all(docsRaw.filter((d) => !d.deletedAt).map(async (d) => ({ ...withId(d), createdAt: d.at, uploadedBy: { fullName: (await users(d.uploadedById))?.fullName ?? "" } })));
    const images = imagesRaw.sort((x, y) => Number(y.isCover) - Number(x.isCover) || x.sortOrder - y.sortOrder).map((i) => ({ ...withId(i), url: `/api/media/${i._id}` }));
    const activity = activityRaw.map((x) => ({ ...withId(x), createdAt: x.at }));
    const cleaners = (await ctx.db.query("users").withIndex("by_status", (q) => q.eq("status", "ACTIVE")).collect()).filter((u) => !u.deletedAt && u.roleId).map((u) => ({ id: u._id, fullName: u.fullName ?? "" }));
    const allApartments = (await activeApartments(ctx)).map((x) => ({ id: x._id, code: x.code, name: x.name }));
    return { ...withId(a), coverImageUrl: a.coverImageId ? `/api/media/${a.coverImageId}` : null, reservations, blocks, inventoryEvents, maintenance, cleaningTasks, expenses, documents, images, activity, cleaners, allApartments, today };
  },
});

export const title = query({
  args: { id: v.id("apartments") },
  returns: v.union(v.null(), v.object({ code: v.string(), name: v.string() })),
  handler: async (ctx, { id }) => {
    await requireActor(ctx);
    const a = await ctx.db.get(id);
    return a ? { code: a.code, name: a.name } : null;
  },
});

/** Settings › apartments list (all, including inactive). */
export const all = query({
  args: {},
  returns: v.array(v.any()),
  handler: async (ctx) => {
    await assertPermission(ctx, "apartments.view", "settings.view");
    return (await ctx.db.query("apartments").collect()).filter((a) => !a.deletedAt).sort((a, b) => a.code.localeCompare(b.code)).map((a) => ({ ...withId(a), coverImageUrl: a.coverImageId ? `/api/media/${a.coverImageId}` : null }));
  },
});
