import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
import type { Id } from "./_generated/dataModel";
import { getSettings } from "./lib/settings";
import { notify } from "./lib/notify";
import { activeApartments, availableApartments, reservationsOverlapping, blocksOverlapping, findConflicts } from "./lib/availability";
import { screenGuest } from "./lib/screening";
import { normalise } from "./customers";
import { nextCode } from "./lib/seq";
import { inventoryEvent, syncApartmentStatus } from "./lib/inventory";
import { audit } from "./lib/audit";
import { computePricing } from "../src/lib/pricing";
import { parseKey, addDaysKey, eachNightKey, nightsBetweenKeys } from "./lib/days";
import { normalizePhone } from "../src/lib/format";

/**
 * Public showroom. Everything here is presentation content a prospective
 * guest may see: apartments, photos, prices and whether nights are free.
 * No guest names, balances or staff data ever leave this module.
 */
const DAY = /^\d{4}-\d{2}-\d{2}$/;

export const catalog = query({
  args: {},
  returns: v.any(),
  handler: async (ctx) => {
    const s = await getSettings(ctx);
    const apts = await activeApartments(ctx);
    const apartments = await Promise.all(
      apts.map(async (a) => {
        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, 10);
        return {
          id: a._id,
          code: a.code,
          name: a.name,
          city: a.city,
          building: a.building ?? null,
          floor: a.floor ?? null,
          bedrooms: a.bedrooms,
          beds: a.beds,
          bathrooms: a.bathrooms,
          maxGuests: a.maxGuests,
          basePrice: a.basePrice,
          weekendPrice: a.weekendPrice ?? null,
          amenities: a.amenities,
          images: images.map((i) => ({ id: i._id, caption: i.caption ?? null, category: i.category })),
        };
      })
    );
    return {
      business: {
        name: s.businessName,
        phone: s.contactPhone,
        whatsapp: s.whatsappNumber || s.contactPhone,
        email: s.contactEmail,
        address: s.address,
        currency: s.currency,
        checkInTime: s.checkInTime,
        checkOutTime: s.checkOutTime,
        weekendDays: s.weekendDays,
        replyMinutes: s.replyMinutes,
        reviewUrl: s.googleReviewUrl || (s.googlePlaceId ? `https://search.google.com/local/writereview?placeid=${encodeURIComponent(s.googlePlaceId)}` : ""),
        mapsUrl: s.googleMapsUrl,
        hasGooglePlace: !!s.googlePlaceId,
        fbPixelId: s.fbPixelId,
      },
      apartments: apartments.filter((a) => a.images.length > 0),
    };
  },
});

export const availability = query({
  args: { checkIn: v.string(), checkOut: v.string() },
  returns: v.any(),
  handler: async (ctx, { checkIn, checkOut }) => {
    if (!DAY.test(checkIn) || !DAY.test(checkOut) || checkOut <= checkIn || nightsBetweenKeys(checkIn, checkOut) > 60) return { items: [] };
    const s = await getSettings(ctx);
    const rows = await availableApartments(ctx, checkIn, checkOut);
    return {
      items: rows.map(({ apartment: a, available }) => {
        const p = computePricing({ basePrice: a.basePrice, weekendPrice: a.weekendPrice ?? null, weekendDays: s.weekendDays, checkIn: parseKey(checkIn), checkOut: parseKey(checkOut) });
        return { id: a._id, available, nights: p.nights, subtotal: p.subtotal, nightlyPrice: p.nightlyPrice };
      }),
    };
  },
});

/** Nights that are already taken for one apartment inside [from, from + days). */
export const calendar = query({
  args: { apartmentId: v.id("apartments"), from: v.string(), days: v.optional(v.number()) },
  returns: v.array(v.string()),
  handler: async (ctx, { apartmentId, from, days }) => {
    if (!DAY.test(from)) return [];
    const n = Math.min(Math.max(days ?? 62, 1), 120);
    const to = addDaysKey(from, n);
    const [rs, bs] = await Promise.all([reservationsOverlapping(ctx, apartmentId, from, to), blocksOverlapping(ctx, apartmentId, from, to)]);
    const booked = new Set<string>();
    for (const r of rs) for (const k of eachNightKey(r.checkIn, r.checkOut)) if (k >= from && k < to) booked.add(k);
    for (const b of bs) for (const k of eachNightKey(b.startDate, b.endDate)) if (k >= from && k < to) booked.add(k);
    return [...booked].sort();
  },
});

/**
 * When was anyone last at the desk? One row, one read — the public site must
 * never subscribe to the users table, where a heartbeat every minute would
 * invalidate a 200-document query for every connected visitor.
 *
 * Returns the raw timestamp rather than a boolean on purpose: the caller
 * decides with its own clock, so the dot goes grey once staff leave even
 * though nothing invalidates the query any more. Nothing else leaks — no
 * names, no count.
 */
export const presence = query({
  args: {},
  returns: v.object({ lastSeenAt: v.union(v.number(), v.null()) }),
  handler: async (ctx) => {
    const row = await ctx.db.query("presence").withIndex("by_key", (q) => q.eq("key", "staff")).unique();
    return { lastSeenAt: row?.lastSeenAt ?? null };
  },
});

/**
 * A visitor left their number. Validated and rate-limited (3 per number per
 * hour, 40 site-wide per 10 minutes) so an ad campaign cannot be abused
 * into spamming the desk. The staff are notified immediately.
 */
export const submitLead = mutation({
  args: {
    name: v.string(),
    phone: v.string(),
    email: v.optional(v.string()),
    apartmentId: v.optional(v.id("apartments")),
    checkIn: v.optional(v.string()),
    checkOut: v.optional(v.string()),
    guests: v.optional(v.number()),
    message: v.optional(v.string()),
    kind: v.optional(v.string()),
    attribution: v.optional(v.object({ utmSource: v.optional(v.string()), utmMedium: v.optional(v.string()), utmCampaign: v.optional(v.string()), utmContent: v.optional(v.string()), fbclid: v.optional(v.string()), referrer: v.optional(v.string()), page: v.optional(v.string()) })),
  },
  returns: v.object({ ok: v.boolean(), error: v.optional(v.string()) }),
  handler: async (ctx, input) => {
    const name = input.name.trim().slice(0, 80);
    const phone = normalizePhone(input.phone);
    const digits = phone.replace(/[^\d]/g, "");
    if (name.length < 2) return { ok: false, error: "Please tell us your name." };
    if (digits.length < 8 || digits.length > 15) return { ok: false, error: "Please enter a valid phone number." };
    const email = input.email?.trim().toLowerCase().slice(0, 120) || undefined;
    if (email && !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return { ok: false, error: "That email does not look right." };
    const checkIn = input.checkIn && DAY.test(input.checkIn) ? input.checkIn : undefined;
    const checkOut = input.checkOut && DAY.test(input.checkOut) && checkIn && input.checkOut > checkIn ? input.checkOut : undefined;
    const kind = ["LEAD_FORM", "WAITLIST", "CALLBACK", "EXIT"].includes(input.kind ?? "") ? input.kind! : "LEAD_FORM";
    const now = Date.now();
    const phoneKey = digits.slice(-8);
    const recent = await ctx.db.query("leads").withIndex("by_phoneKey_createdAt", (q) => q.eq("phoneKey", phoneKey).gt("createdAt", now - 3_600_000)).take(5);
    if (recent.length >= 3) return { ok: false, error: "We already have your request — the team will call you shortly." };
    const burst = await ctx.db.query("leads").withIndex("by_createdAt", (q) => q.gt("createdAt", now - 600_000)).take(50);
    if (burst.length >= 40) return { ok: false, error: "We are receiving many requests right now. Please message us on WhatsApp." };
    const apartment = input.apartmentId ? await ctx.db.get(input.apartmentId) : null;
    const a = input.attribution ?? {};
    const clip = (s?: string) => (s ? s.slice(0, 120) : undefined);
    await ctx.db.insert("leads", {
      name,
      phone,
      phoneKey,
      email,
      apartmentId: apartment && !apartment.deletedAt ? apartment._id : undefined,
      checkIn,
      checkOut,
      guests: Math.min(12, Math.max(1, Math.round(input.guests ?? 1))),
      message: input.message?.trim().slice(0, 600) || undefined,
      kind,
      status: "NEW",
      utmSource: clip(a.utmSource),
      utmMedium: clip(a.utmMedium),
      utmCampaign: clip(a.utmCampaign),
      utmContent: clip(a.utmContent),
      fbclid: clip(a.fbclid),
      referrer: clip(a.referrer),
      page: clip(a.page),
      createdAt: now,
    });
    const roles = await ctx.db.query("roles").collect();
    const reception = roles.find((r) => r.key === "RECEPTION");
    const staff = reception ? (await ctx.db.query("users").withIndex("by_role", (q) => q.eq("roleId", reception._id)).collect()).filter((u) => u.status === "ACTIVE" && !u.deletedAt).map((u) => u._id) : [];
    const what = kind === "WAITLIST" ? "wants to be called when something is free" : kind === "CALLBACK" || kind === "EXIT" ? "asked to be called back" : `wants to book${apartment ? ` ${apartment.code}` : ""}`;
    await notify(ctx, { type: "LEAD_RECEIVED", title: `New enquiry from the website · ${name}`, body: `${name} (${phone}) ${what}${checkIn && checkOut ? ` · ${checkIn} → ${checkOut}` : ""}${a.utmSource ? ` · via ${a.utmSource}` : ""}.`, priority: "HIGH", href: "/leads", entityType: "lead", targetUserIds: staff });
    return { ok: true };
  },
});

/** Place ID for the server-side reviews fetch (an identifier, not a secret). */
export const placeId = query({
  args: {},
  returns: v.string(),
  handler: async (ctx) => (await getSettings(ctx)).googlePlaceId,
});

// ── Social proof (real numbers only) ─────────────────────────
/**
 * Honest marketing signals for the site: how many bookings came in recently,
 * which apartments are in demand, and the last few bookings anonymised
 * ("Someone booked Anfa Sky Studio 2 h ago"). Never names, never invented.
 */
export const pulse = query({
  args: {},
  returns: v.any(),
  handler: async (ctx) => {
    const s = await getSettings(ctx);
    const now = Date.now();
    const d30 = now - 30 * 86_400_000;
    const rows = (await ctx.db.query("reservations").withIndex("by_createdAt", (q) => q.gt("createdAt", d30)).order("desc").take(400)).filter((r) => ["PENDING", "CONFIRMED", "CHECKED_IN", "CHECKED_OUT"].includes(r.status));
    const apts = new Map<string, number>();
    for (const r of rows) apts.set(r.apartmentId, (apts.get(r.apartmentId) ?? 0) + 1);
    const names = new Map<string, string>();
    for (const id of apts.keys()) names.set(id, (await ctx.db.get(id as Id<"apartments">))?.name ?? "");
    const recent = rows.slice(0, 6).map((r) => ({ apartmentId: r.apartmentId, apartment: names.get(r.apartmentId) ?? "", at: r.createdAt, nights: r.nights }));
    const guests = new Set(rows.map((r) => r.customerId)).size;
    return { bookings30: rows.length, guests30: guests, byApartment: Object.fromEntries(apts), recent, replyMinutes: s.replyMinutes };
  },
});

// ── Self-serve booking ───────────────────────────────────────

/**
 * A visitor books an apartment from the site. The reservation is created as
 * PENDING on the calendar (so the nights are held for them) and the desk is
 * asked to confirm by WhatsApp or phone. The guest profile is matched by
 * phone; a blocked or restricted guest silently becomes an enquiry for the
 * admin instead of a reservation — no verdict is ever shown to a visitor.
 */
export const book = mutation({
  args: {
    apartmentId: v.id("apartments"),
    checkIn: v.string(),
    checkOut: v.string(),
    guests: v.number(),
    name: v.string(),
    phone: v.string(),
    email: v.optional(v.string()),
    message: v.optional(v.string()),
    attribution: v.optional(v.object({ utmSource: v.optional(v.string()), utmMedium: v.optional(v.string()), utmCampaign: v.optional(v.string()), utmContent: v.optional(v.string()), fbclid: v.optional(v.string()), referrer: v.optional(v.string()), page: v.optional(v.string()) })),
  },
  returns: v.object({ ok: v.boolean(), error: v.optional(v.string()), code: v.optional(v.string()), total: v.optional(v.number()), pendingReview: v.optional(v.boolean()) }),
  handler: async (ctx, input) => {
    const s = await getSettings(ctx);
    const name = input.name.trim().slice(0, 80);
    const phone = normalizePhone(input.phone);
    const digits = phone.replace(/[^\d]/g, "");
    if (name.length < 2) return { ok: false, error: "Please tell us your name." };
    if (digits.length < 8 || digits.length > 15) return { ok: false, error: "Please enter a valid phone number." };
    if (!DAY.test(input.checkIn) || !DAY.test(input.checkOut) || input.checkOut <= input.checkIn) return { ok: false, error: "Please choose your dates." };
    const nights = nightsBetweenKeys(input.checkIn, input.checkOut);
    if (nights > 60) return { ok: false, error: "For stays over 60 nights, message us on WhatsApp." };
    const apartment = await ctx.db.get(input.apartmentId);
    if (!apartment || apartment.deletedAt || !apartment.isActive) return { ok: false, error: "This apartment is not available any more." };
    const guests = Math.min(apartment.maxGuests, Math.max(1, Math.round(input.guests)));
    const now = Date.now();
    const phoneKey = digits.slice(-8);
    // Abuse guard: 2 online bookings per number per hour, 30 site-wide per 10 min.
    const recent = (await ctx.db.query("leads").withIndex("by_phoneKey_createdAt", (q) => q.eq("phoneKey", phoneKey).gt("createdAt", now - 3_600_000)).take(10)).filter((l) => l.kind === "BOOKING");
    if (recent.length >= 2) return { ok: false, error: "We already have your booking — the team is confirming it now." };
    const burst = (await ctx.db.query("leads").withIndex("by_createdAt", (q) => q.gt("createdAt", now - 600_000)).take(60)).filter((l) => l.kind === "BOOKING");
    if (burst.length >= 30) return { ok: false, error: "We are receiving many bookings right now — please message us on WhatsApp." };
    const conflicts = await findConflicts(ctx, apartment._id, input.checkIn, input.checkOut);
    if (conflicts.length) return { ok: false, error: "Someone just took these nights. Pick other dates or another apartment — or ask us to find you one." };
    const a = input.attribution ?? {};
    const clip = (x?: string) => (x ? x.slice(0, 120) : undefined);
    const email = input.email?.trim().toLowerCase().slice(0, 120) || undefined;
    const message = input.message?.trim().slice(0, 600) || undefined;
    const leadBase = { name, phone, phoneKey, email, apartmentId: apartment._id, checkIn: input.checkIn, checkOut: input.checkOut, guests, message, kind: "BOOKING", utmSource: clip(a.utmSource), utmMedium: clip(a.utmMedium), utmCampaign: clip(a.utmCampaign), utmContent: clip(a.utmContent), fbclid: clip(a.fbclid), referrer: clip(a.referrer), page: clip(a.page), createdAt: now };

    // Guest profile: reuse by phone, screen for risk, create if unknown.
    const screen = await screenGuest(ctx, { phone, email });
    const gate = screen.matches.find((m) => m.riskLevel === "BLOCKED") ?? screen.matches.find((m) => m.riskLevel === "RESTRICTED");
    if (gate) {
      await ctx.db.insert("leads", { ...leadBase, status: "NEW", notes: `Online booking held for review: number matches ${gate.fullName} (${gate.code}, ${gate.riskLevel.toLowerCase()}).`, customerId: gate.id });
      await notify(ctx, { type: "BLOCKED_GUEST_ATTEMPT", title: "Online booking from a flagged guest", body: `${name} (${phone}) tried to book ${apartment.code} for ${input.checkIn} → ${input.checkOut} online. The number matches ${gate.fullName} (${gate.code}). No reservation was created — decide from Enquiries.`, priority: "CRITICAL", href: "/leads", entityType: "customer", entityId: gate.id });
      return { ok: true, pendingReview: true };
    }
    const existing = screen.matches.find((m) => m.matchedBy.includes("phone")) ?? screen.matches[0];
    const parts = name.split(/\s+/);
    let customerId = existing?.id;
    if (!customerId) {
      const code = await nextCode(ctx, "customer");
      customerId = await ctx.db.insert("customers", { code, ...normalise({ firstName: parts[0], lastName: parts.slice(1).join(" ") || "—", phone, email: email ?? null }), notes: "Created from an online booking", isBlacklisted: false, riskLevel: "NORMAL", verificationStatus: "UNVERIFIED", updatedAt: now });
    }
    // Owner account signs the reservation (no commission is created for admins).
    const roles = await ctx.db.query("roles").collect();
    const adminRole = roles.find((r) => r.key === "ADMIN");
    const admin = adminRole ? (await ctx.db.query("users").withIndex("by_role", (q) => q.eq("roleId", adminRole._id)).collect()).find((u) => u.status === "ACTIVE" && !u.deletedAt) : null;
    if (!admin) return { ok: false, error: "Online booking is not available right now — message us on WhatsApp." };
    const pricing = computePricing({ basePrice: apartment.basePrice, weekendPrice: apartment.weekendPrice ?? null, weekendDays: s.weekendDays, checkIn: parseKey(input.checkIn), checkOut: parseKey(input.checkOut) });
    const code = await nextCode(ctx, "reservation");
    const id = await ctx.db.insert("reservations", { code, customerId, apartmentId: apartment._id, checkIn: input.checkIn, checkOut: input.checkOut, originalCheckIn: input.checkIn, originalCheckOut: input.checkOut, recoveredNights: 0, nights: pricing.nights, adults: guests, children: 0, source: "WEBSITE", status: "PENDING", nightlyPrice: pricing.nightlyPrice, discount: 0, totalAmount: pricing.total, deposit: 0, amountPaid: 0, createdById: admin._id, internalNotes: `Booked online by the guest${a.utmSource ? ` · via ${a.utmSource}${a.utmCampaign ? ` / ${a.utmCampaign}` : ""}` : ""}. Confirm by WhatsApp or phone.`, customerRequests: message, earlyCheckout: false, releasedNights: 0, segmentIndex: 1, createdAt: now, updatedAt: now });
    await ctx.db.insert("reservationHistory", { reservationId: id, type: "CREATED", newValue: JSON.stringify({ status: "PENDING", apartment: apartment.code, checkIn: input.checkIn, checkOut: input.checkOut, total: pricing.total, online: true }), at: now });
    await ctx.db.insert("leads", { ...leadBase, status: "CONVERTED", reservationId: id, customerId, handledAt: now });
    await audit(ctx, null, { action: "RESERVATION_CREATED", module: "reservations", entityType: "reservation", entityId: id, entityLabel: code, newValue: { apartment: apartment.code, customer: name, checkIn: input.checkIn, checkOut: input.checkOut, total: pricing.total, status: "PENDING", source: "WEBSITE", campaign: a.utmCampaign ?? null }, reservationId: id, apartmentId: apartment._id, customerId });
    await inventoryEvent(ctx, null, { apartmentId: apartment._id, action: "RESERVATION_CREATED", startDate: input.checkIn, endDate: input.checkOut, previousState: "AVAILABLE", newState: "RESERVED", reservationId: id, source: "WEBSITE", estimatedValue: pricing.total });
    const reception = roles.find((r) => r.key === "RECEPTION");
    const staff = reception ? (await ctx.db.query("users").withIndex("by_role", (q) => q.eq("roleId", reception._id)).collect()).filter((u) => u.status === "ACTIVE" && !u.deletedAt).map((u) => u._id) : [];
    await notify(ctx, { type: "NEW_RESERVATION", title: `Online booking to confirm · ${code}`, body: `${name} (${phone}) booked ${apartment.code} online for ${input.checkIn} → ${input.checkOut} · ${guests} guest${guests > 1 ? "s" : ""} · ${Math.round(pricing.total)} ${s.currency}. Confirm within ${s.replyMinutes} min.`, priority: "HIGH", href: `/reservations/${id}`, entityType: "reservation", entityId: id, targetUserIds: staff });
    await syncApartmentStatus(ctx, apartment._id, s.timezone);
    return { ok: true, code, total: pricing.total };
  },
});
