import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import { assertPermission, can, actorLite, AppError } from "./lib/access";
import { audit } from "./lib/audit";
import { notify } from "./lib/notify";
import { nextCode } from "./lib/seq";
import { getSettings } from "./lib/settings";
import { findConflicts, suggestAlternatives, availableApartments } from "./lib/availability";
import { inventoryEvent, releaseInventory, dissolveHold, syncApartmentStatus } from "./lib/inventory";
import { cancelCommissionsFor } from "./lib/commission";
import { computePricing } from "../src/lib/pricing";
import { parseKey, nightsBetweenKeys, todayKey } from "./lib/days";
import { fmtMoney } from "../src/lib/format";
import { BLOCK_SOURCES, BLOCK_SOURCE_META, type BlockSource } from "../src/lib/domain";
import { loadReservation } from "./reservations";

const ACTIVE = ["CONFIRMED", "PENDING", "CHECKED_IN"];
const conflictError = (conflicts: { label: string }[]) => new AppError(`Not available: ${conflicts.map((c) => c.label).join(", ")}`, "CONFLICT", { conflicts });

async function recomputePaid(ctx: Parameters<typeof loadReservation>[0], reservationId: Parameters<typeof loadReservation>[1]) {
  const payments = await ctx.db.query("payments").withIndex("by_reservation", (q) => q.eq("reservationId", reservationId)).collect();
  const paid = payments.filter((p) => !p.reversedAt && ["PAYMENT", "DEPOSIT", "REFUND"].includes(p.type)).reduce((s, p) => s + p.amount, 0);
  await ctx.db.patch(reservationId, { amountPaid: Math.max(0, paid), updatedAt: Date.now() });
}

// ── Conflict preview with alternatives ───────────────────────
export const checkAvailability = query({
  args: { apartmentId: v.id("apartments"), checkIn: v.string(), checkOut: v.string(), guests: v.number(), excludeReservationId: v.optional(v.id("reservations")) },
  returns: v.object({ available: v.boolean(), conflicts: v.array(v.object({ kind: v.string(), label: v.string(), start: v.string(), end: v.string(), source: v.optional(v.string()) })), alternatives: v.array(v.any()) }),
  handler: async (ctx, input) => {
    await assertPermission(ctx, "reservations.view", "calendar.view");
    if (input.checkOut <= input.checkIn) throw new AppError("Check-out must be after check-in", "VALIDATION");
    const conflicts = await findConflicts(ctx, input.apartmentId, input.checkIn, input.checkOut, { excludeReservationId: input.excludeReservationId });
    const alternatives = conflicts.length ? await suggestAlternatives(ctx, { checkIn: input.checkIn, checkOut: input.checkOut, guests: input.guests, referenceApartmentId: input.apartmentId, excludeReservationId: input.excludeReservationId }) : [];
    return { available: conflicts.length === 0, conflicts: conflicts.map((c) => ({ kind: c.kind, label: c.label, start: c.start, end: c.end, source: c.source })), alternatives };
  },
});

// ── Early check-out ──────────────────────────────────────────
export const earlyCheckout = mutation({
  args: { id: v.id("reservations"), actualCheckOut: v.string(), policy: v.string(), refundAmount: v.optional(v.number()), refundDeposit: v.optional(v.number()), keysReturned: v.boolean(), notes: v.optional(v.union(v.string(), v.null())) },
  returns: v.object({ releasedNights: v.number(), holdId: v.union(v.id("apartmentBlocks"), v.null()) }),
  handler: async (ctx, data) => {
    const user = await assertPermission(ctx, "reservations.checkout");
    const settings = await getSettings(ctx);
    if (!["IMMEDIATE", "AFTER_CLEANING", "KEEP"].includes(data.policy)) throw new AppError("Invalid release policy", "VALIDATION");
    const { r, apartment, customer } = await loadReservation(ctx, data.id);
    if (r.status !== "CHECKED_IN") throw new AppError("Only checked-in guests can check out early.", "VALIDATION");
    if (!data.keysReturned) throw new AppError("Confirm that the keys were returned.", "VALIDATION");
    if (data.actualCheckOut <= r.checkIn) throw new AppError("Actual check-out must be after check-in.", "VALIDATION");
    if (data.actualCheckOut >= r.checkOut) throw new AppError("This is not an early check-out — use the normal check-out.", "VALIDATION");
    const today = todayKey(settings.timezone);
    const releasedNights = nightsBetweenKeys(data.actualCheckOut, r.checkOut);
    const refundAmount = Math.max(0, data.refundAmount ?? 0);
    const refundDeposit = Math.max(0, data.refundDeposit ?? 0);
    const now = Date.now();
    const actor = actorLite(user);
    await ctx.db.patch(r._id, { status: "CHECKED_OUT", actualCheckOut: data.actualCheckOut, earlyCheckout: true, releasedNights: r.releasedNights + releasedNights, checkedOutAt: now, checkedOutById: user.id, checkOutNotes: data.notes || undefined, updatedAt: now });
    await ctx.db.insert("reservationHistory", { reservationId: r._id, type: "CHECKED_OUT", previousValue: JSON.stringify({ status: "CHECKED_IN", plannedCheckOut: r.checkOut }), newValue: JSON.stringify({ status: "CHECKED_OUT", actualCheckOut: data.actualCheckOut, releasedNights, policy: data.policy }), reason: data.notes ?? `Early check-out · ${releasedNights} night${releasedNights > 1 ? "s" : ""} released`, performedById: user.id, at: now });
    await inventoryEvent(ctx, actor, { apartmentId: r.apartmentId, action: "EARLY_CHECKOUT", startDate: r.checkIn, endDate: data.actualCheckOut, previousState: "OCCUPIED", newState: "CLEANING", reason: data.notes, reservationId: r._id });
    if (refundAmount > 0) await ctx.db.insert("payments", { code: await nextCode(ctx, "payment"), reservationId: r._id, customerId: r.customerId, amount: -Math.abs(refundAmount), type: "REFUND", method: r.paymentMethod ?? "CASH", paidAt: now, recordedById: user.id, notes: `Early check-out refund (${releasedNights} unused night${releasedNights > 1 ? "s" : ""})` });
    if (refundDeposit > 0) await ctx.db.insert("payments", { code: await nextCode(ctx, "payment"), reservationId: r._id, customerId: r.customerId, amount: -Math.abs(refundDeposit), type: "DEPOSIT_REFUND", method: r.paymentMethod ?? "CASH", paidAt: now, recordedById: user.id, notes: "Deposit refunded at early check-out" });
    if (refundAmount > 0) await recomputePaid(ctx, r._id);
    await ctx.db.patch(r.apartmentId, { status: "CLEANING", cleaningStatus: "NEEDS_CLEANING", updatedAt: now });
    const cleanerRole = await ctx.db.query("roles").withIndex("by_key", (q) => q.eq("key", "CLEANER")).unique();
    const cleaner = cleanerRole ? (await ctx.db.query("users").withIndex("by_role", (q) => q.eq("roleId", cleanerRole._id)).collect()).find((u) => u.status === "ACTIVE" && !u.deletedAt) : null;
    await ctx.db.insert("cleaningTasks", { apartmentId: r.apartmentId, reservationId: r._id, assigneeId: cleaner?._id, status: "NEEDS_CLEANING", scheduledFor: today, notes: `Early check-out of ${r.code}`, createdAt: now });
    await ctx.db.insert("tasks", { title: `Cleaning ${apartment.code} after early check-out ${r.code}`, type: "CLEANING", apartmentId: r.apartmentId, reservationId: r._id, assigneeId: cleaner?._id, createdById: user.id, priority: "HIGH", dueDate: today, status: "TODO", createdAt: now, updatedAt: now });
    const rel = await releaseInventory(ctx, actor, { apartmentId: r.apartmentId, reservationId: r._id, start: data.actualCheckOut, end: r.checkOut, policy: data.policy as "IMMEDIATE" | "AFTER_CLEANING" | "KEEP", reason: `Early check-out of ${r.code}`, nightlyPrice: r.nightlyPrice, trigger: "EARLY_CHECKOUT" });
    await audit(ctx, actor, { action: "CHECKED_OUT", module: "reservations", entityType: "reservation", entityId: r._id, entityLabel: r.code, previousValue: { plannedCheckOut: r.checkOut }, newValue: { actualCheckOut: data.actualCheckOut, releasedNights, policy: data.policy, refund: refundAmount }, reason: `Early check-out${data.notes ? ": " + data.notes : ""}`, reservationId: r._id, apartmentId: r.apartmentId, customerId: r.customerId });
    await notify(ctx, { type: "APARTMENT_NEEDS_CLEANING", title: `${apartment.code} needs cleaning (early check-out)`, body: `${customer.fullName} left ${apartment.name} ${releasedNights} night${releasedNights > 1 ? "s" : ""} early. ${data.policy === "IMMEDIATE" ? "Nights are already back on sale." : data.policy === "AFTER_CLEANING" ? "Nights reopen once cleaning is done." : "Nights stay blocked."}`, href: "/cleaning", actorId: user.id });
    await notify(ctx, { type: "RESERVATION_MODIFIED", title: "Early check-out", body: `${user.fullName} checked out ${r.code} early — ${releasedNights} night${releasedNights > 1 ? "s" : ""} (${fmtMoney(releasedNights * r.nightlyPrice, settings.currency)}) released for rebooking.`, href: `/reservations/${r._id}`, actorId: user.id });
    await syncApartmentStatus(ctx, r.apartmentId, settings.timezone);
    return { releasedNights, holdId: rel.holdId };
  },
});

// ── Cancellation with release policy ─────────────────────────
export const cancel = mutation({
  args: { id: v.id("reservations"), reason: v.string(), policy: v.string(), refundAmount: v.optional(v.number()), refundMethod: v.optional(v.string()) },
  returns: v.object({ releasedNights: v.number(), holdId: v.union(v.id("apartmentBlocks"), v.null()) }),
  handler: async (ctx, data) => {
    const user = await assertPermission(ctx, "reservations.cancel");
    const settings = await getSettings(ctx);
    const reason = data.reason.trim();
    if (reason.length < 3) throw new AppError("A reason is required", "VALIDATION", { fields: { reason: "A reason is required" } });
    if (!["IMMEDIATE", "APPROVAL", "KEEP"].includes(data.policy)) throw new AppError("Invalid release policy", "VALIDATION");
    const { r, apartment, customer } = await loadReservation(ctx, data.id);
    if (["CANCELLED", "CHECKED_OUT", "NO_SHOW"].includes(r.status)) throw new AppError("This reservation is already closed.", "VALIDATION");
    if (r.status === "CHECKED_IN") throw new AppError("The guest is in house — use check-out or early check-out instead.", "VALIDATION");
    const today = todayKey(settings.timezone);
    const releaseStart = r.checkIn > today ? r.checkIn : today;
    const releasedNights = Math.max(0, nightsBetweenKeys(releaseStart, r.checkOut));
    const refundAmount = Math.max(0, data.refundAmount ?? 0);
    const now = Date.now();
    const actor = actorLite(user);
    await ctx.db.patch(r._id, { status: "CANCELLED", cancelledAt: now, cancelReason: reason, releasedNights: r.releasedNights + releasedNights, updatedAt: now });
    await ctx.db.insert("reservationHistory", { reservationId: r._id, type: "CANCELLED", previousValue: JSON.stringify(r.status), newValue: JSON.stringify("CANCELLED"), reason: `${reason} · inventory: ${data.policy.toLowerCase()}`, performedById: user.id, at: now });
    if (refundAmount > 0) {
      await ctx.db.insert("payments", { code: await nextCode(ctx, "payment"), reservationId: r._id, customerId: r.customerId, amount: -Math.abs(refundAmount), type: "REFUND", method: data.refundMethod ?? "CASH", paidAt: now, recordedById: user.id, notes: `Refund on cancellation: ${reason}` });
      await recomputePaid(ctx, r._id);
    }
    await inventoryEvent(ctx, actor, { apartmentId: r.apartmentId, action: "RESERVATION_CANCELLED", startDate: r.checkIn, endDate: r.checkOut, previousState: r.status, newState: "CANCELLED", reason, reservationId: r._id, source: r.source });
    const rel = releasedNights > 0 ? await releaseInventory(ctx, actor, { apartmentId: r.apartmentId, reservationId: r._id, start: releaseStart, end: r.checkOut, policy: data.policy as "IMMEDIATE" | "APPROVAL" | "KEEP", reason: `Cancellation of ${r.code}: ${reason}`, nightlyPrice: r.nightlyPrice, trigger: "CANCELLED" }) : { nights: 0, holdId: null };
    await cancelCommissionsFor(ctx, r._id, "Reservation cancelled", settings, actor);
    await audit(ctx, actor, { action: "RESERVATION_CANCELLED", module: "reservations", entityType: "reservation", entityId: r._id, entityLabel: r.code, previousValue: r.status, newValue: "CANCELLED", reason, reservationId: r._id, apartmentId: r.apartmentId, customerId: r.customerId });
    await notify(ctx, { type: "RESERVATION_CANCELLED", title: "Reservation cancelled", body: `${user.fullName} cancelled ${r.code} (${customer.fullName}, ${apartment.code}, ${r.checkIn} → ${r.checkOut}): ${reason}. ${releasedNights} night${releasedNights === 1 ? "" : "s"} ${data.policy === "IMMEDIATE" ? "released" : data.policy === "APPROVAL" ? "awaiting your approval" : "kept blocked"}.`, href: `/reservations/${r._id}`, actorId: user.id, targetUserIds: [r.assignedToId, r.createdById] });
    await syncApartmentStatus(ctx, r.apartmentId, settings.timezone);
    return { releasedNights, holdId: rel.holdId };
  },
});

// ── Holds & blocks ───────────────────────────────────────────
export const releaseHold = mutation({
  args: { holdId: v.id("apartmentBlocks"), reason: v.optional(v.string()) },
  returns: v.null(),
  handler: async (ctx, { holdId, reason }) => {
    const user = await assertPermission(ctx, "apartments.block", "reservations.cancel");
    const hold = await ctx.db.get(holdId);
    if (!hold || hold.type !== "HOLD") throw new AppError("Hold not found", "NOT_FOUND");
    if (hold.pendingApproval && !can(user, "apartments.block") && !user.isAdmin) throw new AppError("Only an administrator can approve this release.", "PERMISSION");
    const actor = actorLite(user);
    await dissolveHold(ctx, actor, holdId, reason ?? "Released manually");
    await audit(ctx, actor, { action: "APARTMENT_UNBLOCKED", module: "apartments", entityType: "apartment", entityId: hold.apartmentId, entityLabel: "hold", previousValue: { start: hold.startDate, end: hold.endDate, reason: hold.reason }, newValue: "RELEASED", reason, apartmentId: hold.apartmentId, reservationId: hold.reservationId });
    return null;
  },
});

export const createBlock = mutation({
  args: { apartmentId: v.id("apartments"), startDate: v.string(), endDate: v.string(), source: v.optional(v.string()), reason: v.optional(v.union(v.string(), v.null())), externalRef: v.optional(v.union(v.string(), v.null())), guestName: v.optional(v.union(v.string(), v.null())), amount: v.optional(v.union(v.number(), v.null())), notes: v.optional(v.union(v.string(), v.null())) },
  returns: v.object({ id: v.id("apartmentBlocks") }),
  handler: async (ctx, data) => {
    const user = await assertPermission(ctx, "apartments.block");
    const source = (data.source ?? "OTHER") as BlockSource;
    if (!BLOCK_SOURCES.includes(source)) throw new AppError("Invalid source", "VALIDATION");
    if (data.endDate <= data.startDate) throw new AppError("End date must be after start date.", "VALIDATION");
    const a = await ctx.db.get(data.apartmentId);
    if (!a) throw new AppError("Apartment not found", "NOT_FOUND");
    const meta = BLOCK_SOURCE_META[source];
    const conflicts = await findConflicts(ctx, a._id, data.startDate, data.endDate);
    if (conflicts.length) throw conflictError(conflicts);
    const actor = actorLite(user);
    const id = await ctx.db.insert("apartmentBlocks", { apartmentId: a._id, startDate: data.startDate, endDate: data.endDate, type: meta.external ? "EXTERNAL" : "MANUAL", source, reason: data.reason || meta.label, externalRef: data.externalRef || undefined, guestName: data.guestName || undefined, amount: data.amount ?? undefined, notes: data.notes || undefined, pendingApproval: false, releaseOnCleaning: false, createdById: user.id, createdAt: Date.now() });
    await inventoryEvent(ctx, actor, { apartmentId: a._id, action: meta.external ? "EXTERNAL_BLOCKED" : "BLOCKED", startDate: data.startDate, endDate: data.endDate, previousState: "AVAILABLE", newState: `BLOCKED:${source}`, reason: data.reason ?? meta.label, source, blockId: id, estimatedValue: data.amount ?? nightsBetweenKeys(data.startDate, data.endDate) * a.basePrice });
    await audit(ctx, actor, { action: "APARTMENT_BLOCKED", module: "apartments", entityType: "apartment", entityId: a._id, entityLabel: a.code, newValue: { start: data.startDate, end: data.endDate, source, ref: data.externalRef, guest: data.guestName, amount: data.amount }, reason: data.reason ?? undefined, apartmentId: a._id });
    if (meta.external) await notify(ctx, { type: "RESERVATION_MODIFIED", title: `${meta.short} dates blocked on ${a.code}`, body: `${user.fullName} blocked ${data.startDate} → ${data.endDate} for a ${meta.label.toLowerCase()}${data.guestName ? ` (${data.guestName})` : ""}.`, href: `/apartments/${a._id}?tab=timeline`, actorId: user.id });
    return { id };
  },
});

export const removeBlock = mutation({
  args: { blockId: v.id("apartmentBlocks"), reason: v.optional(v.string()) },
  returns: v.null(),
  handler: async (ctx, { blockId, reason }) => {
    const user = await assertPermission(ctx, "apartments.block");
    const b = await ctx.db.get(blockId);
    if (!b) throw new AppError("Block not found", "NOT_FOUND");
    if (b.type === "MAINTENANCE") throw new AppError("This block belongs to a maintenance ticket. Complete the ticket instead.", "VALIDATION");
    const a = await ctx.db.get(b.apartmentId);
    const actor = actorLite(user);
    if (b.type === "HOLD") {
      await dissolveHold(ctx, actor, blockId, reason ?? "Released manually");
      await audit(ctx, actor, { action: "APARTMENT_UNBLOCKED", module: "apartments", entityType: "apartment", entityId: b.apartmentId, entityLabel: a?.code ?? "", previousValue: { start: b.startDate, end: b.endDate, hold: b.reason }, newValue: "RELEASED", reason, apartmentId: b.apartmentId, reservationId: b.reservationId });
      return null;
    }
    await ctx.db.delete(blockId);
    await inventoryEvent(ctx, actor, { apartmentId: b.apartmentId, action: b.type === "EXTERNAL" ? "EXTERNAL_UNBLOCKED" : "UNBLOCKED", startDate: b.startDate, endDate: b.endDate, previousState: `BLOCKED:${b.source}`, newState: "AVAILABLE", reason, source: b.source, estimatedValue: nightsBetweenKeys(b.startDate, b.endDate) * (a?.basePrice ?? 0) });
    await audit(ctx, actor, { action: "APARTMENT_UNBLOCKED", module: "apartments", entityType: "apartment", entityId: b.apartmentId, entityLabel: a?.code ?? "", previousValue: { start: b.startDate, end: b.endDate, source: b.source, reason: b.reason }, reason, apartmentId: b.apartmentId });
    return null;
  },
});

// ── Extend / shorten / split ─────────────────────────────────
export const extendStay = mutation({
  args: { id: v.id("reservations"), newCheckOut: v.string(), recalculate: v.boolean(), reason: v.optional(v.string()) },
  returns: v.object({ addedNights: v.number(), priceDifference: v.number() }),
  handler: async (ctx, input) => {
    const user = await assertPermission(ctx, "reservations.change_dates");
    const settings = await getSettings(ctx);
    const { r, apartment, customer } = await loadReservation(ctx, input.id);
    if (!ACTIVE.includes(r.status)) throw new AppError("Only active reservations can be extended.", "VALIDATION");
    if (input.newCheckOut <= r.checkOut) throw new AppError("Choose a check-out date later than the current one.", "VALIDATION");
    const added = nightsBetweenKeys(r.checkOut, input.newCheckOut);
    const extra = input.recalculate && can(user, "reservations.change_price") ? computePricing({ basePrice: apartment.basePrice, weekendPrice: apartment.weekendPrice ?? null, weekendDays: settings.weekendDays, checkIn: parseKey(r.checkOut), checkOut: parseKey(input.newCheckOut) }).subtotal : added * r.nightlyPrice;
    const total = Math.round((r.totalAmount + extra) * 100) / 100;
    const conflicts = await findConflicts(ctx, r.apartmentId, r.checkOut, input.newCheckOut, { excludeReservationId: r._id });
    if (conflicts.length) throw conflictError(conflicts);
    const now = Date.now();
    const actor = actorLite(user);
    await ctx.db.patch(r._id, { checkOut: input.newCheckOut, nights: r.nights + added, totalAmount: total, nightlyPrice: Math.round(((total + r.discount) / (r.nights + added)) * 100) / 100, updatedAt: now });
    await ctx.db.insert("reservationHistory", { reservationId: r._id, type: "DATES_CHANGED", previousValue: JSON.stringify({ checkIn: r.checkIn, checkOut: r.checkOut, total: r.totalAmount }), newValue: JSON.stringify({ checkIn: r.checkIn, checkOut: input.newCheckOut, total }), priceDifference: extra, reason: input.reason ?? `Stay extended by ${added} night${added > 1 ? "s" : ""}`, performedById: user.id, at: now });
    await inventoryEvent(ctx, actor, { apartmentId: r.apartmentId, action: "STAY_EXTENDED", startDate: r.checkOut, endDate: input.newCheckOut, previousState: "AVAILABLE", newState: r.status === "CHECKED_IN" ? "OCCUPIED" : "RESERVED", reason: input.reason, reservationId: r._id, estimatedValue: extra });
    await audit(ctx, actor, { action: "DATES_CHANGED", module: "reservations", entityType: "reservation", entityId: r._id, entityLabel: r.code, previousValue: { checkOut: r.checkOut, total: r.totalAmount }, newValue: { checkOut: input.newCheckOut, total, addedNights: added }, reason: input.reason ?? "Stay extended", reservationId: r._id, apartmentId: r.apartmentId, customerId: r.customerId });
    await notify(ctx, { type: "RESERVATION_MODIFIED", title: "Stay extended", body: `${customer.fullName} (${r.code}) now leaves ${input.newCheckOut} · +${fmtMoney(extra, settings.currency)}.`, href: `/reservations/${r._id}`, actorId: user.id, targetUserIds: [r.assignedToId] });
    return { addedNights: added, priceDifference: extra };
  },
});

export const shortenStay = mutation({
  args: { id: v.id("reservations"), newCheckOut: v.string(), policy: v.string(), recalculate: v.boolean(), reason: v.optional(v.string()) },
  returns: v.object({ releasedNights: v.number(), priceDifference: v.number() }),
  handler: async (ctx, input) => {
    const user = await assertPermission(ctx, "reservations.change_dates");
    const settings = await getSettings(ctx);
    if (!["IMMEDIATE", "APPROVAL", "KEEP"].includes(input.policy)) throw new AppError("Invalid release policy", "VALIDATION");
    const { r, apartment } = await loadReservation(ctx, input.id);
    if (!ACTIVE.includes(r.status)) throw new AppError("Only active reservations can be shortened.", "VALIDATION");
    if (input.newCheckOut >= r.checkOut) throw new AppError("Choose an earlier check-out date.", "VALIDATION");
    if (input.newCheckOut <= r.checkIn) throw new AppError("Check-out must be after check-in.", "VALIDATION");
    const today = todayKey(settings.timezone);
    if (r.status === "CHECKED_IN" && input.newCheckOut < today) throw new AppError("Use early check-out for a guest who already left.", "VALIDATION");
    const removed = nightsBetweenKeys(input.newCheckOut, r.checkOut);
    const reduction = input.recalculate && can(user, "reservations.change_price") ? computePricing({ basePrice: apartment.basePrice, weekendPrice: apartment.weekendPrice ?? null, weekendDays: settings.weekendDays, checkIn: parseKey(input.newCheckOut), checkOut: parseKey(r.checkOut) }).subtotal : removed * r.nightlyPrice;
    const total = Math.max(0, Math.round((r.totalAmount - reduction) * 100) / 100);
    const now = Date.now();
    const actor = actorLite(user);
    await ctx.db.patch(r._id, { checkOut: input.newCheckOut, nights: r.nights - removed, totalAmount: total, releasedNights: r.releasedNights + removed, updatedAt: now });
    await ctx.db.insert("reservationHistory", { reservationId: r._id, type: "DATES_CHANGED", previousValue: JSON.stringify({ checkIn: r.checkIn, checkOut: r.checkOut, total: r.totalAmount }), newValue: JSON.stringify({ checkIn: r.checkIn, checkOut: input.newCheckOut, total }), priceDifference: -reduction, reason: input.reason ?? `Stay shortened by ${removed} night${removed > 1 ? "s" : ""}`, performedById: user.id, at: now });
    await inventoryEvent(ctx, actor, { apartmentId: r.apartmentId, action: "STAY_SHORTENED", startDate: input.newCheckOut, endDate: r.checkOut, previousState: r.status === "CHECKED_IN" ? "OCCUPIED" : "RESERVED", newState: input.policy === "IMMEDIATE" ? "AVAILABLE" : "HOLD", reason: input.reason, reservationId: r._id, estimatedValue: reduction });
    await releaseInventory(ctx, actor, { apartmentId: r.apartmentId, reservationId: r._id, start: input.newCheckOut, end: r.checkOut, policy: input.policy as "IMMEDIATE" | "APPROVAL" | "KEEP", reason: `Stay ${r.code} shortened`, nightlyPrice: r.nightlyPrice, trigger: "STAY_SHORTENED" });
    await audit(ctx, actor, { action: "DATES_CHANGED", module: "reservations", entityType: "reservation", entityId: r._id, entityLabel: r.code, previousValue: { checkOut: r.checkOut, total: r.totalAmount }, newValue: { checkOut: input.newCheckOut, total, releasedNights: removed, policy: input.policy }, reason: input.reason ?? "Stay shortened", reservationId: r._id, apartmentId: r.apartmentId, customerId: r.customerId });
    await notify(ctx, { type: "RESERVATION_MODIFIED", title: "Stay shortened", body: `${r.code} now ends ${input.newCheckOut} · ${removed} night${removed > 1 ? "s" : ""} (${fmtMoney(reduction, settings.currency)}) back to inventory.`, href: `/reservations/${r._id}`, actorId: user.id, targetUserIds: [r.assignedToId] });
    return { releasedNights: removed, priceDifference: -reduction };
  },
});

export const splitStay = mutation({
  args: { id: v.id("reservations"), splitDate: v.string(), newApartmentId: v.id("apartments"), recalculate: v.boolean(), reason: v.optional(v.string()) },
  returns: v.object({ newReservationId: v.id("reservations"), newCode: v.string() }),
  handler: async (ctx, input) => {
    const user = await assertPermission(ctx, "reservations.change_apartment");
    const settings = await getSettings(ctx);
    const { r, apartment, customer } = await loadReservation(ctx, input.id);
    if (!ACTIVE.includes(r.status)) throw new AppError("Only active reservations can be split.", "VALIDATION");
    if (input.splitDate <= r.checkIn || input.splitDate >= r.checkOut) throw new AppError("The split date must fall inside the stay.", "VALIDATION");
    const target = await ctx.db.get(input.newApartmentId);
    if (!target || target._id === r.apartmentId) throw new AppError("Choose a different apartment for the second segment.", "VALIDATION");
    if (r.adults + r.children > target.maxGuests) throw new AppError(`${target.code} sleeps at most ${target.maxGuests} guests.`, "VALIDATION");
    const firstNights = nightsBetweenKeys(r.checkIn, input.splitDate);
    const secondNights = nightsBetweenKeys(input.splitDate, r.checkOut);
    const secondPricing = computePricing({ basePrice: target.basePrice, weekendPrice: target.weekendPrice ?? null, weekendDays: settings.weekendDays, checkIn: parseKey(input.splitDate), checkOut: parseKey(r.checkOut) });
    const secondTotal = input.recalculate && can(user, "reservations.change_price") ? secondPricing.total : Math.round(secondNights * r.nightlyPrice * 100) / 100;
    const firstTotal = Math.max(0, Math.round((firstNights * r.nightlyPrice - r.discount) * 100) / 100);
    const groupId = r.stayGroupId ?? r._id;
    const conflicts = await findConflicts(ctx, target._id, input.splitDate, r.checkOut);
    if (conflicts.length) throw conflictError(conflicts);
    const code = await nextCode(ctx, "reservation");
    const overpaid = Math.max(0, r.amountPaid - firstTotal);
    const now = Date.now();
    const actor = actorLite(user);
    const secondId = await ctx.db.insert("reservations", { code, customerId: r.customerId, apartmentId: target._id, checkIn: input.splitDate, checkOut: r.checkOut, originalCheckIn: input.splitDate, originalCheckOut: r.checkOut, nights: secondNights, adults: r.adults, children: r.children, source: r.source, status: r.status === "CHECKED_IN" ? "CONFIRMED" : r.status, nightlyPrice: input.recalculate ? secondPricing.nightlyPrice : r.nightlyPrice, discount: 0, totalAmount: secondTotal, deposit: 0, amountPaid: overpaid, paymentMethod: r.paymentMethod, createdById: r.createdById, assignedToId: r.assignedToId, internalNotes: `Second segment of split stay (from ${r.code})`, customerRequests: r.customerRequests, externalRef: r.externalRef, stayGroupId: groupId, segmentIndex: r.segmentIndex + 1, earlyCheckout: false, releasedNights: 0, recoveredNights: 0, createdAt: now, updatedAt: now });
    await ctx.db.insert("reservationHistory", { reservationId: secondId, type: "CREATED", newValue: JSON.stringify({ status: r.status, apartment: target.code, checkIn: input.splitDate, checkOut: r.checkOut, total: secondTotal, splitFrom: r.code }), reason: input.reason ?? "Split stay", performedById: user.id, at: now });
    await ctx.db.patch(r._id, { checkOut: input.splitDate, nights: firstNights, totalAmount: firstTotal, amountPaid: Math.min(r.amountPaid, firstTotal), stayGroupId: groupId, updatedAt: now });
    await ctx.db.insert("reservationHistory", { reservationId: r._id, type: "DATES_CHANGED", previousValue: JSON.stringify({ checkOut: r.checkOut, total: r.totalAmount }), newValue: JSON.stringify({ checkOut: input.splitDate, total: firstTotal, continuesIn: code }), reason: input.reason ?? `Split stay → ${target.code} from ${input.splitDate}`, performedById: user.id, at: now });
    await inventoryEvent(ctx, actor, { apartmentId: r.apartmentId, action: "STAY_SPLIT", startDate: input.splitDate, endDate: r.checkOut, previousState: r.status === "CHECKED_IN" ? "OCCUPIED" : "RESERVED", newState: "AVAILABLE", reason: `Guest moves to ${target.code}`, reservationId: r._id });
    await inventoryEvent(ctx, actor, { apartmentId: target._id, action: "STAY_SPLIT", startDate: input.splitDate, endDate: r.checkOut, previousState: "AVAILABLE", newState: "RESERVED", reason: `Continuation of ${r.code}`, reservationId: secondId, estimatedValue: secondTotal });
    await audit(ctx, actor, { action: "APARTMENT_CHANGED", module: "reservations", entityType: "reservation", entityId: r._id, entityLabel: r.code, previousValue: { apartment: apartment.code, checkOut: r.checkOut }, newValue: { splitDate: input.splitDate, secondSegment: code, apartment: target.code }, reason: input.reason ?? "Split stay", reservationId: r._id, apartmentId: target._id, customerId: r.customerId });
    await notify(ctx, { type: "RESERVATION_MODIFIED", title: "Stay split", body: `${customer.fullName}: ${apartment.code} until ${input.splitDate}, then ${target.code} until ${r.checkOut} (${code}).`, href: `/reservations/${secondId}`, actorId: user.id, targetUserIds: [r.assignedToId] });
    return { newReservationId: secondId, newCode: code };
  },
});

/** Availability board for the reservation wizard: every active apartment with conflicts and pricing. */
export const availabilityBoard = query({
  args: { checkIn: v.string(), checkOut: v.string(), excludeReservationId: v.optional(v.id("reservations")) },
  returns: v.object({ items: v.array(v.any()), weekendDays: v.array(v.number()) }),
  handler: async (ctx, { checkIn, checkOut, excludeReservationId }) => {
    await assertPermission(ctx, "reservations.view", "calendar.view");
    if (checkOut <= checkIn) throw new AppError("Check-out must be after check-in", "VALIDATION");
    const settings = await getSettings(ctx);
    const rows = await availableApartments(ctx, checkIn, checkOut, excludeReservationId);
    const items = rows.map(({ apartment: a, conflicts, available }) => {
      const p = computePricing({ basePrice: a.basePrice, weekendPrice: a.weekendPrice ?? null, weekendDays: settings.weekendDays, checkIn: parseKey(checkIn), checkOut: parseKey(checkOut) });
      return { id: a._id, code: a.code, name: a.name, building: a.building ?? null, city: a.city, status: a.status, cleaningStatus: a.cleaningStatus, bedrooms: a.bedrooms, beds: a.beds, bathrooms: a.bathrooms, maxGuests: a.maxGuests, basePrice: a.basePrice, weekendPrice: a.weekendPrice ?? null, coverImageId: a.coverImageId ?? null, available, conflicts: conflicts.map((c) => ({ kind: c.kind, label: c.label, start: c.start, end: c.end })), pricing: { nights: p.nights, nightlyPrice: p.nightlyPrice, subtotal: p.subtotal, breakdown: p.breakdown.map((b) => ({ date: b.date.toISOString().slice(0, 10), price: b.price, weekend: b.weekend })) } };
    });
    return { items, weekendDays: settings.weekendDays };
  },
});
